Fins de phrases coupées :
- Le fondu final ne commence plus avant la fin de la voix (il rognait la
dernière phrase quand la vidéo finissait peu après) ; il se raccourcit au
besoin. Même règle côté Python, aperçu et rendu Remotion
- SRT : découpe au milieu de la zone la plus calme de chaque pause, mesurée
sur l'enveloppe sonore (tranches de 10 ms), avec un seuil relatif au
niveau de la voix qui respecte les fins douces ; 300 ms gardées après le
dernier son
Bruits parasites :
- Normalisation de la voix en deux passes, en mode linéaire : le mode
dynamique relevait souffles et bruits entre les phrases (mesuré : -64 dB
avant, -75 dB après dans les pauses)
- Réduction de bruit douce (afftdn), compresseur sans gain ajouté, fondus
de 5 ms aux extrémités ; mix final moins « pompant » (LRA 20)
- Voix Gemini vérifiée : les bruits ajoutés avant le premier ou après le
dernier mot sont retirés
Vérification de lecture comparée lettre à lettre : les petites erreurs de
Whisper ne déclenchent plus de régénérations inutiles.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
Intonation :
- Gemini reçoit une température basse et une graine fixe (réglables) : la
lecture ne change plus de ton d'une génération à l'autre
- SRT : tout le texte est lu d'une seule traite puis découpé phrase par
phrase, au lieu d'une génération (et d'une intonation) par sous-titre ;
un seul traitement de niveau, plus de sauts de volume entre les phrases
Mots coupés :
- La voix est découpée dans ses vrais silences (silencedetect), plus aux
bornes des mots estimées par Whisper qui rognaient les syllabes ; fondus
de 10 ms aux coupes
- Whisper n'est plus amorcé avec le texte attendu : il hallucinait des
répétitions qui masquaient les fins tronquées
Fiabilité :
- Chaque voix Gemini est réécoutée : mots sautés, fin tronquée ou consigne
lue à voix haute déclenchent une nouvelle génération (3 au plus), puis
repli sur Edge avec avertissement
- Consigne de lecture intégrale ajoutée au prompt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
Sans musique (bouton « Passer »), la bande son de la vidéo était supprimée
dès que la voix était activée. Elle sert désormais de fond sonore : conservée
telle quelle, baissée automatiquement sous la voix, puis normalisée avec le
reste. Une musique choisie la remplace toujours.
Même comportement dans l'aperçu, le rendu Remotion et le rendu FFmpeg de
secours. Le bouton indique « Passer (garder le son de la vidéo) ».
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
Le petit logo affiché pendant la vidéo devient optionnel (activé par
défaut), sur les 4 pages Reel (vidéo et images, bureau et mobile) ainsi que
dans l'aperçu. Le grand logo de l'effet de fin reste piloté par son propre
interrupteur. Pris en charge par le rendu Remotion et par le rendu FFmpeg de
secours (show_watermark).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
- Nouveau Reel (bureau et mobile) : bouton « Importer un fichier SRT ».
Tant qu'un SRT est chargé, le texte libre et l'assistant IA sont
désactivés ; le choix de la voix (moteur, voix, ton) reste disponible.
- Service Python : chaque sous-titre est lu séparément, ses silences
retirés, puis posé à son instant de début. Une lecture trop longue est
accélérée (atempo, sans changer la hauteur) pour tenir dans la durée du
sous-titre, avec un avertissement au-delà de ×1,35. Après un repli sur
Edge, tous les sous-titres gardent la même voix.
- Sans voix, les mots de chaque sous-titre s'affichent sur son intervalle.
- Aperçu : sous-titres calés sur le minutage du fichier.
- Tests : lecture SRT (TS), fenêtres, accélération et mixage (Python).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WVqEw4Xycgf8BtwmSftE2M
Montage (étape 3) :
- Nouvelle composition ReelVideo : sous-titres animés mot à mot (3 styles :
Impact, Surligné, Épuré), logo, effet de fin et fondu, en React
- Le service Python prépare l'image (recadrage, HDR, stabilisation, dernière
image figée) et la piste son finale (/prepare-reel) ; Remotion compose
- Reels d'images sur les mêmes composants, avec le vrai minutage de la voix
- Police Montserrat embarquée (plus de dépendance à Google Fonts)
- REEL_RENDERER=ffmpeg conserve le rendu FFmpeg, plus rapide, en secours
- Script de pré-bundle réparé (échouait en silence : require en ESM)
Interface (étape 4) :
- Aperçu en direct avec @remotion/player, identique au rendu final ; la voix
testée cale les sous-titres, sinon minutage estimé
- Choix du style de sous-titres sur les 4 pages Reel
- Vraie progression : étape réelle du rendu, échecs visibles 24 h avec leur
cause ; fin de la barre simulée et du faux « publié avec succès »
Outillage (étape 5) :
- Tests vitest (minutage identique à Python, validation, sécurité) et
pytest ; CI GitHub Actions (tsc, tests, build, ruff)
- Captures d'écran, out.mp4 et scripts de test retirés de la racine
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
Service ffmpeg-api réécrit (ffmpeg-service/app/) :
- Appels FFmpeg asynchrones : le service ne se fige plus pendant un rendu
- Vidéo récupérée par téléchargement (GET /files/…) au lieu de base64 en JSON
- Vraies erreurs HTTP ; échec explicite si la voix demandée est impossible
- 30 voix Gemini + ton de lecture (dynamique, chaleureux, promo, calme),
clé en en-tête, modèle configurable avec repli
- Edge TTS 7 (minutage des mots restauré), secours en voix françaises
- Voix traitée : filtre, compression, niveau constant ; musique bouclée et
baissée automatiquement sous la voix ; mix final à -14 LUFS
- Sous-titres calés mot à mot (Whisper pour Gemini et la voix d'origine,
à la place de ffsubsync), style Montserrat, placés hors des boutons Reels
- Vidéo : plus de retouche luminosité forcée, scaling lanczos, HDR iPhone
converti, BT.709, AAC 48 kHz 192k ; la vidéo s'allonge si la voix dépasse
- Grand logo de fin affiché après la voix ; FFmpeg 7.0.2 épinglé, polices et
modèle Whisper intégrés à l'image ; tests pytest et ruff
Application :
- Sélecteur de voix partagé (4 pages) : moteur, 30 voix, ton, écoute
- Reel images : minutage réel des mots, interrupteur voix respecté
- sync-info ne génère plus de voix à chaque frappe (estimation locale)
- Stabilisation désactivée par défaut, route /reels/preview inutilisée retirée
- Log « [ReelQueue] Worker démarré » pour vérifier la version déployée
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
L'ancienne file comptait les posts « processing » : un redémarrage pendant
un rendu la bloquait définitivement. Les rendus passent désormais par une
table reel_jobs, réservée avec FOR UPDATE SKIP LOCKED, avec heartbeat et
reprise des jobs interrompus au démarrage (2 tentatives maximum).
- Pipeline vidéo et pipeline images (Remotion) extraits des routes vers
server/services/reels/, les routes ne font que valider (zod) et mettre en file
- Rendus d'images suivis en base au lieu d'une Map en mémoire
- storeName conservé pour les jobs mis en attente
- Publication Facebook en binaire : l'URL relative /uploads/... transmise
auparavant n'était pas téléchargeable par Facebook
- Job en échec si toutes les pages échouent ou si la voix demandée manque
(le service Python renseigne enfin tts_error)
- La voix choisie sur la page images est réellement utilisée
- sync-info mesure la voix Gemini avec sa clé
- Timeouts sur les appels au service FFmpeg, vignettes via execFile,
/debug-ffmpeg protégé par la clé API, plus de voix anglaise en secours
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
Default tts_engine changed from "edge" to "gemini".
Both /process-reel and /preview-tts now try Gemini first,
fall back to Edge TTS on failure.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Gemini returns raw PCM (audio/L16;codec=pcm;rate=24000), not a real WAV
file with a header, so FFmpeg fails to auto-detect the format. Pass
-f s16le, -ar (from mime type), and -ac 1 explicitly so FFmpeg can
decode the byte stream. Also surface FFmpeg stderr on failure instead
of swallowing it.
https://claude.ai/code/session_01QBvwHAMZzVYfWvy1U5paat
Replace the texttospeech.googleapis.com call (which requires a separate
GCP project with Cloud TTS enabled and billing) with the Gemini native
TTS endpoint (gemini-2.5-flash-preview-tts). This uses the same Gemini
API key already configured in the app, with no extra GCP setup needed.
Voice mapping: fr-FR-Standard-A/C -> Kore (female), B/D -> Charon (male).
Audio is returned as WAV and converted to MP3 via FFmpeg. Subtitle sync
uses ffsubsync as Gemini TTS does not return word boundaries.
https://claude.ai/code/session_01QBvwHAMZzVYfWvy1U5paat
Replace \uXXXX surrogate-pair escapes with the actual emoji characters
in print() calls. Lone surrogates are invalid in UTF-8, causing
UnicodeEncodeError on stdout encode and aborting TTS generation before
the Gemini API call.
https://claude.ai/code/session_01QBvwHAMZzVYfWvy1U5paat
- Add response structure logging to see actual keys returned by Gemini API
- Log timepoints count and first item to diagnose word boundary parsing
- Add 'unexpected timepoint format' warning for easier debugging
- Convert emoji to ASCII-safe versions for container logs
- Edge TTS fallback list now includes fr-FR-RemyMultilingualNeural
- Removed 'Neural not in voice' check that rejected Gemini voices (fr-FR-Standard-A etc) before falling back - these are valid voices and Edge-tts handles them properly
- Improved is_male check to match 'Remy' not 'Remi'
- Store Gemini API key globally in appConfig (single key for all users)
- Add /api/settings/gemini GET/POST/DELETE routes for API key management
- Backend: add tts_engine parameter ("edge" or "gemini") to FFmpeg service
- Backend: add generate_tts_gemini() using Google Cloud TTS REST API
- Frontend: Settings page shows Google Gemini API key input card
- Frontend: new-reel, mobile/new-reel, remotion-video, mobile/remotion-video
pages now have Edge/Gemini engine toggle and French voice selector
- Fix tts-preview route to extract ttsVoice from req.body instead of
undefined voice variable
- Remove piper_url from ReelRequest model and all function signatures
- Remove generate_tts_piper() function and Piper branch in generate_tts_with_subs()
- Remove /api/piper/config routes from server/routes.ts
- Remove piperConfig table, schemas and storage methods
- Remove piper_config migration
- Remove Piper TTS settings UI card
- Update "Piper TTS" labels to "TTS — voix activée"
edge_tts is now the only TTS engine, using precise word-boundary timing
Remove Minimax Speech API and Freesound integrations entirely.
Add Piper TTS as the sole TTS provider via configurable HTTP URL.
- Add piper_config DB table (url field, per-user)
- Add GET/POST /api/piper/config routes
- Python: replace generate_tts_minimax with generate_tts_piper (GET ?text=, WAV→MP3)
- Simplify generate_tts_with_subs: piper_url param replaces tts_provider+minimax fields
- Drop ttsVoice/ttsProvider from all UI, API, and background job params
- Settings page: replace Minimax+Freesound cards with single Piper URL input
- Remove freeSoundService init from server startup and CSP headers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass GroupId query param to Minimax T2A v2 API — required for paid
plan quota allocation. Without it, Minimax defaults to (0/0 used).
- shared/schema.ts: groupId field on minimaxConfig table
- server/migrate.ts: ADD COLUMN IF NOT EXISTS group_id
- server/routes/reels.ts: fetch and pass groupId alongside apiKey
- server/services/ffmpeg.ts: minimax_group_id in request interface/body
- ffmpeg-service/main.py: GroupId in URL, threaded through all call sites
- client/src/pages/settings.tsx: Group ID input in Minimax settings card
- Python: capture tts_error_msg on exception, return in response
- Python: log tts_provider, minimax_api_key presence before call
- ffmpeg.ts: read tts_error from response, log and return it
- reels.ts: store TTS error in post.generationError for visibility
- Add minimax_config table (migration + schema + storage CRUD)
- Add GET/POST /api/minimax/config routes
- Pass tts_provider + minimax_api_key through ffmpeg service
- Add generate_tts_minimax() in Python using Minimax T2A v2 API
- Fall back to ffsubsync for subtitle sync (no WordBoundary events)
- Add Minimax config card in Settings page
- Add provider toggle (Edge TTS / Minimax) + French voices in new-reel
Some voices/texts do not produce WordBoundary events from edge_tts.
When word_boundaries is empty, generate_ass_from_word_boundaries returns
without writing the ASS file, causing FFmpeg to fail with ENOENT.
Fallback to the old ffsubsync path when no boundaries are captured.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ffmpeg-service/main.py: replace ffsubsync path with exact word-boundary timing from edge_tts for TTS subtitles. Karaoke styling preserved.
- server/services/ttsSync.ts: fix word count to match TTS-cleaned text, remove artificial punctuationPause subtraction, strip punctuation tokens from count.
- server/routes/reels.ts: remove redundant ttsSyncService calls in preview/background; word_duration is ignored by Python, these only wasted TTS generations.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>