mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-12 01:36:54 +02:00
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
64 lines
2.1 KiB
Python
64 lines
2.1 KiB
Python
"""Synthèse vocale : moteur au choix, repli sur Edge, voix traitée et mots minutés."""
|
|
|
|
import logging
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
from .. import align, audio, proc
|
|
from ..align import Word
|
|
from . import edge, gemini
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class VoiceTrack:
|
|
path: Path # WAV 48 kHz traité
|
|
duration: float
|
|
words: list[Word] # mots affichés, minutés depuis le début de la voix
|
|
engine: str
|
|
voice: str
|
|
warnings: list[str] = field(default_factory=list)
|
|
|
|
|
|
async def synthesize(
|
|
*,
|
|
text: str,
|
|
display_text: str,
|
|
engine: str | None,
|
|
voice: str | None,
|
|
style: str | None,
|
|
gemini_api_key: str | None,
|
|
workdir: Path,
|
|
) -> VoiceTrack:
|
|
warnings: list[str] = []
|
|
raw: Path | None = None
|
|
spoken: list[Word] = []
|
|
used_engine, used_voice = "edge", ""
|
|
|
|
if (engine or "gemini") == "gemini":
|
|
if not gemini_api_key:
|
|
warnings.append("Clé Gemini absente : voix Edge utilisée à la place.")
|
|
else:
|
|
try:
|
|
raw, used_voice = await gemini.synthesize(text, voice, style, gemini_api_key, workdir)
|
|
used_engine = "gemini"
|
|
except Exception as error: # noqa: BLE001 — repli sur Edge
|
|
log.warning("Gemini TTS en échec, repli sur Edge : %s", error)
|
|
warnings.append(f"Gemini indisponible ({error}) : voix Edge utilisée à la place.")
|
|
|
|
if raw is None:
|
|
raw, spoken, used_voice = await edge.synthesize(text, voice, style, workdir)
|
|
|
|
processed = workdir / "voice.wav"
|
|
await audio.process_voice(raw, processed)
|
|
duration = (await proc.probe(processed)).duration
|
|
|
|
if not spoken:
|
|
# Gemini ne donne pas le minutage : Whisper le retrouve dans l'audio
|
|
spoken = await align.transcribe(processed, hint=text)
|
|
|
|
words = align.align_words(display_text, spoken, duration)
|
|
log.info("Voix prête : %s/%s, %.1f s, %d mots", used_engine, used_voice, duration, len(words))
|
|
return VoiceTrack(processed, duration, words, used_engine, used_voice, warnings)
|