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.
ffmpegis already a hard system requirement of muvid, and itssubtitlesfilter is libass. The only new Python dependency ispysubs2(MIT, pure Python, zero dependencies of its own), behind thelyricvidextra.The intermediate is a deliverable. The
.assfile 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
What the result's |
|
The single ASS style every event references. |
|
Families to try, in order, when the treatment's family is not installed. |
|
|
Functions
|
Register how one |
The motion families this renderer can draw. |
|
|
Render a Scene as an ASS (Advanced SubStation Alpha) document. |
|
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>}¶
motionname -> 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 inmuvid.lyricvid.spec.MOTIONS, never a branch in the writer.
- muvid.lyricvid.render_ass.RENDERER_NAME = 'lyricvid.render_ass'¶
What the result’s
metareports 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/\coverrides, 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.
Examples
>>> list_motions() ['cut', 'fade', 'pop', 'rise', 'typewriter', 'wipe']
- muvid.lyricvid.render_ass.register_motion(name)[source]¶
Register how one
motionfamily draws itself.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 flatcolorsource 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:
- Returns:
A
RenderResultcarrying the mp4, its measured duration, the.assunderartifacts['ass'], and ametarecording 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
subtitlesfilter (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/PlayResYare 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:
- Return type:
- Returns:
The
.assdocument, 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
\posmeans whatCuesays 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 TrueColours reverse to
&HBBGGRR&— red is&H0000FF&, not&HFF0000&:>>> r'\c&H0000FF&' in doc True >>> r'\c&HFF0000&' in doc Falsesizeis 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')