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: MusicVideoProject and the schema dataclasses.

  • Hosted productions (muvid.Project, needs nw): a music video or a lyric video a host (the reelee studio) places and serves — an nw.Project whose footage operations are muvid.footage.service.

  • Visualizer (muvid.visualize, needs only ffmpeg + 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, description='')[source]

Bases: object

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, description='')[source]

Bases: object

Pointer to an environment folder under environments/<name>/.

class muvid.MusicVideoProject(root)[source]

Bases: object

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, *, title='', song_path=None, copy_song=True, exist_ok=False)[source]

Create a fresh project directory.

If song_path is given, the audio is copied (or moved if copy_song=False) into song/, probed for duration, and registered in project.json.

Return type:

MusicVideoProject

log_decision(kind, **payload)[source]

Append a one-line JSON entry to .muvid/decisions.jsonl.

Return type:

None

set_song(source, *, copy=True)[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 in project.json.

Return type:

SongInfo

update_spec(**changes)[source]

Read, replace, write. Returns the new spec.

Return type:

ProjectSpec

class muvid.ProjectSpec(*, schema_version=1, title='', song=None, characters=(), environments=(), sections=(), shots=(), global_style='', notes='')[source]

Bases: object

The top-level project SSOT, persisted as project.json.

class muvid.SectionSpec(*, id, start_s, end_s, label='', energy='', mood='')[source]

Bases: object

A non-overlapping span of the song with a label.

label is free-form (“intro”, “verse”, “chorus”, “bridge”, “outro”) so users can use whatever taxonomy fits their song.

class muvid.ShotSpec(*, id, start_s, end_s, section_id='', render_strategy='image_to_video', environment='', characters=(), description='', camera='', framing='medium', notes='')[source]

Bases: object

A timeline-locked visual unit of the music video.

[start_s, end_s) is half-open. Shots within a project are sorted by start_s and 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, duration_s, sample_rate=0, bitrate=0, bpm=None)[source]

Bases: object

Metadata for the master audio file.

muvid.align_lyrics(root, *, aligner='scribe-greedy', **aligner_kwargs)[source]

Build lyrics/alignment.annot from transcript + lyrics.md.

aligner selects the alignment strategy (see muvid.align.list_aligners()); extra kwargs are forwarded to the chosen aligner. Defaults to scribe-greedy.

Returns the path to the alignment store.

Return type:

str

muvid.default_project_root(name)[source]

Where a project called name belongs by default, as an absolute path.

root is a required positional on every other verb here and on muvid.project.MusicVideoProject, deliberately — a pipeline that guessed which project it was operating on would be worse than one that asks. But required everywhere left no code answering where a NEW project should go, so the only written-down answer lived in prose: the muvid skill and the README both said ~/muvid/<song-stem>, which put a real project’s 36 MB in an app-named directory under $HOME that nothing owns. This is that answer in code, where it can be tested and where callers — the skill, the README, the CLI, a script — can ask instead of restating it. It creates nothing.

The CLI’s serve is the one exception to “required”: it defaults root to the cwd. See muvid/paths.py — the default is right, and it was the UI’s missing is-this-a-project precondition that made it write.

Return type:

str

muvid.init_project(root, *, title='', song=None)[source]

Create a new music video project. Returns the absolute root path.

Return type:

str

muvid.render(root, *, quality='balanced', force=False, budget=None, allow_unpriced=False)[source]

Render every shot. Returns the produced mp4 paths.

budget (USD): when set, refuses to start if estimate_render_cost() exceeds it — **or if any part of the project could not be priced at all**. Pass None (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 to total_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 --budget unusable 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.

Return type:

list[str]

muvid.render_audio_video(audio, image=None, *, visual='auto', saveas=None, size=(1920, 1080), fps=24, title=None, layout=None, title_style=None, normalize=False, loudness=None, crf=18, preset='medium', audio_bitrate='384k', gop_seconds=2.0, options=None, workdir=None)[source]

Render audio into a video, using visual for the picture.

The video is exactly as long as the audio, 16:9, H.264/yuv420p + AAC — what YouTube asks for. With normalize=True the audio is brought to a fixed EBU R128 loudness with a two-pass loudnorm, which is what makes a batch of songs play back at a consistent level.

Parameters:
  • audio (str | Path) – The song (.wav is preferred when you have it — YouTube re-encodes regardless, so give it the cleanest input).

  • image (str | Path | None) – Cover art. Used for the picture, and composed onto a 16:9 canvas.

  • visual (Union[str, Callable[[VisualContext], VisualPlan | Path | str]]) – A registered strategy name ("still", "ken_burns", "cqt", "bars", "spectrum", "waves", "scope"), "auto", or any callable (see muvid.visualize.visuals).

  • saveas (str | Path | None) – Output path (default: <audio-stem>.mp4).

  • size (tuple[int, int]) – Canvas size; the default is 1080p.

  • fps (int) – Frame rate.

  • title (str | None) – Burn this title into the frame.

  • layout (CoverLayout | None) – How the cover sits on the canvas.

  • title_style (TitleStyle | None) – How the title is drawn.

  • normalize (bool) – Loudness-normalize the audio (two-pass EBU R128).

  • loudness (Loudness | None) – The loudness target; a YouTube-appropriate default is used when omitted.

  • gop_seconds (float) – Encoder knobs.

  • options (dict | None) – Strategy-specific options, passed to the visual.

  • workdir (str | Path | None) – Where intermediates go (a temporary directory by default).

Return type:

RenderResult

Returns:

A RenderResult.

Raises:

ValueError – size has an odd dimension — H.264 at yuv420p (the only pixel format every player decodes) cannot encode one.

muvid.status(root)[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.

Return type:

dict

muvid.transcribe_song(root, *, api_key=None)[source]

Run ElevenLabs Scribe on the project’s song.

Writes the raw response to lyrics/transcript.json and a draft lyrics/lyrics.md with auto-detected line breaks. The user is expected to edit lyrics.md to fix mishears and add real section tags. Returns the path to the lyrics markdown.

Return type:

str

Modules

align

Lyric → audio alignment.

characters

Character cards + reference image curation via lookbook.

choreo

Choreo — event-driven visual music, muvid's second subgenre plugin.

compose

Compose all rendered shots into the final music video.

contracts

Adapters between muvid's SSOT and sibling-package shapes.

cost

Cost estimation for a muvid project.

data

environments

Environment cards + canonical establishing image generation.

events

Surface fal progress events into the muvid project.

facade

Top-level facade — the verbs the CLI / skill / UI all call.

footage

Footage-aligned music video — align several device recordings of one song, assemble.

importing

Bring a finished production into a host's projects dir as a muvid.Project.

lyrics

Lyrics — transcription and markdown round-trip.

lyricvid

Lyric video (kinetic typography) — muvid's first subgenre plugin.

mcp

muvid MCP server — the music-visualizer tool surface for a remote connector.

montage

Montage — a beat-cut video from a pool of stills and clips (subgenre plugin).

paths

The ONE place muvid decides where its data lives.

project

Project facade — folder layout, persistence, and a small dol-backed mall.

renderers

Render dispatch — turn a single ShotSpec into shots/<id>/output.mp4.

schema

Schema for an muvid project — the SSOT data shapes.

script

Script (screenplay) markdown ↔ ShotSpec list.

subgenres

muvid subgenres — the plugin surface for another kind of video.

ui

Minimal local web UI for an muvid project.

visualize

Turn a song (+ optional cover) into a visualizer music video.