Files
Tahgibu/tools/verify-external.py
2026-10-24 12:01:52 -03:00

49 lines
3.2 KiB
Python

#!/usr/bin/env python3
"""Verifica con herramientas INDEPENDIENTES (Pillow, ffprobe) los archivos que escribe Tahgibu.
Uso: mkdir -p /tmp/tgb-art && TGB_TEST_ARTIFACTS=/tmp/tgb-art <ruta>/test_io_files && python3 tools/verify-external.py /tmp/tgb-art"""
import base64, io, json, subprocess, sys, shutil
from PIL import Image, ImageSequence
d = sys.argv[1]
ok = True
def check(cond, msg):
global ok
print((" OK " if cond else " FALLO ") + msg)
ok &= bool(cond)
png0 = Image.open(f"{d}/muestra_f0.png"); png0.load()
check(png0.format == "PNG" and png0.mode == "RGBA" and png0.size == (48, 32), f"PNG exportado lo abre Pillow: {png0.format} {png0.mode} {png0.size}")
v2 = json.load(open(f"{d}/muestra_v2.tgb"))
check(v2["format"] == "TAHGIBU" and v2["version"] == 2 and v2["fps"] == 12 and (v2["width"], v2["height"]) == (48, 32), "cabecera formato 2 coincide con el ejemplo histórico")
v2_frames = [Image.open(io.BytesIO(base64.b64decode(f))).convert("RGBA") for f in v2["frames"]]
check(len(v2_frames) == 3 and all(f.size == (48, 32) for f in v2_frames), "formato 2: 3 fotogramas PNG válidos en Base64 (decodificados con base64+Pillow)")
check(list(png0.getdata()) == list(v2_frames[0].getdata()), "el PNG exportado == fotograma 0 del .tgb v2")
v3 = json.load(open(f"{d}/muestra_v3.tgb"))
check(v3["version"] == 3 and "tgb3" in v3 and len(v3["frames"]) == 3, "formato 3: 'frames' aplanados + bloque 'tgb3'")
v3_flat = [Image.open(io.BytesIO(base64.b64decode(f))).convert("RGBA") for f in v3["frames"]]
check(all(list(a.getdata()) == list(b.getdata()) for a, b in zip(v3_flat, v2_frames)), "formato 3: los fotogramas aplanados == formato 2")
imgs = [Image.open(io.BytesIO(base64.b64decode(s))) for s in v3["tgb3"]["images"]]
check(all(i.size == (48, 32) and i.mode == "RGBA" for i in imgs), f"formato 3: {len(imgs)} imágenes de celda válidas")
check([f["duration_ms"] for f in v3["tgb3"]["frames"]] == [100, 250, 40], "formato 3: duraciones por fotograma")
ap = Image.open(f"{d}/muestra.apng")
check(getattr(ap, "is_animated", False) and ap.n_frames == 3, f"APNG: Pillow lo reconoce animado con {getattr(ap, 'n_frames', '?')} fotogramas")
frames, durs = [], []
for fr in ImageSequence.Iterator(ap):
frames.append(fr.convert("RGBA").copy()); durs.append(fr.info.get("duration"))
check(durs == [100, 250, 40], f"APNG: duraciones {durs}")
check(all(list(a.getdata()) == list(b.getdata()) for a, b in zip(frames, v2_frames)), "APNG: cada fotograma == fotograma del proyecto (píxel a píxel)")
if shutil.which("ffprobe"):
out = subprocess.run(["ffprobe", "-v", "error", "-count_frames", "-select_streams", "v:0", "-show_entries", "stream=nb_read_frames,width,height,codec_name",
"-of", "json", f"{d}/muestra.apng"], capture_output=True, text=True)
info = json.loads(out.stdout or "{}").get("streams", [{}])[0]
check(out.returncode == 0 and info.get("codec_name") == "apng" and int(info.get("nb_read_frames", 0)) == 3,
f"ffprobe: codec={info.get('codec_name')} fotogramas={info.get('nb_read_frames')} {info.get('width')}x{info.get('height')}")
else:
print(" (ffprobe no disponible: omitido)")
print("RESULTADO:", "TODO CORRECTO" if ok else "HAY FALLOS")
sys.exit(0 if ok else 1)