mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-12 01:36:54 +02:00
Intonation : - Gemini reçoit une température basse et une graine fixe (réglables) : la lecture ne change plus de ton d'une génération à l'autre - SRT : tout le texte est lu d'une seule traite puis découpé phrase par phrase, au lieu d'une génération (et d'une intonation) par sous-titre ; un seul traitement de niveau, plus de sauts de volume entre les phrases Mots coupés : - La voix est découpée dans ses vrais silences (silencedetect), plus aux bornes des mots estimées par Whisper qui rognaient les syllabes ; fondus de 10 ms aux coupes - Whisper n'est plus amorcé avec le texte attendu : il hallucinait des répétitions qui masquaient les fins tronquées Fiabilité : - Chaque voix Gemini est réécoutée : mots sautés, fin tronquée ou consigne lue à voix haute déclenchent une nouvelle génération (3 au plus), puis repli sur Edge avec avertissement - Consigne de lecture intégrale ajoutée au prompt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
80 lines
2.6 KiB
Python
80 lines
2.6 KiB
Python
"""Exécution asynchrone de FFmpeg/ffprobe : le serveur reste disponible
|
|
pendant un encodage (l'ancien subprocess.run bloquait toute l'API)."""
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class CommandError(RuntimeError):
|
|
"""Commande externe en échec, avec la fin de sa sortie d'erreur."""
|
|
|
|
def __init__(self, cmd: list[str], returncode: int, stderr: str):
|
|
tail = "\n".join(stderr.strip().splitlines()[-15:])
|
|
super().__init__(f"{cmd[0]} a échoué (code {returncode}) :\n{tail}")
|
|
self.returncode = returncode
|
|
self.stderr = stderr
|
|
|
|
|
|
async def run(cmd: list[str], timeout: float = 900, *, stderr_output: bool = False) -> str:
|
|
"""Lance une commande et renvoie sa sortie standard (ou d'erreur, où FFmpeg
|
|
écrit les mesures de ses filtres d'analyse)."""
|
|
log.debug("exec: %s", " ".join(cmd))
|
|
process = await asyncio.create_subprocess_exec(
|
|
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
|
)
|
|
try:
|
|
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout)
|
|
except TimeoutError as error:
|
|
process.kill()
|
|
await process.wait()
|
|
raise CommandError(cmd, -1, f"délai de {timeout:.0f} s dépassé") from error
|
|
if process.returncode != 0:
|
|
raise CommandError(cmd, process.returncode, stderr.decode(errors="replace"))
|
|
return (stderr if stderr_output else stdout).decode(errors="replace")
|
|
|
|
|
|
@dataclass
|
|
class MediaInfo:
|
|
duration: float
|
|
has_audio: bool
|
|
width: int = 0
|
|
height: int = 0
|
|
color_transfer: str = ""
|
|
|
|
@property
|
|
def is_hdr(self) -> bool:
|
|
# HLG (iPhone) ou PQ (HDR10)
|
|
return self.color_transfer in ("arib-std-b67", "smpte2084")
|
|
|
|
|
|
async def probe(path: Path) -> MediaInfo:
|
|
out = await run(
|
|
[
|
|
"ffprobe",
|
|
"-v",
|
|
"error",
|
|
"-print_format",
|
|
"json",
|
|
"-show_format",
|
|
"-show_streams",
|
|
str(path),
|
|
],
|
|
timeout=60,
|
|
)
|
|
data = json.loads(out)
|
|
streams = data.get("streams", [])
|
|
video = next((s for s in streams if s.get("codec_type") == "video"), {})
|
|
duration = float(data.get("format", {}).get("duration") or video.get("duration") or 0)
|
|
return MediaInfo(
|
|
duration=duration,
|
|
has_audio=any(s.get("codec_type") == "audio" for s in streams),
|
|
width=int(video.get("width") or 0),
|
|
height=int(video.get("height") or 0),
|
|
color_transfer=video.get("color_transfer") or "",
|
|
)
|