Communitygithub.com

Lumos12345/video-text-extraction

Extract lyrics/subtitles with timestamps from local video/audio: whisper transcript + burned-in (hardsub) text OCR.

What is video-text-extraction?

video-text-extraction is a Claude Code agent skill that extract lyrics/subtitles with timestamps from local video/audio: whisper transcript + burned-in (hardsub) text OCR.

Works with~Claude Code~Codex CLI~Cursor
npx skills add Lumos12345/video-text-extraction

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

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/.m4a files; karaoke/AMV/music videos where lyrics are rendered into the picture.
  • Any request wanting an .lrc or .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. .m4s from 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)

  1. Soft subtitle stream → extract directly. Done.
  2. 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.
  3. 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 medium is the sweet spot; large-v3 is more accurate but slow on CPU.
  • medium on 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>/status VmRSS instead.
  • Do NOT use VAD filtering for singing. vad_filter=True drops 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:

  1. Install tesseract without sudo by extracting .debs to a prefix (details in the reference). Symlink fixes for liblept.so.5 and libarchive.so.13 are documented there.
  2. Find the text region: read one frame with vision_analyze and ask for the pixel coordinates of the caption band. Captions are usually lower-left on music videos — do NOT guess.
  3. 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).
  4. Crop the band, upscale 2–3x, grayscale, OCR each frame (--psm 6 or --psm 7).
  5. Dedupe: consecutive frames repeating a line = one lyric line; the first clean read gives its start time.
  6. 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

  • .m4s is 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, then libarchive.so.13, then libgif.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, then ldd … | grep "not found" to see everything still missing in a single pass. Two extra gotchas: libarchive13 has no candidate on newer Ubuntu (use libarchive13t64), and liblept5 is the package carrying the real liblept.so.5.0.4 — the -dev package only ships dangling symlinks.
  • A short/garbled OCR read is often a crop problem, not an OCR problem. Before tuning --psm or thresholds, vision_analyze the 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 .sh script and run bash script.sh instead of inline export ...; tesseract ....
  • HuggingFace is slow/blocked in mainland China. Always set HF_ENDPOINT=https://hf-mirror.com AND HF_HUB_DISABLE_XET=1 (the xet backend 401s against the mirror).
  • Check the model download is actually progressing. A faster-whisper run 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.

Related Skills