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):
Tokenize each lyric line into normalized words.
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.
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.Sections inherit
[start, end]from the union of their lines; if the user provided explicitstart_s/end_son 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 viaregister_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
LyricsDocto 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 withwords: [...]. For"user"this can be empty if you passuser_line_timings=.... For"whisperx-lite"the transcript is ignored — the aligner runs on the audio directly (path passed viaaudio_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:
- 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_sis 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.mdfor the literature motivating this aligner. The slot exists so callers can already writealigner="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_timingsis a list of{"line_index": int, "start_s": float, "end_s": float}. If omitted, this aligner falls back to the manual anchors already onlyrics(the// 12.5end-of-line markers inlyrics.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
transcriptargument is ignored whenaudio_pathis given: we re-transcribe locally with faster-whisper and then run the same greedy match used byscribe-greedy. Whenaudio_pathis not given, we fall through and just use the suppliedtranscript.Trade-offs vs
scribe-greedy: free + offline; slower; less accurate on singing; needs torch + faster-whisper installed.
- 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 byalign_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.SqliteStorefile (.annot).Uses
lacing.tracks.subtitle.SubtitleBuilderfor the standard(sections, lines, words)tier set.