muvid
muvid — tools to make music videos.
Two independent halves:
Narrative pipeline (needs the AI extras:
falaw,lacing,lookbook): transcribe a song, align lyrics, define characters and environments, write a shot script, render and compose. The verbs below are also the CLI. Project model:MusicVideoProjectand the schema dataclasses.Visualizer (
muvid.visualize, needs onlyffmpeg+mixing): turn a song and a cover into a still / Ken Burns / audio-reactive music video, plus a thumbnail. Deterministic, no AI, no network.>>> from muvid.visualize import render_audio_video >>> render_audio_video("song.wav", image="cover.png")
The narrative-pipeline names are imported lazily so that import muvid (and
hence import muvid.visualize) does not require the heavy AI extras — the
import of a given name only pulls its dependencies when you actually use it.
- class muvid.CharacterRef(*, name: str, description: str = '')[source]
Pointer to a character folder under
characters/<name>/.The folder contains the canonical card.json + curated reference images. We only carry the name + a quick description here so the project SSOT stays small.
- class muvid.EnvironmentRef(*, name: str, description: str = '')[source]
Pointer to an environment folder under
environments/<name>/.
- class muvid.MusicVideoProject(root: str | Path)[source]
Filesystem-backed music video project.
All write methods touch the disk immediately; readers always re-read the SSOT (no in-memory cache) so external edits are picked up.
- classmethod init(root: str | Path, *, title: str = '', song_path: str | Path | None = None, copy_song: bool = True, exist_ok: bool = False) MusicVideoProject[source]
Create a fresh project directory.
If
song_pathis given, the audio is copied (or moved ifcopy_song=False) intosong/, probed for duration, and registered inproject.json.
- log_decision(kind: str, **payload: Any) None[source]
Append a one-line JSON entry to
.muvid/decisions.jsonl.
- set_song(source: str | Path, *, copy: bool = True) SongInfo[source]
Register an audio file as this project’s song.
The file is copied (or moved) to
song/, probed for duration with ffprobe, and recorded inproject.json.
- update_spec(**changes: Any) ProjectSpec[source]
Read, replace, write. Returns the new spec.
- class muvid.ProjectSpec(*, schema_version: int = 1, title: str = '', song: SongInfo | None = None, characters: tuple[CharacterRef, ...] = (), environments: tuple[EnvironmentRef, ...] = (), sections: tuple[SectionSpec, ...] = (), shots: tuple[ShotSpec, ...] = (), global_style: str = '', notes: str = '')[source]
The top-level project SSOT, persisted as
project.json.
- class muvid.SectionSpec(*, id: str, start_s: float, end_s: float, label: str = '', energy: str = '', mood: str = '')[source]
A non-overlapping span of the song with a label.
labelis free-form (“intro”, “verse”, “chorus”, “bridge”, “outro”) so users can use whatever taxonomy fits their song.
- class muvid.ShotSpec(*, id: str, start_s: float, end_s: float, section_id: str = '', render_strategy: Literal['lipsync', 'image_to_video', 'text_to_video', 'animation', 'still'] = 'image_to_video', environment: str = '', characters: tuple[str, ...] = (), description: str = '', camera: str = '', framing: str = 'medium', notes: str = '')[source]
A timeline-locked visual unit of the music video.
[start_s, end_s)is half-open. Shots within a project are sorted bystart_sand should be non-overlapping (the validator warns otherwise — overlap can be intentional for transitions but isn’t supported by the basic compositor).
- class muvid.SongInfo(*, audio_path: str, duration_s: float, sample_rate: int = 0, bitrate: int = 0, bpm: float | None = None)[source]
Metadata for the master audio file.
- muvid.align_lyrics(root: str | Path, *, aligner: str = 'scribe-greedy', **aligner_kwargs) str[source]
Build
lyrics/alignment.annotfrom transcript + lyrics.md.alignerselects the alignment strategy (seemuvid.align.list_aligners()); extra kwargs are forwarded to the chosen aligner. Defaults toscribe-greedy.Returns the path to the alignment store.
- muvid.init_project(root: str | Path, *, title: str = '', song: str | Path | None = None) str[source]
Create a new music video project. Returns the absolute root path.
- muvid.render(root: str | Path, *, quality: str = 'balanced', force: bool = False, budget: float | None = None, allow_unpriced: bool = False) list[str][source]
Render every shot. Returns the produced mp4 paths.
budget(USD): when set, refuses to start ifestimate_render_cost()exceeds it — or if any part of the project could not be priced at all. PassNone(CLI:--budget=-1) to skip the gate entirely.allow_unpriced: proceed despite unpriceable work, after reading what it is. The gate is otherwise TWO conditions, and the second is the one muvid#47 was filed about: a price this code could not determine contributes nothing tototal_amount, so comparing the number alone let an unpriceable shot clear any budget. Unknown is not zero.The escape exists because a threshold and an approval are different things. A hard refusal with no way past it would make
--budgetunusable for a project containing one exotic model — so the default refuses, and a caller who has read the names can accept them. What it must never become is a silent default.
- muvid.render_audio_video(audio: str | Path, image: str | Path | None = None, *, visual: str | Callable[[VisualContext], VisualPlan | Path | str] = 'auto', saveas: str | Path | None = None, size: tuple[int, int] = (1920, 1080), fps: int = 24, title: str | None = None, layout: CoverLayout | None = None, title_style: TitleStyle | None = None, normalize: bool = False, loudness: Loudness | None = None, crf: int = 18, preset: str = 'medium', audio_bitrate: str = '384k', gop_seconds: float = 2.0, options: dict | None = None, workdir: str | Path | None = None) RenderResult[source]
Render
audiointo a video, usingvisualfor the picture.The video is exactly as long as the audio, 16:9, H.264/yuv420p + AAC — what YouTube asks for. With
normalize=Truethe audio is brought to a fixed EBU R128 loudness with a two-passloudnorm, which is what makes a batch of songs play back at a consistent level.- Parameters:
audio – The song (
.wavis preferred when you have it — YouTube re-encodes regardless, so give it the cleanest input).image – Cover art. Used for the picture, and composed onto a 16:9 canvas.
visual – A registered strategy name (
"still","ken_burns","cqt","bars","spectrum","waves","scope"),"auto", or any callable (seemuvid.visualize.visuals).saveas – Output path (default:
<audio-stem>.mp4).size – Canvas size; the default is 1080p.
fps – Frame rate.
title – Burn this title into the frame.
layout – How the cover sits on the canvas.
title_style – How the title is drawn.
normalize – Loudness-normalize the audio (two-pass EBU R128).
loudness – The loudness target; a YouTube-appropriate default is used when omitted.
gop_seconds (crf / preset / audio_bitrate /) – Encoder knobs.
options – Strategy-specific options, passed to the visual.
workdir – Where intermediates go (a temporary directory by default).
- Returns:
A
RenderResult.- Raises:
ValueError –
sizehas an odd dimension — H.264 at yuv420p (the only pixel format every player decodes) cannot encode one.
- muvid.status(root: str | Path) dict[source]
Return a summary dict of the project’s current state.
Useful for the skill / UI to show the user where they are in the pipeline. No side effects.
Returns a structured shape with stage progression, per-shot render status, and (when an alignment store exists) a word-confidence histogram. Stable enough to be programmatic; pass through
format_status()for human-readable text.
- muvid.transcribe_song(root: str | Path, *, api_key: str | None = None) str[source]
Run ElevenLabs Scribe on the project’s song.
Writes the raw response to
lyrics/transcript.jsonand a draftlyrics/lyrics.mdwith auto-detected line breaks. The user is expected to editlyrics.mdto fix mishears and add real section tags. Returns the path to the lyrics markdown.