Skip to content

feat: video ingest by clip ranges — timeline selection, range-scoped extraction - #780

Merged
YaelAnaya merged 10 commits into
mainfrom
feat/clip-ranges
Aug 25, 2026
Merged

feat: video ingest by clip ranges — timeline selection, range-scoped extraction#780
YaelAnaya merged 10 commits into
mainfrom
feat/clip-ranges

Conversation

@JArmandoAnaya

Copy link
Copy Markdown
Contributor

When a user ingests a video, they can select one or more time ranges on a preview timeline, and frame extraction happens only inside those ranges. Ranges are part of what the source is — the same clip registered over different ranges is a second source, exactly as a different extraction rate is today. The full decision record is the first comment on #769.

By layer:

  • KernelTimeRange and one canonicalization (clamp to the clip, sort, merge overlaps and touches; a selection covering the whole clip collapses to the empty one, so "whole clip" has a single identity spelling). VideoProvenance.ranges joins extraction_fps in the source's identity: the service pre-check compares the pair and the source-origin index gains a canonical-ranges term — migration 16, FORMAT_VERSION 15 → 16, no backfill because an empty selection omits its JSON key and lands on the same index term as every pre-existing row. Extraction keeps the no-seek whole-clip read and applies the selection as a select filter over integer output frame numbers; frames keep their t = 0 grid indices, so a selected frame is byte-identical to its whole-clip twin and content addressing collapses the overlap of two selections over one file. One exact frame count — per half-open range ceil(end × fps) − ceil(start × fps) — replaces the floor estimate that undercounted by one on every fractional product.
  • Wire and surfaces — an optional ranges multipart field on video registration, parsed against the kernel's own bounds so a malformed selection is 422 VALIDATION_ERROR; VideoProvenanceOut publishes the canonical form back; a repeatable --range START:END on visionset ingest; a matching ranges parameter on the MCP ingest tool. The ingest run request is untouched — ranges never ride on the run. openapi.json, the generated client and docs/content/mcp-tools.md are regenerated.
  • Frontend — the clip panel takes an editor shape: a compact preview player with the cut's facts beside it, and a hand-rolled multi-range timeline spanning the panel under both (no new dependencies). The bar is a filmstrip of exactly as many sampled frames as fit its width; selection is whole seconds — a drag paints second cells, handles and arrows walk boundaries, only the marker seconds are labelled, as plain numbers. Unselected stretches wash out under a theme-aware scrim while a selected clip stays full-colour inside a full-height primary frame. A click inside a clip previews it and playback started from any control stops on the clip's boundary, snapped, never past it. A clip the browser cannot decode gets no timeline, one line saying it will be ingested whole, and an unchanged registration path. The estimate arithmetic is mirrored from the kernel in one shared module.
  • Docs — ingest, cli, ui, sources, api and persistence describe the selection, the exact count, the discrete timeline and the reshaped index.

The branch is rebased onto current main; the one adaptation that needed its own commit swaps the timeline onto the lucide icon set and rewrites a test that the new synthetic-DOM-event gate refuses.

Found, not fixed:

  • The server accepts whatever ffmpeg demuxes while the preview player decodes the browser's narrower set; in that gap the screen states the clip will be ingested whole. A numeric start/end fallback for undecodable clips is deferred, recorded on Video ingest by clip ranges: timeline selection, range-scoped extraction #769.
  • Byte-level dedup across selections holds within one installed ffmpeg build, the ceiling the video-processor port already documents.

Test plan: targeted suites ran green at every phase (kernel, server, CLI, MCP, contract parity, ui-core vitest, scripts gates), the count formula and the select construction were mutation-verified, and scripts/check.sh passed in full — python, frontend, generated artifacts, both chromium suites. CI on this pull request is the exhaustive gate.

Closes #769

A source's cut is now (extraction_fps, ranges): TimeRange values are
canonicalized in the kernel — clamped, sorted, merged, whole-clip collapsing
to the empty selection — and stored on VideoProvenance, so the same clip over
a different selection is a second source. The source-origin index grows a
canonical-ranges term (migration 16, FORMAT_VERSION 15 -> 16); rows written
before ranges existed need no backfill because an empty selection omits its
JSON key. Extraction keeps the no-seek whole-clip read and applies the
selection as a select filter over integer output frame numbers; frames keep
their t=0 grid indices, so a selected frame is byte-identical to its
whole-clip twin and content addressing collapses overlap. One exact count
formula (ceil per half-open range) replaces the floor estimate that
undercounted by one on fractional products. cf. #769
Video registration accepts a range selection everywhere a source is made:
an optional ranges multipart field (a JSON array parsed against the kernel's
own TimeRange, so a malformed selection is 422 VALIDATION_ERROR, never a
traceback), a repeatable --range START:END on visionset ingest, and a ranges
parameter on the MCP ingest tool. VideoProvenanceOut and the CLI/MCP wire
projection publish the canonical selection back — the identity spelling, not
what the caller typed. IngestStart is untouched: ranges never ride on the
run. openapi.json, the generated client and docs/content/mcp-tools.md are
regenerated; the wire sample carries a non-empty selection so parity checks
the new half. cf. #769
Choosing a clip now mounts a player over an object URL and a hand-rolled
multi-range track under it: drag creates a range, handles drag and nudge by
one grid step (shift for ten), Delete removes, a click seeks, and the readout
speaks in the merged form while segments stay as typed. The selection rides
raw into registration — the kernel canonicalizes — and step 2 echoes the
canonical ranges beside the probe. Both frame counts move from floor to the
mirrored exact formula (clipRanges.ts), so the estimate now matches what
extraction emits at every fractional boundary. A clip the browser cannot
decode gets no timeline and one line saying it will be ingested whole;
registration proceeds unchanged. No new dependencies. cf. #769
The range selection is documented where each surface already speaks: what a
selection is and how it canonicalizes (sources, ingest), the exact ceil count
superseding the floor estimate (ingest), the timeline and its degradation for
a clip the browser cannot decode (ingest, ui), --range on the CLI, the ranges
multipart field (api), and the reshaped five-term source-origin index with
migration 16 (persistence). The vocabulary stays 'range'; 'partial' keeps
meaning damage salvage. cf. #769
…d sizes its player

Three review findings on the ingest timeline. A click inside a selected range
now previews it the way an editor timeline does — the player seeks to that
moment, plays, and stops where the range ends; a pause, ours or the person's,
retires the preview, and a click outside every range scrubs without playing.
Handle and track drags own their pointer (preventDefault + select-none), so
dragging a handle no longer starts the browser's text selection and drags the
page instead of the handle. The player is bounded to max-w-md — a preview,
not a hero — while the track keeps the full panel width for drag precision.
cf. #769
…he cut, timeline under both

The video row no longer strands a small player against a field of whitespace.
The timeline gains an aside slot: at md and up the compact player sits left
and the cut's facts sit beside it — the extraction rate, the frame count and
the selection, one aligned group instead of three corners — with the identity
hint under them; below md the block stacks. The track spans the full panel
under both, editor-style, flanked by 0:00 and the clip's duration, and an
empty track invites the drag from inside itself, replacing the orphaned line
that used to float below it. The selection sentence moved into the shared
module (selectionSummary) so the fact column and the kernel's canonical form
speak identically. cf. #769
max-w-md/max-h-56 to max-w-2xl/max-h-84 — the same 3:2 cap, 50% larger; the
cut column still sits beside it at wide viewports and the track keeps the
panel's full width. cf. #769
Accepted redesign: selection is discrete — a drag paints second cells, a
range starts on an exact second and ends on one (the clip's partial final
second is the one shorter cell), and handles and arrows walk boundaries.
Faint per-second ticks make the cells legible, and only the marker seconds
are labelled under the track, as plain numbers — no ruler. The playhead
clamps to the track's inside, where 100% used to paint it on the border
outside the rounded box. cf. #769
Three pieces from review. Playback started anywhere — the track or the
player's own controls — now stops at the end of the clip it began inside:
the boundary is armed by the play event itself, held in a ref the throttled
timeupdate reads synchronously, and the stop snaps the indicator back onto
the clip's edge instead of overshooting past it. The taller bar fills with
exactly as many sampled frames as fit its width (slot = bar height x the
clip's own aspect, one frame per slot center, rebuilt on real resizes), the
movie-maker strip. And a selection now reads against that imagery: the
unselected stretches wash out under a theme-aware scrim while a selected
clip stays full-color inside a full-height two-pixel primary frame — the
tint that vanished into the thumbnails is gone. cf. #769
Rebase adaptation: the clip timeline draws lucide's X — the Tabler package
left the dependency tree and the icon gate retires it — and its preview test
arms the play boundary through the testing library's own event helper, since
the boundary gate now refuses a hand-built synthetic DOM event as an API.
The b2iH tokens the component speaks (primary, muted, border, background,
ring) survive the preset move unchanged. cf. #769
@YaelAnaya
YaelAnaya merged commit dd82ab6 into main Aug 25, 2026
15 checks passed
@YaelAnaya
YaelAnaya deleted the feat/clip-ranges branch August 25, 2026 09:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Video ingest by clip ranges: timeline selection, range-scoped extraction

2 participants