transcribe: add Deepgram nova-3 as an alternative provider - #160
transcribe: add Deepgram nova-3 as an alternative provider#160shoaib90 wants to merge 3 commits into
Conversation
Scribe is the only transcription path today, so an ElevenLabs key is a
hard requirement even for someone who already pays for another ASR.
The pipeline turns out to be far less coupled to ElevenLabs than it
looks: transcribe.py is the only module that touches the API, and every
downstream consumer reads just five fields per token —
{type, text, start, end, speaker_id}. So this is an adapter, not a second
pipeline. pack_transcripts.py and render.py --build-subtitles consume the
output unchanged.
Mapping from results.channels[0].alternatives[0].words[]:
punctuated_word (fallback: word) -> text
start / end -> passthrough
speaker: 0 -> speaker_id: "speaker_0"
(no discriminator) -> type: "word"
(none) -> type: "spacing", synthesized per
inter-word gap
Request params are filler_words=true and punctuate=true, with
smart_format deliberately OFF: it rewrites numbers and dates, which is
exactly the normalization Hard Rule 8 forbids.
Deepgram sends no `spacing` token. pack_transcripts.py would group
phrases correctly regardless, since it also breaks on
(start - prev_end), but synthesizing them keeps this a faithful drop-in
for any consumer that reads them.
Reuses transcribe.py's extract_audio/peak_dbfs/count_audio_tracks/
transcript_path rather than copying, so the audio front-end and the cache
key cannot drift between the two providers. Caching, the silent-track
guard and --audio-track all behave identically.
Known gap, documented in SKILL.md: Deepgram's standard STT returns no
audio-event tokens, so Scribe's (laughter)/(applause)/(sigh) beat markers
are unavailable. It does diarize. --num-speakers is accepted for parity
but ignored, since Deepgram auto-detects and takes no hint.
--convert <response.json> runs the mapping offline with no API call, which
makes the schema testable for free.
Forked to shoaib90/video-use and opened three focused upstream PRs (browser-use#158 quality controls, browser-use#159 configurable subtitles, browser-use#160 Deepgram). Records the branch layout and, more usefully, why each PR branch was built by replaying changes onto clean main rather than cherry-picking out of local: local's commits bundle render.py work with kb/ updates, and two separate concerns touch build_final_composite and main(), so hunk-level splitting would have produced fragile branches. Also notes that transcribe_whisper.py is deliberately held back, since SKILL.md's anti-patterns name local Whisper explicitly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed across 2 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Two review findings, both valid. 1. The cache was keyed only on the file path, which transcript_path() shares with transcribe.py — deliberately, because everything downstream expects exactly one transcripts/<stem>.json per source. So if a Scribe transcript already existed, this tool returned Scribe's JSON with a "cached:" message and never contacted Deepgram. The "alternative provider" could not actually produce Deepgram output without manually clearing the cache. The key also ignored --model and --spacing-threshold, so re-running with different options returned a stale transcript. Since the path has to stay shared, the identity of the cached result now lives in the file: every transcript is stamped with _provider, _model and _spacing_threshold, and check_cached() reads those back. A mismatch is refused with a message naming what differs, rather than silently returning the wrong file. A missing _provider means Scribe, since transcribe.py writes its response verbatim. --force always re-transcribes and overwrites. Verified: with a Scribe transcript present, the run now refuses, exits non-zero, and leaves that file untouched. 2. Omitting --language documented auto-detection but sent no detect_language, so Deepgram just applied its default language. detect_language=true is now sent in that branch. While testing that against a live nova-3 response, the detected language turned out to be reported on the channel (results.channels[0].detected_language) — not results.language or metadata.language, which is where this code was looking, so language_code came back null even once detection was enabled. It now reads the channel and also records _language_confidence. Expected operator errors (cache mismatch, silent track, API failure) exit with a plain message instead of a traceback.
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="helpers/transcribe_deepgram.py">
<violation number="1" location="helpers/transcribe_deepgram.py:356">
P2: Network failures from `requests.post` still escape this handler and print a traceback. Catch `requests.exceptions.RequestException` alongside `RuntimeError` (or wrap it in `call_deepgram`).</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| spacing_threshold=args.spacing_threshold, | ||
| force=args.force, | ||
| ) | ||
| except RuntimeError as exc: |
There was a problem hiding this comment.
P2: Network failures from requests.post still escape this handler and print a traceback. Catch requests.exceptions.RequestException alongside RuntimeError (or wrap it in call_deepgram).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/transcribe_deepgram.py, line 356:
<comment>Network failures from `requests.post` still escape this handler and print a traceback. Catch `requests.exceptions.RequestException` alongside `RuntimeError` (or wrap it in `call_deepgram`).</comment>
<file context>
@@ -255,15 +342,21 @@ def main() -> None:
+ spacing_threshold=args.spacing_threshold,
+ force=args.force,
+ )
+ except RuntimeError as exc:
+ # Cache mismatches and API/silent-track failures are expected operator
+ # errors, not bugs. Report them plainly rather than as a traceback.
</file context>
| except RuntimeError as exc: | |
| except (RuntimeError, requests.exceptions.RequestException) as exc: |
Follow-up to the cache-validation review comment, found in real use. The cache identity covered provider, model and spacing-threshold but not `language` — and language changes the transcript more drastically than any of them. On code-switched audio (Hindi/English alternating mid-sentence) `--language en` returns fluent English nonsense and drops roughly a third of the words; `--language multi` transcribes both. Re-running with a different language therefore has to re-transcribe, and previously it returned the stale file instead. Measured on one clip: en "Within me a golf course car" multi "एक भी दिन नहीं लेके गया मैं अभी तक office car" Across 14 clips, `multi` recovered 37% more words than `en`. Also documents in SKILL.md that the spoken language must be established before transcribing a batch, since a wrong --language neither errors nor reports low confidence, and that detect_language cannot cover code-switching because it commits to one language per file. That guidance belongs in the skill rather than only in a helper's --help: the cut is reasoned from the transcript, so a language mismatch corrupts the edit and not just the captions.
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="SKILL.md">
<violation number="1" location="SKILL.md:66">
P3: The new Setup bullet tells the agent to "pass `--language multi`" for code-switched speech, but this Setup section is provider-agnostic (it covers both ELEVENLABS_API_KEY and DEEPGRAM_API_KEY), and "transcribing a batch" maps to `transcribe_batch.py`, which is the ElevenLabs Scribe-only path (`from transcribe import ...`). `multi` is not an ISO language code and is only validated/supported on the Deepgram path, where it is explicitly scoped to `transcribe_deepgram.py` in the Helpers section. An agent following this under the ElevenLabs default would pass `--language multi` to the Scribe API, contradicting the "does not error" claim. Scope the `--language multi` advice to the Deepgram provider (e.g. "when using transcription_deepgram.py, pass `--language multi`").</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
| - `ELEVENLABS_API_KEY` → `transcribe.py` (Scribe). Tags audio events: `(laughter)`, `(applause)`, `(sigh)`. | ||
| - `DEEPGRAM_API_KEY` → `transcribe_deepgram.py` (nova-3). Same on-disk schema, so everything downstream is identical, and it diarizes. But it returns **no audio-event tokens**, so the `(laughs)`/`(applause)` beat signals in *Cut craft* are unavailable — lean on silence gaps and `timeline_view` instead. Prefer Scribe for reaction-heavy or multi-speaker material where audio events carry the beats. | ||
|
|
||
| - **Establish what language is actually spoken before transcribing a batch.** Transcribe one clip, read it against a frame, then run the rest. A wrong `--language` does not error or report low confidence — it returns fluent, grammatical nonsense and silently drops words. For code-switched speech (e.g. Hindi and English alternating mid-sentence) pass `--language multi`; `detect_language` cannot help, because it commits to a single language per file. This matters beyond captions: the cut is reasoned from the transcript, so a language mismatch corrupts the edit itself. |
There was a problem hiding this comment.
P3: The new Setup bullet tells the agent to "pass --language multi" for code-switched speech, but this Setup section is provider-agnostic (it covers both ELEVENLABS_API_KEY and DEEPGRAM_API_KEY), and "transcribing a batch" maps to transcribe_batch.py, which is the ElevenLabs Scribe-only path (from transcribe import ...). multi is not an ISO language code and is only validated/supported on the Deepgram path, where it is explicitly scoped to transcribe_deepgram.py in the Helpers section. An agent following this under the ElevenLabs default would pass --language multi to the Scribe API, contradicting the "does not error" claim. Scope the --language multi advice to the Deepgram provider (e.g. "when using transcription_deepgram.py, pass --language multi").
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At SKILL.md, line 66:
<comment>The new Setup bullet tells the agent to "pass `--language multi`" for code-switched speech, but this Setup section is provider-agnostic (it covers both ELEVENLABS_API_KEY and DEEPGRAM_API_KEY), and "transcribing a batch" maps to `transcribe_batch.py`, which is the ElevenLabs Scribe-only path (`from transcribe import ...`). `multi` is not an ISO language code and is only validated/supported on the Deepgram path, where it is explicitly scoped to `transcribe_deepgram.py` in the Helpers section. An agent following this under the ElevenLabs default would pass `--language multi` to the Scribe API, contradicting the "does not error" claim. Scope the `--language multi` advice to the Deepgram provider (e.g. "when using transcription_deepgram.py, pass `--language multi`").</comment>
<file context>
@@ -62,6 +62,8 @@ First-time install lives in `install.md` (clone, deps, ffmpeg, skill registratio
- `ELEVENLABS_API_KEY` → `transcribe.py` (Scribe). Tags audio events: `(laughter)`, `(applause)`, `(sigh)`.
- `DEEPGRAM_API_KEY` → `transcribe_deepgram.py` (nova-3). Same on-disk schema, so everything downstream is identical, and it diarizes. But it returns **no audio-event tokens**, so the `(laughs)`/`(applause)` beat signals in *Cut craft* are unavailable — lean on silence gaps and `timeline_view` instead. Prefer Scribe for reaction-heavy or multi-speaker material where audio events carry the beats.
+
+- **Establish what language is actually spoken before transcribing a batch.** Transcribe one clip, read it against a frame, then run the rest. A wrong `--language` does not error or report low confidence — it returns fluent, grammatical nonsense and silently drops words. For code-switched speech (e.g. Hindi and English alternating mid-sentence) pass `--language multi`; `detect_language` cannot help, because it commits to a single language per file. This matters beyond captions: the cut is reasoned from the transcript, so a language mismatch corrupts the edit itself.
- `ffmpeg` + `ffprobe` on PATH.
- Python deps installed (`uv sync` or `pip install -e .` inside the repo).
</file context>
Why
Scribe is the only transcription path, so an ElevenLabs key is a hard requirement even for someone who already pays for another ASR. This adds Deepgram as an option — it does not change or deprecate anything.
Why it's small
The pipeline is far less coupled to ElevenLabs than it first appears.
transcribe.pyis the only module that touches the API, and every downstream consumer reads just five fields per token:{type, text, start, end, speaker_id}. Everything else in a Scribe response is unused.So this is an adapter, not a second pipeline.
pack_transcripts.pyandrender.py --build-subtitlesconsume the output unchanged.Mapping from
results.channels[0].alternatives[0].words[]:punctuated_word(fallbackword)textstart/endspeaker: 0speaker_id: "speaker_0"type: "word"type: "spacing", synthesized per inter-word gapDetails worth reviewing
smart_formatis deliberately OFF, withfiller_words=trueandpunctuate=trueon.smart_formatrewrites numbers and dates, which is precisely the normalization Hard Rule 8 forbids.Synthesized
spacingtokens. Deepgram sends none.pack_transcripts.pywould group phrases correctly anyway, since it also breaks onstart - prev_end, but emitting them keeps this a faithful drop-in for any consumer that reads them.Shared audio front-end. It imports
extract_audio,peak_dbfs,count_audio_tracksandtranscript_pathfromtranscribe.pyrather than copying them, so the audio handling and the cache key cannot drift between providers. Caching, the silent-track guard and--audio-trackall behave identically.--convert <response.json>runs the mapping offline with no API call, which makes the schema testable for free.Known gap, documented in SKILL.md
Deepgram's standard STT returns no audio-event tokens, so Scribe's
(laughter)/(applause)/(sigh)markers are unavailable — the beat signals Cut craft leans on. It does diarize. SKILL.md now says to prefer Scribe for reaction-heavy or multi-speaker material, and to lean on silence gaps plustimeline_viewotherwise.--num-speakersis accepted for parity withtranscribe.pybut ignored and warned about, since Deepgram auto-detects and takes no hint.Testing
Verified end to end on real footage: a live
nova-3call produced correct word timings and diarization,pack_transcripts.pygrouped phrases with correctS0/S1tags, andrender.py --build-subtitlesproduced amaster.srtwith correct Hard Rule 5 output-timeline offsets.tests/passes (16 tests, 15 subtests).One field note in case it's useful to others: on a clip where the speaker opened with "Um, so ninety percent…", Deepgram dropped the leading
Um,while preserving a mid-sentenceuh,. Scribe kept both. Worth knowing if filler removal is the goal.Summary by cubic
Adds Deepgram nova-3 as an alternative transcription provider, so a Deepgram key now works where only an ElevenLabs one did before. It emits the same on-disk schema, so
pack_transcripts.pyandrender.py --build-subtitlesconsume the output unchanged.Details worth reviewing
transcribe.pyso the two providers can't drift apart._provider,_model,_language, and_spacing_threshold, and a mismatch is refused with a message rather than silently returning the wrong file.--forcere-transcribes.--languageis omitted, and the detected language is read from the channel;_language_confidenceis recorded. A wrong--languagesilently returns fluent nonsense on code-switched audio, so SKILL.md now says to establish the spoken language before a batch and pass--language multifor code-switching.smart_formatstays off to avoid number/date normalization;filler_wordsandpunctuatestay on.spacingtokens per inter-word gap because Deepgram sends none.--convert <response.json>maps an existing response offline, with no API call.(laughter)/(applause)markers are unavailable; SKILL.md now steers reaction-heavy or multi-speaker material to Scribe.--num-speakersis accepted for parity but ignored, with a warning.Written for commit 3f4c6ac. Summary will update on new commits.