Files
ncmr-songs-videos/scripts
irene afe397da3f refactor: draw the src/scripts boundary, drop dead files
Make each directory answerable by path alone: src/ is what the Remotion build
compiles, scripts/ is what you run by hand.

Removed:
- `ffmpeg.sh` — never worked; every parameter was assigned `$1`, so bake/bypass
  were broken from the start. Superseded by `scripts/ffmpeg_tools.py bake|still`.
- `src/lrc2srt.ts` — the TypeScript port; `scripts/lrc2srt.py` is the script that
  was actually maintained, and it now handles the multi-timestamp cases the TS
  version did.
- `scripts/bom-gen.ts` — superseded by `scripts/bom_gen.py`.
- `scripts/ffmpegusage.txt` — legacy command notebook. Every recipe in it is now
  reproduced by `ffmpeg_tools.py`, including the `-map 0:v:0 -map 1:a:0 -c:v copy`
  mux and the FLAC `-compression_level 12` case, so it has no unique content left.

Also drops the `bom-gen` alias from the dispatcher (dash-case was TypeScript
naming) and documents the boundary in README.md and REMOTION-QUICKSTART.md.

`npm run lint` (eslint src && tsc) passes with src/ reduced to Remotion input.
2026-09-10 18:18:04 +09:00
..

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 nvenc is unavailable 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:

  • mux uses -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.
  • --format presets are aac, mp3, flac, opus and copy. flac is the default because these are archival masters; use aac/opus for anything you are actually shipping. --bitrate applies to the lossy ones.
  • -shortest is set where a still image is looped, so the video ends with the audio instead of running on for ever.
  • -movflags +faststart is 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. hwprobe.py reports what this host can actually encode, so you can tell at a glance whether a recipe will need a hardware-specific encoder.


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

  • master is the protected template. Do per-song work on <song>/master, and features on <song>/<feature>. See README.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-run and --help.