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
132 lines
4.9 KiB
Python
132 lines
4.9 KiB
Python
"""Catalogue des voix et des styles de lecture."""
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Voice:
|
|
id: str
|
|
label: str
|
|
gender: str # "female" | "male"
|
|
|
|
|
|
# Voix natives de Gemini TTS (toutes multilingues, françaises comprises).
|
|
GEMINI_VOICES: tuple[Voice, ...] = (
|
|
Voice("Kore", "Kore — ferme", "female"),
|
|
Voice("Aoede", "Aoede — légère", "female"),
|
|
Voice("Leda", "Leda — jeune", "female"),
|
|
Voice("Zephyr", "Zephyr — lumineuse", "female"),
|
|
Voice("Callirrhoe", "Callirrhoe — décontractée", "female"),
|
|
Voice("Autonoe", "Autonoe — lumineuse", "female"),
|
|
Voice("Despina", "Despina — douce", "female"),
|
|
Voice("Erinome", "Erinome — claire", "female"),
|
|
Voice("Laomedeia", "Laomedeia — enjouée", "female"),
|
|
Voice("Achernar", "Achernar — tendre", "female"),
|
|
Voice("Gacrux", "Gacrux — mûre", "female"),
|
|
Voice("Pulcherrima", "Pulcherrima — affirmée", "female"),
|
|
Voice("Vindemiatrix", "Vindemiatrix — délicate", "female"),
|
|
Voice("Sulafat", "Sulafat — chaleureuse", "female"),
|
|
Voice("Charon", "Charon — informative", "male"),
|
|
Voice("Puck", "Puck — enjouée", "male"),
|
|
Voice("Fenrir", "Fenrir — enthousiaste", "male"),
|
|
Voice("Orus", "Orus — ferme", "male"),
|
|
Voice("Enceladus", "Enceladus — soufflée", "male"),
|
|
Voice("Iapetus", "Iapetus — claire", "male"),
|
|
Voice("Umbriel", "Umbriel — décontractée", "male"),
|
|
Voice("Algieba", "Algieba — veloutée", "male"),
|
|
Voice("Algenib", "Algenib — rocailleuse", "male"),
|
|
Voice("Rasalgethi", "Rasalgethi — informative", "male"),
|
|
Voice("Alnilam", "Alnilam — ferme", "male"),
|
|
Voice("Schedar", "Schedar — posée", "male"),
|
|
Voice("Achird", "Achird — amicale", "male"),
|
|
Voice("Zubenelgenubi", "Zubenelgenubi — naturelle", "male"),
|
|
Voice("Sadachbia", "Sadachbia — vive", "male"),
|
|
Voice("Sadaltager", "Sadaltager — experte", "male"),
|
|
)
|
|
|
|
EDGE_VOICES: tuple[Voice, ...] = (
|
|
Voice("fr-FR-VivienneMultilingualNeural", "Vivienne", "female"),
|
|
Voice("fr-FR-DeniseNeural", "Denise", "female"),
|
|
Voice("fr-FR-EloiseNeural", "Eloise", "female"),
|
|
Voice("fr-FR-RemyMultilingualNeural", "Rémy", "male"),
|
|
Voice("fr-FR-HenriNeural", "Henri", "male"),
|
|
)
|
|
|
|
# Consignes de lecture ajoutées au texte envoyé à Gemini.
|
|
STYLES: dict[str, tuple[str, str]] = {
|
|
"neutral": ("Neutre", ""),
|
|
"dynamic": (
|
|
"Dynamique",
|
|
"Lis ce texte en français sur un ton dynamique et enthousiaste, avec un rythme "
|
|
"entraînant, comme une vidéo courte sur les réseaux sociaux",
|
|
),
|
|
"warm": (
|
|
"Chaleureux",
|
|
"Lis ce texte en français sur un ton chaleureux, souriant et proche, comme si tu conseillais un ami",
|
|
),
|
|
"calm": (
|
|
"Calme",
|
|
"Lis ce texte en français sur un ton calme, posé et rassurant, sans te presser",
|
|
),
|
|
"promo": (
|
|
"Promo",
|
|
"Lis ce texte en français comme une annonce promotionnelle énergique, en "
|
|
"insistant sur les offres et les prix",
|
|
),
|
|
}
|
|
|
|
_GEMINI_BY_ID = {v.id.lower(): v for v in GEMINI_VOICES}
|
|
_EDGE_BY_ID = {v.id: v for v in EDGE_VOICES}
|
|
|
|
|
|
def resolve_gemini_voice(voice: str | None) -> Voice:
|
|
"""Voix Gemini à utiliser.
|
|
|
|
Accepte aussi les anciens identifiants « fr-FR-Standard-X » encore stockés
|
|
dans des jobs (B/D = voix masculine).
|
|
"""
|
|
if voice and voice.lower() in _GEMINI_BY_ID:
|
|
return _GEMINI_BY_ID[voice.lower()]
|
|
if voice and voice.endswith(("-B", "-D")) or voice == "male":
|
|
return _GEMINI_BY_ID["charon"]
|
|
return _GEMINI_BY_ID["kore"]
|
|
|
|
|
|
def voice_gender(voice: str | None) -> str:
|
|
"""Genre d'une voix, quel que soit le moteur d'origine."""
|
|
if not voice:
|
|
return "female"
|
|
if voice.lower() in _GEMINI_BY_ID:
|
|
return _GEMINI_BY_ID[voice.lower()].gender
|
|
if voice in _EDGE_BY_ID:
|
|
return _EDGE_BY_ID[voice].gender
|
|
if voice == "male" or voice.endswith(("-B", "-D")):
|
|
return "male"
|
|
return "male" if any(name in voice for name in ("Remy", "Henri", "Paul")) else "female"
|
|
|
|
|
|
def resolve_edge_voice(voice: str | None) -> Voice:
|
|
"""Voix Edge à utiliser ; une voix Gemini est remplacée par une voix Edge du même genre."""
|
|
if voice in _EDGE_BY_ID:
|
|
return _EDGE_BY_ID[voice]
|
|
gender = voice_gender(voice)
|
|
return next(v for v in EDGE_VOICES if v.gender == gender)
|
|
|
|
|
|
def edge_fallbacks(primary: Voice) -> list[Voice]:
|
|
"""Voix françaises de secours du même genre (jamais d'anglais)."""
|
|
same = [v for v in EDGE_VOICES if v.gender == primary.gender and v != primary]
|
|
return [primary, *same]
|
|
|
|
|
|
def style_instruction(style: str | None) -> str:
|
|
return STYLES.get(style or "neutral", STYLES["neutral"])[1]
|
|
|
|
|
|
def catalog() -> dict:
|
|
return {
|
|
"gemini": [v.__dict__ for v in GEMINI_VOICES],
|
|
"edge": [v.__dict__ for v in EDGE_VOICES],
|
|
"styles": [{"id": k, "label": v[0]} for k, v in STYLES.items()],
|
|
}
|