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:
@@ -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():
|
||||
|
||||
Referencia en una nueva incidencia
Block a user