Replaces the scattered TypeScript/shell helpers with one Python toolchain that runs as plain files (`python3 scripts/tools.py <tool> ...`) with no third-party dependencies. New tools - `_common.py` shared plumbing (Report, run, ffmpeg discovery, argparse ergonomics) - `hwprobe.py` what this machine can actually encode (software + hardware) - `srt2ass.py` SRT -> ASS, the missing step between lrc2srt and baking - `ffmpeg_tools.py` mux / bake / still / strip / probe, with hardware encode - `bom_gen.py` BOM generator, ported from scripts/bom-gen.ts - `tools.py` dispatcher so any tool is one command away - `README.md` usage for all of the above Rewritten - `lrc2srt.py` now handles multiple timestamps per line, timestamp-less cue lines and blank/comment lines, and survives malformed input. The legacy version emitted an empty cue plus a stray cue for every line. Hardware encoding This host is AMD (Strix Halo, VCN 4.0.5), so nvenc in the legacy notes is reference only. `hwprobe.py` probes and reports what genuinely works here: VA-API via /dev/dri/renderD128 gives h264_vaapi, hevc_vaapi and av1_vaapi. `ffmpeg_tools.py --video auto` picks the hardware encoder when the render node is usable and falls back to libx264/libx265 when it is not. Two bugs found and fixed while wiring this up: - the VA-API capability probe fed frames straight to the encoder without `format=nv12,hwupload`, so ffmpeg failed with "Function not implemented" and the toolchain silently fell back to software on a machine where hardware works. VA-API encoders only accept GPU-resident frames; the probe now uploads. - `bake --video copy` only failed when the copied stream happened to be VA-API. Burning subtitles always changes pixels, so it now refuses `copy` outright with a clear message. Also makes -q/-n usable on either side of the mode word, and ignores __pycache__ in .gitignore.
7.2 KiB
NCMR songs-videos — tools
Helper scripts for building NCMR song videos: converting lyrics to subtitles, burning them in, muxing audio, and working out what this machine can encode.
Everything here is Python standard library only. No virtualenv, no install step, no dependencies to break in six months. Run them straight from the repo:
python3 scripts/tools.py --help
Quick start
cd ~/dev/mikkeli/ncmr-songs-videos
# 1. What can this machine actually encode?
python3 scripts/tools.py hwprobe
# 2. Lyrics -> subtitles
python3 scripts/tools.py lrc2srt song.lrc -o song.srt
python3 scripts/tools.py srt2ass song.srt -o song.ass
# 3. Audio + still image -> a video (the "PV" case)
python3 scripts/tools.py ffmpeg still cover.png song.flac -o pv.mkv
# 4. Burn the subtitles in, copy the audio through untouched
python3 scripts/tools.py ffmpeg bake pv.mkv song.ass -o final.mp4 --format flac
# 5. Record what untracked assets went in
python3 scripts/tools.py bom --dir .
Every subcommand takes --help. Every subcommand accepts --dry-run (-n) to
print the work it would do without doing it, which is the fastest way to see
what ffmpeg command you are about to run.
The tools
| Tool | Purpose |
|---|---|
tools.py |
Dispatcher. python3 scripts/tools.py <tool> [args...]. |
hwprobe.py |
Report which encoders this machine can really use, and why not. |
lrc2srt.py |
LRC lyrics → SRT subtitles. |
srt2ass.py |
SRT → ASS, for burn-in via ffmpeg's ass filter. |
ffmpeg_tools.py |
mux, bake, still, strip, probe — the ffmpeg wrappers. |
bom_gen.py |
Write a BOM.yaml (or BOM.md) of a song's untracked assets. |
_common.py |
Shared helpers. Not a command; imported by the rest. |
Each script also runs standalone (python3 scripts/hwprobe.py) and can be
imported as a module. The dispatcher is a convenience, not a requirement.
Hardware encoding
This machine is an AMD Strix Halo (Radeon 8060S). There is no NVIDIA card,
so the nvenc recipes in ffmpegusage.txt are reference only — they will not
run here. What does work is VA-API via the Mesa radeonsi driver:
| Codec | Encoder | Status |
|---|---|---|
| H.264 | h264_vaapi |
works |
| HEVC | hevc_vaapi |
works |
| AV1 | av1_vaapi |
works |
| H.264/HEVC | libx264/libx265 |
software fallback, always available |
ffmpeg_tools.py picks a hardware encoder automatically when one is usable and
falls back to libx264 when it is not, so the same command works on a machine
without a usable GPU. Override it when you want to:
--video auto # default: hardware if possible, else software
--video x264 # force software H.264
--video x265 # force software HEVC
--video copy # no re-encode at all
--codec hevc # which hardware codec family to try with --video auto
--device PATH # VA-API render node (default /dev/dri/renderD128)
If hardware encoding stops working
The usual cause is permissions on the DRM render node. /dev/dri/renderD128 is
owned by root:render, so the user needs to be in the render group:
getent group render # who is in it
python3 scripts/tools.py hwprobe # says plainly whether the device opens
Group membership only applies to new logins. If id shows render but
hwprobe still reports the device as unreadable, the running process is holding
stale credentials — log out and back in, or start the process under the group
explicitly:
sg render -c 'python3 scripts/hwprobe.py'
hwprobe never guesses. It opens the device, runs a two-frame test encode
through each candidate encoder, and reports what actually worked.
python3 scripts/tools.py hwprobe --shell # export-friendly KEY=value output
Subtitles
LRC is the timestamped-lyrics format lyric sites emit; SRT is what most players read; ASS is what you need to burn styled text into a video with ffmpeg.
python3 scripts/tools.py lrc2srt song.lrc -o song.srt --shift -0.5
lrc2srt handles the messy real-world cases: [mm:ss], [mm:ss.xx] and
[mm:ss.xxx] timestamps, several timestamps on one line (repeated choruses —
each becomes its own cue), [ar:]/[ti:] metadata tags, and blank lines inside
a cue. The final cue is held for a few seconds rather than dropped; adjust with
--tail. Use --shift to nudge everything earlier or later if the lyrics drift
against the audio.
python3 scripts/tools.py srt2ass song.srt -o song.ass --play-res 1920x1080
srt2ass converts to ASS and gives ffmpeg something it can burn in. --play-res
sets the script resolution used for font scaling; match it to your video so text
sizes come out as designed. --dry-run prints the ASS to stdout for inspection.
ffmpeg wrappers
Long ffmpeg command lines are easy to get subtly wrong and impossible to remember. These wrap the five the project actually needs.
# Attach audio to a video-only file; video stream copied, never re-encoded.
python3 scripts/tools.py ffmpeg mux render.mp4 song.flac -o pv.mkv --format flac
# Burn subtitles into a video.
python3 scripts/tools.py ffmpeg bake pv.mkv song.ass -o final.mp4 --format flac
# One still image + audio -> a video.
python3 scripts/tools.py ffmpeg still cover.png song.flac -o pv.mkv --fps 60
# Pull the audio back out as MP3.
python3 scripts/tools.py ffmpeg strip final.mp4 -o final.mp3
# What is actually in this file?
python3 scripts/tools.py ffmpeg probe final.mp4
Notes on the choices baked in:
muxuses-c:v copy. The Remotion render is already encoded; re-encoding it to add audio costs quality and time for nothing. Containers are picked from the output extension.--formatpresets areaac,mp3,flac,opusandcopy.flacis the default because these are archival masters; useaac/opusfor anything you are actually shipping.--bitrateapplies to the lossy ones.-shortestis set where a still image is looped, so the video ends with the audio instead of running on for ever.-movflags +faststartis set for MP4 output, so the file starts playing before it has fully downloaded.
For anything these do not cover, use --dry-run to print the generated command,
copy it, and edit it — the wrapper is a convenience, not a cage. The legacy
recipes this grew out of are preserved in ffmpegusage.txt.
Bill of materials
Binary assets are git-ignored, so a clone does not tell you what went into a
song. bom_gen.py walks the asset directories and records it:
python3 scripts/tools.py bom --dir . --format yaml
It writes BOM.yaml listing every untracked file under audio/, images/,
fonts/ and videos/ with its size, plus whatever description you give it. Use
--format md for Markdown, --format json for machine-readable output, and
-y to skip the interactive description prompt. If PyYAML is available it is
used for clean output; otherwise a small built-in writer produces the same
schema. Neither is required.
Conventions
masteris the protected template. Do per-song work on<song>/master, and features on<song>/<feature>. SeeREADME.md.- Binary assets stay out of git; the BOM is how you know what was used.
- Keep new tools in
scripts/, stdlib only, with a--dry-runand--help.