rtg compresser : une ligne par vidéo et pourcentage en direct ; bannière x265 masquée

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mf2kATMtqVABHSYdxJMnim
This commit is contained in:
FabriceandClaude Opus 5.5 committed 2026-10-09 13:36:38 +00:00
1 parent 96b1e53a2f
commit 608334b9d8
1 file changed
+68 -18
+68 -18
View File
@@ -10,6 +10,7 @@ interrompu ne laisse jamais un fichier abîmé sous le vrai nom.
"""
import os
import subprocess
import sys
import threading
import time
from concurrent.futures import ThreadPoolExecutor
@@ -42,7 +43,7 @@ def repartition(p):
return jobs, threads, max(2, threads // 2), coeurs
def _commande(src, dest, palier, p, threads, threads_sd, hauteur_src, stats):
def _commande(src, dest, palier, p, threads, threads_sd, hauteur_src):
hd = palier == "hd"
if hd:
vf = "scale=-2:1080:flags=lanczos" if hauteur_src > 1080 else None
@@ -54,7 +55,7 @@ def _commande(src, dest, palier, p, threads, threads_sd, hauteur_src, stats):
# pool de threads x265. hvc1 : sans lui, Safari refuse la vidéo.
vopts = ["-c:v", "libx265", "-preset", p["preset"], "-profile:v", "main",
"-crf", str(p["crf_hevc"]),
"-x265-params", f"keyint=120:min-keyint=24:aq-mode=3:pools={threads}",
"-x265-params", f"keyint=120:min-keyint=24:aq-mode=3:pools={threads}:log-level=error",
"-maxrate", "8M", "-bufsize", "16M", "-tag:v", "hvc1"]
audio = "96k"
else:
@@ -67,9 +68,9 @@ def _commande(src, dest, palier, p, threads, threads_sd, hauteur_src, stats):
"-crf", str(p["crf_h264"]), "-g", "120", "-keyint_min", "24",
"-threads", str(threads_sd), "-maxrate", "5M", "-bufsize", "10M"]
audio = "64k"
cmd = ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error"]
if stats:
cmd.append("-stats")
# -progress pipe:1 : avancement lisible par RTG (out_time_us=…) au lieu de
# la ligne -stats, illisible quand deux encodages tournent ensemble.
cmd = ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error", "-nostats", "-progress", "pipe:1"]
cmd += ["-i", src, "-map", "0:v:0", "-map", "0:a:0?"] + vopts
if vf:
cmd += ["-vf", vf]
@@ -89,6 +90,27 @@ def _hauteur(src):
return 1080
def _duree(src):
r = subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "csv=p=0", src], capture_output=True, text=True)
try:
return float(r.stdout.strip())
except ValueError:
return 0.0
def _suivre(proc, palier, duree, avancement):
"""Lit la sortie -progress de ffmpeg et met à jour avancement[palier] (0-100)."""
for ligne in proc.stdout:
if ligne.startswith(("out_time_us=", "out_time_ms=")) and duree > 0:
try:
avancement[palier] = min(100.0, int(ligne.split("=")[1]) / 1e6 / duree * 100)
except ValueError:
pass
elif ligne.startswith("progress=end"):
avancement[palier] = 100.0
def sorties(voyage, src):
base = os.path.splitext(os.path.basename(src))[0]
dossier = os.path.join(voyage, "videos")
@@ -104,8 +126,8 @@ def deja_faite(voyage, src, rapide=False):
def compresser(voyage, cfg, analyse, force=False, progression=None):
"""Compresse les rushs du voyage. `progression(nom, etat)` est appelée à
chaque changement (pour l'interface). Renvoie True si tout a réussi."""
"""Compresse les rushs du voyage. `progression(nom, etat, pourcentage)` est
appelée à chaque changement (pour l'interface). Renvoie True si tout a réussi."""
videos = analyse.videos
os.makedirs(os.path.join(voyage, "videos"), exist_ok=True)
p = parametres_video(cfg)
@@ -126,27 +148,55 @@ def compresser(voyage, cfg, analyse, force=False, progression=None):
verrou = threading.Lock()
bilan = {"faits": 0, "sautes": 0, "echecs": 0, "octets_src": 0, "octets_web": 0}
def signaler(nom, etat):
def signaler(nom, etat, detail=None):
if progression:
progression(nom, etat)
progression(nom, etat, detail)
def un_rush(src):
total = len(videos)
en_direct = jobs == 1 and sys.stdout.isatty()
def un_rush(arg):
i, src = arg
nom = os.path.basename(src)
rang = f"[{i:3d}/{total}]"
hd, sd = sorties(voyage, src)
if not force and lisible(hd) and lisible(sd):
with verrou:
bilan["sautes"] += 1
dire(f"⏭️ {rang} {nom} — déjà faite")
signaler(nom, "deja_faite")
return True
h = _hauteur(src)
dire(f"🎬 {nom} ({h}p) — encodage des 2 paliers en parallèle…")
signaler(nom, "en_cours")
h, duree = _hauteur(src), _duree(src)
dire(f"🎬 {rang} {nom} ({h}p, {int(duree) // 60} min {int(duree) % 60:02d} s) — encodage…")
signaler(nom, "en_cours", 0)
tmp_hd = os.path.join(os.path.dirname(hd), ".tmp-" + os.path.basename(hd))
tmp_sd = os.path.join(os.path.dirname(sd), ".tmp-" + os.path.basename(sd))
stats = jobs == 1
procs = [subprocess.Popen(_commande(src, tmp_hd, "hd", p, threads, threads_sd, h, stats)),
subprocess.Popen(_commande(src, tmp_sd, "sd", p, threads, threads_sd, h, stats))]
codes = [pr.wait() for pr in procs]
avancement = {"hd": 0.0, "sd": 0.0}
procs, lecteurs = [], []
for palier, tmp in (("hd", tmp_hd), ("sd", tmp_sd)):
pr = subprocess.Popen(_commande(src, tmp, palier, p, threads, threads_sd, h),
stdout=subprocess.PIPE, text=True)
th = threading.Thread(target=_suivre, args=(pr, palier, duree, avancement), daemon=True)
th.start()
procs.append(pr)
lecteurs.append(th)
dernier = -1
while any(pr.poll() is None for pr in procs):
time.sleep(0.5)
moyenne = int((avancement["hd"] + avancement["sd"]) / 2)
if en_direct:
print(f"\r 1080p {avancement['hd']:3.0f} % · 720p {avancement['sd']:3.0f} % ",
end="", flush=True)
elif moyenne // 25 > dernier // 25 and moyenne < 100:
dire(f" {nom} : {moyenne} %")
if moyenne != dernier:
signaler(nom, "en_cours", moyenne)
dernier = moyenne
if en_direct:
print()
for th in lecteurs:
th.join(timeout=5)
codes = [pr.returncode for pr in procs]
if codes == [0, 0] and lisible(tmp_hd) and lisible(tmp_sd):
os.replace(tmp_hd, hd)
os.replace(tmp_sd, sd)
@@ -174,7 +224,7 @@ def compresser(voyage, cfg, analyse, force=False, progression=None):
t0 = time.time()
with ThreadPoolExecutor(jobs) as pool:
resultats = list(pool.map(un_rush, videos))
resultats = list(pool.map(un_rush, enumerate(videos, 1)))
duree = int(time.time() - t0)
dire()
if bilan["faits"]: