diff --git a/client/src/pages/new-reel.tsx b/client/src/pages/new-reel.tsx index fc71ee4..a6729de 100644 --- a/client/src/pages/new-reel.tsx +++ b/client/src/pages/new-reel.tsx @@ -66,8 +66,8 @@ export default function NewReel() { const [scheduledDate, setScheduledDate] = useState(undefined); const [musicVolume, setMusicVolume] = useState([25]); const [ttsEnabled, setTtsEnabled] = useState(true); - const [ttsVoice, setTtsVoice] = useState("female"); - const [drawText, setDrawText] = useState(false); + const [ttsVoice, setTtsVoice] = useState("fr-FR-VivienneMultilingualNeural"); + const [drawText, setDrawText] = useState(true); // État audio preview const [isPlaying, setIsPlaying] = useState(null); @@ -742,28 +742,63 @@ export default function NewReel() { Voix du narrateur -
-
setTtsVoice("female")} - className={`cursor-pointer p-3 rounded-md border-2 text-center transition-all ${ttsVoice === 'female' - ? 'border-primary bg-primary/10' - : 'border-transparent bg-background hover:bg-accent' - }`} - > -
Féminine
-
Vivienne
-
-
setTtsVoice("male")} - className={`cursor-pointer p-3 rounded-md border-2 text-center transition-all ${ttsVoice === 'male' - ? 'border-primary bg-primary/10' - : 'border-transparent bg-background hover:bg-accent' - }`} - > -
Masculine
-
Rémy
-
+
+ {[ + { 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) => ( +
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') + ? `border-primary ${voice.style}` + : 'border-transparent bg-muted/30' + }`} + > +
+ {voice.label} + {(ttsVoice === voice.id || (ttsVoice === 'female' && voice.id === 'fr-FR-VivienneMultilingualNeural')) && } +
+
{voice.type}
+
+ ))}
+ +
+ +
+

Le texte sera automatiquement synchronisé avec la voix. Les #hashtags et émojis ne seront pas lus. diff --git a/ffmpeg-service/main.py b/ffmpeg-service/main.py index 6e00ea8..0b6df29 100644 --- a/ffmpeg-service/main.py +++ b/ffmpeg-service/main.py @@ -54,23 +54,23 @@ async def generate_tts_with_subs( # Determine gender of requested voice to choose appropriate fallbacks is_male = any(name in voice for name in ["Remy", "Henri", "Paul"]) - + if is_male: fallback_voices = [ - voice, # Try requested voice first - "fr-FR-HenriNeural", # Primary Male fallback + voice, # Try requested voice first + "fr-FR-HenriNeural", # Primary Male fallback "fr-FR-PaulNeural", # Secondary Male fallback ] else: fallback_voices = [ - voice, # Try requested voice first - "fr-FR-VivienneNeural", # Primary Female fallback - "fr-FR-DeniseNeural", # Secondary Female fallback + voice, # Try requested voice first + "fr-FR-VivienneNeural", # Primary Female fallback + "fr-FR-DeniseNeural", # Secondary Female fallback ] - + # Always add English fallback as last resort fallback_voices.append("en-US-JennyNeural") - + # Remove duplicates while preserving order fallback_voices = list(dict.fromkeys(fallback_voices)) @@ -434,5 +434,50 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)): return {"success": False, "detail": str(e)} +@app.post("/preview-tts") +async def preview_tts(request: ReelRequest, x_api_key: str = Header(None)): + if x_api_key != API_KEY: + raise HTTPException(status_code=401, detail="Invalid API Key") + + try: + job_id = str(uuid.uuid4()) + job_dir = TEMP_DIR / job_id + job_dir.mkdir() + + tts_audio_path = job_dir / "preview.mp3" + tts_vtt_path = job_dir / "preview.vtt" # Not used but needed for function sig + + if not request.text: + raise HTTPException(status_code=400, detail="Text required for preview") + + clean_text = clean_text_for_tts(request.text) + + # 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" + + await generate_tts_with_subs(clean_text, voice, tts_audio_path, tts_vtt_path) + + if not tts_audio_path.exists(): + raise Exception("TTS generation failed (file missing)") + + with open(tts_audio_path, "rb") as f: + audio_bytes = f.read() + audio_b64 = base64.b64encode(audio_bytes).decode("utf-8") + + shutil.rmtree(job_dir) + return {"success": True, "audio_base64": audio_b64} + + except Exception as e: + if "job_dir" in locals(): + shutil.rmtree(job_dir, ignore_errors=True) + return {"success": False, "detail": str(e)} + + if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/server/routes/reels.ts b/server/routes/reels.ts index b81e838..9be4704 100644 --- a/server/routes/reels.ts +++ b/server/routes/reels.ts @@ -237,6 +237,31 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => { } }); +/** + * Prévisualiser la voix TTS + * POST /api/reels/tts-preview + */ +reelsRouter.post('/reels/tts-preview', async (req: Request, res: Response) => { + try { + const { text, voice } = req.body; + + if (!text) { + return res.status(400).json({ error: 'Texte requis' }); + } + + const result = await ffmpegService.previewTTS(text, voice); + + if (!result.success) { + return res.status(500).json({ error: result.error || 'Erreur de génération TTS' }); + } + + res.json({ success: true, audioBase64: result.audioBase64 }); + } catch (error) { + console.error('❌ Error generating TTS preview:', error); + res.status(500).json({ error: 'Erreur lors de la génération de la voix' }); + } +}); + /** * Créer et publier un Reel * POST /api/reels diff --git a/server/services/ffmpeg.ts b/server/services/ffmpeg.ts index c423a25..77195d0 100644 --- a/server/services/ffmpeg.ts +++ b/server/services/ffmpeg.ts @@ -263,6 +263,46 @@ export class FFmpegService { return false; } } + async previewTTS( + text: string, + voice: string + ): Promise<{ success: boolean; audioBase64?: string; error?: string }> { + const config = this.ensureConfigured(); + + try { + const response = await fetch(`${config.apiUrl}/preview-tts`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': config.apiKey, + }, + body: JSON.stringify({ + text, + tts_voice: voice + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + return { success: false, error: `FFmpeg API error: ${response.status} - ${errorText}` }; + } + + const data = await response.json(); + + if (!data.success) { + return { success: false, error: data.detail }; + } + + return { success: true, audioBase64: data.audio_base64 }; + + } catch (error) { + console.error('❌ TTS Preview error:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + } } export const ffmpegService = new FFmpegService(); diff --git a/task.md b/task.md index dfbf8b2..07722cb 100644 --- a/task.md +++ b/task.md @@ -60,6 +60,19 @@ Ajout d'une fonctionnalité complète de création de Reels Facebook permettant - [x] Integrate CameraRecorder into `new-reel.tsx` - [x] Add quality settings panel in camera UI +### Phase 5: Reels Enhancements ⚡ + +- [x] **Enable Text Overlay by Default** in `new-reel.tsx` +- [x] **Expand French Voice Options** + - [x] Research available French voices (Edge-TTS) + - [x] Update frontend selection list + - [x] Update backend validation if needed +- [x] **Add Voice Test Button** (Preview TTS) + +### Phase 6: Workflow Initialization 🚫 + +- [~] Run `/bmad-bmgd-workflows-workflow-init` (Annulé: Application, pas un Jeu) + ## Progress Log - **22 Jan 2026** - Analyse complète et PRD créé