#!/usr/bin/env python3 """On-device eval for a minus model dir (AX650 NPU): ad + screen-state heads. Reuses the model dir's own infer_vlm_fused.py helpers (patchify, prompt build, layer order) so the run is exactly the production path, but keeps the full logits. python3 eval_v02.py ad python3 eval_v02.py screen """ import csv, json, os, sys, time import numpy as np sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import infer_vlm_fused as F AD_Q = "Is this an advertisement? Answer Yes or No." SC_Q = "Classify this TV screen: PLAYING, PAUSED, DIALOG, MENU, or SCREENSAVER?" SCREEN = (("PLAYING", (9436, 60595)), ("PAUSED", (7055, 16137)), ("DIALOG", (20546, 23238)), ("MENU", (554, 857)), ("SCREENSAVER", (8421, 14151))) def logits_for(models, path, prompt): from PIL import Image t0 = time.time() patches = F.patchify_image_512(Image.open(path).convert("RGB")) v = models["vision"].run(None, {"pixel_values": patches})[0].astype(np.float32, copy=False) ids = F.build_prompt(models["tokenizer"], prompt) n = min(len(ids), F.PREFILL_LEN) arr = np.array(ids[:n], dtype=np.int64) pos = np.where(arr == F.IMG_TOKEN)[0] d = np.zeros((1, F.PREFILL_LEN, F.HIDDEN_SIZE), np.float32) d[0, :n] = models["embeds"][arr].astype(np.float32, copy=False) d[0, pos[0]:pos[0] + min(len(pos), v.shape[1])] = v[0, :min(len(pos), v.shape[1])] mask = np.full((1, F.PREFILL_LEN, F.PREFILL_LEN), -65536.0, np.float32) mask[0, :n, :n] = np.triu(np.ones((n, n), np.float32), k=1) * -65536.0 idx = np.arange(F.PREFILL_LEN, dtype=np.int32).reshape(1, F.PREFILL_LEN) for i, lt in enumerate(F.LAYER_TYPES): if lt == "conv": d = models["fused"][i].run(None, {"hidden": d, "conv_state_in": np.zeros((1, F.HIDDEN_SIZE, F.CONV_L_CACHE), np.float32)})[0] else: d = models["fused"][i].run(None, {"hidden": d, "mask": mask, "indices": idx})[0] lg = models["decode_post"].run(None, {"input": d[:, n - 1:n]})[0].flatten() return lg, (time.time() - t0) * 1000, n def main(): mode = sys.argv[1] models = F.load_models() res, times = [], [] if mode == "ad": root, out = sys.argv[2], sys.argv[3] items = [(os.path.join(root, d, f), 1 if d == "ads" else 0) for d in ("ads", "non_ads") for f in sorted(os.listdir(os.path.join(root, d))) if f.lower().endswith((".png", ".jpg", ".jpeg"))] for k, (p, y) in enumerate(items): lg, ms, n = logits_for(models, p, AD_Q) yl, nl = float(lg[list(F.YES_IDS)].max()), float(lg[list(F.NO_IDS)].max()) z = np.array([lg[t] for t in list(F.YES_IDS) + list(F.NO_IDS)]); z = np.exp(z - z.max()) res.append({"path": p, "label": y, "pred": int(yl > nl), "p_yes": float(z[:len(F.YES_IDS)].sum() / z.sum()), "ms": ms}) times.append(ms) if k % 50 == 0: print(f"{k}/{len(items)} n_tokens={n} {ms:.0f}ms", flush=True) acc = np.mean([r["pred"] == r["label"] for r in res]) adr = np.mean([r["pred"] for r in res if r["label"] == 1]); nar = np.mean([1 - r["pred"] for r in res if r["label"] == 0]) summ = {"accuracy": acc, "ad_recall": adr, "nonad_recall": nar, "n": len(res), "fp": sum(r["pred"] and not r["label"] for r in res), "fn": sum(r["label"] and not r["pred"] for r in res)} else: man, root, out = sys.argv[2], sys.argv[3], sys.argv[4] items = [(os.path.join(root, os.path.basename(r["path"])), r["label"]) for r in csv.DictReader(open(man))] for k, (p, y) in enumerate(items): lg, ms, n = logits_for(models, p, SC_Q) pred = max(SCREEN, key=lambda c: max(lg[t] for t in c[1]))[0] res.append({"path": p, "label": y, "pred": pred, "ms": ms}); times.append(ms) if k % 50 == 0: print(f"{k}/{len(items)} n_tokens={n} {ms:.0f}ms", flush=True) conf = {} for r in res: conf.setdefault(r["label"], {}).setdefault(r["pred"], 0); conf[r["label"]][r["pred"]] += 1 summ = {"accuracy": np.mean([r["pred"] == r["label"] for r in res]), "n": len(res), "confusion": conf, "playing_misread_as_action": sum(r["label"] == "PLAYING" and r["pred"] in ("PAUSED", "SCREENSAVER", "DIALOG", "MENU") for r in res)} summ.update({"avg_ms": float(np.mean(times)), "median_ms": float(np.median(times)), "max_ms": float(np.max(times))}) json.dump({"summary": summ, "results": res}, open(out, "w"), indent=1, default=float) print(json.dumps(summ, indent=1, default=float)) if __name__ == "__main__": main()