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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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