muvid.footage.scoring.grid¶
The song-time score grid: ScoreTrack, resample-to-grid, robust normalization,
the fused ScoreTensor, and crash-consistent persistence.
The keystone of the footage-scoring layer (thorwhalen/muvid#13): every clip’s every metric
is resolved onto ONE fixed-rate song-time grid (t0=0, hop_s≈0.1 → ~10 Hz), so tracks
stack into a tensor S[clip, frame, metric] that BOTH the auto composer (the Viterbi
selector) and the Phase-2 multichannel editor read. Frame k ↔ song time t0 + k*hop_s,
identical across clips.
Design decisions (see misc/docs/footage_scoring_design.md — LOCKED post-critique):
Raw is the SSOT. Extractors emit
raw_values+ a coveragemask(NaN where masked). Normalization is per-metric-global across all clips (robust median/IQR, percentile-clipped), so it can only be computed once every clip’s raw track for a metric is in hand — it is a tensor-assembly step, not a per-extractor one. The normalizedvaluesare derived fromraw+ the storednormparams, so re-normalizing is free and the editor can show raw and normalized.NaN never reaches a serializer. The manifest stores
nullfor an all-masked metric’s norm params; the MCP wire maps masked entries tonulland relies on themaskarray.The manifest is the SSOT for grid geometry (
t0/hop_s/n); a per-track geometry mismatch is a load-time assertion, never a silent 2× misalignment.Persistence is crash-consistent: each clip’s
.npzis written to a temp thenos.replaced;manifest.jsonis written LAST via tmp+rename, so a reader sees either the whole prior state or the whole new one. The dance itself ismuvid.footage.workspace.atomic_write_bytes— the ONE implementation the project manifest, the alignments and the render metadata share (muvid#17 item 4); this module used to carry its own copy, which is how the other records came to have none.
Pure numpy — no cv2/torch/ffmpeg here. Imported only under the muvid[scoring] extra
(never on the import-light muvid.genre_music_video path).
Module Attributes
Default grid step (seconds) → 10 Hz. |
Functions
|
A stable, ORDER-INDEPENDENT fingerprint of a set of alignments (offsets + durations). |
|
Map raw values to a robust [0,1] score (higher = better), preserving NaN. |
|
Assemble per-clip |
|
Per-metric-GLOBAL robust norm params from every clip's raw track for one metric. |
|
Number of grid frames spanning |
|
|
|
Reconstruct the |
|
Whether persisted scores match the CURRENT song + alignments (else they are stale). |
|
Resample irregular |
|
Persist per-clip raw arrays + a manifest, crash-consistently. |
|
|
|
Classes
|
The fused |
|
One |
- muvid.footage.scoring.grid.DEFAULT_HOP_S = 0.1¶
Default grid step (seconds) → 10 Hz. Ample for a UI and beat-level selection.
- class muvid.footage.scoring.grid.ScoreTensor(clip_ids, metrics, t0, hop_s, n, S, M, raw, norms)[source]¶
Bases:
objectThe fused
S[clip, frame, metric](normalized) +M[clip, frame]mask.Scarries NaN where masked (never used for reward — the selector readsM).rawkeeps the un-normalized values for the editor/tooltips. Geometry (t0/hop_s/n) is authoritative here (validated against the manifest at load).
- class muvid.footage.scoring.grid.ScoreTrack(clip_id, metric, t0, hop_s, raw_values, mask, direction='higher_better')[source]¶
Bases:
objectOne
(clip, metric)curve on the shared song-time grid.Extractors produce
raw_values(NaN where masked) +mask;directionsays whether higher raw = better ("higher_better") or lower = better ("lower_better", e.g. a distance like LSE-D — inverted at normalization). The normalizedvaluesare NOT stored here; they are derived at tensor assembly fromraw_values+ the per-metric globalnormparams (seeapply_norm()).
- muvid.footage.scoring.grid.align_fingerprint(alignments)[source]¶
A stable, ORDER-INDEPENDENT fingerprint of a set of alignments (offsets + durations).
The SSOT (shared by the scoring job’s idempotency key AND the read-time staleness guard): a re-align that moves any offset changes this fingerprint, so persisted scores computed against the old offsets are detected as stale. Sorting the full triples (not just by clip_id) makes it independent of alignment order.
- Return type:
- muvid.footage.scoring.grid.apply_norm(raw, norm, *, direction)[source]¶
Map raw values to a robust [0,1] score (higher = better), preserving NaN.
z = (raw − median)/IQR(IQR==0 → neutral 0.5), clipped to[p5,p95]mapped to [0,1];lower_bettermetrics are inverted so the output is always higher_better.- Return type:
ndarray
- muvid.footage.scoring.grid.build_tensor(tracks, *, t0, hop_s, n, metrics=None, clip_ids=None, norms=None)[source]¶
Assemble per-clip
ScoreTracklists into a normalizedScoreTensor.tracksmapsclip_id -> [ScoreTrack, ...]. The metric axis is the union of metrics present (or the explicitmetricsorder); a clip missing a listed metric contributes an all-masked column (so the tensor is rectangular and a missing extractor never shifts columns). Normalization is per-metric-global (computed here ifnormsis not supplied). Every track’s geometry must match(t0, hop_s, n)— a mismatch raises (the SSOT rule).- Return type:
- muvid.footage.scoring.grid.compute_norm(raw_tracks, masks, *, direction)[source]¶
Per-metric-GLOBAL robust norm params from every clip’s raw track for one metric.
Pools all valid (unmasked, finite) raw values across clips →
{median, iqr, p5, p95}. ReturnsNoneif there are no valid values (an all-masked metric) — the caller storesnullandapply_norm()then yields all-NaN (never a divide-by-zero or a NaN in the manifest).
- muvid.footage.scoring.grid.grid_len(song_duration, hop_s=0.1)[source]¶
Number of grid frames spanning
[0, song_duration]athop_s.- Return type:
- muvid.footage.scoring.grid.load_tensor(project_root)[source]¶
Reconstruct the
ScoreTensorfrom persisted arrays + manifest, orNone.Geometry + norms come from the manifest (SSOT); each clip’s
.npzsupplies raw+mask. A clip/metric absent from a.npzis filled as an all-masked column.- Return type:
- muvid.footage.scoring.grid.manifest_is_current(manifest, *, song_hash, align_fingerprint)[source]¶
Whether persisted scores match the CURRENT song + alignments (else they are stale).
Guards the read path: a re-align (or a new song) that raced the score job’s rmtree, or a manifest predating the fingerprint, is detected here so no stale/mislabeled scores are served to the editor or the weighted selector.
- Return type:
- muvid.footage.scoring.grid.resample_to_grid(sample_times, sample_values, *, t0, hop_s, n, max_gap_s=None)[source]¶
Resample irregular
(song_time, value)samples onto the fixed grid.Vectorized (O(n_samples + n)): samples are mean-binned onto the grid, then linearly interpolated across bins that have no sample — but ONLY within the sampled span and ONLY across gaps ≤
max_gap_s(so a real coverage gap stays masked, never invented). Outside[first_sample, last_sample]the grid is masked (no extrapolation).- Parameters:
sample_times (
Sequence[float]) – song-time (s) of each sample (any order; NaN values dropped).sample_values (
Sequence[float]) – the sample values (parallel tosample_times).t0/hop_s/n – the grid geometry (frame k ↔
t0 + k*hop_s, k in[0, n)).max_gap_s (
float|None) – bridge gaps up to this many seconds (default4*hop_s).
- Return type:
tuple[ndarray,ndarray]- Returns:
(values, mask)—valuesfloat32[n] (NaN where masked),maskbool[n].
- muvid.footage.scoring.grid.save_scores(project_root, tracks, *, t0, hop_s, n, metrics, song_hash, align_fingerprint='', beat_times=(), downbeat_times=(), extra=None)[source]¶
Persist per-clip raw arrays + a manifest, crash-consistently.
Each clip →
scores/{clip_id}.npz(raw + mask per metric), written tmp+``os.replace``. The per-metric globalnormparams + grid geometry + beats +song_hashgo inscores/manifest.json, written LAST via tmp+rename — so a concurrent reader sees a whole consistent state or the prior one, never a torn mix. NaN never enters the manifest (all-masked metric →nullnorm).- Return type: