muvid.lyricvid.spec¶
The treatment spec — what a director (human or model) decides, as data.
This is the SSOT both runtimes share: the Claude Code skill and the production LLM call produce this, and every renderer consumes this. It is stdlib-only and import-safe on purpose.
The shape is two layers, and the split is the single most important decision in the whole subgenre:
directionWHAT and WHY — mood, palette, typography, motion vocabulary, and a rationale a human can read when choosing between options. This is the part a model is genuinely good at.
scenesHOW — an archetype from a closed set, plus parameters. Never raw geometry, never raw timings.
Three rules follow, and they are not stylistic:
No coordinates from the model. Every archetype computes its own geometry in Python. The Visual Lyrics authors reached this the hard way and reported that LLM-generated bounding boxes “often result in layouts with misalignment and overlap issues”; the archetype removes the need to generate one at all.
No timings from the model. Word onsets, the beat grid and section bounds come from muvid’s aligner. The model picks a quantisation policy (
Quantize) and Python applies it. This kills the entire class of “the words drift out of sync” bugs by construction.Closed vocabularies everywhere. Archetypes, motion families, easings and cut styles are enums. Closed sets are what make the schema constrainable, two options diffable, and a renderer total rather than best-effort.
An invalid spec is not an error to hand back to a model when it can be
projected onto the valid space instead — see repair(). Deterministic
repair is cheaper, faster and more predictable than a retry, and it means a
slightly-wrong model output still renders.
Module Attributes
How words are placed on screen. |
|
How a single word arrives. |
|
What a word does when it is no longer current. |
|
What the animation clock is quantised to. |
|
Where a shape outline may come from. |
|
The shape kinds that carry no reference to anything outside the spec. |
Functions
|
Take whatever a caller or a model produced and return a renderable spec. |
The JSON Schema for a |
|
|
Project |
|
Return a list of human-readable problems. |
Every closed vocabulary, for prompts and UI. |
Classes
|
The song-level creative decision. |
|
Colours, as |
|
One treatment, applied to part of the song. |
|
Where a |
|
The quantisation POLICY. |
|
A complete, renderable treatment. |
|
Type choices. |
- muvid.lyricvid.spec.ARCHETYPES: dict[str, str] = {'calligram': "Each line becomes a slanting streak of UPRIGHT letters, one letter per slot, the streaks fanning open as they descend — the Apollinaire 'Il pleut' construction. Use for a calligram or concrete poem whose shape is made by the run of the text itself rather than by an outline; prefer 'shape_fill' when the shape is a picture the words pour into, and 'concrete_page' when the layout is simply lines on a page.", 'concrete_page': "The whole lyric is typeset as a fixed page — one CENTRED HORIZONTAL ROW per line — and each word ignites in reading order as it is sung. The page never reflows. Use when the poem is lines on a page. It cannot slant, indent or shape anything: for a calligram or a concrete poem whose picture is made by the run of the text, use 'calligram'; for words poured into an outline, use 'shape_fill'.", 'glyph_pages': "A few lines per page, every CHARACTER shown dim from the page's start and each lighting up at the moment its own sound is sung. For learning to read a script (kana, an alphabet) along with a song. Needs per-glyph times (muvid.lyricvid.glyph_align); without them a word's characters are spread over the word and marked unmeasured. params: lines_on_screen, size, emphasis (the target characters, highlighted strongly; others get milder grey and highlight), fold_marks.", 'karaoke_wipe': 'Two lines at the bottom, the current one wiped syllable by syllable as it is sung. The classic karaoke treatment; the most legible option.', 'one_word_centred': 'One word at a time, large, centred. The default lyric-video look: unmissable, works at any aspect ratio, reads on a phone.', 'scatter': 'Words appear away from centre and drift, density rising with energy. Use for chaos, crowds, or an instrumental-heavy chorus.', 'shape_fill': 'Words packed into the outline of a shape, filling it as the song proceeds. Use when the song has one strong concrete image.', 'stacked_lines': 'Lines accumulate down the frame and hold, so the viewer can read back what has already been sung. Good for narrative or dense lyrics.', 'text_on_path': 'Words follow a curve across the frame. Cheap, distinctive, and good for a single repeated hook.'}¶
How words are placed on screen. The renderer owns the geometry; the spec only names the family and its knobs.
- class muvid.lyricvid.spec.Direction(*, mood='', palette=<factory>, typography=<factory>, motion_vocabulary=('fade', ), rationale='')[source]¶
Bases:
objectThe song-level creative decision. The half a model is actually good at.
- muvid.lyricvid.spec.INLINE_SHAPE_KINDS: frozenset[str] = frozenset({'named', 'svg_path'})¶
The shape kinds that carry no reference to anything outside the spec. A surface serving untrusted callers (the MCP tools) admits ONLY these unless it has itself fetched and scoped the image.
- muvid.lyricvid.spec.MOTIONS: dict[str, str] = {'cut': 'Appears instantly. Hardest, most rhythmic.', 'fade': 'Fades up over a fraction of a beat.', 'pop': 'Fades up with a slight overshoot in scale, then settles.', 'rise': 'Fades up while moving a short distance upward.', 'typewriter': "Letters appear one at a time across the word's duration.", 'wipe': 'Revealed left-to-right, like a karaoke wipe.'}¶
How a single word arrives. Composable with the archetype rather than part of it.
- muvid.lyricvid.spec.PERSISTENCE: dict[str, str] = {'clear_on_line': 'Cleared when its line ends.', 'clear_on_section': 'Cleared when its section ends.', 'dim': 'Stays but recedes, so the current word leads. Keeps context readable.', 'hold': 'Stays exactly as it arrived, forever. The page fills up.'}¶
What a word does when it is no longer current.
- class muvid.lyricvid.spec.Palette(*, bg='#101014', fg='#f4f4f0', accent='#e0533d', dim='#4a4a52')[source]¶
Bases:
objectColours, as
#rrggbb.dimis the un-sung state where one exists.
- muvid.lyricvid.spec.QUANTIZE: dict[str, str] = {'beat': 'Snapped to the nearest beat. Rhythmic, forgiving of alignment error.', 'downbeat': 'Snapped to the nearest bar start. Slow, deliberate.', 'line': "The whole line arrives together, at the line's start.", 'syllable': 'Sub-word timing, where the aligner provides it.', 'word': 'Each word ignites at its own measured onset. Tightest sync.'}¶
What the animation clock is quantised to. The MODEL picks one of these; the numbers behind them always come from measurement, never from the model.
- muvid.lyricvid.spec.SHAPE_KINDS: dict[str, str] = {'mask_image': 'An image whose dark ink (or alpha) is the outline. Trusted callers only: the value names a file.', 'named': 'A built-in outline: circle, heart, star, apple, square.', 'svg_path': 'An SVG path string (M/L/H/V/C/S/Q/T/Z) supplied inline.'}¶
Where a shape outline may come from. A closed set for the same reason the others are — and additionally because
valueis INTERPRETED by the renderer, which makeskinda trust boundary:mask_imagenames a file to open, and a treatment that arrives from a remote caller must not be able to point that at a host path. The spec only admits the kinds; where amask_imagevalue is allowed to come FROM is the caller-facing surface’s decision (seeMASK_IMAGE_TRUSTED).
- class muvid.lyricvid.spec.Scene(*, applies_to=('*', ), archetype='one_word_centred', motion='fade', persistence='clear_on_line', timing=<factory>, shape=None, params=<factory>)[source]¶
Bases:
objectOne treatment, applied to part of the song.
applies_tonames sections by label (as the lyrics document labels them), or"*"for the whole song. Sections muvid found but the spec does not mention fall back to the first"*"scene, so a one-scene spec is valid and complete.
- class muvid.lyricvid.spec.ShapeRef(*, kind='named', value='circle')[source]¶
Bases:
objectWhere a
shape_fill/concrete_pageoutline comes from.kind='named'uses a built-in outline;kind='mask_image'traces an image the caller supplied;kind='svg_path'takes a path directly. The model may name a shape, but it never draws one.
- class muvid.lyricvid.spec.Timing(*, quantize_to='word', cut_style='hard', attack_s=0.12, lead_s=0.0)[source]¶
Bases:
objectThe quantisation POLICY. Never actual times.
- class muvid.lyricvid.spec.TreatmentSpec(*, spec_version='1.0', title='', direction=<factory>, scenes=<factory>)[source]¶
Bases:
objectA complete, renderable treatment.
>>> s = TreatmentSpec() >>> validate(s) [] >>> TreatmentSpec.from_dict(s.to_dict()) == s True- classmethod from_dict(d)[source]¶
Build from a plain mapping — TOTAL over what a model or a remote caller sends.
Missing keys default. Wrong-shaped values are coerced toward the field’s type rather than raised on: a string where a list was expected becomes a one-element list (
applies_to: "chorus"used to become('c','h','o','r','u','s')), a numeric string becomes a number, an unknown key is dropped, aNonesub-object is the default. What cannot be coerced falls to the default andrepair()reports the enum-level substitutions afterwards.- Return type:
>>> s = TreatmentSpec.from_dict({'scenes': [None, {'applies_to': 'chorus', ... 'timing': 'fast', 'shape': 'circle'}], ... 'direction': {'palette': {'foo': 1, 'bg': '#000000'}, ... 'motion_vocabulary': 'pop', ... 'typography': {'weight': '700', 'tracking': None}}}) >>> s.scenes[1].applies_to, s.direction.motion_vocabulary (('chorus',), ('pop',)) >>> s.direction.typography.weight, s.direction.typography.tracking (700, 0.0) >>> s.direction.palette.bg, s.scenes[1].shape ('#000000', None)
- class muvid.lyricvid.spec.Typography(*, family='DejaVu Sans', weight=700, case='as_written', tracking=0.0, max_line_chars=28)[source]¶
Bases:
objectType choices.
familyis resolved against installed/bundled fonts, and an unavailable family falls back rather than failing the render.
- muvid.lyricvid.spec.coerce(obj)[source]¶
Take whatever a caller or a model produced and return a renderable spec.
Accepts a
TreatmentSpec, a mapping, or a JSON string — and repairs it. This is the one entry point production code should use.- Return type:
- muvid.lyricvid.spec.json_schema()[source]¶
The JSON Schema for a
TreatmentSpec.>>> s = json_schema() >>> s['properties']['scenes']['items']['properties']['archetype']['enum'][0] 'one_word_centred'
- muvid.lyricvid.spec.repair(spec)[source]¶
Project
speconto the valid space. Returns(spec, notes).A model that names a motion that doesn’t exist has made a small, mechanical mistake; projecting it onto the nearest legal value renders something good now, where a retry costs a round trip and may fail the same way. Every substitution is reported so the caller can show or log it.
- Return type:
>>> fixed, notes = repair(TreatmentSpec(scenes=(Scene(archetype='swirl'),))) >>> fixed.scenes[0].archetype 'one_word_centred' >>> notes ["scenes[0].archetype 'swirl' -> 'one_word_centred'"]