Files
funkpower 0b849c39c4 cueseg: chapter by vectors somebody else made, where there are some
The hashed bag of words asks only whether two stretches repeat the same
words, and cannot tell that ships and vessels are one subject. An embedding
can. segment_with takes one vector per window and falls back to the words
when it is given none, or the wrong number of them — a caller that embedded
some other recording's windows is asking about that recording.

windows_of gives the text to embed, in the order the vectors come back in,
so nobody has to reimplement the windowing pass to use this.

Bound in all three languages, and on the command line as --windows and
--embeddings, which pipe through emb. The C ABI version goes to 2:
catena_segment_cues gained an argument.
2026-09-20 09:12:04 -04:00

150 lines
6.1 KiB
Markdown

# cueseg
A timed transcript into topic chapters.
Part of [catena](../../README.md).
## What it does
Finds where a recording changes subject, and writes one [`segment`
record](../../docs/records.md#segment--one-topic-chapter) per chapter: a start, an end, a
title taken from the chapter's own first sentence, and an excerpt.
No model and no network. The text is hashed into a small lexical vector a
window at a time, and a topic boundary is where two consecutive windows stop
resembling each other.
## When you would use it
- **Chapter marks for a player.** A three-hour recording with no marks is
unnavigable; the boundaries this finds are exactly the marks a scrubber
needs.
- **Feeding a summariser one subject at a time.** A whole transcript is too
much context and a paragraph is not enough; a chapter is the unit that gives
a useful answer.
- **Navigation in a search result.** Knowing which chapter a hit falls in is
often more useful than the timestamp alone.
- **Titles before anybody has written any.** The extracted titles are meant to
be replaced by something better later, and to be usable until they are.
## Install
```sh
cargo install --git https://git.pumpin.online/funkpower/catena cueseg
```
## Example
A two-hour recording of two subjects:
```
$ cueseg --duration 7200 transcript.ndjson
{"v":1,"start":0.0,"end":1190.6666666666667,"title":"The Mill Stood At The Northern End Of","excerpt":"the mill stood at the northern end of the valley where the fall of the river was steepest and the ice came latest…"}
{"v":1,"start":1190.6666666666667,"end":2381.3333333333335,"title":"On One Household And The Benefit Was Spread","excerpt":"on one household and the benefit was spread across four hundred families…"}
{"v":1,"start":2381.3333333333335,"end":3572.0,"title":"Stood At The Northern End Of The Valley","excerpt":"stood at the northern end of the valley where the fall of the river was steepest…"}
{"v":1,"start":3572.0,"end":4479.0,"title":"Steepest And The Ice Came Latest Its Wheel","excerpt":"steepest and the ice came latest its wheel was undershot which was wasteful…"}
{"v":1,"start":4479.0,"end":5386.0,"title":"It From The Journals Of Either House Parliament","excerpt":"it from the journals of either house parliament dissolved itself again amid furious objections…"}
{"v":1,"start":5386.0,"end":6293.0,"title":"At All Which Left The Committee With No","excerpt":"at all which left the committee with no record of the vote…"}
{"v":1,"start":6293.0,"end":7200.0,"title":"House Parliament Dissolved Itself Again Amid Furious Objections","excerpt":"house parliament dissolved itself again amid furious objections from every bench…"}
```
Straight from a subtitle file:
```sh
cuenorm episode.vtt | cueseg --duration 7245 > chapters.ndjson
```
As chapter marks for a player:
```sh
cueseg --duration 7245 transcript.ndjson | jq -r '"\(.start | floor)\t\(.title)"'
```
## Options
| Option | Effect |
|---|---|
| `--windows` | Print the text to embed, one window per line, and stop |
| `--embeddings <FILE>` | One vector per window, as `emb` writes them |
| `--duration <SECONDS>` | How long the recording is |
| `--man` | Write this page as a manual page, in roff |
Without it, the duration is taken from the end of the last cue. That is
earlier than the end of the file whenever a recording ends in music or
silence, and a chapter that claims to run to the end of the transcript rather
than the end of the file is a mark that lands in the wrong place.
## Better boundaries, if you have embeddings
By default the windows are compared by the words they repeat. That is fast,
needs nothing, and cannot tell that a stretch about *ships* and a stretch
about *vessels* are about one thing. An embedding can.
`--windows` prints the text to embed, one window per line; `--embeddings`
takes one vector per line back, in the same order:
```sh
cueseg --windows episode.srt | emb --model nomic-embed-text > windows.ndjson
cueseg --embeddings windows.ndjson episode.srt
```
The count has to match, and a mismatch is refused rather than guessed at:
```
$ cueseg --embeddings two-vectors.ndjson episode.srt
cueseg: 2 vectors for 44 windows — run --windows to see what to embed
$ echo $?
65
```
Everything else is the same: the same windows, the same threshold, the same
minimum chapter length. Only the question "are these two stretches about the
same thing" is answered better.
## Exit codes
| Code | Meaning |
|---|---|
| `0` | Done |
| `64` | The command line was wrong, or the duration was not a length |
| `65` | The cue stream could not be read |
| `74` | The file could not be read |
## How it finds a boundary
1. **Windows.** Cues are gathered into stretches of about seventy-five
seconds — long enough to have a subject, short enough that a boundary
lands within a minute of the real one.
2. **Vectors.** Each window's content words are hashed into a small vector of
counts. Filler, discourse markers and the vague nouns that carry a
conversation are dropped: leaving them in makes every window resemble every
other.
3. **Valleys.** A boundary is where two consecutive windows resemble each
other less than their neighbours do, and less than the recording's own
average by half a standard deviation. Valleys are taken deepest first, each
at least a chapter's length from the ones already taken.
4. **A floor and a ceiling.** Between four and twenty chapters, targeting one
every fifteen minutes. A chapter mark every two minutes is not navigation,
it is noise; and a chapter that runs too long is no more navigable than no
chapter at all, so an over-long span is divided evenly whether or not the
text suggested a break there.
5. **A fallback.** A recording where nothing stands out is cut evenly, which
is better than one chapter of three hours.
## Input
Either the cue stream [`cuenorm`](../cuenorm/README.md) writes, or a subtitle file.
## As a library
```rust
for chapter in cueseg::segment(&cues, 7245.0) {
println!("{:.0}s {}", chapter.start, chapter.title);
}
```
## See also
- [`cuenorm`](../cuenorm/README.md) — what to run before this.
- [`cuepack`](../cuepack/README.md) — the other thing to do with a cue stream.