muvid.align

Lyric → audio alignment.

We have: - a transcript (Scribe / faster-whisper) with word-level (text, start, end) - a user-edited LyricsDoc with section labels + line text + optional

manual line-start anchors

We want a lacing store with three tiers (sections, lines, words) so the rest of the system can ask “which lines fall in shot X” without re-implementing interval math.

Strategy (greedy token-match):

  1. Tokenize each lyric line into normalized words.

  2. Walk the transcript word stream once, assigning each transcript word to the next unmatched lyric word that matches (case- and punctuation-insensitive). Tolerate small mismatches (transcript word missing in lyrics, vice versa) with a small lookahead window.

  3. From the matched words, derive line [start, end] as (first_matched_word.start, last_matched_word.end). If a line has no matched words, fall back to the user’s manual anchor (if any), then to a linear interpolation between neighboring anchored lines.

  4. Sections inherit [start, end] from the union of their lines; if the user provided explicit start_s / end_s on a section, those win.

The result is written as a lacing.SqliteStore so it round-trips and can be edited by other tools.

muvid.align.AlignerName

Built-in aligner names. align_lyrics(aligner=...) accepts these plus any name added later via register_aligner().

class muvid.align.AlignerSpec(*, name: str, description: str, fn: object, requires: tuple[str, ...] = ())[source]

One row in the aligner registry.

class muvid.align.AlignmentResult(*, sections: 'tuple[SectionAlignment, ...]')[source]
lines_in(start_s: float, end_s: float) list[LineAlignment][source]

Lines that fall (at least partially) inside [start_s, end_s].

class muvid.align.LineAlignment(*, line_index: 'int', section_label: 'str', text: 'str', start_s: 'float | None', end_s: 'float | None', word_alignments: 'tuple[WordAlignment, ...]')[source]
class muvid.align.SectionAlignment(*, label: 'str', title: 'str', start_s: 'float | None', end_s: 'float | None', lines: 'tuple[LineAlignment, ...]')[source]
class muvid.align.WordAlignment(*, line_index: int, token_index: int, text: str, start_s: float, end_s: float, confidence: float = 1.0)[source]

One alignment between a lyric token and a transcript word.

muvid.align.align_lyrics(lyrics: LyricsDoc, transcript: dict, *, duration_s: float = 0.0, aligner: str = 'scribe-greedy', **aligner_kwargs) AlignmentResult[source]

Align a LyricsDoc to a transcript.

Parameters:
  • lyrics – User-edited lyrics document (the ground-truth text).

  • transcript – Aligner-specific input. For "scribe-greedy" this is a Scribe / faster-whisper response with words: [...]. For "user" this can be empty if you pass user_line_timings=.... For "whisperx-lite" the transcript is ignored — the aligner runs on the audio directly (path passed via audio_path=).

  • duration_s – Used to extrapolate end times for lines with no matched words and no later anchor.

  • aligner – Name of a registered aligner. See list_aligners().

  • **aligner_kwargs – Forwarded to the aligner.

Returns:

AlignmentResult.

muvid.align.align_scribe_greedy(lyrics: LyricsDoc, transcript: dict, *, duration_s: float = 0.0, lookahead: int = 6) AlignmentResult[source]

Greedy token-match against a word-timestamped transcript.

Cheap, network-only (assumes the transcript came from Scribe or similar). Tolerates 1-character mishears between sung and written text. duration_s is used only when extrapolating end times.

muvid.align.align_stars(lyrics: LyricsDoc, transcript: dict, *, duration_s: float = 0.0, **kwargs) AlignmentResult[source]

Singing-specific alignment (STARS / similar). Not yet implemented.

See misc/docs/alignment_references.md for the literature motivating this aligner. The slot exists so callers can already write aligner="stars" and get a clear NotImplementedError.

muvid.align.align_user_provided(lyrics: LyricsDoc, transcript: dict, *, duration_s: float = 0.0, user_line_timings: list[dict] | None = None) AlignmentResult[source]

Use line-level timings the caller has already determined.

Useful when the user has hand-anchored every line, or when an external aligner has produced line_index (start, end) timings and you don’t want any token-matching.

user_line_timings is a list of {"line_index": int, "start_s": float, "end_s": float}. If omitted, this aligner falls back to the manual anchors already on lyrics (the // 12.5 end-of-line markers in lyrics.md).

muvid.align.align_whisperx_lite(lyrics: LyricsDoc, transcript: dict, *, duration_s: float = 0.0, audio_path: str | Path | None = None, model_size: str = 'tiny', lookahead: int = 6) AlignmentResult[source]

Local-only aligner that re-uses faster-whisper (no API).

The transcript argument is ignored when audio_path is given: we re-transcribe locally with faster-whisper and then run the same greedy match used by scribe-greedy. When audio_path is not given, we fall through and just use the supplied transcript.

Trade-offs vs scribe-greedy: free + offline; slower; less accurate on singing; needs torch + faster-whisper installed.

muvid.align.list_aligners() list[str][source]

Return all registered aligner names, sorted.

muvid.align.register_aligner(name: str, fn, *, description: str, requires: tuple[str, ...] = ()) None[source]

Register an aligner under name.

The function should accept (lyrics, transcript, *, duration_s, **kw) -> AlignmentResult. Extra keyword arguments forwarded by align_lyrics() are passed through.

muvid.align.write_alignment_store(alignment: AlignmentResult, *, path: str | Path, asset_id: str = 'song:audio', rate: int = 1000) None[source]

Write alignment to a lacing.SqliteStore file (.annot).

Uses lacing.tracks.subtitle.SubtitleBuilder for the standard (sections, lines, words) tier set.