catena
Small programs that each do one part of turning a pile of files into a searchable corpus, and hand the next one text on stdout.
Every one of them is a filter: bytes in, records out, diagnostics on stderr, and an exit code that means something. None of them needs a database, a GPU, a queue or a config file.
The tools
Working out what you have
booksniff |
What a file actually is, whatever its name claims |
pdftriage |
Whether a PDF's text layer is worth reading, or whether it has to be OCRed |
bibclean |
Catalogue records audited and repaired, with a reason for every change |
Turning documents into passages
mdscrub |
Converted markdown in, clean markdown out |
chunkmd |
Markdown into passages that carry their page, chapter and role |
Turning recordings into passages
cuenorm |
Any subtitle format in, one normalised cue stream out |
cuepack |
Cues into passages worth embedding, keeping the times they were said at |
cueseg |
A timed transcript into topic chapters |
Searching
emb |
Lines into vectors, against an embedding server |
rrf |
Several ranked lists into one, without the database |
The libraries underneath
Every tool is a library crate with a thin command on top, so anything a command does can be done in process instead.
corpus-core |
The record types and the stream plumbing everything shares |
textkit |
Token counting, sentence and paragraph splitting |
cuekit |
The cue model, and the three subtitle formats it is read from |
catena-cli |
The parts of a filter every command does the same way |
Reference
- The records — the shapes that travel between the tools.
- The contract — what every one of these programs promises.
A worked pipeline
A book, from a converted chapter to passages an index can hold:
$ mdscrub chapter.md | chunkmd --min-tokens 20
{"v":1,"idx":0,"text":"The obligation ran both ways, and contemporaries said so plainly…","tokens":56,"page":41,"chapter":"III. The Second Estate","role":"body","start_time":null,"end_time":null,"flags":[]}
{"v":1,"idx":1,"text":"What the rolls do not say, and what has to be inferred…","tokens":143,"page":42,"chapter":"III. The Second Estate","role":"body","start_time":null,"end_time":null,"flags":[]}
A recording, through the same shape:
$ cuenorm episode.vtt | cuepack --seconds 10
{"v":1,"idx":0,"text":"The mill stood at the northern end of the valley, where the fall of the river was steepest and the ice came latest.","tokens":27,"page":null,"chapter":null,"role":"body","start_time":1.0,"end_time":8.0,"flags":[]}
{"v":1,"idx":1,"text":"Its wheel was undershot, which was wasteful,","tokens":11,"page":null,"chapter":null,"role":"body","start_time":11.0,"end_time":19.0,"flags":[]}
Both produce the same record, so one index answers across a book and a lecture and returns both with a location you can cite — a page number for one, a timestamp for the other.