afe397da3f
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.
206 lines
7.3 KiB
Markdown
206 lines
7.3 KiB
Markdown
# 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
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
--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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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.
|
|
|
|
```sh
|
|
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.
|
|
|
|
```sh
|
|
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.
|
|
|
|
```sh
|
|
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.
|
|
|
|
```sh
|
|
# 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:
|
|
|
|
```sh
|
|
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`.
|