mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +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
59 lines
2.1 KiB
Python
59 lines
2.1 KiB
Python
"""Synthèse vocale Edge (gratuite, fournit le minutage des mots)."""
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
import edge_tts
|
|
|
|
from .. import config
|
|
from ..align import Word
|
|
from ..voices import edge_fallbacks, resolve_edge_voice
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
# Réglages de lecture par style (Edge n'interprète pas de consigne en texte)
|
|
STYLE_PROSODY = {
|
|
"dynamic": ("+8%", "+2Hz"),
|
|
"promo": ("+10%", "+3Hz"),
|
|
"calm": ("-8%", "-2Hz"),
|
|
"warm": ("-2%", "+0Hz"),
|
|
}
|
|
|
|
|
|
async def synthesize(
|
|
text: str, voice: str | None, style: str | None, workdir: Path
|
|
) -> tuple[Path, list[Word], str]:
|
|
"""Génère la voix ; renvoie le MP3, les mots minutés et la voix utilisée."""
|
|
rate, pitch = STYLE_PROSODY.get(style or "", ("+0%", "+0Hz"))
|
|
last_error: Exception | None = None
|
|
|
|
for candidate in edge_fallbacks(resolve_edge_voice(voice)):
|
|
target = workdir / "edge.mp3"
|
|
try:
|
|
communicate = edge_tts.Communicate(
|
|
text,
|
|
candidate.id,
|
|
rate=rate,
|
|
pitch=pitch,
|
|
# edge-tts 7 ne renvoie plus que les phrases par défaut
|
|
boundary="WordBoundary",
|
|
proxy=config.OUTBOUND_PROXY,
|
|
)
|
|
words: list[Word] = []
|
|
with target.open("wb") as out:
|
|
async for chunk in communicate.stream():
|
|
if chunk["type"] == "audio":
|
|
out.write(chunk["data"])
|
|
elif chunk["type"] == "WordBoundary":
|
|
start = chunk["offset"] / 10_000_000
|
|
words.append(Word(chunk["text"], start, start + chunk["duration"] / 10_000_000))
|
|
if target.stat().st_size == 0:
|
|
raise RuntimeError("audio vide")
|
|
log.info("Edge TTS : voix=%s, %d mots minutés", candidate.id, len(words))
|
|
return target, words, candidate.id
|
|
except Exception as error: # noqa: BLE001 — on essaie la voix suivante
|
|
log.warning("Edge TTS %s en échec : %s", candidate.id, error)
|
|
last_error = error
|
|
|
|
raise RuntimeError(f"Aucune voix Edge disponible : {last_error}")
|