3.4.0: los BRAW se convierten sin Resolve, y lo que no compensa no se reconvierte

Munka no podía reconvertir archivos que antes sí. Auditado sobre la carpeta de Andrea Galeano:

- Los BRAW nunca habían salido: 87 clips (496 GB), tres tandas por el guion de Resolve y ni un
  clip convertido. Ahora los lee el decodificador de Blackmagic que instala Resolve (braw.py,
  en un proceso aparte), sin abrir Resolve, y ffmpeg los codifica como cualquier otro clip.
  Probado con clips del NAS: 15,6 GB → 394 MB con sus 7.004 cuadros, VMAF 96,0 contra el BRAW
  revelado sin pérdida. Las conversiones de BRAW que fallaron por Resolve pasan solas al lector.
- Lo que «antes podía» eran reconversiones de lo ya convertido: ahorraban un 5 % con pérdida.
  Siguen sin ofrecerse, y lo que ya viene comprimido de cámara solo se ofrece si compensa.
- La tarjeta codifica con «uhq»: entre un 19 y un 55 % menos de peso con la misma calidad
  medida, siempre H.265 de 10 bits.
- Una conversión que no aligera (más del 80 % del clip, o reconversión de una conversión) no
  reemplaza al original: queda «no compensa» y lo que salió va a _Para borrar\descartados.

De paso: los MP4 guardan las etiquetas de la cámara; un intento que no pasó la verificación se
puede reintentar; un H.264 de más de 4.096 px de lado va por CPU; el motivo de un fallo se lee
entero; el guion de Resolve llega con tildes y borra su ficha.

Pruebas: prueba_orden.py 33 (una con un BRAW de verdad), probar.py 19, prueba_funcional.py 30,
prueba_ventana.py 18 desde el código y 17 con el paquete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Este commit está contenido en:
Labstream
2026-10-05 13:24:34 -05:00
padre 477857db45
commit 0819f2da89
Se han modificado 12 ficheros con 1767 adiciones y 147 borrados

Ver fichero

@@ -19,7 +19,7 @@ AQUI = os.path.dirname(os.path.abspath(__file__))
TMP = tempfile.mkdtemp(prefix='organilab-smoke-')
APP = os.path.join(TMP, 'app')
os.makedirs(APP)
for f in ('organizador.py', 'reglas.py', 'asistente.py', 'actualizador.py', 'resolve_bridge.py', 'firma.py', 'ventana.py', 'instalar.py'):
for f in ('organizador.py', 'reglas.py', 'asistente.py', 'actualizador.py', 'resolve_bridge.py', 'braw.py', 'firma.py', 'ventana.py', 'instalar.py'):
shutil.copy(os.path.join(AQUI, f), APP)
shutil.copytree(os.path.join(AQUI, 'static'), os.path.join(APP, 'static'))
RAIZ = os.path.join(TMP, 'Cliente Smoke')
@@ -64,15 +64,25 @@ shutil.copy2(os.path.join(RAIZ, '2026', '03- Marzo', '01- Botox', 'Reel botox.mp
mk('Raro/algo.xyz', 2026, 2, 2, 10)
os.makedirs(os.path.join(RAIZ, 'Vacia', 'dentro'), exist_ok=True)
# un video de verdad para el convertidor (6 s, 1280x720, H.264)
# dos videos de verdad para el convertidor (6 s, 1280x720, H.264): uno con tanto grano y tanta tasa que sí
# adelgaza, y otro ya comprimido como sale de una cámara, que el convertidor no debe ofrecer
FF = shutil.which('ffmpeg')
CLIP = os.path.join(RAIZ, '2026', '03- Marzo', 'Grabacion', 'C0020.MP4')
CLIP2 = os.path.join(RAIZ, '2026', '03- Marzo', 'Grabacion', 'C0021.MP4')
if FF:
subprocess.run([FF, '-hide_banner', '-loglevel', 'error', '-y', '-f', 'lavfi', '-i', 'testsrc=duration=6:size=1280x720:rate=25',
'-f', 'lavfi', '-i', 'sine=frequency=440:duration=6', '-c:v', 'libx264', '-preset', 'ultrafast', '-crf', '18',
'-pix_fmt', 'yuv420p', '-c:a', 'aac', '-shortest', CLIP], check=True)
t = time.mktime(time.struct_time((2025, 6, 10, 12, 0, 0, 0, 0, -1)))
os.utime(CLIP, (t, t))
for destino, imagen, crf in ((CLIP, 'testsrc=duration=6:size=1280x720:rate=25,noise=alls=40:allf=t', '8'),
(CLIP2, 'testsrc=duration=6:size=1280x720:rate=25', '30')):
subprocess.run([FF, '-hide_banner', '-loglevel', 'error', '-y', '-f', 'lavfi', '-i', imagen,
'-f', 'lavfi', '-i', 'sine=frequency=440:duration=6', '-c:v', 'libx264', '-preset', 'ultrafast', '-crf', crf,
'-pix_fmt', 'yuv420p', '-c:a', 'aac', '-shortest', destino], check=True)
t = time.mktime(time.struct_time((2025, 6, 10, 12, 0, 0, 0, 0, -1)))
os.utime(destino, (t, t))
# y, si se da uno, un BRAW de verdad (variable ORGANILAB_BRAW_PRUEBA): los de arriba son bytes al azar
BRAW = os.environ.get('ORGANILAB_BRAW_PRUEBA') or ''
if BRAW:
shutil.copy(BRAW, os.path.join(RAIZ, '2026', '03- Marzo', 'Grabacion', 'A001_03051200_C003.braw'))
t = time.mktime(time.struct_time((2026, 3, 5, 12, 0, 0, 0, 0, -1)))
os.utime(os.path.join(RAIZ, '2026', '03- Marzo', 'Grabacion', 'A001_03051200_C003.braw'), (t, t))
# ---------------------------------------------------------------- el servidor
with socket.socket() as s_:
@@ -320,7 +330,10 @@ def p_motores():
assert m.get('ffmpeg'), 'sin ffmpeg'
disp = [k for k, v in m['formatos'].items() if v]
assert 'h265_10bit' in disp or 'h264' in disp, disp
return 'formatos disponibles: ' + ', '.join(disp) + ' · GPU: ' + (m['gpu'].get('nombre') or 'no')
assert 'ok' in (m.get('braw') or {}), m.get('braw')
assert get('/api/estado')['herramientas']['braw']['ok'] in (True, False)
return 'formatos disponibles: ' + ', '.join(disp) + ' · GPU: ' + (m['gpu'].get('nombre') or 'no') + \
' · lector de BRAW: ' + ('sí' if m['braw']['ok'] else 'no (' + (m['braw'].get('motivo') or '')[:40] + ')')
def p_analizar():
@@ -332,7 +345,10 @@ def p_analizar():
clip = [x for x in cand if x['ruta'].lower().endswith('c0020.mp4')]
assert clip and not clip[0]['excluido'], (clip or cand)[:1]
IDS['ruta'] = clip[0]['ruta']
return 'leyó %d clip(s) · C0020.MP4 %s %s' % (len(cand), clip[0]['codec'], clip[0]['res'])
comprimido = [x for x in cand if x['ruta'].lower().endswith('c0021.mp4')]
assert comprimido and comprimido[0]['excluido'].startswith('ya viene comprimido (H.264'), (comprimido or cand)[:1]
return 'leyó %d clip(s) · C0020.MP4 %s %s a %d kb/s: se ofrece · C0021.MP4 a %d kb/s: no' % (
len(cand), clip[0]['codec'], clip[0]['res'], clip[0]['kbps'], comprimido[0]['kbps'])
def p_convertir():
@@ -353,6 +369,26 @@ def p_convertir():
return '%s: convertido (%s), verificado y reemplazado; original apartado' % (fmt, t['motor'])
def p_braw():
m = get('/api/optimizar/motores')['braw']
if not BRAW:
return 'sin clip de prueba (ORGANILAB_BRAW_PRUEBA): solo se comprueba que el lector responde (%s)' % ('sí' if m['ok'] else 'no')
assert m['ok'], m
cand = get('/api/optimizar/candidatos' + q(raiz=RAIZ, meses=0, min_mb=0, formato='h265_10bit'))['candidatos']
clip = [x for x in cand if x['ruta'].lower().endswith('a001_03051200_c003.braw')]
assert clip and clip[0]['motor'] == 'braw' and not clip[0]['excluido'], (clip or cand)[:1]
c, r = post('/api/optimizar/encolar', {'raiz': RAIZ, 'rutas': [clip[0]['ruta']], 'perfil': 'equilibrado', 'formato': 'h265_10bit', 'chroma': '420', 'color': 'rec709', 'iniciar': True})
assert c == 200 and r['ids'], r
assert esperar(lambda: not get('/api/estado')['cola']['activa'], 240), 'la cola no terminó'
t = next(x for x in get('/api/optimizar/cola' + q(raiz=RAIZ))['trabajos'] if x['id'] == r['ids'][0])
assert t['estado'] == 'verificado' and t['motor'] == 'braw', (t['estado'], t['motor'], (t.get('log') or '')[-300:])
assert os.path.isfile(t['salida']) and os.path.isfile(clip[0]['ruta']) # el original sigue en su sitio
c, rr = post('/api/optimizar/reemplazar', {'ids': r['ids']})
assert rr['resultados'][0]['ok'], rr
assert os.path.isfile(t['salida']) and not os.path.isfile(clip[0]['ruta'])
return 'BRAW %s leído sin Resolve, convertido (%s), verificado y reemplazado' % (clip[0]['res'], (t.get('log') or '').split(' · ')[3][:40])
def p_cola_control():
c, r = post('/api/optimizar/control', {'accion': 'quitar', 'ids': [IDS['trabajo']]})
assert c == 200
@@ -502,7 +538,7 @@ for nombre, fn in (('Interfaz sin nada externo', p_estatico), ('Registrar carpet
('Historial y papelera', p_historial), ('Repetidos', p_repetidos), ('Stock viejo', p_stock),
('Carpetas vacías', p_vacias), ('Mapa interactivo', p_mapa), ('Colores en Windows', p_colores),
('Motores de conversión', p_motores), ('Leer clips (ffprobe)', p_analizar),
('Convertir, verificar, reemplazar', p_convertir), ('Control de la cola', p_cola_control),
('Convertir, verificar, reemplazar', p_convertir), ('Convertir un BRAW', p_braw), ('Control de la cola', p_cola_control),
('Respaldar', p_respaldo), ('Asistente / hacer de una vez', p_asistente),
('Pieza nueva', p_pieza), ('Bandeja de entrada y tarjeta', p_entrada), ('Orden automático', p_automatico),
('Arrastrar y buscar carpeta', p_soltar_y_buscar), ('Actualizaciones', p_actualizacion_offline),