tituli.video¶
Video output through ffmpeg: stills to clips, overlays onto footage, crawls.
Everything here composites rendered PNGs; ffmpeg never draws text. That is
deliberate twice over: the system ffmpeg on a developer Mac commonly lacks
drawtext/libass (this one does), and typographically drawtext is
crude. What ffmpeg is good at — overlay, fade, crop, encoding — is
all it is asked to do, and require_filters() fails loudly if even that
is missing rather than emitting a text-free video.
The overlay chain follows the shape verified in production on a found-image
film: each PNG is a looped input with its own duration (a shorter input
silently vanishes under eof_action=pass), faded on its own clock, shifted
with setpts, gated with enable=between(t, …), and the audio is
stream-copied so the finished mix is never re-encoded.
Never burn text into stills that a camera move will pan and zoom — composite
onto the finished motion video, which is what overlay() does.
Functions
|
Give every overlay a layout, rendering payloads; raise naming any that can't be. |
The ffmpeg binary: |
|
|
Names of the filters this ffmpeg build has. |
|
Raise |
|
A still held for |
|
Composite timed overlays onto |
|
Scroll a tall image up through a |
|
Encode a lazy stream of RGB frames (the kinetic path: per-glyph reveals). |
|
|
|
Whether |
|
How many audio streams |
Exceptions
ffmpeg is missing, or lacks a filter tituli needs. |
- exception tituli.video.FfmpegError[source]¶
Bases:
RuntimeErrorffmpeg is missing, or lacks a filter tituli needs.
- tituli.video.audio_streams(video)[source]¶
How many audio streams
videohas, via ffprobe.- Return type:
- tituli.video.crawl(tall, dst, *, size, speed_px_s, fps=30, audio=None, crf=18)[source]¶
Scroll a tall image up through a
sizewindow atspeed_px_s.The image should already carry its own lead-in and tail (as
tituli.credits.credits_crawl()does), so the clip starts and ends on an empty frame. Duration is(image height - window height) / speed.- Return type:
- tituli.video.ffmpeg_path()[source]¶
The ffmpeg binary:
$TITULI_FFMPEGor the first on PATH.- Return type:
- tituli.video.frames_to_video(frames, dst, *, size, fps=30, audio=None, crf=18)[source]¶
Encode a lazy stream of RGB frames (the kinetic path: per-glyph reveals).
- Return type:
- tituli.video.materialize(overlays, *, frame)[source]¶
Give every overlay a layout, rendering payloads; raise naming any that can’t be.
- Return type:
>>> from tituli.frame import Frame >>> from tituli.schedule import Label, TimedOverlay >>> f = Frame.blank((640, 360)) >>> done = materialize([TimedOverlay(None, 0, 2, payload=Label("Eliza", "Earl, 1787"))], frame=f) >>> done[0].layout is not None True
- tituli.video.overlay(video, overlays, dst, *, size=None, frame=None, delivery='youtube', workdir=None, crf=18, preset='medium', engine=None, pad_start=0.0, pad_mode='clone')[source]¶
Composite timed overlays onto
videoin one ffmpeg pass; audio copied.Every overlay is rendered: one with a
layoutas it is; one with animage(a frame-sized PIL image or path, e.g. a title page) as that picture; one with only apayload(aLabelfromschedule_labels, or a dict withtext/attribution/kind) is laid out here againstframe(default: a blank frame of the video’s size withdelivery’s reserved zones) at itsslot. Anything that cannot be rendered makes the call raise, naming it — nothing is ever silently left off the film.pad_start(seconds) lengthens the film at the head before compositing: the first frame is held (pad_mode="clone") or the head is black ("black"), and the audio is delayed to match (and so re-encoded, AAC). Overlay times are on the padded timeline —TimedOverlay(None, 0, 3, image="title.png", fade_in=0)withpad_start=0.8is “the title over the intro, the song starting under it 0.8 s in”.- Return type:
Path
- tituli.video.require_filters(*names)[source]¶
Raise
FfmpegErrornaming any missing filter.- Return type: