#!/usr/bin/env python import argparse, json, re, time, os, platform, shutil, subprocess, sys, threading from collections import OrderedDict, deque from concurrent.futures import ThreadPoolExecutor from pathlib import Path import av import numpy as np from datasets import load_dataset def sec(s): p = [int(x) for x in str(s).split(':')] return p[-1] + 60 * p[-2] + (3600 * p[-3] if len(p) > 2 else 0) def norm(x): return re.sub(r'\s+', ' ', re.sub(r'.*?', '', str(x or ''), flags=re.S)).strip().lower().strip(' .') def correct(pred, gold, fmt): p, g = norm(pred), norm(gold) if fmt == 'binary': return (p.startswith('yes') and g.startswith('yes')) or (p.startswith('no') and g.startswith('no')) if fmt in {'number', 'percentage'}: a = re.search(r'-?\d+(?:\.\d+)?', p); b = re.search(r'-?\d+(?:\.\d+)?', g) return bool(a and b and float(a.group()) == float(b.group())) if fmt == 'time': a = re.search(r'\d{1,2}:\d{2}:\d{2}', p); b = re.search(r'\d{1,2}:\d{2}:\d{2}', g) return bool(a and b and a.group() == b.group()) return p == g or (g and g in p) def sample_frames(out, n): out = [out[i] for i in np.linspace(0, len(out) - 1, min(n, len(out))).round().astype(int)] return out + [out[0]] if len(out) == 1 else out def frames(src, start, end, n=64): def decode(): c = av.open(str(src)); s = c.streams.video[0] c.seek(max(0, int(start / float(s.time_base))), stream=s, any_frame=True) out = [] for index, f in enumerate(c.decode(s)): t = float(f.pts * s.time_base) if f.pts is not None else None if index == 0 and t is not None and t < max(0, start - 60): c.close(); return [] if t is not None and t > end: break image = f.to_ndarray(format='rgb24') if t is None or t >= start: out.append(image) c.close(); return out out = decode() if not out: # Keep the run complete for corrupt/misindexed segments; mark them via zero-valued frames. out = [np.zeros((224, 224, 3), dtype=np.uint8)] * n out = sample_frames(out, n) if out else [] if not out: raise RuntimeError(f'no frames in {src} for [{start},{end}]') arr = np.stack(out) return arr, {'fps': 5.0, 'duration': len(arr) / 5.0, 'total_num_frames': len(arr), 'frames_indices': list(range(len(arr))), 'video_backend': 'opencv', 'do_sample_frames': False} def fixed64_frames(src, output_fps, n=64): c = av.open(str(src)); s = c.streams.video[0] out = [f.to_ndarray(format='rgb24') for f in c.decode(s)] c.close() if len(out) != n: raise RuntimeError(f'expected {n} fixed frames in {src}, decoded {len(out)}') fps = float(output_fps) if fps <= 0: raise RuntimeError(f'invalid output_fps={output_fps} for {src}') arr = np.stack(out) return arr, {'fps': fps, 'duration': len(arr) / fps, 'total_num_frames': len(arr), 'frames_indices': list(range(len(arr))), 'video_backend': 'opencv', 'do_sample_frames': False} def fixed_absmeta_frames(src, metadata, n=80): c = av.open(str(src)) stream = c.streams.video[0] out = [f.to_ndarray(format='rgb24') for f in c.decode(stream)] c.close() if len(out) != n: raise RuntimeError(f'expected {n} fixed frames in {src}, decoded {len(out)}') if not isinstance(metadata, dict): raise RuntimeError(f'missing video_metadata for {src}') frame_indices = metadata.get('frames_indices') if metadata.get('video_backend') != 'precomputed_absolute_pts' or not isinstance(frame_indices, list): raise RuntimeError(f'invalid absolute video_metadata for {src}') if len(frame_indices) != n: raise RuntimeError(f'expected {n} frame indices in video_metadata for {src}, got {len(frame_indices)}') return np.stack(out), dict(metadata) class VideoCache: def __init__(self, root, max_gib): self.root, self.limit = Path(root), int(max_gib * 1024 ** 3) self.root.mkdir(parents=True, exist_ok=True) self.entries, self.size, self.lock = OrderedDict(), 0, threading.Lock() self.hits = self.misses = self.bytes = 0 def get(self, src): src = Path(src) key = f'{src.parent.parent.name}_{src.parent.name}_{src.name}' dst = self.root / key with self.lock: if dst.exists(): self.hits += 1; self.entries[dst] = dst.stat().st_size; self.entries.move_to_end(dst) return dst self.misses += 1 size = src.stat().st_size while self.entries and self.size + size > self.limit: old, old_size = self.entries.popitem(last=False) old.unlink(missing_ok=True); self.size -= old_size if size > self.limit: return src tmp = dst.with_suffix(dst.suffix + f'.{os.getpid()}.partial') try: shutil.copyfile(src, tmp); os.replace(tmp, dst) self.entries[dst] = size; self.size += size; self.bytes += size return dst except OSError: tmp.unlink(missing_ok=True) return src def read_jsonl(path): if not path.exists(): return [] return [json.loads(line) for line in path.read_text().splitlines() if line.strip()] def load_rows(data_root): rows = [] for dataset_name, rel in [('heico', 'heico-focus-vqa/data/segment/test.parquet'), ('lapchole', 'lapchole-focus-vqa/data/segment/test.parquet')]: ds = load_dataset('parquet', data_files=str(Path(data_root) / rel), split='train') rows.extend([{**r, '_dataset': dataset_name, '_input_index': len(rows) + i} for i, r in enumerate(ds)]) return rows def load_fixed64_rows(dataset_jsonl): path = Path(dataset_jsonl) rows = [] for i, line in enumerate(path.read_text().splitlines()): if not line.strip(): continue r = json.loads(line) if int(r.get('derived_frame_count', 0)) != 64 or int(r.get('verified_frame_count', 0)) != 64: raise ValueError(f'row {i} id={r.get("id")} is not verified fixed64') rows.append({**r, '_dataset': str(r.get('dataset', 'unknown')).replace('-focus-vqa', ''), '_input_index': len(rows), '_fixed64_root': str(path.parent)}) return rows def row_id(row): if row.get('id') is not None: return row['id'] metadata = row.get('segment_metadata') or {} if metadata.get('id') is None: raise ValueError('row is missing both id and segment_metadata.id') return metadata['id'] def row_answer_format(row): return row.get('answer_format') or (row.get('segment_metadata') or {}).get('answer_format') def row_video_name(row): return row.get('video') or (row.get('segment_metadata') or {}).get('video', '') def row_timestamp_start(row): return row.get('timestamp_start') or (row.get('segment_metadata') or {}).get('timestamp_start', '00:00:00') def load_fixed80_absmeta_requests(dataset_jsonl, nframes=80): path = Path(dataset_jsonl) rows = [] for i, line in enumerate(path.read_text().splitlines()): if not line.strip(): continue r = json.loads(line) qid = row_id(r) if 'answer' in r: raise ValueError(f'row {i} id={qid} unexpectedly contains an answer') messages = r.get('messages') if not isinstance(messages, list) or [m.get('role') for m in messages] != ['system', 'user']: raise ValueError(f'row {i} id={qid} must contain system,user messages only') user_text = messages[1].get('content') if not isinstance(user_text, str) or user_text.count('