| # 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 | |
| } | |