The harness compared these tools against the code they replaced — Python imported from book-pipeline, podcast-pipeline and knowledge-mcp, and JavaScript copied out of the podcast media server and indexer, each pinned to a commit. All of it has since been deleted, so the live half cannot run and the golden half pins answers against a source nobody can consult. What was worth keeping was never the comparison. `KNOWN.md` argued, at length, why a dozen of these tools deliberately do something a reader would not expect: why pdfgutter measures the crop box throughout, why avplan finds the encoder rather than reading one out of the environment, why mdscrub will not manufacture a page boundary. Those arguments are about this code and stand on their own, so they are now `docs/decisions.md`, rewritten to say what each tool does and why rather than what it does differently. Binding coverage is unaffected: the bindings carry 24 tests of their own, a node smoke test against the built package, and a typecheck.
cuekit
The cue model, and the three subtitle formats it is read from.
Part of catena.
What it is
A library. Three formats go in — SubRip, WebVTT and SubStation Alpha — and
one cue stream comes out.
Everything downstream works on cues and never has to know which of the three a
recording arrived with.
cuenorm is a command over this.
When you would use it
- Reading subtitles in your own program, without shelling out.
- Adding a fourth format, or handling one of these three differently.
- Anywhere timed text is the input and the format is whatever somebody happened to have.
Reading a file
let cues = cuekit::parse(&content);
The format is worked out from the content, not from the name, because a subtitle file is named for the format somebody expected it to be in. When it is already known:
use cuekit::Format;
let cues = cuekit::parse_as(&content, Format::WebVtt);
assert_eq!(cuekit::detect(&content), Format::WebVtt);
The parsers are also available directly — cuekit::srt::parse (which reads
WebVTT too) and cuekit::ass::parse.
The cue
use cuekit::Cue;
let cue = Cue::new(12.5, 15.0, "the mill stood at the northern end");
assert_eq!(cue.seconds(), 2.5);
Deliberately smaller than a chunk. A cue is what a subtitle file holds — a few seconds, often half a sentence — and turning cues into passages worth retrieving is a later stage's job.
seconds() is negative if the file says a cue ends before it starts, which
some do.
Merging the repeats
let merged = cuekit::merge_repeats(&cues);
Consecutive cues with identical text become one, keeping the earliest start and the latest end.
Automatic captioning emits a rolling window: the same line appears in cue after cue as the window slides, so a ten-minute stretch of speech can arrive as three times as many cues as there are sentences. Indexed as they come, every one of those is a separate hit for the same words at almost the same timestamp.
Cleaning a cue's text
assert_eq!(cuekit::text::clean("<v Host>[Music] The mill stood"), "The mill stood");
Styling and speaker tags go; bracketed sound cues — [Music], [Applause],
[inaudible] — go with them, because they are not speech and a search that
matches them matches every recording with a theme tune.
Timestamps
assert_eq!(cuekit::srt::timestamp("00:01:23,456"), 83.456);
assert_eq!(cuekit::ass::timestamp("0:01:23.45"), 83.45);
Both return NaN rather than an error for something that is not a timestamp,
which is what lets a malformed block be dropped whole instead of guessed at.
cuekit::jsnum holds the number parsing these use. It reproduces the two
JavaScript conversions the original parser was written against, which disagree
about almost everything an ill-formed timestamp can contain — an empty string
is zero to one of them and not a number to the other. Reproducing both is what
makes a malformed file drop the same cues here as there.
See also
cuenorm— the command over this.cuepackandcueseg— what reads cues.- The records — the cue shape on the wire.