diff --git a/client/src/pages/settings.tsx b/client/src/pages/settings.tsx
index b0e1640..8791986 100644
--- a/client/src/pages/settings.tsx
+++ b/client/src/pages/settings.tsx
@@ -31,6 +31,7 @@ export default function Settings() {
const [ffmpegApiKey, setFfmpegApiKey] = useState("");
const [externalApiKey, setExternalApiKey] = useState("");
const [minimaxApiKey, setMinimaxApiKey] = useState("");
+ const [minimaxGroupId, setMinimaxGroupId] = useState("");
const { toast } = useToast();
const { data: session } = useQuery<{ id: string; username: string; role: string }>({
@@ -264,6 +265,7 @@ export default function Settings() {
const hasExistingExternalApiConfig = !!(externalApiConfig as any)?.configured;
const hasExistingMinimaxConfig = !!(minimaxConfig as any)?.apiKey;
+ const hasExistingMinimaxGroupId = !!(minimaxConfig as any)?.groupId;
const saveMinimaxMutation = useMutation({
mutationFn: () => {
@@ -271,11 +273,15 @@ export default function Settings() {
if (minimaxApiKey && minimaxApiKey.trim() !== "") {
payload.apiKey = minimaxApiKey;
}
+ if (minimaxGroupId && minimaxGroupId.trim() !== "") {
+ payload.groupId = minimaxGroupId;
+ }
return apiRequest('POST', '/api/minimax/config', payload);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/minimax/config'] });
setMinimaxApiKey("");
+ setMinimaxGroupId("");
toast({
title: "Configuration sauvegardée",
description: "Votre clé API Minimax a été enregistrée",
@@ -680,6 +686,24 @@ export default function Settings() {
)}
+
+
Group ID Minimax
+
setMinimaxGroupId(e.target.value)}
+ placeholder={hasExistingMinimaxGroupId ? "••••••••••••••••" : "1234567890"}
+ data-testid="input-minimax-group-id"
+ />
+
+ {hasExistingMinimaxGroupId ? (
+ ✓ Group ID configuré - Laissez vide pour conserver
+ ) : (
+ "Requis pour les quotas corrects — trouvez-le sur platform.minimaxi.com dans votre profil"
+ )}
+
+
Voix françaises disponibles :
@@ -693,7 +717,7 @@ export default function Settings() {
saveMinimaxMutation.mutate()}
- disabled={saveMinimaxMutation.isPending || (!minimaxApiKey.trim() && !hasExistingMinimaxConfig)}
+ disabled={saveMinimaxMutation.isPending || (!minimaxApiKey.trim() && !minimaxGroupId.trim() && !hasExistingMinimaxConfig)}
data-testid="button-save-minimax"
className="w-full"
>
diff --git a/ffmpeg-service/main.py b/ffmpeg-service/main.py
index fd43a7f..ebda2fe 100644
--- a/ffmpeg-service/main.py
+++ b/ffmpeg-service/main.py
@@ -161,6 +161,7 @@ class ReelRequest(BaseModel):
tts_voice: str = "fr-FR-VivienneMultilingualNeural"
tts_provider: str = "edge_tts" # "edge_tts" or "minimax"
minimax_api_key: Optional[str] = None
+ minimax_group_id: Optional[str] = None
draw_text: bool = True
stabilize: bool = False # Stabilisation vidéo via vidstab
enable_ending_effect: bool = True
@@ -194,9 +195,9 @@ 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:
+def generate_tts_minimax(text: str, voice_id: str, api_key: str, audio_path: Path, group_id: Optional[str] = None) -> None:
"""Generate TTS audio using Minimax Speech API (T2A v2)."""
- url = "https://api.minimax.io/v1/t2a_v2"
+ url = f"https://api.minimax.io/v1/t2a_v2?GroupId={group_id}" if group_id else "https://api.minimax.io/v1/t2a_v2"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
@@ -254,6 +255,7 @@ async def generate_tts_with_subs(
delay: float = 0.0,
tts_provider: str = "edge_tts",
minimax_api_key: Optional[str] = None,
+ minimax_group_id: Optional[str] = None,
):
"""Generate TTS audio with word-level synchronized subtitles.
@@ -267,8 +269,8 @@ async def generate_tts_with_subs(
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)
+ print(f"🔊 Minimax TTS with voice: {voice}, group_id={bool(minimax_group_id)}")
+ generate_tts_minimax(text, voice, minimax_api_key, audio_path, group_id=minimax_group_id)
audio_duration = None
try:
@@ -879,7 +881,7 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
# Clean text for TTS (remove hashtags/emojis)
tts_clean_text = clean_text_for_tts(request.text)
print(f"🔊 TTS enabled. Provider: {request.tts_provider}, Voice: {request.tts_voice}")
- print(f"🔊 TTS has minimax_api_key: {bool(request.minimax_api_key)}")
+ print(f"🔊 TTS has minimax_api_key: {bool(request.minimax_api_key)}, group_id: {bool(request.minimax_group_id)}")
print(f"🔊 TTS cleaned: '{tts_clean_text[:80]}...'")
# Check for male/female voice map (edge_tts only)
@@ -906,6 +908,7 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
delay=2.0,
tts_provider=request.tts_provider,
minimax_api_key=request.minimax_api_key,
+ minimax_group_id=request.minimax_group_id,
)
# Verify files were created
@@ -1297,6 +1300,9 @@ async def preview_tts(request: ReelRequest, x_api_key: str = Header(None)):
tts_audio_path,
tts_srt_path,
display_text=clean_text_for_display(request.text),
+ tts_provider=request.tts_provider,
+ minimax_api_key=request.minimax_api_key,
+ minimax_group_id=request.minimax_group_id,
)
if not tts_audio_path.exists():
diff --git a/server/migrate.ts b/server/migrate.ts
index 3e3973a..2fe75aa 100644
--- a/server/migrate.ts
+++ b/server/migrate.ts
@@ -164,6 +164,12 @@ export async function migrate() {
);
`);
+ // minimax_config.group_id
+ await client.query(`
+ ALTER TABLE "minimax_config"
+ ADD COLUMN IF NOT EXISTS "group_id" text;
+ `);
+
console.log("[Migration] Safe migration completed.");
} catch (error) {
console.error("[Migration] Error during migration:", error);
diff --git a/server/routes/reels.ts b/server/routes/reels.ts
index f245878..9659927 100644
--- a/server/routes/reels.ts
+++ b/server/routes/reels.ts
@@ -354,12 +354,14 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
const finalWordDuration = wordDuration;
let minimaxApiKey: string | undefined;
+ let minimaxGroupId: 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;
+ minimaxGroupId = minimaxCfg.groupId ?? undefined;
}
// Traiter la vidéo via FFmpeg
@@ -370,6 +372,7 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
ttsVoice,
ttsProvider,
minimaxApiKey,
+ minimaxGroupId,
wordDuration: finalWordDuration,
fontSize,
musicVolume,
@@ -409,15 +412,17 @@ reelsRouter.post('/reels/tts-preview', async (req: Request, res: Response) => {
}
let minimaxApiKey: string | undefined;
+ let minimaxGroupId: 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;
+ minimaxGroupId = minimaxCfg.groupId ?? undefined;
}
- const result = await ffmpegService.previewTTS(text, voice, ttsProvider, minimaxApiKey);
+ const result = await ffmpegService.previewTTS(text, voice, ttsProvider, minimaxApiKey, minimaxGroupId);
if (!result.success) {
return res.status(500).json({ error: result.error || 'Erreur de génération TTS' });
@@ -610,16 +615,18 @@ async function processReelBackground(
const finalWordDuration = wordDuration ?? 0.6;
let minimaxApiKeyBg: string | undefined;
+ let minimaxGroupIdBg: string | undefined;
if (ttsEnabled && ttsProvider === 'minimax') {
const minimaxCfg = await storage.getMinimaxConfig(userId);
if (minimaxCfg?.apiKey) {
minimaxApiKeyBg = minimaxCfg.apiKey;
+ minimaxGroupIdBg = minimaxCfg.groupId ?? undefined;
} else {
console.error('❌ [Background] Minimax API key not found in DB for user:', userId);
}
}
- console.log('🔊 [Background] TTS config:', { ttsEnabled, ttsProvider, ttsVoice, hasMinimaxKey: !!minimaxApiKeyBg });
+ console.log('🔊 [Background] TTS config:', { ttsEnabled, ttsProvider, ttsVoice, hasMinimaxKey: !!minimaxApiKeyBg, hasGroupId: !!minimaxGroupIdBg });
const ffmpegResult = await ffmpegService.processReelFromUrl(resolveInternalUrl(media.originalUrl), {
text: overlayText,
@@ -628,6 +635,7 @@ async function processReelBackground(
ttsVoice,
ttsProvider,
minimaxApiKey: minimaxApiKeyBg,
+ minimaxGroupId: minimaxGroupIdBg,
wordDuration: finalWordDuration,
fontSize,
musicVolume,
diff --git a/server/services/ffmpeg.ts b/server/services/ffmpeg.ts
index 8f2bdf5..36fbb5d 100644
--- a/server/services/ffmpeg.ts
+++ b/server/services/ffmpeg.ts
@@ -15,6 +15,7 @@ interface FFmpegReelRequest {
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)
+ minimax_group_id?: string; // Group ID Minimax (requis pour quotas corrects)
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)
@@ -76,6 +77,7 @@ export class FFmpegService {
ttsVoice?: string;
ttsProvider?: string;
minimaxApiKey?: string;
+ minimaxGroupId?: string;
wordDuration?: number;
fontSize?: number;
musicVolume?: number;
@@ -97,6 +99,7 @@ export class FFmpegService {
tts_voice: options.ttsVoice,
tts_provider: options.ttsProvider,
minimax_api_key: options.minimaxApiKey,
+ minimax_group_id: options.minimaxGroupId,
word_duration: options.wordDuration ?? 0.6,
font_size: options.fontSize ?? 64,
music_volume: options.musicVolume ?? 0.25,
@@ -181,6 +184,7 @@ export class FFmpegService {
ttsVoice?: string;
ttsProvider?: string;
minimaxApiKey?: string;
+ minimaxGroupId?: string;
wordDuration?: number;
fontSize?: number;
musicVolume?: number;
@@ -202,6 +206,7 @@ export class FFmpegService {
tts_voice: options.ttsVoice,
tts_provider: options.ttsProvider,
minimax_api_key: options.minimaxApiKey,
+ minimax_group_id: options.minimaxGroupId,
word_duration: options.wordDuration ?? 0.6,
font_size: options.fontSize ?? 64,
music_volume: options.musicVolume ?? 0.25,
@@ -304,7 +309,8 @@ export class FFmpegService {
text: string,
voice: string,
ttsProvider?: string,
- minimaxApiKey?: string
+ minimaxApiKey?: string,
+ minimaxGroupId?: string
): Promise<{ success: boolean; audioBase64?: string; error?: string }> {
const config = this.ensureConfigured();
@@ -320,6 +326,7 @@ export class FFmpegService {
tts_voice: voice,
tts_provider: ttsProvider,
minimax_api_key: minimaxApiKey,
+ minimax_group_id: minimaxGroupId,
}),
});
diff --git a/shared/schema.ts b/shared/schema.ts
index b2c4b43..702cc27 100644
--- a/shared/schema.ts
+++ b/shared/schema.ts
@@ -146,6 +146,7 @@ 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(),
+ groupId: text("group_id"),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});