- {[
+ {(ttsProvider === "edge_tts" ? [
{ id: "fr-FR-VivienneMultilingualNeural", label: "Vivienne", type: "Femme (Défaut)", style: "bg-pink-500/10 border-pink-500/50" },
{ id: "fr-FR-RemyMultilingualNeural", label: "Rémy", type: "Homme", style: "bg-blue-500/10 border-blue-500/50" },
{ id: "fr-FR-DeniseNeural", label: "Denise", type: "Femme (Calme)", style: "bg-purple-500/10 border-purple-500/50" },
{ id: "fr-FR-HenriNeural", label: "Henri", type: "Homme (Grave)", style: "bg-slate-500/10 border-slate-500/50" },
{ id: "fr-FR-EloiseNeural", label: "Éloïse", type: "Enfant", style: "bg-orange-500/10 border-orange-500/50" }
- ].map((voice) => (
+ ] : [
+ { id: "French_Female_News", label: "News Féminin", type: "Femme (Défaut)", style: "bg-pink-500/10 border-pink-500/50" },
+ { id: "French_Male_Speech_New", label: "Speech Masculin", type: "Homme", style: "bg-blue-500/10 border-blue-500/50" },
+ { id: "French_MovieLeadFemale", label: "Cinéma Femme", type: "Femme (Dramatique)", style: "bg-purple-500/10 border-purple-500/50" },
+ { id: "French_CasualMan", label: "Casual Homme", type: "Homme (Décontracté)", style: "bg-slate-500/10 border-slate-500/50" },
+ { id: "French_FemaleAnchor", label: "Présentatrice", type: "Femme (Média)", style: "bg-rose-500/10 border-rose-500/50" },
+ { id: "French_MaleNarrator", label: "Narrateur", type: "Homme (Narration)", style: "bg-indigo-500/10 border-indigo-500/50" }
+ ]).map((voice) => (
setTtsVoice(voice.id)}
- className={`cursor-pointer p-3 rounded-md border-2 transition-all hover:bg-accent ${ttsVoice === voice.id || (ttsVoice === 'female' && voice.id === 'fr-FR-VivienneMultilingualNeural')
+ className={`cursor-pointer p-3 rounded-md border-2 transition-all hover:bg-accent ${ttsVoice === voice.id
? `border-primary ${voice.style}`
: 'border-transparent bg-muted/30'
}`}
>
{voice.label}
- {(ttsVoice === voice.id || (ttsVoice === 'female' && voice.id === 'fr-FR-VivienneMultilingualNeural')) && }
+ {ttsVoice === voice.id && }
{voice.type}
@@ -802,7 +831,8 @@ export default function NewReel() {
try {
const response = await apiRequest('POST', '/api/reels/tts-preview', {
text: textToTest,
- voice: ttsVoice
+ voice: ttsVoice,
+ ttsProvider,
});
const data = await response.json();
if (data.success && data.audioBase64) {
@@ -823,7 +853,15 @@ export default function NewReel() {
- {syncInfo && (
+ {ttsProvider === "minimax" && (
+
+ )}
+
+ {ttsProvider === "edge_tts" && syncInfo && (
Sync texte/voix
diff --git a/client/src/pages/settings.tsx b/client/src/pages/settings.tsx
index e0b43c2..b0e1640 100644
--- a/client/src/pages/settings.tsx
+++ b/client/src/pages/settings.tsx
@@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
-import { Settings as SettingsIcon, Bell, Key, Shield, Cloud, Brain, Image, Upload, X, Video, Plug } from "lucide-react";
+import { Settings as SettingsIcon, Bell, Key, Shield, Cloud, Brain, Image, Upload, X, Video, Plug, Mic } from "lucide-react";
import { useQuery, useMutation } from "@tanstack/react-query";
import Sidebar from "@/components/sidebar";
import TopBar from "@/components/topbar";
@@ -30,6 +30,7 @@ export default function Settings() {
const [ffmpegApiUrl, setFfmpegApiUrl] = useState("");
const [ffmpegApiKey, setFfmpegApiKey] = useState("");
const [externalApiKey, setExternalApiKey] = useState("");
+ const [minimaxApiKey, setMinimaxApiKey] = useState("");
const { toast } = useToast();
const { data: session } = useQuery<{ id: string; username: string; role: string }>({
@@ -60,6 +61,10 @@ export default function Settings() {
enabled: isAdmin,
});
+ const { data: minimaxConfig } = useQuery({
+ queryKey: ['/api/minimax/config'],
+ });
+
useEffect(() => {
if (cloudinaryConfig) {
setCloudName((cloudinaryConfig as any).cloudName || "");
@@ -258,6 +263,32 @@ export default function Settings() {
});
const hasExistingExternalApiConfig = !!(externalApiConfig as any)?.configured;
+ const hasExistingMinimaxConfig = !!(minimaxConfig as any)?.apiKey;
+
+ const saveMinimaxMutation = useMutation({
+ mutationFn: () => {
+ const payload: any = {};
+ if (minimaxApiKey && minimaxApiKey.trim() !== "") {
+ payload.apiKey = minimaxApiKey;
+ }
+ return apiRequest('POST', '/api/minimax/config', payload);
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['/api/minimax/config'] });
+ setMinimaxApiKey("");
+ toast({
+ title: "Configuration sauvegardée",
+ description: "Votre clé API Minimax a été enregistrée",
+ });
+ },
+ onError: () => {
+ toast({
+ title: "Erreur",
+ description: "Impossible de sauvegarder la configuration Minimax",
+ variant: "destructive",
+ });
+ },
+ });
const saveExternalApiMutation = useMutation({
mutationFn: () => apiRequest('POST', '/api/settings/external-api', { apiKey: externalApiKey }),
@@ -620,6 +651,57 @@ export default function Settings() {
+
+
+
+
+ Configuration Minimax Speech (TTS)
+
+
+ Configurez l'API Minimax pour générer des voix françaises premium dans les Reels
+
+
+
+
+
+
setMinimaxApiKey(e.target.value)}
+ placeholder={hasExistingMinimaxConfig ? "••••••••••••••••" : "eyJhbGci..."}
+ data-testid="input-minimax-api-key"
+ />
+
+ {hasExistingMinimaxConfig ? (
+ ✓ Clé API configurée - Laissez vide pour conserver
+ ) : (
+ "Votre clé API depuis platform.minimaxi.com"
+ )}
+
+
+
+
Voix françaises disponibles :
+
+ • French_Female_News
+ • French_Male_Speech_New
+ • French_MovieLeadFemale
+ • French_CasualMan
+ • French_FemaleAnchor
+ • French_MaleNarrator
+
+
+
+
+
+
{isAdmin && (
diff --git a/ffmpeg-service/main.py b/ffmpeg-service/main.py
index c3d02a0..cf8e10b 100644
--- a/ffmpeg-service/main.py
+++ b/ffmpeg-service/main.py
@@ -159,6 +159,8 @@ class ReelRequest(BaseModel):
music_volume: float = 0.25
tts_enabled: bool = False
tts_voice: str = "fr-FR-VivienneMultilingualNeural"
+ tts_provider: str = "edge_tts" # "edge_tts" or "minimax"
+ minimax_api_key: Optional[str] = None
draw_text: bool = True
stabilize: bool = False # Stabilisation vidéo via vidstab
enable_ending_effect: bool = True
@@ -192,6 +194,34 @@ def clean_text_for_tts(text: str) -> str:
return " ".join(text.split())
+def generate_tts_minimax(text: str, voice_id: str, api_key: str, audio_path: Path) -> None:
+ """Generate TTS audio using Minimax Speech API (T2A v2)."""
+ url = "https://api.minimax.io/v1/t2a_v2"
+ headers = {
+ "Authorization": f"Bearer {api_key}",
+ "Content-Type": "application/json",
+ }
+ payload = {
+ "model": "speech-2.8-hd",
+ "text": text,
+ "voice_setting": {"voice_id": voice_id},
+ "audio_setting": {"format": "mp3", "sample_rate": 24000, "bitrate": 128},
+ }
+ response = requests.post(url, headers=headers, json=payload, timeout=60)
+ response.raise_for_status()
+ data = response.json()
+
+ base_resp = data.get("base_resp", {})
+ if base_resp.get("status_code") != 0:
+ raise Exception(f"Minimax API error: {base_resp.get('status_msg', 'unknown error')}")
+
+ audio_hex = data["data"]["audio"]
+ audio_bytes = bytes.fromhex(audio_hex)
+ with open(audio_path, "wb") as f:
+ f.write(audio_bytes)
+ print(f"✅ Minimax TTS audio saved: {audio_path.stat().st_size} bytes")
+
+
async def generate_tts_with_subs(
text: str,
voice: str,
@@ -199,13 +229,44 @@ async def generate_tts_with_subs(
ass_path: Path,
display_text: Optional[str] = None,
delay: float = 0.0,
+ tts_provider: str = "edge_tts",
+ minimax_api_key: Optional[str] = None,
):
"""Generate TTS audio with word-level synchronized subtitles.
Uses edge_tts.Communicate.stream() to capture WordBoundary events,
providing millisecond-accurate subtitle timing instead of linear estimation.
+ For Minimax provider, falls back to ffsubsync (no word boundaries available).
"""
+ # Minimax branch: no WordBoundary events, use ffsubsync for subtitle sync
+ if tts_provider == "minimax":
+ if not minimax_api_key:
+ raise Exception("Minimax API key required but not provided")
+ text_to_display = display_text if display_text else text
+ print(f"🔊 Minimax TTS with voice: {voice}")
+ generate_tts_minimax(text, voice, minimax_api_key, audio_path)
+
+ audio_duration = None
+ try:
+ dur_cmd = [
+ "ffprobe", "-v", "error", "-show_entries", "format=duration",
+ "-of", "default=noprint_wrappers=1:nokey=1", str(audio_path),
+ ]
+ dur_proc = subprocess.run(dur_cmd, stdout=subprocess.PIPE, text=True)
+ audio_duration = float(dur_proc.stdout.strip())
+ print(f"⏱️ Minimax TTS Duration: {audio_duration:.2f}s")
+ except Exception as e:
+ print(f"⚠️ Could not measure Minimax TTS duration: {e}")
+
+ unsynced_srt_path = audio_path.with_suffix(".unsynced.srt")
+ synced_srt_path = audio_path.with_suffix(".synced.srt")
+ generate_unsynced_srt(text_to_display, unsynced_srt_path, total_duration=audio_duration)
+ run_ffsubsync(audio_path, unsynced_srt_path, synced_srt_path)
+ convert_srt_to_ass(synced_srt_path, ass_path, font_size=65, delay=delay)
+ print("✅ Minimax TTS synchronisation completed with ffsubsync")
+ return
+
# Determine gender of requested voice to choose appropriate fallbacks
is_male = any(name in voice for name in ["Remi", "Henri", "Paul"])
@@ -796,15 +857,15 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
print(f"🔊 TTS enabled. Original: '{request.text}'")
print(f"🔊 TTS cleaned: '{tts_clean_text}'")
- # Check for male/female voice map
+ # Check for male/female voice map (edge_tts only)
voice = request.tts_voice
- if voice == "male":
- voice = "fr-FR-RemyMultilingualNeural"
- elif voice == "female":
- voice = "fr-FR-VivienneMultilingualNeural"
- elif not voice or "Neural" not in voice:
- # Default if invalid
- voice = "fr-FR-VivienneMultilingualNeural"
+ if request.tts_provider != "minimax":
+ if voice == "male":
+ voice = "fr-FR-RemyMultilingualNeural"
+ elif voice == "female":
+ voice = "fr-FR-VivienneMultilingualNeural"
+ elif not voice or "Neural" not in voice:
+ voice = "fr-FR-VivienneMultilingualNeural"
print(f"🔊 Using voice: {voice}")
@@ -818,6 +879,8 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
tts_ass_path,
display_text=clean_text_for_display(request.text),
delay=2.0,
+ tts_provider=request.tts_provider,
+ minimax_api_key=request.minimax_api_key,
)
# Verify files were created
@@ -1189,12 +1252,13 @@ async def preview_tts(request: ReelRequest, x_api_key: str = Header(None)):
# Determine voice (reuse logic)
voice = request.tts_voice
- if voice == "male":
- voice = "fr-FR-RemyMultilingualNeural"
- elif voice == "female":
- voice = "fr-FR-VivienneMultilingualNeural"
- elif not voice or "Neural" not in voice:
- voice = "fr-FR-VivienneMultilingualNeural"
+ if request.tts_provider != "minimax":
+ if voice == "male":
+ voice = "fr-FR-RemyMultilingualNeural"
+ elif voice == "female":
+ voice = "fr-FR-VivienneMultilingualNeural"
+ elif not voice or "Neural" not in voice:
+ voice = "fr-FR-VivienneMultilingualNeural"
# Pass original text for subtitles (implied in SRT for preview too if needed, though mostly audio)
# Preview TTS generation doesn't technically need the full 2s video delay, but if the frontend plays it against standard timing it might.
diff --git a/server/migrate.ts b/server/migrate.ts
index eca3cb5..3e3973a 100644
--- a/server/migrate.ts
+++ b/server/migrate.ts
@@ -152,6 +152,18 @@ export async function migrate() {
);
`);
+ // minimax_config
+ await client.query(`
+ CREATE TABLE IF NOT EXISTS "minimax_config" (
+ "id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "user_id" varchar NOT NULL REFERENCES "users"("id") ON DELETE cascade,
+ "api_key" text NOT NULL,
+ "created_at" timestamp DEFAULT now(),
+ "updated_at" timestamp DEFAULT now(),
+ CONSTRAINT minimax_config_user_id_unique UNIQUE ("user_id")
+ );
+ `);
+
console.log("[Migration] Safe migration completed.");
} catch (error) {
console.error("[Migration] Error during migration:", error);
diff --git a/server/routes.ts b/server/routes.ts
index 8ef0b7e..7e3d172 100644
--- a/server/routes.ts
+++ b/server/routes.ts
@@ -10,7 +10,7 @@ import passport from "./auth";
import { z } from "zod";
import { openRouterService } from "./services/openrouter";
import { minioService as cloudinaryService, buildMinioUrl } from "./services/minio";
-import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema, updateCloudinaryConfigSchema, insertOpenrouterConfigSchema, updateOpenrouterConfigSchema, insertFreesoundConfigSchema, updateFreesoundConfigSchema, insertUserSchema, postMedia, type SocialPage } from "@shared/schema";
+import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema, updateCloudinaryConfigSchema, insertOpenrouterConfigSchema, updateOpenrouterConfigSchema, insertFreesoundConfigSchema, updateFreesoundConfigSchema, updateMinimaxConfigSchema, insertMinimaxConfigSchema, insertUserSchema, postMedia, type SocialPage } from "@shared/schema";
import type { User, InsertUser, ScheduledPost, FreesoundConfig } from "@shared/schema";
import { freeSoundService } from "./services/freesound";
import { analyticsRouter } from "./routes/analytics";
@@ -477,6 +477,52 @@ export async function registerRoutes(app: Express): Promise {
}
});
+ // Minimax Configuration Routes
+ app.get("/api/minimax/config", requireAuth, async (req, res) => {
+ try {
+ const user = req.user as User;
+ const config = await storage.getMinimaxConfig(user.id);
+ if (!config) {
+ return res.json(null);
+ }
+ return res.json({
+ ...config,
+ apiKey: config.apiKey ? "********" : "",
+ });
+ } catch (error) {
+ console.error("Error fetching Minimax config:", error);
+ res.status(500).json({ error: "Failed to fetch configuration" });
+ }
+ });
+
+ app.post("/api/minimax/config", requireAuth, async (req, res) => {
+ try {
+ const user = req.user as User;
+ const data = updateMinimaxConfigSchema.parse(req.body);
+
+ let config = await storage.getMinimaxConfig(user.id);
+ const updateData: any = { ...data };
+
+ if (!updateData.apiKey || updateData.apiKey === "********") {
+ delete updateData.apiKey;
+ }
+
+ if (config) {
+ config = await storage.updateMinimaxConfig(user.id, updateData);
+ } else {
+ if (!data.apiKey) {
+ return res.status(400).json({ error: "API Key is required" });
+ }
+ config = await storage.createMinimaxConfig({ ...data as any, userId: user.id });
+ }
+
+ res.json({ success: true });
+ } catch (error) {
+ console.error("Error saving Minimax config:", error);
+ res.status(500).json({ error: "Failed to save configuration" });
+ }
+ });
+
// Reels & Music Routes
app.use("/api", requireAuth, reelsRouter);
app.use("/api/remotion", requireAuth, remotionRouter);
diff --git a/server/routes/reels.ts b/server/routes/reels.ts
index 61bf3ea..6a4e64a 100644
--- a/server/routes/reels.ts
+++ b/server/routes/reels.ts
@@ -298,6 +298,7 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
overlayText,
ttsEnabled,
ttsVoice,
+ ttsProvider,
wordDuration = 0.6,
fontSize = 64,
musicVolume = 0.25,
@@ -352,12 +353,23 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
const finalWordDuration = wordDuration;
+ let minimaxApiKey: string | undefined;
+ if (ttsEnabled && ttsProvider === 'minimax') {
+ const minimaxCfg = await storage.getMinimaxConfig(user.id);
+ if (!minimaxCfg?.apiKey) {
+ return res.status(400).json({ error: 'Clé API Minimax non configurée. Allez dans les Paramètres.' });
+ }
+ minimaxApiKey = minimaxCfg.apiKey;
+ }
+
// Traiter la vidéo via FFmpeg
const result = await ffmpegService.processReelFromUrl(resolveInternalUrl(media.originalUrl), {
text: overlayText,
musicUrl: finalMusicUrl,
ttsEnabled,
ttsVoice,
+ ttsProvider,
+ minimaxApiKey,
wordDuration: finalWordDuration,
fontSize,
musicVolume,
@@ -389,13 +401,23 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
*/
reelsRouter.post('/reels/tts-preview', async (req: Request, res: Response) => {
try {
- const { text, voice } = req.body;
+ const user = req.user as User;
+ const { text, voice, ttsProvider } = req.body;
if (!text) {
return res.status(400).json({ error: 'Texte requis' });
}
- const result = await ffmpegService.previewTTS(text, voice);
+ let minimaxApiKey: string | undefined;
+ if (ttsProvider === 'minimax') {
+ const minimaxCfg = await storage.getMinimaxConfig(user.id);
+ if (!minimaxCfg?.apiKey) {
+ return res.status(400).json({ error: 'Clé API Minimax non configurée. Allez dans les Paramètres.' });
+ }
+ minimaxApiKey = minimaxCfg.apiKey;
+ }
+
+ const result = await ffmpegService.previewTTS(text, voice, ttsProvider, minimaxApiKey);
if (!result.success) {
return res.status(500).json({ error: result.error || 'Erreur de génération TTS' });
@@ -487,6 +509,7 @@ async function processReelBackground(
description?: string;
ttsEnabled?: boolean;
ttsVoice?: string;
+ ttsProvider?: string;
pageIds: string[];
scheduledFor?: string;
wordDuration?: number;
@@ -506,6 +529,7 @@ async function processReelBackground(
description,
ttsEnabled,
ttsVoice,
+ ttsProvider,
pageIds,
scheduledFor,
wordDuration,
@@ -585,11 +609,21 @@ async function processReelBackground(
const finalWordDuration = wordDuration ?? 0.6;
+ let minimaxApiKeyBg: string | undefined;
+ if (ttsEnabled && ttsProvider === 'minimax') {
+ const minimaxCfg = await storage.getMinimaxConfig(userId);
+ if (minimaxCfg?.apiKey) {
+ minimaxApiKeyBg = minimaxCfg.apiKey;
+ }
+ }
+
const ffmpegResult = await ffmpegService.processReelFromUrl(resolveInternalUrl(media.originalUrl), {
text: overlayText,
musicUrl: finalMusicUrl,
ttsEnabled,
ttsVoice,
+ ttsProvider,
+ minimaxApiKey: minimaxApiKeyBg,
wordDuration: finalWordDuration,
fontSize,
musicVolume,
diff --git a/server/services/ffmpeg.ts b/server/services/ffmpeg.ts
index f59b907..da0650b 100644
--- a/server/services/ffmpeg.ts
+++ b/server/services/ffmpeg.ts
@@ -13,6 +13,8 @@ interface FFmpegReelRequest {
music_url?: string; // OU URL directe de la musique
tts_enabled?: boolean; // Activation du TTS
tts_voice?: string; // Voix TTS (ex: fr-FR-VivienneNeural)
+ tts_provider?: string; // Provider TTS: "edge_tts" (défaut) ou "minimax"
+ minimax_api_key?: string; // Clé API Minimax (requis si tts_provider=minimax)
word_duration?: number; // Durée par mot (default: 0.6s)
font_size?: number; // Taille police (default: 24)
music_volume?: number; // Volume musique (default: 0.25)
@@ -71,6 +73,8 @@ export class FFmpegService {
musicUrl?: string;
ttsEnabled?: boolean;
ttsVoice?: string;
+ ttsProvider?: string;
+ minimaxApiKey?: string;
wordDuration?: number;
fontSize?: number;
musicVolume?: number;
@@ -90,6 +94,8 @@ export class FFmpegService {
music_url: options.musicUrl,
tts_enabled: options.ttsEnabled,
tts_voice: options.ttsVoice,
+ tts_provider: options.ttsProvider,
+ minimax_api_key: options.minimaxApiKey,
word_duration: options.wordDuration ?? 0.6,
font_size: options.fontSize ?? 64,
music_volume: options.musicVolume ?? 0.25,
@@ -172,6 +178,8 @@ export class FFmpegService {
musicUrl?: string;
ttsEnabled?: boolean;
ttsVoice?: string;
+ ttsProvider?: string;
+ minimaxApiKey?: string;
wordDuration?: number;
fontSize?: number;
musicVolume?: number;
@@ -191,6 +199,8 @@ export class FFmpegService {
music_url: options.musicUrl,
tts_enabled: options.ttsEnabled,
tts_voice: options.ttsVoice,
+ tts_provider: options.ttsProvider,
+ minimax_api_key: options.minimaxApiKey,
word_duration: options.wordDuration ?? 0.6,
font_size: options.fontSize ?? 64,
music_volume: options.musicVolume ?? 0.25,
@@ -285,7 +295,9 @@ export class FFmpegService {
}
async previewTTS(
text: string,
- voice: string
+ voice: string,
+ ttsProvider?: string,
+ minimaxApiKey?: string
): Promise<{ success: boolean; audioBase64?: string; error?: string }> {
const config = this.ensureConfigured();
@@ -298,7 +310,9 @@ export class FFmpegService {
},
body: JSON.stringify({
text,
- tts_voice: voice
+ tts_voice: voice,
+ tts_provider: ttsProvider,
+ minimax_api_key: minimaxApiKey,
}),
});
diff --git a/server/storage.ts b/server/storage.ts
index e2cf15e..1fa1847 100644
--- a/server/storage.ts
+++ b/server/storage.ts
@@ -39,6 +39,9 @@ import {
type InsertAudioTrack,
appConfig,
type AppConfig,
+ minimaxConfig,
+ type MinimaxConfig,
+ type InsertMinimaxConfig,
} from "@shared/schema";
import { db } from "./db";
import { eq, and, gte, lte, desc, asc, isNull, inArray } from "drizzle-orm";
@@ -609,6 +612,24 @@ export class DatabaseStorage implements IStorage {
const [created] = await db.insert(appConfig).values(data).returning();
return created;
}
+ // Minimax Config
+ async getMinimaxConfig(userId: string): Promise {
+ const [config] = await db.select().from(minimaxConfig).where(eq(minimaxConfig.userId, userId));
+ return config || undefined;
+ }
+
+ async createMinimaxConfig(config: InsertMinimaxConfig): Promise {
+ const [newConfig] = await db.insert(minimaxConfig).values(config).returning();
+ return newConfig;
+ }
+
+ async updateMinimaxConfig(userId: string, config: Partial): Promise {
+ const [updated] = await db.update(minimaxConfig)
+ .set({ ...config, updatedAt: new Date() })
+ .where(eq(minimaxConfig.userId, userId))
+ .returning();
+ return updated;
+ }
}
export const storage = new DatabaseStorage();
diff --git a/shared/schema.ts b/shared/schema.ts
index 026f3a4..6b88bde 100644
--- a/shared/schema.ts
+++ b/shared/schema.ts
@@ -142,6 +142,14 @@ export const freesoundConfig = pgTable("freesound_config", {
updatedAt: timestamp("updated_at").defaultNow(),
});
+export const minimaxConfig = pgTable("minimax_config", {
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
+ userId: varchar("user_id").notNull().unique().references(() => users.id, { onDelete: "cascade" }),
+ apiKey: text("api_key").notNull(),
+ createdAt: timestamp("created_at").defaultNow(),
+ updatedAt: timestamp("updated_at").defaultNow(),
+});
+
export const media = pgTable("media", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
userId: varchar("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
@@ -489,4 +497,20 @@ export type AppConfig = typeof appConfig.$inferSelect;
export type InsertFreesoundConfig = z.infer;
export type UpdateFreesoundConfig = z.infer;
+export const insertMinimaxConfigSchema = createInsertSchema(minimaxConfig).omit({
+ id: true,
+ createdAt: true,
+ updatedAt: true,
+});
+
+export const updateMinimaxConfigSchema = insertMinimaxConfigSchema.omit({
+ userId: true
+}).partial({
+ apiKey: true,
+});
+
+export type MinimaxConfig = typeof minimaxConfig.$inferSelect;
+export type InsertMinimaxConfig = z.infer;
+export type UpdateMinimaxConfig = z.infer;
+