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 os
import subprocess import subprocess
import sys
import threading import threading
import time import time
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
@@ -42,7 +43,7 @@ def repartition(p):
return jobs, threads, max(2, threads // 2), coeurs 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" hd = palier == "hd"
if hd: if hd:
vf = "scale=-2:1080:flags=lanczos" if hauteur_src > 1080 else None 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. # pool de threads x265. hvc1 : sans lui, Safari refuse la vidéo.
vopts = ["-c:v", "libx265", "-preset", p["preset"], "-profile:v", "main", vopts = ["-c:v", "libx265", "-preset", p["preset"], "-profile:v", "main",
"-crf", str(p["crf_hevc"]), "-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"] "-maxrate", "8M", "-bufsize", "16M", "-tag:v", "hvc1"]
audio = "96k" audio = "96k"
else: 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", "-crf", str(p["crf_h264"]), "-g", "120", "-keyint_min", "24",
"-threads", str(threads_sd), "-maxrate", "5M", "-bufsize", "10M"] "-threads", str(threads_sd), "-maxrate", "5M", "-bufsize", "10M"]
audio = "64k" audio = "64k"
cmd = ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error"] # -progress pipe:1 : avancement lisible par RTG (out_time_us=…) au lieu de
if stats: # la ligne -stats, illisible quand deux encodages tournent ensemble.
cmd.append("-stats") cmd = ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error", "-nostats", "-progress", "pipe:1"]
cmd += ["-i", src, "-map", "0:v:0", "-map", "0:a:0?"] + vopts cmd += ["-i", src, "-map", "0:v:0", "-map", "0:a:0?"] + vopts
if vf: if vf:
cmd += ["-vf", vf] cmd += ["-vf", vf]
@@ -89,6 +90,27 @@ def _hauteur(src):
return 1080 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): def sorties(voyage, src):
base = os.path.splitext(os.path.basename(src))[0] base = os.path.splitext(os.path.basename(src))[0]
dossier = os.path.join(voyage, "videos") 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): def compresser(voyage, cfg, analyse, force=False, progression=None):
"""Compresse les rushs du voyage. `progression(nom, etat)` est appelée à """Compresse les rushs du voyage. `progression(nom, etat, pourcentage)` est
chaque changement (pour l'interface). Renvoie True si tout a réussi.""" appelée à chaque changement (pour l'interface). Renvoie True si tout a réussi."""
videos = analyse.videos videos = analyse.videos
os.makedirs(os.path.join(voyage, "videos"), exist_ok=True) os.makedirs(os.path.join(voyage, "videos"), exist_ok=True)
p = parametres_video(cfg) p = parametres_video(cfg)
@@ -126,27 +148,55 @@ def compresser(voyage, cfg, analyse, force=False, progression=None):
verrou = threading.Lock() verrou = threading.Lock()
bilan = {"faits": 0, "sautes": 0, "echecs": 0, "octets_src": 0, "octets_web": 0} 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: 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) nom = os.path.basename(src)
rang = f"[{i:3d}/{total}]"
hd, sd = sorties(voyage, src) hd, sd = sorties(voyage, src)
if not force and lisible(hd) and lisible(sd): if not force and lisible(hd) and lisible(sd):
with verrou: with verrou:
bilan["sautes"] += 1 bilan["sautes"] += 1
dire(f"⏭️ {rang} {nom} — déjà faite")
signaler(nom, "deja_faite") signaler(nom, "deja_faite")
return True return True
h = _hauteur(src) h, duree = _hauteur(src), _duree(src)
dire(f"🎬 {nom} ({h}p) — encodage des 2 paliers en parallèle…") dire(f"🎬 {rang} {nom} ({h}p, {int(duree) // 60} min {int(duree) % 60:02d} s) — encodage…")
signaler(nom, "en_cours") signaler(nom, "en_cours", 0)
tmp_hd = os.path.join(os.path.dirname(hd), ".tmp-" + os.path.basename(hd)) 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)) tmp_sd = os.path.join(os.path.dirname(sd), ".tmp-" + os.path.basename(sd))
stats = jobs == 1 avancement = {"hd": 0.0, "sd": 0.0}
procs = [subprocess.Popen(_commande(src, tmp_hd, "hd", p, threads, threads_sd, h, stats)), procs, lecteurs = [], []
subprocess.Popen(_commande(src, tmp_sd, "sd", p, threads, threads_sd, h, stats))] for palier, tmp in (("hd", tmp_hd), ("sd", tmp_sd)):
codes = [pr.wait() for pr in procs] 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): if codes == [0, 0] and lisible(tmp_hd) and lisible(tmp_sd):
os.replace(tmp_hd, hd) os.replace(tmp_hd, hd)
os.replace(tmp_sd, sd) os.replace(tmp_sd, sd)
@@ -174,7 +224,7 @@ def compresser(voyage, cfg, analyse, force=False, progression=None):
t0 = time.time() t0 = time.time()
with ThreadPoolExecutor(jobs) as pool: 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) duree = int(time.time() - t0)
dire() dire()
if bilan["faits"]: if bilan["faits"]: