Skip to content

feat: add Groq Whisper as a free transcription provider - #114

Closed
moocow4844 wants to merge 5 commits into
browser-use:mainfrom
moocow4844:feat/groq-whisper-transcription
Closed

feat: add Groq Whisper as a free transcription provider#114
moocow4844 wants to merge 5 commits into
browser-use:mainfrom
moocow4844:feat/groq-whisper-transcription

Conversation

@moocow4844

@moocow4844 moocow4844 commented Jul 28, 2026

Copy link
Copy Markdown

What

Adds Groq's hosted whisper-large-v3-turbo as a second transcription backend alongside ElevenLabs Scribe, and makes it the default when a GROQ_API_KEY is present.

Both scripts gain --provider auto|groq|elevenlabs. auto prefers Groq and falls back to Scribe, so existing ElevenLabs-only setups keep working with no change.

Why

transcribe.py only spoke to Scribe, so running any part of video-use required a paid ElevenLabs key. Groq hosts Whisper free and returns word-level timestamps, which is the one thing the rest of the pipeline actually needs from a transcript. call_groq() maps Groq's verbose_json into the Scribe payload shape (words[] of {text, start, end, type, speaker_id}), so pack_transcripts.py, render.py, and timeline_view.py consume it unchanged.

Trade-offs a reviewer should know

Groq does not diarize and does not tag audio events. That is fine for single-speaker footage, which is most of it, but it matters for two documented use cases:

  • Multi-speaker footage (interviews, panels) loses S0/S1 tags in takes_packed.md, and phrases then only break on silence. --provider elevenlabs is the answer, and SKILL.md now says so at the point of use.
  • Audio-event beats ((laughs), (applause)) don't appear. SKILL.md points at reading them off the waveform in timeline_view instead.

--num-speakers now routes to ElevenLabs when its key is available, and warns when only Groq is — previously the flag would have been silently ignored.

Groq's free tier also caps uploads at 25 MB (~25 min per take). Audio is extracted as 16k mono FLAC rather than WAV for Groq, per Groq's documented preference, and an over-cap take fails before upload with a message naming the fix rather than a raw 413 partway through a batch.

Correctness fixes found while testing

  • Non-monotonic word timestamps. Whisper occasionally emits a word starting before the previous one ended (observed −0.42s on real output). Hard Rule 6 snaps cut edges to word boundaries, so a negative span would have reached the EDL. _normalize_groq_words() clamps the sequence monotonic and drops empty tokens; real silence gaps are untouched, since clamping only moves a start that was already behind.
  • Blank .env values shadowed real keys. The shipped .env.example placeholder ELEVENLABS_API_KEY= beat a key exported in the environment. Pre-existing; blank values are now skipped.

Filler words

Hard Rule 8 requires verbatim ASR with fillers intact, and SKILL.md's anti-pattern list warned that Whisper normalizes them. That holds for local CPU-grade settings but not for hosted turbo — verified output keeps um, uh, and stutters (we, we fixed this). That anti-pattern entry is corrected rather than removed.

Docs

.env.example, README.md, install.md, and SKILL.md now cover both providers, the comparison table, the multi-speaker caveat, the size cap, and the flag. Install now accepts either key and sanity-checks whichever was given.

Test plan

No test suite exists in this repo, so verification was live API calls rather than mocks:

  • Single-file and 4-worker batch transcription from a clean state, feeding pack_transcripts.py
  • Fillers and stutters preserved in output
  • Real 1.2s silences produce correct phrase breaks in takes_packed.md (3 phrases from a 3-utterance clip)
  • Zero timestamp violations (end < start, or overlap with previous word) across outputs
  • Error paths: missing key per provider, no keys at all, size guard, cache hit, diarization warning
  • py_compile on all helpers

Not covered: the ElevenLabs path was not re-run (no key on hand), but it is untouched apart from receiving .wav via the same extract_audio call it always did.


Summary by cubic

Adds Groq Whisper as a free transcription provider and makes it the default when GROQ_API_KEY is set; ElevenLabs Scribe remains available for diarization and audio events via --provider or --num-speakers. Centralizes cache staleness logic to keep CLI and batch behavior consistent and avoid unnecessary reads on auto.

  • New Features

    • Added Groq whisper-large-v3-turbo; response mapped to the Scribe shape (words[] {text,start,end,type,speaker_id}) so downstream tools work unchanged.
    • Provider selection: --provider auto|groq|elevenlabs; auto prefers Groq and falls back to Scribe.
    • Diarization routing: --num-speakers upgrades to ElevenLabs when its key is present; warns if only Groq is available.
    • Audio extraction: FLAC for Groq, WAV for Scribe; batch mode logs the chosen provider; docs and .env.example updated.
  • Bug Fixes

    • Provider-aware caching: transcripts now store provider; explicit --provider or a --num-speakers-driven switch re-transcribes when mismatched; under auto the cache wins; legacy transcripts are left untouched. Centralized staleness in is_transcript_stale() so single and batch paths agree and auto avoids reading cached files.
    • Backward compatibility: transcribe_one() defaults to elevenlabs for programmatic callers; CLIs pass --provider explicitly.
    • Clamped non-monotonic Whisper word timestamps and dropped empty tokens to keep cut boundaries valid.
    • Ignored blank .env values and tolerated export KEY=value; install snippet now writes the correct var name (GROQ_API_KEY or ELEVENLABS_API_KEY).
    • Checked Groq’s 25 MB upload cap before upload with a clear error.

Written for commit 8b70671. Summary will update on new commits.

Review in cubic

transcribe.py only supported ElevenLabs Scribe, so running the helpers required a paid
ElevenLabs key. Groq hosts whisper-large-v3-turbo free and returns word-level timestamps,
which is the one thing the rest of video-use actually needs from a transcript.

- load_api_key() becomes load_provider_key(), returning (provider, key). Groq is preferred
  when GROQ_API_KEY is present, with ElevenLabs as the fallback, so existing setups keep
  working untouched and no key means a clear error naming both options.
- call_groq() maps Groq's verbose_json into the Scribe payload shape the rest of the
  pipeline consumes: words[] of {text, start, end, type, speaker_id}. Groq does not
  diarize, so speaker_id is None - fine for single-speaker footage, and callers that need
  diarization should keep using ElevenLabs.
- transcribe_batch passes the resolved provider through to each worker and prints which
  one is in use.
- Env lookup also tolerates 'export KEY=value' lines.

Verified: python3 -m py_compile on both files; no remaining references to load_api_key.
The Groq path worked end to end against the live API, but shipping it as the
default surfaced four gaps and left every doc still claiming ElevenLabs is the
only backend.

Correctness:

- Whisper occasionally emits a word that starts before the previous one ended
  (observed: -0.42s). Cut edges snap to word boundaries, so a negative span
  would reach the EDL. _normalize_groq_words() clamps the sequence monotonic
  and drops empty tokens. Real silences are untouched - clamping only moves a
  start that was already behind.
- Groq's free tier caps uploads at 25 MB. call_groq() now checks before the
  upload and raises a message naming the fix, instead of a raw 413 partway
  through a batch.
- Groq audio is extracted as 16k mono FLAC rather than WAV. Lossless, smaller,
  and what Groq documents as the preferred upload format. Scribe still gets WAV.
- Blank .env values no longer shadow a real key set elsewhere. The shipped
  .env.example placeholder 'ELEVENLABS_API_KEY=' silently beat an exported key.

Provider selection:

- Both scripts take --provider auto|groq|elevenlabs. With both keys set there
  was previously no way to reach Scribe, making diarization unreachable - a
  problem for interviews and panels, both listed content types.
- --num-speakers routes to ElevenLabs when available, and warns when only Groq
  is, since Groq cannot diarize and would otherwise ignore the flag silently.
- Dropped a non-portable ~/.agent-ops/env lookup that no other user has.

Docs: .env.example, README, install.md and SKILL.md now cover both providers,
the multi-speaker caveat, the size cap and the flag. SKILL.md's anti-pattern
list claimed Whisper normalizes fillers; that is a local-CPU-settings problem,
not hosted, and hosted Groq preserves them.

Verified against the live Groq API: single and batch transcription into
pack_transcripts.py from a clean state, fillers and stutters preserved, real
silence gaps producing correct phrase breaks, zero timestamp violations, plus
the missing-key, wrong-provider, size-guard, cache-hit and diarization-warning
paths.

@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 6 files

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

Fix all with cubic | Re-trigger cubic

Comment thread install.md Outdated
Comment thread helpers/transcribe_batch.py
Comment thread helpers/transcribe.py Outdated
Three issues from the cubic review on browser-use#114, all valid.

install.md wrote every pasted key as GROQ_API_KEY (P1). An agent following the
snippet literally after the user chose ElevenLabs would store an ElevenLabs
credential under the Groq name, auto-select would then pick Groq, and the first
transcription would fail with a 401. The snippet now parameterises the variable
name and says why it matters.

--provider had no effect on an already-transcribed source (P2). transcribe_one
returned the cache on filename alone, and transcribe_batch filtered on file
existence before the worker ever ran. That made the remedy SKILL.md documents
for both multi-speaker footage and over-cap takes - "re-run that source with
--provider elevenlabs" - a silent no-op.

Transcripts now record which provider wrote them, and a source cached under a
different provider is re-transcribed when the provider was named explicitly.
Under auto the cache always wins, so no run re-uploads by accident, and a
transcript predating the field counts as unknown rather than mismatched -
nobody's existing ElevenLabs cache gets re-billed by this change. Hard Rule 9
gains the corresponding exception.

transcribe_one's provider default was groq (P2). An external caller on the old
signature would have sent an ElevenLabs key to Groq; the default is now
elevenlabs, preserving pre-Groq behaviour. Both CLIs pass it explicitly, so the
auto default is unaffected.

Verified against the live API: provider recorded on write; auto and matching
--provider both hit cache; mismatched --provider reaches the other backend
(confirmed by a deliberate 401 rather than a silent cache return); a failed
re-transcribe leaves the old transcript intact; provider-less transcripts are
never re-uploaded; same for the batch path; full clean batch -> pack unchanged.
@moocow4844

Copy link
Copy Markdown
Author

Thanks — all three findings were valid and are fixed in 2a5ed03.

P1 (install.md) — real bug. The prose said to write the key "under the matching name" but the snippet hardcoded GROQ_API_KEY, so an agent following it literally after the user chose ElevenLabs would store an ElevenLabs credential under the Groq name and fail with a 401 on first transcription. The snippet now parameterises the variable name.

P2 (cache vs --provider) — worse than described, because SKILL.md documents --provider elevenlabs as the remedy for both multi-speaker footage and over-cap takes, and the cache made that a silent no-op. It bit in two places: transcribe_one returned on filename alone, and transcribe_batch filtered on file existence before the worker ran.

Transcripts now record the provider that wrote them, and a mismatched source is re-transcribed only when the provider was named explicitly. Two deliberate constraints:

  • Under auto the cache always wins, so no run re-uploads by accident — Hard Rule 9 gains just this one exception rather than being loosened.
  • A transcript predating the field counts as unknown, not mismatched, so nobody's existing ElevenLabs cache gets re-billed by upgrading.

P2 (transcribe_one default) — agreed, defaulted to elevenlabs to preserve the pre-Groq API contract. Both CLIs pass the provider explicitly, so auto is unaffected.

Verified against the live API: provider recorded on write; auto and matching --provider hit cache; mismatched --provider actually reaches the other backend (confirmed via a deliberate 401 rather than a silent cache return); a failed re-transcribe leaves the old transcript intact; provider-less transcripts are never re-uploaded; same for the batch path.

@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 4 files (changes from recent commits).

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

Fix all with cubic | Re-trigger cubic

Comment thread helpers/transcribe.py Outdated
resolve_provider() upgrades auto to ElevenLabs when --num-speakers is given and
a key is available, but both CLIs derived provider_explicit separately as
`args.provider != "auto"`. That stayed False through the upgrade, so a source
already cached under Groq was served from cache: the user asked for diarization
and silently got a transcript whose speaker_id is None throughout.

Root cause is placement rather than the boolean itself. Deciding "was this
deliberate?" outside the function that makes the routing decision let the two
drift, which is exactly how this survived the previous pass. resolve_provider()
now returns (provider, api_key, explicit) and settles both together, so a future
routing rule cannot forget to update the staleness signal.

Neither direction re-uploads without cause. --num-speakers with only a Groq key
leaves explicit False, so a Groq cache still hits and the existing no-diarization
warning is the only output. An ElevenLabs cache is never replaced by a Groq
re-transcribe on this path either, since that would trade diarization away.

Verified: explicit resolves correctly across all five provider/num-speaker
combinations; the reported case now reaches Scribe (confirmed by a deliberate
401 rather than a cache return); no spurious re-upload when only Groq is
configured; no downgrade of an ElevenLabs cache; full clean batch -> pack
unchanged at 4 phrases.

@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 4 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="helpers/transcribe.py">

<violation number="1" location="helpers/transcribe.py:270">
P3: Every cache hit now reads and JSON-parses the whole transcript file just to fetch the `provider` field, and transcribe_batch.py duplicates this same read for each file before calling transcribe_one — a real (if modest) perf regression on large batches of already-transcribed videos versus the old plain `exists()` check.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

Comment thread helpers/transcribe_batch.py Outdated
Comment thread helpers/transcribe.py Outdated
The staleness predicate was written twice — once in transcribe_one, once in
transcribe_batch.is_cached — and the batch script reached across the module
boundary for the underscore-prefixed _cached_provider to do it. Same shape of
mistake as the previous commit: a rule duplicated at two call sites drifts, and
these two disagreeing would mean the batch path skipping a source the
single-file path would redo.

is_transcript_stale(path, provider, provider_explicit) is now the only place
that decides, and both call sites ask it. _cached_provider goes back to being
private to the module.

The helper also short-circuits before touching the disk when the run did not
name a backend, since nothing else can invalidate a cache. That reverses the
read regression this feature introduced rather than merely narrowing it: the
common `auto` path now parses no transcript at all, where before the change it
parsed one per cached file, and the batch path reads nothing regardless because
it decides staleness up front. Only an explicit-provider run pays a single read,
and only to answer a question it was asked.

Verified: staleness truth table across cached-provider x requested-provider x
explicit, including transcripts predating the provider field, which stay
non-stale so no existing cache is re-billed; auto path performs zero reads and
explicit performs one, asserted by intercepting Path.read_text; clean batch ->
pack unchanged at 4 phrases; cache hits, explicit mismatch, --num-speakers
switch and batch pending-selection all behave as before.
@moocow4844 moocow4844 closed this Jul 28, 2026
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