Files
catena/examples/shell/02-video-to-chapters.sh
funkpower 58be244478 examples: programs that run, in four languages
Eight of them, each doing something a pipeline actually has to do, and each
starting from something anyone can fetch — a public domain book from Project
Gutenberg, a NASA broadcast, or a sample carried in the file.

Shell, because the tools are filters first: a book to passages and what its
notes cite; a recording to chapter marks, citable passages and the one
ffmpeg call that makes it play on a phone; passages into pgvector and a
search that asks twice and reconciles.

Rust, for the case where the caller already holds the text and a subprocess
would be serialising for nothing. Python, for the shape a pipeline stage
takes once the reading is a library call. TypeScript, for the app side —
chapter marks and deep links, and fusing two rankings in process.

Three of them end with what re-ranking means, because it is two different
things under one name: fusing ranked lists, which is what `rrf` does and
needs no model, and a cross-encoder scoring query and passage together,
which is accurate, slow, and runs over what retrieval already found.
2026-09-21 08:12:13 -04:00

110 lines
4.7 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
# A recording to chapter marks, citable passages, and a copy that plays.
#
# Takes the captions a video already carries, normalises them to one cue
# stream, and turns that into chapter marks for a player and passages worth
# embedding — each keeping the second it was said at. Then asks whether the
# file itself would play on a phone, and prints the one ffmpeg call that
# fixes it.
#
# examples/shell/02-video-to-chapters.sh [url] [work-dir]
#
# The default is a NASA upload: a work of the US government, so public
# domain, and captioned by a person rather than by a model. Any URL yt-dlp
# can reach works, and any local subtitle file works too — skip to the
# cuenorm line and feed it your own.
#
# Needs jq, ffmpeg, and yt-dlp with impersonation support, which YouTube now
# requires before it will hand over captions:
#
# pipx install "yt-dlp[default,curl-cffi]"
#
# and the catena commands on PATH.
set -eu
url="${1:-https://www.youtube.com/watch?v=h0wLZJeYGxw}"
work="${2:-./work}"
mkdir -p "$work"
# Captions written by a person where there are any, and a model's otherwise.
# Automatic captions roll — each line is re-sent as it scrolls up the screen —
# so the same sentence arrives three times and every passage says it thrice.
if [ -z "$(find "$work" -name '*.vtt' -print -quit 2>/dev/null)" ]; then
echo "fetching captions"
yt-dlp --skip-download --write-subs --sub-langs 'en.*' --sub-format vtt \
--output "$work/%(id)s.%(ext)s" "$url" >/dev/null 2>&1 ||
yt-dlp --skip-download --write-auto-subs --sub-langs 'en.*' --sub-format vtt \
--output "$work/%(id)s.%(ext)s" "$url" >/dev/null
fi
# Where several tracks came back, the smallest is the written one: a rolling
# track repeats every line and is four or five times the size.
subs="$(find "$work" -name '*.vtt' -exec ls -S {} + | tail -1)"
[ -n "$subs" ] || { echo "no captions on that video; try another URL" >&2; exit 1; }
echo " captions: $(basename "$subs")"
echo
echo "== cues"
cuenorm "$subs" > "$work/cues.ndjson"
jq -s -r '" \(length) cues over \((map(.end) | max) / 60 | floor)m\((map(.end) | max) % 60 | floor)s"' "$work/cues.ndjson"
jq -r '" [\(.start | floor)s] \(.text[0:62])"' "$work/cues.ndjson" | head -3
# Where the subject changes, found from the words alone — no model, no
# network. The titles come out of each chapter's own first sentence, and are
# meant to be replaced by something better later.
echo
echo "== chapters"
cueseg < "$work/cues.ndjson" > "$work/chapters.ndjson"
jq -r '" \(.start | floor)s–\(.end | floor)s \(.title)"' "$work/chapters.ndjson"
# Cues packed to a size worth embedding, each carrying the seconds it spans,
# so a search hit can be played back at the moment it was said.
echo
echo "== passages"
cuepack < "$work/cues.ndjson" > "$work/passages.ndjson"
jq -s -r '" \(length) passages, \(map(.tokens) | add / length | floor) tokens on average"' "$work/passages.ndjson"
jq -s -r '.[0] | " [\(.start_time | floor)s–\(.end_time | floor)s] \(.text[0:150])…"' "$work/passages.ndjson"
echo
echo "== chapter marks, as a player would take them"
jq -r '"\(.start | floor) \(.title)"' "$work/chapters.ndjson" |
awk '{ t = $1; $1 = ""; printf " %02d:%02d:%02d%s\n", t/3600, (t%3600)/60, t%60, $0 }'
# And the file itself. A recording indexed but unplayable is half a job: a
# phone will not open VP9 in a WebM container, whatever the transcript says.
# Twenty seconds of it is enough to judge the streams, and spares the
# download.
echo
echo "== would it play on a phone"
video="$(find "$work" -type f \( -name '*.webm' -o -name '*.mkv' -o -name '*.mp4' \) -print -quit)"
if [ -z "$video" ]; then
echo " fetching twenty seconds of it"
yt-dlp -f 'worstvideo[ext=webm]+worstaudio[ext=webm]/worst' \
--download-sections '*0-20' --force-keyframes-at-cuts \
--output "$work/%(id)s.%(ext)s" "$url" >/dev/null 2>&1 || true
video="$(find "$work" -type f \( -name '*.webm' -o -name '*.mkv' -o -name '*.mp4' \) -print -quit)"
fi
[ -n "$video" ] || { echo " nothing to judge"; exit 0; }
avplan --json "$video" | jq -r '
" \(.streams.container): \(.streams.video) video, \(.streams.audio // "no") audio",
(if .playable then " plays as it stands" else (.reasons[] | " · \(.)") end)'
# Exit 0 means there is a plan to print; 1 means the file already plays. The
# encoder is whatever this machine actually has — videotoolbox on a Mac,
# NVENC or VAAPI on a Linux box, libx264 where there is no card. Nothing is
# run: converting is expensive and irreversible enough to read first.
echo
if plan="$(avplan --output "$work/for-phone.mp4" "$video")"; then
echo " the one call that fixes it:"
echo
printf ' %s\n' "$plan"
echo
echo " run it with: eval \"\$(avplan --output out.mp4 FILE)\""
else
echo " nothing to convert"
fi
echo
echo "written to $work/"