3.8.1: se convierte todo lo que ahorre al menos un 30 %, con la estimación medida por códec de origen

Munka: «si un archivo se puede optimizar así sea un 30 % se convierta». La regla deja de mirar si el clip pasa de
1 o 2 veces la tasa objetivo y mira el ahorro estimado (ajustable, 30 % de entrada). La estimación sale de las 286
conversiones hechas: H.264 y AV1 de cámara quedan en una fracción, H.265 casi igual. Entran los 11 AV1 de Línea
Gene; dejan de ofrecerse los H.265 que no adelgazan y lo apartado en Duplicados.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Este commit está contenido en:
Labstream
2026-10-14 10:12:17 -05:00
padre 7eb5899f04
commit eafd1984f3
Se han modificado 5 ficheros con 142 adiciones y 48 borrados

Ver fichero

@@ -42,7 +42,7 @@ import actualizador
asistente.O = sys.modules[__name__]
actualizador.O = sys.modules[__name__]
VERSION = "3.8.0"
VERSION = "3.8.1"
BASE = os.path.dirname(os.path.abspath(sys.executable if getattr(sys, "frozen", False) else __file__))
# Empaquetado: el programa corre con el motor que viaja en su propia carpeta. OrganizadorLabstream.exe es
@@ -121,6 +121,7 @@ CONFIG_DEFECTO = {
"ffprobe": "ffprobe",
"python_resolve": "", # vacío = el mismo intérprete
"braw": "", # carpeta del decodificador de Blackmagic RAW; vacío = se busca sola
"ahorro_minimo": 30, # % que tiene que ahorrar un clip convertido para que se ofrezca
},
# las subcarpetas con las que nace una pieza nueva (las que más se repiten en las piezas del estudio)
"pieza": {"subcarpetas": ["Guion", "Videos", "Imagenes", "Musica", "Elements", "IA", "Portada", "Exportado"]},
@@ -3445,21 +3446,44 @@ def kbps_objetivo(ancho, alto, fps, perfil):
return max(1500, int(ancho * alto * fps * bpp / 1000))
# Desde cuántas veces la tasa objetivo compensa recomprimir un clip que ya viene comprimido entre cuadros.
# H.265, AV1 y VP9 rinden como la salida: de ahí solo se baja quitando calidad, y hace falta que el origen
# vaya muy holgado (el doble). Un H.264 necesita unas 1,6 veces los bits de H.265 para verse igual: basta
# con que no esté ya por debajo del objetivo. Medido el 5-oct-2026 con clips de V:\Galeano contra su original:
# un H.264 4.6K de 33 Mb/s (1,08 veces el objetivo) queda en la mitad con nota 96,7 sobre 100; uno de
# 121 Mb/s, en la quinta parte. Lo que guarda cada cuadro entero (XAVC-I, ProRes, BRAW) no pasa por aquí.
RECOMPRIMIR_DESDE = {"hevc": 2.0, "av1": 2.0, "vp9": 2.0, "h264": 1.0, "mpeg4": 0.7, "mpeg2video": 0.5}
# Cuánto pesa lo que sale, según de dónde viene. La cola codifica a calidad constante (cq) con un techo de 1,8
# veces la tasa objetivo: lo que sale depende de cómo venía el clip, no de la tasa objetivo. Medido el
# 14-oct-2026 sobre las 286 conversiones hechas (Galeano, Línea Gene, Diana Méndez), lo que pesó cada una:
# - lo que guarda cada cuadro entero (BRAW, XAVC-I): entre el 1 y el 6 % (entre 0,3 y 0,85 veces el objetivo);
# - un H.264 de cámara: entre el 9 y el 26 %; el de 4.6K a 33 Mb/s (5-oct), la mitad;
# - un AV1 de cámara o de móvil (85-99 Mb/s): entre el 9 y el 11 %, su codificador gasta mucho;
# - un H.265 de cámara: entre el 85 y el 99 %, aunque fuera holgado. De ahí no se baja sin quitar calidad.
# Así que lo comprimido entre cuadros sale como mucho a 0,6 veces su tasa (MPEG-4 y MPEG-2, menos) y nunca por
# encima del objetivo; un H.265, a 0,93 veces la suya, hasta el techo. Hasta la 3.8 se miraba solo si el origen
# pasaba de 1 vez (H.264) o 2 veces (H.265, AV1, VP9) el objetivo: once AV1 de Línea Gene que habrían quedado en
# la décima parte no se ofrecían, y en cambio salían H.265 que luego no adelgazaban.
SALE_A = {"h264": 0.6, "av1": 0.6, "vp9": 0.6, "mpeg4": 0.45, "mpeg2video": 0.35}
NOMBRE_CODEC = {"hevc": "H.265", "av1": "AV1", "vp9": "VP9", "h264": "H.264", "mpeg4": "MPEG-4", "mpeg2video": "MPEG-2"}
def poco_que_ganar(f, objetivo):
"""True si el clip ya viene comprimido y tan cerca de lo que pesaría convertido que no compensa tocarlo."""
if not f["kbps"] or f["codec"] not in RECOMPRIMIR_DESDE or "intra" in (f["perfil"] or "").lower():
return False
return f["kbps"] <= RECOMPRIMIR_DESDE[f["codec"]] * objetivo
def _kbps_salida(f, objetivo):
"""La tasa de video (kb/s) con que saldrá un clip a H.265, según su códec y su tasa (ver SALE_A)."""
src = f["kbps"] or 0
if not src or "intra" in (f["perfil"] or "").lower():
return objetivo
if f["codec"] == "hevc":
return min(src * 0.93, objetivo * 1.8)
if f["codec"] in SALE_A:
return min(src * SALE_A[f["codec"]], objetivo)
return objetivo
def ahorro_minimo():
"""Lo que tiene que ahorrar un clip convertido para que se ofrezca, en %. Munka, 14-oct-2026: «si un archivo
se puede optimizar así sea un 30 % se convierta, la idea es aprovechar lo máximo el espacio»."""
try:
return max(0, min(90, int(_opt("ahorro_minimo", 30))))
except (TypeError, ValueError):
return 30
def _mbps(kbps):
return f"{kbps / 1000:.1f}".replace(".", ",") if kbps < 10000 else f"{kbps / 1000:.0f}"
def _salio_del_convertidor(f, salidas):
@@ -3482,11 +3506,17 @@ def estimar_bytes(f, perfil, formato):
pix = (f["ancho"] or 1920) * (f["alto"] or 1080) * (f["fps"] or 30)
return int(fm["mbps_1080p30"] * 1e6 / 8 * dur * pix / (1920 * 1080 * 30))
obj = kbps_objetivo(f["ancho"] or 1920, f["alto"] or 1080, f["fps"], perfil) * fm.get("bpp_mult", 1.0)
if fm["codec"] == "hevc":
obj = _kbps_salida(f, obj)
return int(obj * 1000 / 8 * dur) + int(256000 / 8 * dur)
def candidatos(raiz, meses, perfil, min_mb, incluir_exportados=False, formato="h265_10bit"):
def candidatos(raiz, meses, perfil, min_mb, incluir_exportados=False, formato="h265_10bit", ahorro=None):
fm = FORMATOS.get(formato, FORMATOS["h265_10bit"])
try:
ahorro = ahorro_minimo() if ahorro in (None, "") else max(0, min(90, int(ahorro)))
except (TypeError, ValueError):
ahorro = ahorro_minimo()
lee_braw = bool(decodificador_braw().get("ok"))
reubicar_trabajos(raiz)
con = db()
@@ -3514,6 +3544,10 @@ def candidatos(raiz, meses, perfil, min_mb, incluir_exportados=False, formato="h
# los BRAW los lee el decodificador de Blackmagic; solo si falta se intenta por el guion de Resolve
motor = ("braw" if lee_braw else "resolve") if f["ext"] == ".braw" else "ffmpeg"
obj = int(kbps_objetivo(f["ancho"] or 1920, f["alto"] or 1080, f["fps"], perfil) * fm.get("bpp_mult", 1.0))
est = estimar_bytes(f, perfil, formato)
# el ahorro, contra lo que pesa el clip por su tasa (es lo mismo que su peso, y no cuenta lo que no es video)
base = f["kbps"] * 1000 / 8 * f["dur"] if f["kbps"] and f["dur"] else f["bytes"]
gana = round((1 - est / base) * 100) if base else 100
if _salio_del_convertidor(f, salidas):
motivo_no = "ya es una conversión hecha con este programa"
elif f["es_stock"]:
@@ -3522,9 +3556,11 @@ def candidatos(raiz, meses, perfil, min_mb, incluir_exportados=False, formato="h
motivo_no = "tiene canal alfa (HEVC lo perdería)"
elif f["ruta"] in probados:
motivo_no = "ya se probó con esta calidad: " + probados[f["ruta"]].replace("no compensa: ", "").split(". ")[0]
elif fm["codec"] == "hevc" and poco_que_ganar(f, obj):
motivo_no = (f"ya viene comprimido ({NOMBRE_CODEC.get(f['codec'], f['codec'])} a {f['kbps'] / 1000:.0f} Mb/s): "
"recomprimirlo ahorraría poco y le quitaría calidad")
elif os.sep + R.DUPLICADOS + os.sep in f["ruta"]:
motivo_no = "está apartado como repetido (Duplicados): mejor borrarlo que convertirlo"
elif fm["codec"] == "hevc" and f["dur"] and gana < ahorro:
motivo_no = (f"convertido ahorraría solo un {max(gana, 0)} % ({NOMBRE_CODEC.get(f['codec'], f['codec'])} a {_mbps(f['kbps'] or 0)} Mb/s): "
f"se convierte desde un {ahorro} %")
elif motor == "resolve" and formato != "h265_10bit":
motivo_no = "sin el decodificador de Blackmagic los BRAW van por Resolve, y solo salen a H.265 10 bits"
elif tope and not es_viejo(f, tope):
@@ -3533,7 +3569,6 @@ def candidatos(raiz, meses, perfil, min_mb, incluir_exportados=False, formato="h
motivo_no = "es un exportado / master"
elif not f["ancho"] or not f["dur"]:
motivo_no = "no se pudo leer (ffprobe)"
est = estimar_bytes(f, perfil, formato)
out.append({
"ruta": f["ruta"], "rel": os.path.relpath(f["ruta"], raiz), "bytes": f["bytes"], "codec": f["codec"], "perfil": f["perfil"],
"pix_fmt": f["pix_fmt"], "res": f"{f['ancho']}x{f['alto']}", "fps": f["fps"], "dur": f["dur"], "kbps": f["kbps"],
@@ -4525,7 +4560,7 @@ def _no_compensa(t, sale=None):
if not entra or not sale:
return ""
repetida = R.es_conversion(os.path.basename(t["ruta"]))
if not repetida and sale <= 0.8 * entra:
if not repetida and sale <= (1 - max(5, ahorro_minimo() - 10) / 100) * entra:
return ""
return (f"no compensa: pesa el {round(sale / entra * 100)} % del clip del que salió"
+ (", que ya era una conversión de este programa" if repetida else "") + ". Se queda el que estaba.")
@@ -7314,7 +7349,8 @@ class Manejador(BaseHTTPRequestHandler):
if ruta == "/api/optimizar/candidatos":
o = CONFIG["optimizar"]
return self._json({"candidatos": candidatos(os.path.normpath(q["raiz"]), q.get("meses", o["meses_minimos"]), q.get("perfil", o["perfil"]),
q.get("min_mb", o["min_mb"]), q.get("exportados") == "1", q.get("formato", o.get("formato", "h265_10bit")))})
q.get("min_mb", o["min_mb"]), q.get("exportados") == "1", q.get("formato", o.get("formato", "h265_10bit")),
q.get("ahorro"))})
if ruta == "/api/optimizar/motores":
return self._json(motores())
if ruta == "/api/optimizar/cola":
@@ -7482,7 +7518,7 @@ class Manejador(BaseHTTPRequestHandler):
return self._json({"ok": True})
if ruta == "/api/config":
# solo ajustes de uso; las rutas de herramientas y la URL de actualización se cambian a mano en config.json
permitidas = {"optimizar": {"meses_minimos", "min_mb", "perfil", "formato", "chroma", "color", "salida", "sufijo", "auto_reemplazar",
permitidas = {"optimizar": {"meses_minimos", "min_mb", "ahorro_minimo", "perfil", "formato", "chroma", "color", "salida", "sufijo", "auto_reemplazar",
"horario", "pausar_si_edito", "aligerar", "avisar"},
"respaldo": {"destino"}, "actualizaciones": {"auto"}, "escaneo": {"profundo"}}
for k, v in d.items():