# context.py — build the VLM input under a token/cost budget (§4). # Recent frames at full detail, older history as sparse downscaled keyframes, plus an # explicit memory of what we already said (structural anti-repeat). Causal by construction: # `history` only ever contains frames with timestamp <= now. # Frame budget is env-tunable (CTX_RECENT_S / CTX_HIST_STRIDE_S) so the perception lever can be # swept without code edits; defaults reproduce the shipped behavior. import os RECENT_S = float(os.environ.get("CTX_RECENT_S", "6.0")) # last N s at full res (~2N frames @ 2 fps) HIST_STRIDE_S = float(os.environ.get("CTX_HIST_STRIDE_S", "2.0")) # older history: 1 keyframe / N s STEP = 0.5 def build_context(question, history, said): """history: ascending list of (t, frame_path); said: list of emitted strings.""" now = history[-1][0] recent = [(t, p) for (t, p) in history if now - t <= RECENT_S] older = [(t, p) for (t, p) in history if now - t > RECENT_S] keyframes = older[::max(1, int(HIST_STRIDE_S / STEP))] # subsample older frames return { "question": question, "now": now, "recent_frames": recent, # sent at native 480x270 "history_frames": keyframes, # downscaled in vlm_client "already_said": said, # anti-repeat memory }