Video / Audio Text Extraction
Produce a timed transcript or lyrics file from a local media file (.mp4, .m4s, .mkv, .m4a, .mp3). Handles the common case where a downloaded video has no soft subtitle track but does have burned-in (hardsub) on-screen text, and the case where you only have audio.
When to use
- "Extract the lyrics / subtitles from this video and mark the start time of each line."
- "Transcribe this audio with timestamps."
- Downloaded Bilibili/YouTube
.m4s/.m4afiles; karaoke/AMV/music videos where lyrics are rendered into the picture. - Any request wanting an
.lrcor.srt-style output.
Step 0 — Inspect first, always
ffprobe -v quiet -print_format json -show_streams -show_format FILE | python3 -m json.tool
Key things to learn:
- Streams present.
.m4sfrom Bilibili is often video-only (audio lives in a separate.m4s/.m4a). If there's no audio stream, find the companion audio file in the same folder by matching duration. - Soft subtitles?
ffprobe ... -select_streams s— if a subtitle stream exists, extract it directly (ffmpeg -i FILE -map 0:s:0 out.srt) and skip all OCR/whisper work. This is the fastest path. - Duration. Use it to match the correct audio file when the video has none.
- The title/credit card is not the caption band. Frame ~0 often carries a big title + performer overlay in a different position and font from the actual lyric subtitles. Deriving crop coordinates from it yields a band that misses every lyric. Sample a frame from the middle of the song (with a long lyric line visible) instead — and read the title card separately if you need the song/artist name.
- Confirm which song before transcribing. A filename and a title card can disagree with what the user calls the track. If the audio/lyrics don't match the name you expect, say so before producing a full transcript — and if the video is one song among several, say which one you actually transcribed.
Step 1 — Decide the text source (priority order)
- Soft subtitle stream → extract directly. Done.
- Burned-in on-screen text (hardsub) → OCR frames (Step 3). This gives the official, correctly-spelled text. Prefer this whenever the video visibly renders lyrics/captions.
- Audio only, no on-screen text → whisper transcription (Step 2).
If BOTH on-screen text and an audio transcript exist, use OCR for the words and whisper for timing/orientation — OCR text is authoritative spelling, whisper nails the timing grid but mishears sung words badly (e.g. it heard "pedophiles" for "petals", "fry right" for a sung vowel). Cross-check each other.
Step 2 — Audio transcription (whisper)
Install once (see references/whisper-setup.md for the mainland-China HF mirror fix):
uv venv /tmp/wvenv
/tmp/wvenv/bin/python -m pip install faster-whisper
Extract 16 kHz mono WAV, optionally normalized:
ffmpeg -y -i AUDIO -af "loudnorm=I=-16:TP=-1.5:LRA=11,highpass=f=80" -ar 16000 -ac 1 -c:a pcm_s16le /tmp/song.wav
Transcribe (script in scripts/transcribe.py):
HF_HUB_DISABLE_XET=1 HF_ENDPOINT=https://hf-mirror.com \
/tmp/wvenv/bin/python scripts/transcribe.py /tmp/song.wav medium
- Model
mediumis the sweet spot;large-v3is more accurate but slow on CPU. mediumon CPU (int8) transcribes a ~2.5 min song in a couple of minutes; the process sits at low RSS during model load, then jumps to ~1.4 GB RSS while actually transcribing — don't panic-kill it during the load phase, check/proc/<pid>/statusVmRSS instead.- Do NOT use VAD filtering for singing.
vad_filter=Truedrops quiet/reverbed vocal lines and can reduce a 3-minute song to a handful of segments. Transcribe the whole track. - Working "singing" tune (no VAD):
beam_size=5,condition_on_previous_text=True, temperature fallback[0.0,0.2,0.4,0.6,0.8,1.0],no_speech_threshold=0.8,log_prob_threshold=-1.5,compression_ratio_threshold=2.8,word_timestamps=True,hallucination_silence_threshold=1.5. (Defaults drop whole phrases and emit silence-hallucinations on instrumental gaps.)
Step 3 — Burned-in text OCR (the high-value path)
Full playbook: references/hardsub-ocr.md. Summary:
- Install tesseract without sudo by extracting
.debs to a prefix (details in the reference). Symlink fixes forliblept.so.5andlibarchive.so.13are documented there. - Find the text region: read one frame with
vision_analyzeand ask for the pixel coordinates of the caption band. Captions are usually lower-left on music videos — do NOT guess. - Extract frames densely:
ffmpeg -i SRC -vf fps=2 /tmp/frames/f_%04d.png(2 fps → ~0.5s resolution; frame N is at t = N/2 s). - Crop the band, upscale 2–3x, grayscale, OCR each frame (
--psm 6or--psm 7). - Dedupe: consecutive frames repeating a line = one lyric line; the first clean read gives its start time.
- Low-contrast text: fall back to
vision_analyze. Tesseract fails on pale serif text over a bright/photo background. The vision model reads it reliably — use it for the frames tesseract returns garbage on. This hybrid (tesseract bulk + vision spot-fill) is the production pattern.
Output
Produce an .lrc when the user asks for lyrics + timestamps ([mm:ss.xx]line), else plain MM:SS text. Save to the path the user names (commonly their Windows Desktop when on WSL, e.g. /mnt/c/Users/<user>/Desktop/).
Handoff: the .lrc you produce here is exactly the input the guitar-tabs skill wants for its LRC-synced chord chart / per-line chord alignment. When a lyrics-then-tab request comes as one task, keep the .lrc around and feed it straight into guitar-tabs (its scripts/chord_analyze.py takes <audio> <lyrics.lrc> and prints a chord per lyric line). The two skills chain: video-text-extraction → timed lyrics → guitar-tabs → 弹唱谱.
Pitfalls
.m4sis usually video-only. Don't assume it has audio. Match the companion audio by duration.- Don't trust whisper spelling for sung vocals. Verify against on-screen text when available.
- VAD kills quiet vocals — see Step 2.
- Tesseract needs an exact crop. A band off by ~100px vertically returns noise. Get coordinates from vision first.
- Tesseract's shared-lib deps come one at a time. Each run dies with a different
error while loading shared libraries: X—liblept.so.5, thenlibarchive.so.13, thenlibgif.so.7, and so on. Don't give up and don't install one-per-command: download the whole list at once (see the reference), extract, thenldd … | grep "not found"to see everything still missing in a single pass. Two extra gotchas:libarchive13has no candidate on newer Ubuntu (uselibarchive13t64), andliblept5is the package carrying the realliblept.so.5.0.4— the-devpackage only ships dangling symlinks. - A short/garbled OCR read is often a crop problem, not an OCR problem. Before tuning
--psmor thresholds,vision_analyzethe cropped band itself. If vision sees no text either, the crop is wrong. This one check saves a lot of pointless filter tuning. - Terminal commands with
export/ pipes may require approval. Bundle tesseract env vars into a.shscript and runbash script.shinstead of inlineexport ...; tesseract .... - HuggingFace is slow/blocked in mainland China. Always set
HF_ENDPOINT=https://hf-mirror.comANDHF_HUB_DISABLE_XET=1(the xet backend 401s against the mirror). - Check the model download is actually progressing. A
faster-whisperrun that sits at ~5 MB RSS and 0 CPU jiffies is stuck fetching the model; a real run climbs to ~1.4 GB RSS. Inspect/proc/<pid>/status(VmRSS) rather than killing it or waiting blindly.