Files
Socialflow/ffmpeg-service/app/tts/qwen.py
T
Claude 44daa88a49 Voix : plus d'accélération sur les SRT, moteur local Qwen3-TTS
SRT : la voix n'est plus jamais accélérée (atempo sans plafond, jusqu'à 2×
et plus, rendait la voix incompréhensible). Un morceau plus long que son
sous-titre déborde et décale les suivants ; les sous-titres suivent la voix,
la vidéo s'allonge et un avertissement signale le décalage.

Rythme : style par défaut « neutre », consignes Gemini sans « rythme
entraînant », débit Edge des styles dynamique/promo ramené à +0 %/+3 %.

Nouveau moteur « qwen » : service qwen-tts (Qwen3-TTS sur CPU, gratuit,
français, ton piloté par consigne, clonage de voix depuis qwen-tts/voices).
Même vérification Whisper que Gemini, généralisée ; repli qwen → gemini →
edge, et une lecture sous 85 % du texte est écartée (mots sautés/faux).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Upu97wMmsRkBoj6iVM4rH6
2026-09-29 09:39:47 +00:00

57 lines
2.1 KiB
Python

"""Synthèse vocale locale Qwen3-TTS (service qwen-tts, sans clé ni quota)."""
import logging
from pathlib import Path
import httpx
from .. import config
from ..voices import qwen_instruction, resolve_qwen_voice
log = logging.getLogger(__name__)
class QwenError(RuntimeError):
pass
def _headers() -> dict[str, str]:
return {"x-api-key": config.QWEN_TTS_API_KEY} if config.QWEN_TTS_API_KEY else {}
async def synthesize(
text: str, voice: str | None, style: str | None, workdir: Path, attempt: int = 0
) -> tuple[Path, str]:
"""Génère la voix ; renvoie un WAV brut et la voix utilisée."""
if not config.QWEN_TTS_URL:
raise QwenError("service Qwen TTS non configuré (QWEN_TTS_URL)")
qwen_voice = resolve_qwen_voice(voice)
payload = {"text": text, "voice": qwen_voice, "instruct": qwen_instruction(style), "language": "French"}
log.info("Qwen TTS : voix=%s style=%s (%d car.)", qwen_voice, style, len(text))
try:
async with httpx.AsyncClient(timeout=config.QWEN_TTS_TIMEOUT) as client:
response = await client.post(
f"{config.QWEN_TTS_URL}/synthesize", json=payload, headers=_headers()
)
except httpx.HTTPError as error:
raise QwenError(f"service Qwen TTS injoignable : {error!r}") from error
if response.status_code != 200:
raise QwenError(f"Qwen TTS : HTTP {response.status_code} {response.text[:300]}")
raw = workdir / f"qwen_raw_{attempt}.wav"
raw.write_bytes(response.content)
return raw, response.headers.get("x-voice", qwen_voice)
async def list_voices() -> list[dict] | None:
"""Voix proposées par le service (clonées comprises) ; None s'il ne répond pas."""
if not config.QWEN_TTS_URL:
return None
try:
async with httpx.AsyncClient(timeout=5) as client:
response = await client.get(f"{config.QWEN_TTS_URL}/voices", headers=_headers())
response.raise_for_status()
return response.json()["voices"]
except Exception as error: # noqa: BLE001 — moteur simplement signalé indisponible
log.warning("Service Qwen TTS indisponible : %s", error)
return None