Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions graphify/transcribe.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,32 @@ def is_url(path: str) -> bool:
return any(path.startswith(p) for p in URL_PREFIXES)


def _ytdlp_env_opts() -> dict:
"""Optional yt-dlp options passed through the environment.

YouTube serves ``Sign in to confirm you're not a bot`` to datacenter IPs;
yt-dlp's own FAQ prescribes browser cookies for that, and its extractor
needs a JavaScript runtime for full format coverage. Both are reachable
from the CLI but were impossible to pass through the embedded API here,
so mirror the transcriber's env convention (GRAPHIFY_WHISPER_MODEL):

- ``GRAPHIFY_YTDLP_COOKIES``: path to a Netscape cookies.txt
(same as ``yt-dlp --cookies``).
- ``GRAPHIFY_YTDLP_JS_RUNTIMES``: comma-separated runtime names,
e.g. ``node`` or ``deno,node`` (same as ``yt-dlp --js-runtimes``).
"""
opts: dict = {}
cookies = os.environ.get("GRAPHIFY_YTDLP_COOKIES")
if cookies:
opts['cookiefile'] = cookies
runtimes = os.environ.get("GRAPHIFY_YTDLP_JS_RUNTIMES")
if runtimes:
opts['js_runtimes'] = {
r.strip(): {'path': None} for r in runtimes.split(',') if r.strip()
}
return opts


def download_audio(url: str, output_dir: Path) -> Path:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressiondownload_audio()

high coupling complexity (Ca·Ce = 12).

Grounded coupling-delta finding (deterministic), not an LLM guess.

"""Download audio-only stream from a URL using yt-dlp.

Expand Down Expand Up @@ -78,6 +104,7 @@ def download_audio(url: str, output_dir: Path) -> Path:
'noplaylist': True,
'postprocessors': [], # no ffmpeg needed — use native audio
}
ydl_opts.update(_ytdlp_env_opts())

print(f" downloading audio: {url[:80]} ...", flush=True)
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
Expand Down
59 changes: 59 additions & 0 deletions tests/test_transcribe.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,62 @@ def raise_import(*args, **kwargs):
results = transcribe_all([str(video)], output_dir=tmp_path / "out")

assert results == []


# ---------------------------------------------------------------------------
# _ytdlp_env_opts — cookies / JS-runtime pass-through (GRAPHIFY_YTDLP_*)
# ---------------------------------------------------------------------------

def test_ytdlp_env_opts_empty_by_default(monkeypatch):
"""No env vars set -> no extra yt-dlp options (behaviour unchanged)."""
from graphify.transcribe import _ytdlp_env_opts
monkeypatch.delenv("GRAPHIFY_YTDLP_COOKIES", raising=False)
monkeypatch.delenv("GRAPHIFY_YTDLP_JS_RUNTIMES", raising=False)
assert _ytdlp_env_opts() == {}


def test_ytdlp_env_opts_cookies(monkeypatch):
"""GRAPHIFY_YTDLP_COOKIES maps to yt-dlp's cookiefile option."""
from graphify.transcribe import _ytdlp_env_opts
monkeypatch.setenv("GRAPHIFY_YTDLP_COOKIES", "/home/user/cookies.txt")
monkeypatch.delenv("GRAPHIFY_YTDLP_JS_RUNTIMES", raising=False)
assert _ytdlp_env_opts() == {"cookiefile": "/home/user/cookies.txt"}


def test_ytdlp_env_opts_js_runtimes(monkeypatch):
"""GRAPHIFY_YTDLP_JS_RUNTIMES maps to yt-dlp's js_runtimes dict."""
from graphify.transcribe import _ytdlp_env_opts
monkeypatch.delenv("GRAPHIFY_YTDLP_COOKIES", raising=False)
monkeypatch.setenv("GRAPHIFY_YTDLP_JS_RUNTIMES", "node, deno")
assert _ytdlp_env_opts() == {
"js_runtimes": {"node": {"path": None}, "deno": {"path": None}},
}


def test_download_audio_applies_env_opts(monkeypatch, tmp_path):
"""download_audio() forwards the env-derived options into YoutubeDL."""
from graphify import transcribe as tr

monkeypatch.setenv("GRAPHIFY_YTDLP_COOKIES", str(tmp_path / "c.txt"))
monkeypatch.setenv("GRAPHIFY_YTDLP_JS_RUNTIMES", "node")

captured = {}

class FakeYDL:
def __init__(self, opts):
captured.update(opts)
def __enter__(self):
return self
def __exit__(self, *a):
return False
def extract_info(self, url, download=True):
return {"ext": "m4a"}

fake_mod = MagicMock()
fake_mod.YoutubeDL = FakeYDL
with patch("graphify.transcribe._get_yt_dlp", return_value=fake_mod):
tr.download_audio("https://www.youtube.com/watch?v=x", tmp_path / "dl")

assert captured["cookiefile"] == str(tmp_path / "c.txt")
assert captured["js_runtimes"] == {"node": {"path": None}}
assert captured["noplaylist"] is True # base options still intact