Skip to content

Instantly share code, notes, and snippets.

@bogdan
Last active September 9, 2026 12:53
Show Gist options
  • Select an option

  • Save bogdan/f06835149fcb5aba4ef20cc22663284f to your computer and use it in GitHub Desktop.

Select an option

Save bogdan/f06835149fcb5aba4ef20cc22663284f to your computer and use it in GitHub Desktop.

Meeting Processing — Cost Report

What it costs to run one meeting through the transcription pipeline, measured against meeting 850 with live LLM calls on 2026-09-09.

Meeting 850 — online GP consultation (ja-JP, 5.0 min): patient with recurrent unilateral pulsatile headache since their early 20s, now increasing in frequency, with visual aura, nausea, photophobia/phonophobia, and sleep-deprivation / red-wine triggers. Doctor rules out red flags, does a brief video neuro exam, diagnoses migraine with aura, prescribes a triptan + antiemetic + prophylactic, and advises a headache diary with follow-up. A short, single-complaint case with a clean two-speaker diarization — a light-to-typical load for the pipeline.

Pipeline

Meeting#... (meeting.rb:279)
  └─ TranscriptionJob         extract audio → GCS upload → STT BatchRecognize
       └─ TranscriptionPollJob    poll STT → Gemini speaker ID
            └─ TranscriptCorrectionJob   Claude Opus transcript correction
                 └─ TokenizationJob        local tokenize + Gemini romaji fallback
                      └─ PHIDetectionJob     Gemini PHI detection

ThirdPartyDetectionJob is not in this chain — it runs a local RF-DETR model (bin/detect_third_party), so it costs CPU/GPU time only, no metered API.

Billable service calls

# Call site Service Billing basis
1 transcription_job.rb:96 stt_audio.attach Google Cloud Storage Storage + write op for the WAV (purged after poll)
2 transcription_job.rb:62Transcriber.submit_batch STT v2 BatchRecognize, chirp_3 + diarization + word offsets Per audio-minute
3 transcription_poll_job.rb:59SpeakerIdentification.call Vertex AI Gemini gemini-3.1-flash-lite Per token
4 transcript_correction_job.rb:11TranscriptCorrection.find_issues Vertex AI Claude claude-opus-4-6 Per token
5 tokenization_job.rb:19SuikaTokenizer.to_romajigemini_to_romaji Vertex AI Gemini gemini-3.1-flash-lite Per token, per uncached CJK token
6 phi_detection_job.rb:12PHIDetection.run Vertex AI Gemini gemini-3.1-flash-lite Per token

Not billed: Transcriber.poll_batch (GetOperation polling — quota only), MediaUtils ffmpeg/ffprobe (local compute), SuikaTokenizer MeCab path (local).

Price assumptions

Exact rates for the model builds in the code (chirp_3, gemini-3.1-flash-lite, claude-opus-4-6) are not separately published. These are early-2026 GCP / Vertex list prices used as proxies — treat as ±30%.

Service Input Output
STT v2 batch, chirp_3 $0.016 / min
Gemini flash-lite $0.10 / 1M tok $0.40 / 1M tok
Claude Opus $15.00 / 1M tok $75.00 / 1M tok
GCS standard $0.02 / GB-month + $0.005 / 1k writes

STT free tier (first 60 min/month) is ignored as marginal cost.

Meeting 850 — input size

Metric Value
Case Online GP consultation — migraine with aura (new/worsening recurrent headache)
Video length (original_duration_seconds) 300.0 s (5 min 0 s)
STT wall-clock (stt_elapsed_seconds) 180 s
Language ja-JP
Transcript segments (turns) 54
Transcript words 1,972
Transcript text 3,537 chars
formatted_transcript (fed to PHI detection) 4,105 chars
Tokens stored (meeting.tokens) 2,107 (453 unique)
Correction issues found 9

Measured token usage — meeting 850

Step Model Input tok Output tok
Speaker ID Gemini flash-lite 360 19
Transcript correction Claude Opus 11,243 1,428
PHI detection Gemini flash-lite 2,626 40
Romaji fallback Gemini flash-lite 51 (×1 call) 13

Only 1 of 453 unique tokens ("拍") hit the Gemini romaji fallback; MeCab handled the rest.

Cost — meeting 850 (5.0 min)

Step Calculation Cost
Transcript correction (Claude Opus) 11,243 × $15/M + 1,428 × $75/M $0.27575
STT BatchRecognize 5.0 × $0.016 $0.08000
PHI detection 2,626 × $0.10/M + 40 × $0.40/M $0.00028
Speaker ID 360 × $0.10/M + 19 × $0.40/M $0.00004
Romaji fallback 51 × $0.10/M + 13 × $0.40/M $0.00001
GCS WAV (~9.6 MB, minutes) storage + 1 write ~$0.00010
Total ≈ $0.3562

≈ $0.356 per meeting — $0.071 per audio-minute.

Cost split: transcript correction 77%, STT 22%, everything else < 0.1%.

Extrapolation by duration

STT scales exactly with duration; correction input scales ~linearly with transcript length; correction output grows sub-linearly. "Other LLM" is the Gemini speaker-ID + PHI + romaji calls combined.

Duration STT Correction (Opus) Other LLM Total Per min
5 min (850) $0.080 $0.276 ~$0.0004 ~$0.356 $0.071
15 min $0.240 ~$0.75 ~$0.001 ~$0.99 $0.066
30 min $0.480 ~$1.45 ~$0.002 ~$1.93 $0.064
60 min $0.960 ~$2.85 ~$0.003 ~$3.81 $0.064

Cost drivers & levers

  • Transcript correction dominates (77%). At Opus rates the single correction call costs $0.276 for a 5-min meeting — 3.4× the STT cost — and it scales ~linearly with transcript length, so the ratio only grows for longer meetings. Model choice on this one call is the biggest cost lever in the pipeline — a cheaper model here (Sonnet-class, or a Gemini flash/pro tier) would cut the meeting total by an order of magnitude.
  • STT is the second lever (22%). If the batch_recognize call qualifies for dynamic-batch pricing (~$0.003/min instead of $0.016/min), STT drops to $0.015 and the meeting to **$0.29**. Worth confirming which rate BatchRecognize + inline_response_config actually bills at.
  • Speaker ID, PHI, romaji are rounding error — together < $0.0004.
  • Retries multiply. Each LLM job has retry_on StandardError, attempts: 3. A transient failure on the correction job re-runs the whole Opus call (+$0.28 each). A worst-case double STT submission (documented SIGKILL race, transcription_job.rb:10-12) adds ~$0.08.
  • Prompt caching is unused. ClaudeClient.request_body sends no cache_control blocks (cache_creation_input_tokens: 0 observed). The ~600-token instruction header is re-billed at full input rate every call; caching it would shave only ~$0.009/meeting, so not worth it at current volume.

Reproduce

bin/rails runner '
m = Meeting.find(850)
segs = m.original_transcription

def g(p, model)
  x = GeminiClient.client.generate_content(model: GeminiClient.model_path(model),
    contents: [{role:"user",parts:[{text:p}]}],
    generation_config: {response_mime_type:"application/json", temperature:0}).usage_metadata
  {in: x.prompt_token_count, out: x.candidates_token_count}
end

def c(p, model)
  r = ClaudeClient.client.raw_predict(endpoint: ClaudeClient.model_path(model),
    http_body: {data: ClaudeClient.request_body(p), content_type: "application/json"})
  JSON.parse(r.data)["usage"].slice("input_tokens", "output_tokens")
end

puts g(SpeakerIdentification.build_prompt(segs, doctor_label: Doctor.name, patient_label: Patient.name), GeminiClient::DEFAULT_MODEL).inspect
puts c(TranscriptCorrection.build_prompt(segs), TranscriptCorrection::MODEL).inspect
puts g(PHIDetection.prompt(m.formatted_transcript), GeminiClient::DEFAULT_MODEL).inspect
'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment