muvid.lyricvid.render_ass

The default lyric-video renderer: a Scene as ASS.

One Scene in, an .ass subtitle document and a burnt-in mp4 out. This is the default backend for the lyric-video subgenre for three reasons, in the order they mattered:

  • It is frame-exact. libass positions and animates text against the video’s own clock, so a word that the aligner measured at 12.34 s is drawn at 12.34 s — not at “whatever frame the compositor got to”.

  • It adds no non-Python dependency. ffmpeg is already a hard system requirement of muvid, and its subtitles filter is libass. The only new Python dependency is pysubs2 (MIT, pure Python, zero dependencies of its own), behind the lyricvid extra.

  • The intermediate is a deliverable. The .ass file is a plain text document a human can open, retime and restyle, then re-burn — so a render that is 95% right is editable rather than a reason to re-run the pipeline.

The whole of the renderer’s opinion lives in one table, Cue -> ASS:

Fonts are resolved through fontconfig when it is available: a family the machine does not have falls back (to the next of FONT_FALLBACKS) rather than failing the render, and the substitution is recorded in the result’s meta so a caller can see that the type is not what the treatment asked for.

Import-safe: stdlib plus the two stdlib-only muvid modules it types against. pysubs2, ffmpeg and the verifier are imported inside the functions that need them, so importing this module costs a caller nothing.

Module Attributes

RENDERER_NAME

What the result's meta reports as the renderer that made the file.

STYLE_NAME

The single ASS style every event references.

FONT_FALLBACKS

Families to try, in order, when the treatment's family is not installed.

MOTION_FNS

motion name -> the events it draws.

Functions

register_motion(name)

Register how one motion family draws itself.

list_motions()

The motion families this renderer can draw.

scene_to_ass(scene, *[, font])

Render a Scene as an ASS (Advanced SubStation Alpha) document.

render(scene, *, audio, output, workdir[, ...])

Burn the scene over a solid background and mux the song.

muvid.lyricvid.render_ass.FONT_FALLBACKS: tuple[str, ...] = ('DejaVu Sans', 'Liberation Sans', 'Noto Sans', 'Arial', 'Helvetica', 'sans-serif')

Families to try, in order, when the treatment’s family is not installed. DejaVu first because it is what a Linux render box actually has.

muvid.lyricvid.render_ass.MOTION_FNS: dict[str, Callable[[_Placed], list[_Event]]] = {'cut': <function _motion_cut>, 'fade': <function _motion_fade>, 'pop': <function _motion_pop>, 'rise': <function _motion_rise>, 'typewriter': <function _motion_typewriter>, 'wipe': <function _motion_wipe>}

motion name -> the events it draws. muvid’s house registry idiom (register_visual, register_archetype, register_selection_strategy): a new motion is a function plus an entry in muvid.lyricvid.spec.MOTIONS, never a branch in the writer.

muvid.lyricvid.render_ass.RENDERER_NAME = 'lyricvid.render_ass'

What the result’s meta reports as the renderer that made the file.

muvid.lyricvid.render_ass.STYLE_NAME = 'Lyric'

The single ASS style every event references. One style plus per-event \fs/\c overrides, rather than a style per (size, colour) pair: a scene routinely holds hundreds of distinct sizes, and a styles table that long is unreadable to the human this file is also written for.

muvid.lyricvid.render_ass.list_motions()[source]

The motion families this renderer can draw.

Return type:

list[str]

Examples

>>> list_motions()
['cut', 'fade', 'pop', 'rise', 'typewriter', 'wipe']
muvid.lyricvid.render_ass.register_motion(name)[source]

Register how one motion family draws itself.

Return type:

Callable[[Callable[[_Placed], list[_Event]]], Callable[[_Placed], list[_Event]]]

Examples

>>> @register_motion('doctest-demo')
... def _demo(p): return []
>>> 'doctest-demo' in list_motions()
True
>>> del MOTION_FNS['doctest-demo']
muvid.lyricvid.render_ass.render(scene, *, audio, output, workdir, ass_path=None, crf=18)[source]

Burn the scene over a solid background and mux the song.

Three steps, and the middle one is the whole renderer: write the .ass, generate a flat color source at the canvas’s size and rate, and let libass draw the document onto it while the song is mapped through untouched.

The video runs for the song’s measured duration rather than the scene’s, so a treatment that stops short of the last bar still produces a video the length of the track — which is what verify_video()’s duration check compares against, and it is run here on the finished file.

Parameters:
  • scene (Scene) – The compiled scene.

  • audio (Path) – The song. Its duration sets the video’s.

  • output (Path) – Where the mp4 goes.

  • workdir (Path) – Directory for intermediates — this render owns it.

  • ass_path (Path | None) – Where to keep the subtitle document (default: workdir/<output stem>.ass). It is a deliverable, not a temp file.

  • crf (int) – x264 quality, lower is better. 18 is visually lossless for flat colour and text.

Return type:

RenderResult

Returns:

A RenderResult carrying the mp4, its measured duration, the .ass under artifacts['ass'], and a meta recording the renderer, the font actually used (and what was asked for, when they differ), the cue/event counts and any verification failure.

Raises:

FfmpegError – This ffmpeg has no subtitles filter (it is a libass build option, so a working ffmpeg is not enough), or the burn failed.

muvid.lyricvid.render_ass.scene_to_ass(scene, *, font=None)[source]

Render a Scene as an ASS (Advanced SubStation Alpha) document.

The document is built through pysubs2 (MIT, pure Python, no dependencies of its own) rather than by string-formatting the format’s header by hand: it owns the [Script Info] / [V4+ Styles] field order and the centisecond time format, and it parses back, so the output is round-trip checkable rather than merely plausible.

PlayResX/PlayResY are the canvas, so libass’s coordinates are the video’s pixels 1:1 and the same document burns correctly at 1080p or 4K only by changing the canvas it was compiled for.

Parameters:
  • scene (Scene) – The compiled scene. Every number in it is already resolved.

  • font (str | None) – Family to typeset in. Defaults to the scene’s typography; an uninstalled family falls back (see _resolve_font()).

Return type:

str

Returns:

The .ass document, as text.

Examples

>>> from muvid.lyricvid.scene import Canvas, Cue, Scene
>>> from muvid.lyricvid.spec import Typography
>>> scene = Scene(
...     canvas=Canvas(width=1920, height=1080, fps=30),
...     duration=2.0,
...     background='#101014',
...     cues=(
...         Cue(text='red', x=0.25, y=0.5, size=0.2, t_in=0.0, t_full=0.0,
...             t_out=1.0, t_gone=1.0, colour='#ff0000', motion='cut'),
...         Cue(text='fade', x=0.5, y=0.25, size=0.1, t_in=1.0, t_full=1.2,
...             t_out=1.8, t_gone=2.0, colour='#e0533d', layer=2),
...     ),
...     typography=Typography(family='DejaVu Sans'),
... )
>>> doc = scene_to_ass(scene)

The normalised centre becomes absolute pixels, and the style anchors at centre-centre (alignment 5) so \pos means what Cue says it means:

>>> r'\pos(480,540)' in doc      # 0.25*1920, 0.5*1080
True
>>> r'\pos(960,270)' in doc      # 0.5*1920, 0.25*1080
True

Colours reverse to &HBBGGRR& — red is &H0000FF&, not &HFF0000&:

>>> r'\c&H0000FF&' in doc
True
>>> r'\c&HFF0000&' in doc
False

size is a fraction of canvas height, so 0.2 of 1080 is a 216px font, and the motions emit their own tags:

>>> r'\fs216' in doc, r'\fad(200,200)' in doc
(True, True)

It parses back — the round trip is what makes the document a deliverable rather than a guess:

>>> import pysubs2
>>> subs = pysubs2.SSAFile.from_string(doc)
>>> len(subs.events), subs.events[0].style, subs.events[1].layer
(2, 'Lyric', 2)
>>> subs.info['PlayResX'], subs.info['PlayResY']
('1920', '1080')