Add Google Cloud Speech-to-Text v2 as an online speech-to-text engine - #14561
Conversation
Pure REST implementation with word level timings, using the Google.Apis.Auth package already shipped for Google TTS to turn a service-account key into a bearer token. No new NuGet packages and no new language strings. Flow: ensure bucket -> upload audio -> batchRecognize -> poll -> delete object. Word offsets are range-checked against the billed duration, and a transcript ending far before the billed duration is logged as possibly truncated. IOnlineSttEngine gains an optional MaxChunkSeconds so the shared chunker can split by duration; Google caps a file with word timings at 20 minutes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
I went through the merged code against the runs the numbers in your description come from. Four things I would raise, in order of how much they matter. 1. The engine sends 32 kbit/s MP3, and every accuracy number came from lossless FLAC
That limit does not apply here. The audio goes to Cloud Storage, which has no such cap, and This matters because all the figures quoted for this engine, the 13,175 timed words and the 2. No
|
|
Ran it against the live API. Short version: your REST shapes are correct, the defaults Method: the exact request body What works exactly as you wrote it
One bug: the first word of every result is being droppedproto3 JSON omits zero-valued fields, and the REST API follows that, so a word starting at { "endOffset": "1.560s", "word": "Ay" }
{ "startOffset": "1.560s", "endOffset": "1.800s", "word": "vay" }
Fixed in #14567 by treating an absent Also worth knowing: The audio format is worth changingSame source audio, same request, only the encoding differs:
A sample of what actually changed: I do not have ground truth for this clip, so I am not claiming flac is 14% more accurate. Measured cost of the change: an 18 minute chunk is 20.7 MB as flac against 4.1 MB as mp3. All of this is in #14567 along with the dynamic batching option and the truncation |
|
One more from the live run, and I think this one is more serious than the rest, so flagging Chirp returns a whole file as a single result. Measured just now against the live API:
The word timings are fetched, parsed, range checked, and then dropped on the floor at Fixed in #14567 by cutting segments from the word timings with That change made four existing assertions fail, including one of yours, because they For context on what the output should look like: on a 145 minute episode the same word |
|
Closing the loop on the live testing you asked for: I ran the engine end to end, not just The full round trip works
That last line is your range check earning its keep on live data. Google returned Two things worth knowingChirp is deterministic. Same audio, same config, two separate The language setting materially changes the transcription. Same audio, same everything
Real differences, not punctuation: One caveat, stated plainlyI could not use a service account key. My account on that project has So everything from the credential onwards is genuinely exercised: token exchange, bucket Everything above is with #14567 applied. Without it the same run produces one subtitle line |
…r truncation Four follow-ups to #14561, from reviewing it against the runs the accuracy figures in that PR come from. Send flac instead of 32 kbit/s mp3. IsOnlineSttEngine is true for the Google engine, so audio extraction fell through to the mp3 branch, which targets ~32 kbit/s because it "keeps a 2-hour video well under OpenAI's 25 MB upload limit". That limit does not apply here: the audio goes to Cloud Storage, and it is already split into 18 minute chunks. The measured numbers for this engine, 13,175 timed words and 54.3% speech density, all came from 16 kHz mono flac, so mp3 is not the input the evidence describes. An 18 minute chunk is 20.7 MB as flac against 4.1 MB as mp3, measured. The new flac case passes -sample_fmt s16 explicitly: without it ffmpeg encodes 24-bit flac from an AAC source, 78% larger and bigger than the raw PCM it replaces. Allow DYNAMIC_BATCHING. Without processingStrategy every run bills at $0.016 per minute rather than $0.003, so a 140 minute episode costs about $2.23 instead of $0.42. It carries no latency guarantee, so it is an opt-in checkbox and stays off by default, though it measured 13.6x realtime. Recover silent truncation rather than only logging it. The detection added in #14561 is right, but a transcript that stops early is still returned as if it were complete. This was observed on real media: an 18 minute chunk returned words only to 398 s and discarded the remaining 11.4 minutes with the operation reporting success. The tail is now re-cut from a second before the last word, resubmitted once, and merged back. Bound the word range check when no billed duration is reported. The guard read "billed > 0 && end > billed + 1", so a missing totalBilledDuration left no upper bound at all and the 6,324 s offset inside a 1,080 s chunk that motivated the guard would pass. It now falls back to the largest resultEndOffset, which is reported per result. 282 SpeechToText tests pass, including four new ones.
Bump the version to v5.2.0-rc4 in Se.cs and English.json, and add the change-log section covering the 25 pull requests merged since v5.2.0-rc3 (every merged PR ancestor-checked against the rc3 tag). Grouped: the Google Cloud STT engine (SubtitleEdit#14561) with muaz978's follow-up (SubtitleEdit#14567); the four per-line clone engines (SubtitleEdit#14562, SubtitleEdit#14570); the two blank video fixes (SubtitleEdit#14583, SubtitleEdit#14584); the two Enter-runs-OK PRs (SubtitleEdit#14587, SubtitleEdit#14589). Left out as internal-only or in-RC regressions: test/nullable warning cleanups (SubtitleEdit#14579, SubtitleEdit#14580) and the STT window layout fix for the new engine (SubtitleEdit#14572). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Summary
Adds Google Cloud Speech-to-Text to the Speech to text engine list, using the v2 API over plain REST with word level timings.
Google.Apis.Auth, which SE already ships for Google TTS. Everything else isHttpClient+System.Text.Json.batchRecognize→ poll operation → delete object. BatchRecognize only reads from Cloud Storage; the bucket<project>-subtitle-edit-sttis created on first use with a one-day lifecycle rule (override viaGoogleCloudSttBucketNamein Settings.json).IOnlineSttEnginegains an optionalMaxChunkSeconds; the shared chunker now also splits by duration. Google caps a file with word timings at 20 minutes, the engine uses 18.Background: the plugin proposed in SubtitleEdit/plugins#289 ships a 40 MB self-contained app per platform. Folding the engine into SE costs a few tens of KB because the gRPC/auth stack is already present.
Setup for users
Speech-to-Text v2 rejects API keys. The user needs a Google Cloud project with billing, the Speech-to-Text API enabled, a service account with the roles Cloud Speech Client and Storage Admin, and its JSON key. Defaults: region
us, modelchirp_3, empty language hint = automatic detection.Testing
tests/UI/.../GoogleCloud/GoogleCloudSttServiceTests.cs: host selection, bucket naming, request body, response parsing incl. corrupt words and per-file errors, proto duration parsing.🤖 Generated with Claude Code