feat(describe): scene-cut + stride frame winnowing; deterministic extraction master
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>