Skip to content

transcribe: add Deepgram nova-3 as an alternative provider - #160

Open
shoaib90 wants to merge 3 commits into
browser-use:mainfrom
shoaib90:pr/deepgram-transcriber
Open

transcribe: add Deepgram nova-3 as an alternative provider#160
shoaib90 wants to merge 3 commits into
browser-use:mainfrom
shoaib90:pr/deepgram-transcriber

Conversation

@shoaib90

@shoaib90 shoaib90 commented Sep 7, 2026

Copy link
Copy Markdown

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.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}. Everything else in a Scribe response is unused.

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[]:

Deepgram → contract
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

Details worth reviewing

smart_format is deliberately OFF, with filler_words=true and punctuate=true on. smart_format rewrites numbers and dates, which is precisely the normalization Hard Rule 8 forbids.

Synthesized spacing tokens. Deepgram sends none. pack_transcripts.py would group phrases correctly anyway, since it also breaks on start - 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_tracks and transcript_path from transcribe.py rather than copying them, so the audio handling and the cache key cannot drift between providers. Caching, the silent-track guard and --audio-track all 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 plus timeline_view otherwise.

--num-speakers is accepted for parity with transcribe.py but ignored and warned about, since Deepgram auto-detects and takes no hint.

Testing

Verified end to end on real footage: a live nova-3 call produced correct word timings and diarization, pack_transcripts.py grouped phrases with correct S0/S1 tags, and render.py --build-subtitles produced a master.srt with 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-sentence uh,. 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.py and render.py --build-subtitles consume the output unchanged.

Details worth reviewing

  • Reuses the audio front-end and cache key from transcribe.py so the two providers can't drift apart.
  • The shared cache is now validated per provider and options: transcripts are stamped with _provider, _model, _language, and _spacing_threshold, and a mismatch is refused with a message rather than silently returning the wrong file. --force re-transcribes.
  • Language auto-detection is actually sent when --language is omitted, and the detected language is read from the channel; _language_confidence is recorded. A wrong --language silently returns fluent nonsense on code-switched audio, so SKILL.md now says to establish the spoken language before a batch and pass --language multi for code-switching.
  • Expected operator errors (cache mismatch, silent track, API failure) exit with a plain message instead of a traceback.
  • smart_format stays off to avoid number/date normalization; filler_words and punctuate stay on.
  • Synthesizes spacing tokens per inter-word gap because Deepgram sends none.
  • --convert <response.json> maps an existing response offline, with no API call.
  • Deepgram returns no audio-event tokens, so (laughter) / (applause) markers are unavailable; SKILL.md now steers reaction-heavy or multi-speaker material to Scribe.
  • --num-speakers is accepted for parity but ignored, with a warning.

Written for commit 3f4c6ac. Summary will update on new commits.

Review in cubic

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.
shoaib90 pushed a commit to shoaib90/video-use that referenced this pull request Sep 7, 2026
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>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 2 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread helpers/transcribe_deepgram.py
Comment thread helpers/transcribe_deepgram.py
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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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:

@cubic-dev-ai cubic-dev-ai Bot Sep 8, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Suggested change
except RuntimeError as exc:
except (RuntimeError, requests.exceptions.RequestException) as exc:
Fix with cubic

Comment thread helpers/transcribe_deepgram.py
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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Comment thread SKILL.md
- `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.

@cubic-dev-ai cubic-dev-ai Bot Sep 8, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Fix with cubic

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant