feat(describe): scene-cut + stride frame winnowing; deterministic extraction
Add a content-aware stage on top of the existing dHash frame qualification in
VideoProcessor.process(): qualified frames at Hamming distance >= 25 are tagged
scene cuts and always kept; non-scene-cut qualified frames arriving < 5s after
the last KEPT frame are stride-dropped. All three gates (dHash change, scene
cut, stride floor) measure against the single last-kept reference, which
advances only on a kept frame. Thresholds are config-backed under `describe`
(scene_cut_threshold=25, min_stride_seconds=5.0) and honored at both process()
call sites via VideoProcessor.__init__. Per-segment winnowing metrics
(raw/dhash_qualified/scene_cut/stride_dropped/kept) emit as one INFO line; no
behavior is gated on them. The dHash body, metadata header, JSONL schema, and
per-sensor change-detection are unchanged.
extract.py: importance is now a hard per-category filter (ignore->0, low-><=2
per category, normal/high uncapped) via _apply_category_caps, applied before the
first-frame guarantee. The AI-fallback selector is now deterministic greedy
max-temporal-spread (seeded lowest-frame_id, lowest-frame_id tie-break) instead
of random.sample; the `random` import and stale "advisory" wording are gone.
Reimplementation on top of main (not a merge of the ~198-commit-old PR branch).
The MobileViT embedding stage is deliberately NOT ported: three local evals
proved it inert on screencasts, and it does not earn its complexity or 21 MB
wheel weight. No new runtime dependency, no new assets.
Design decisions:
- Config keys `describe.scene_cut_threshold` (25) / `describe.min_stride_seconds`
(5.0); module constants SCENE_CUT_THRESHOLD / MIN_STRIDE_SECONDS are the
defaults and the values tests import. No config for DHASH_THRESHOLD.
- Per-frame keep decision is a pure module-level _winnow_decision(); process()
owns reference and counter state.
- Counters: raw = every decoder-yielded frame (incl. pts=None and masked skips);
dhash_qualified = first frame + dHash-8 gate passers (== kept + stride_dropped);
scene_cut subset of kept. Identities raw >= dhash_qualified >= kept,
kept == dhash_qualified - stride_dropped, scene_cut <= kept hold in all cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>