feat: add Groq Whisper as a free transcription provider - #114
Conversation
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.
There was a problem hiding this comment.
All reported issues were addressed across 6 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
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.
|
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 P2 (cache vs 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:
P2 ( Verified against the live API: provider recorded on write; |
There was a problem hiding this comment.
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
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.
There was a problem hiding this comment.
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
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.
What
Adds Groq's hosted
whisper-large-v3-turboas a second transcription backend alongside ElevenLabs Scribe, and makes it the default when aGROQ_API_KEYis present.Both scripts gain
--provider auto|groq|elevenlabs.autoprefers Groq and falls back to Scribe, so existing ElevenLabs-only setups keep working with no change.Why
transcribe.pyonly 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'sverbose_jsoninto the Scribe payload shape (words[]of{text, start, end, type, speaker_id}), sopack_transcripts.py,render.py, andtimeline_view.pyconsume 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:
S0/S1tags intakes_packed.md, and phrases then only break on silence.--provider elevenlabsis the answer, and SKILL.md now says so at the point of use.(laughs),(applause)) don't appear. SKILL.md points at reading them off the waveform intimeline_viewinstead.--num-speakersnow 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
_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..envvalues shadowed real keys. The shipped.env.exampleplaceholderELEVENLABS_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, andSKILL.mdnow 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:
pack_transcripts.pytakes_packed.md(3 phrases from a 3-utterance clip)end < start, or overlap with previous word) across outputspy_compileon all helpersNot covered: the ElevenLabs path was not re-run (no key on hand), but it is untouched apart from receiving
.wavvia the sameextract_audiocall it always did.Summary by cubic
Adds Groq Whisper as a free transcription provider and makes it the default when
GROQ_API_KEYis set; ElevenLabs Scribe remains available for diarization and audio events via--provideror--num-speakers. Centralizes cache staleness logic to keep CLI and batch behavior consistent and avoid unnecessary reads onauto.New Features
whisper-large-v3-turbo; response mapped to the Scribe shape (words[] {text,start,end,type,speaker_id}) so downstream tools work unchanged.--provider auto|groq|elevenlabs;autoprefers Groq and falls back to Scribe.--num-speakersupgrades to ElevenLabs when its key is present; warns if only Groq is available..env.exampleupdated.Bug Fixes
provider; explicit--provideror a--num-speakers-driven switch re-transcribes when mismatched; underautothe cache wins; legacy transcripts are left untouched. Centralized staleness inis_transcript_stale()so single and batch paths agree andautoavoids reading cached files.transcribe_one()defaults toelevenlabsfor programmatic callers; CLIs pass--providerexplicitly..envvalues and toleratedexport KEY=value; install snippet now writes the correct var name (GROQ_API_KEYorELEVENLABS_API_KEY).Written for commit 8b70671. Summary will update on new commits.