#!/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('