mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-22 06:28:10 +02:00
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
57 lines
2.1 KiB
Python
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
|