Skip to content

researcher: caption-track-first YouTube transcript path (wire transcripts into notes) #4

Description

@TimSimpsonJr

Summary

Add a caption-track-first transcript path to the researcher plugin's fetch_media.py so YouTube (and other yt-dlp-supported) videos contribute their transcript text to research notes. Prefer YouTube's own captions; fall back to Whisper only when no English captions exist.

Why

Today the video → transcript path is unwired end-to-end, not just missing captions:

  1. The pipeline never requests transcription. Stage 4c invokes fetch_media.py without --transcribe (skills/researcher/SKILL.md:585), so a YouTube URL only gets its audio downloaded and embedded as ![[video_id.mp3]] — no transcript is ever produced on a real run.
  2. Even with --transcribe, the transcript is discarded. transcribe_audio() returns text and it's stashed on the manifest dict (result["transcript"]), but rewrite_media_refs() only substitutes the URL with an ![[local_path]] embed. The transcript string never enters the content, so it never reaches the summarizer or the note (scripts/fetch_media.py main loop, ~L485–510).
  3. scripts/prompts/extract_transcript.txt is a dead prompt — referenced nowhere.
  4. Separately, the Whisper CLI currently crashes on Windows with a UnicodeEncodeError (cp1252 codec can't encode a character) — a broken fallback.

Design (decisions locked via brainstorming)

Transcript ladder (stop at first hit)

  1. Manual English captions (en, en-US, en-GB, …)
  2. Auto-generated English captions
  3. Whisper on downloaded audio — only if --whisper-fallback enabled and whisper installed
  4. Nothing — leave the video as a plain source link, log, continue

Auto-translated captions are never used. Steps 1–2 need only yt-dlp (metadata + subtitle fetch, no media download). English throughout.

Changes in fetch_media.py

New functions (module's "never raise, return None on failure" contract):

  • _probe_video(url) -> dict | Noneyt-dlp --dump-json --skip-download; returns metadata incl. subtitles/automatic_captions maps and the video title in one call.
  • _vtt_to_text(vtt: str) -> str — pure, unit-tested: strips WEBVTT header, NOTE blocks, cue-timing lines, inline <...> timing tags; de-duplicates YouTube's rolling auto-caption repetition into clean paragraphs.
  • fetch_captions(url, lang="en") -> dict | None — probe → pick track by ladder → download just that track (--write-subs/--write-auto-subs --sub-langs --skip-download --sub-format vtt) → _vtt_to_text. Returns {"text", "source": "manual"|"auto", "lang", "title"}.
  • get_video_transcript(...) — full ladder orchestrator.

transcribe_audio() gets the cp1252 fix: run whisper subprocess with PYTHONUTF8=1 / PYTHONIOENCODING=utf-8 in env and errors="replace". The Whisper path reuses download_video() to pull audio into assets, then deletes the MP3 + .meta sidecar after transcription (transcript replaces audio; no dead code).

What the note gets

  • Video URL rewritten from ![[video_id.mp3]] embed → plain source link [{title}]({url}).
  • Each transcript appended to the end of the content body (robust against multiple videos; no mid-paragraph splicing):
    ## Transcript — {title} (source: auto-generated captions)
    
    {clean transcript text}
  • Summarizer + Haiku fallback read the raw content string, so the transcript flows into the note summary as ordinary article text.
  • Videos are pulled out of the rewrite_media_refs manifest path; images/documents keep going through it unchanged.

CLI surface & config

New/changed flags on fetch_media.py:

  • --transcribe-videos — captions-first, on by default; --no-transcribe-videos opts out
  • --sub-lang en — preferred caption language (default en)
  • --whisper-fallback — enable step 3 (default off)
  • --whisper-model base — kept
  • --save-transcript-artifact — also write a standalone {video_id}.transcript.md asset (opt-in)
  • Remove the old --transcribe flag (never wired into the pipeline; output was discarded)

New keys in config_manager.default_config: "caption_language": "en", "whisper_fallback": false. Stage 4c reads these and translates to flags.

SKILL.md Stage 4c + cleanup

  • Stage 4c invocation (SKILL.md:585) gains --transcribe-videos --sub-lang {caption_language}, plus --whisper-fallback when config enables it and whisper is available.
  • Update "Media embeds" prose (SKILL.md ~604, ~1126–1128): videos now yield an inline ## Transcript section, not an audio embed.
  • Remove dead scripts/prompts/extract_transcript.txt.

Testing

  • _vtt_to_text: fixtures for clean manual VTT and messy auto-generated rolling VTT; assert dedup + tag stripping.
  • fetch_captions: mock subprocess.run so probe returns crafted subtitles/automatic_captions maps and track download writes a fixture VTT; assert ladder prefers manual over auto, returns right source/title, returns None when no English captions.
  • get_video_transcript: captions-win, whisper-fallback-win, whisper-disabled paths.
  • Content transform: assert ## Transcript appended, URL replaced with plain link, no ![[...mp3]] embed.
  • transcribe_audio: assert utf-8 env passed to subprocess.
  • Existing image/document/video/transcribe_audio tests stay green (download_video retained).

Edge cases

  • Vimeo and other yt-dlp sites ride the same path (yt-dlp abstracts site differences).

Open question (has a recommended default — not blocking)

Transcript length cap. A 1-hour auto-caption transcript can run ~10k words, inflating summarizer input. Proposed: --max-transcript-chars default 100,000 (effectively unlimited for normal content, a guard against pathological cases), appending a [transcript truncated] marker when exceeded. Implementer may take this default unless changed.

Files touched

  • scripts/fetch_media.py (new functions, cp1252 fix, video handling rewrite, CLI flags)
  • scripts/config_manager.py (two new default-config keys)
  • skills/researcher/SKILL.md (Stage 4c invocation + media-embeds prose)
  • scripts/prompts/extract_transcript.txt (delete)
  • tests/test_fetch_media.py (new tests)
  • requirements.txt (note: captions need only yt-dlp; whisper optional)

Context

Design produced in a brainstorming session on 2026-07-13. A throwaway standalone caption downloader was built separately for immediate in-session use; this issue tracks the real plugin integration.

Metadata

Metadata

Assignees

No one assigned

    Labels

    autonomous-safeCode-only follow-up; eligible for autonomous pickup

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions