Chapter Insertion
Chapter Inserter writes chapter metadata into an existing video. It can use fixed timestamps, detect fade boundaries with commercial-detector-ml, or combine both in one ordered plan. The final write is an FFmpeg stream-copy remux, so media streams are not re-encoded.
Job shape
Each job has one input, one output, and at least one plan segment:
{
"input_source": "file:///media/episode.mkv",
"output_source": "file:///media/episode-with-chapters.mkv",
"segments": [
{"type": "fixed", "at": "00:01:00"},
{"type": "auto"},
{"type": "fixed", "at": "end-00:01:00"}
]
}Input and output may name the same source. The service stages the full input before writing the result, so in-place chapter updates are supported.
Segment types
| Type | Fields | Behavior |
|---|---|---|
fixed | required at | Adds exactly one user-selected boundary; invalid geometry fails the job. |
auto | optional start, end | Searches a region for fades; unusable detected boundaries are reported as dropped. |
There is no separate insertion mode. Detect everything with one auto segment:
{"segments": [{"type": "auto"}]}Use only fixed segments to write a manual plan.
Timestamp formats
at, start, and end accept:
HH:MM:SSwith optional milliseconds;- bare seconds, with optional milliseconds;
- an end-relative value such as
end-00:01:00or-60.
An auto segment inherits an omitted start from the previous fixed mark and an omitted end from the next fixed mark. At the plan edges it falls back to the configured start and end margins.
An explicit auto boundary may narrow its inherited window but cannot cross it. Adjacent auto segments require the first segment's end and the second segment's start so the seam is unambiguous.
Detection and output options
| Field | Default | Meaning |
|---|---|---|
anchor | mid | Use the mid, start, or end of a detected fade. |
start_margin | 5 seconds | Ignore the beginning of the file. |
end_margin | 20 seconds | Ignore the end of the file. |
min_gap | 30 seconds | Minimum spacing between boundaries. |
threshold | model default | Override the trained detection threshold; must be between 0 and 1. |
title_format | Act {i} | Format generated chapter titles. |
existing_chapters | skip | skip, replace, compare, or fail. |
dry_run | false | Detect and report without writing a file. |
Plan limits
The service configuration caps submitted plans:
chapter_inserter:
plan_limits:
max_segments: 200
max_fixed_marks: 100The limits catch accidental values such as milliseconds supplied as seconds and are passed to the Python process with every request.
Runtime
The Go service owns gRPC, Kafka, River, source staging, and status reporting. A Python subprocess performs ML detection and remuxing. Configure the Python executable, model path, and device (auto, cpu, or cuda) under chapter_inserter in config.yaml.
The default River configuration uses one worker because detection is CPU/GPU intensive and a 120-minute job timeout.
Start both halves in development with:
mise run dev:chapter-inserter