From 94e0e6d632f1f42429421538c250f20331961902 Mon Sep 17 00:00:00 2001 From: Armin Sander Date: Wed, 16 Sep 2026 13:46:44 +0200 Subject: [PATCH 1/4] google-transcribe: add numerals support via digit-sequence class token --- ...-google-transcribe-numerals-class-token.md | 47 ++++++++ examples/transcribe.rs | 15 ++- .../google-transcribe/src/class_tokens.rs | 110 ++++++++++++++++++ services/google-transcribe/src/client.rs | 50 ++++++-- services/google-transcribe/src/lib.rs | 34 +++++- services/google-transcribe/src/transcribe.rs | 22 +--- 6 files changed, 245 insertions(+), 33 deletions(-) create mode 100644 docs/adr/0005-google-transcribe-numerals-class-token.md create mode 100644 services/google-transcribe/src/class_tokens.rs diff --git a/docs/adr/0005-google-transcribe-numerals-class-token.md b/docs/adr/0005-google-transcribe-numerals-class-token.md new file mode 100644 index 0000000..e829e8e --- /dev/null +++ b/docs/adr/0005-google-transcribe-numerals-class-token.md @@ -0,0 +1,47 @@ +# ADR 0005: Google transcribe `numerals` as a class-token adaptation hint + +Date: 2026-09-16 +Status: Accepted + +## Context + +Deepgram's transcriber exposes a `numerals` option that formats recognized +numbers as digits. Google Cloud Speech-to-Text V2 has no equivalent +formatting toggle: `RecognitionConfig`/`RecognitionFeatures` contain no +numerals field, and Google already emits recognized numbers as digits by +default (built-in ITN). + +What Google does offer is *recognition biasing* via `SpeechAdaptation`: +an inline `PhraseSet` whose phrases may contain class tokens such as +`$OOV_CLASS_DIGIT_SEQUENCE` ("nine four one two" → `9412`). This changes +what the recognizer hears, not how the transcript is formatted, but the +observable contract for callers is the same as Deepgram's `numerals`: +numbers come out as digits. + +## Decision + +- `google_transcribe::Params` gains a `numerals: bool`. When set, the + service sends an inline `SpeechAdaptation` containing a single phrase + with the `$OOV_CLASS_DIGIT_SEQUENCE` class token. +- The full class-token vocabulary (27 strings across the `$OOV_CLASS_*` + and bare `$*` families) is documented as public constants in + `services/google-transcribe/src/class_tokens.rs`. Only + `$OOV_CLASS_DIGIT_SEQUENCE` is wired up; the rest exist as reference + documentation. +- The hint is sent unconditionally when `numerals` is requested. There is + no model/locale filter: Google publishes no (model × locale) support + matrix for class tokens (the class-tokens page is locale-only), and + Google silently ignores tokens unsupported for the request's locale. +- The example CLI accepts `--numerals` for the Google provider. + +## Consequences + +- `--numerals` now works for both Deepgram and Google with the same + observable effect, though the underlying mechanisms differ (formatting + vs. recognition bias). On Google the effect covers digit sequences + specifically, not every numeric phrase. +- Token availability depends on the selected model and locale; see + . +- If a support filter is ever needed, per-locale availability data must be + sourced fresh from Google's class-tokens page; it is not derivable from + the API reference or the proto crate. diff --git a/examples/transcribe.rs b/examples/transcribe.rs index d7fc2f3..9a18f6d 100644 --- a/examples/transcribe.rs +++ b/examples/transcribe.rs @@ -335,12 +335,16 @@ async fn start_conversation( // https://docs.cloud.google.com/speech-to-text/docs/speech-to-text-supported-languages let params = google_transcribe::Params { - model: provider_args.model.map(str::to_owned).unwrap_or_else(|| { - env::var("GOOGLE_TRANSCRIBE_MODEL").unwrap_or_else(|_| "latest_long".to_owned()) - }), - language: languages.join_csv(), - diarization: provider_args.diarization, region, + transcribe: google_transcribe::TranscribeParams { + model: provider_args.model.map(str::to_owned).unwrap_or_else(|| { + env::var("GOOGLE_TRANSCRIBE_MODEL") + .unwrap_or_else(|_| "latest_long".to_owned()) + }), + language: languages.join_csv(), + diarization: provider_args.diarization, + numerals: provider_args.numerals, + }, }; GoogleTranscribe.conversation(params, conversation).await } @@ -449,6 +453,7 @@ impl Provider { Provider::Google => { capabilities.region = true; capabilities.diarization = true; + capabilities.numerals = true; capabilities.model = true; } Provider::Aristech => { diff --git a/services/google-transcribe/src/class_tokens.rs b/services/google-transcribe/src/class_tokens.rs new file mode 100644 index 0000000..cfb3cfd --- /dev/null +++ b/services/google-transcribe/src/class_tokens.rs @@ -0,0 +1,110 @@ +//! Google Cloud Speech-to-Text V2 class tokens for speech adaptation. +//! +//! Class tokens are placeholders that can be embedded in `PhraseSet` phrases to +//! bias the recognizer toward a whole class of values (numbers, dates, phone +//! numbers, ...) without enumerating every possible value. They are sent inside +//! an inline `PhraseSet` via `RecognitionConfig.adaptation`. +//! +//! Token availability varies by locale and transcription model; Google +//! silently ignores tokens that are not supported for the request's locale. +//! The authoritative per-locale table is published at +//! . +//! +//! Two naming families exist: the `$OOV_CLASS_*` prefixed tokens and the bare +//! `$*` tokens. Seven base names exist in both forms; the two families are not +//! interchangeable. + +/// A sequence of letters `[a-z]` and/or digits, for example "a1b2c3". +pub const OOV_CLASS_ALPHANUMERIC_SEQUENCE: &str = "$OOV_CLASS_ALPHANUMERIC_SEQUENCE"; + +/// A sequence of letters `[a-z]`, for example "cqbcf". +pub const OOV_CLASS_ALPHA_SEQUENCE: &str = "$OOV_CLASS_ALPHA_SEQUENCE"; + +/// An AM radio frequency, for example "twelve twenty" → `1220`. +pub const OOV_CLASS_AM_RADIO_FREQUENCY: &str = "$OOV_CLASS_AM_RADIO_FREQUENCY"; + +/// A digit sequence of any length, for example "nine four one two" → `9412`. +pub const OOV_CLASS_DIGIT_SEQUENCE: &str = "$OOV_CLASS_DIGIT_SEQUENCE"; + +/// An FM radio frequency, for example "one oh four point three" → `104.3`. +pub const OOV_CLASS_FM_RADIO_FREQUENCY: &str = "$OOV_CLASS_FM_RADIO_FREQUENCY"; + +/// A street number for an address in the target locale (prefixed variant), +/// for example "one hundred ninety one" → `191`. +pub const OOV_CLASS_ADDRESSNUM: &str = "$OOV_CLASS_ADDRESSNUM"; + +/// A full date using numbers (prefixed variant), for example +/// "nine nine nine two thousand fourteen" → `9.9.2014` (locale-dependent format). +pub const OOV_CLASS_FULLDATE: &str = "$OOV_CLASS_FULLDATE"; + +/// A phone number as used in the target locale (prefixed variant), for example +/// "six five oh five five five six one oh one" → `650-555-6101`. +pub const OOV_CLASS_FULLPHONENUM: &str = "$OOV_CLASS_FULLPHONENUM"; + +/// A numerical value including whole numbers, fractions, and decimals +/// (prefixed variant), for example "twenty two" → `22`. +pub const OOV_CLASS_OPERAND: &str = "$OOV_CLASS_OPERAND"; + +/// An ordinal number (prefixed variant), for example "third" → `3rd`. +pub const OOV_CLASS_ORDINAL: &str = "$OOV_CLASS_ORDINAL"; + +/// A percentage value including the percent sign (prefixed variant), for +/// example "ten point five percent" → `10.5%`. +pub const OOV_CLASS_PERCENT: &str = "$OOV_CLASS_PERCENT"; + +/// A postal code as used in the target locale (prefixed variant), for example +/// "one zero zero one zero" → `10010`. +pub const OOV_CLASS_POSTALCODE: &str = "$OOV_CLASS_POSTALCODE"; + +/// A temperature in degrees, for example "minus one" → `-1`. +pub const OOV_CLASS_TEMPERATURE: &str = "$OOV_CLASS_TEMPERATURE"; + +/// A television channel number, for example "two zero two" → `202`. +pub const OOV_CLASS_TV_CHANNEL: &str = "$OOV_CLASS_TV_CHANNEL"; + +/// A street number for an address in the target locale, for example +/// "one hundred ninety one" → `191`. +pub const ADDRESSNUM: &str = "$ADDRESSNUM"; + +/// A full date using numbers, for example "nine nine nine two thousand +/// fourteen" → `9.9.2014` (locale-dependent format). +pub const FULLDATE: &str = "$FULLDATE"; + +/// A phone number as used in the target locale, for example +/// "one eight hundred five five five four oh oh one" → `+1-800-555-4001`. +pub const FULLPHONENUM: &str = "$FULLPHONENUM"; + +/// A numerical value including whole numbers, fractions, and decimals, for +/// example "twenty two" → `22`. +pub const OPERAND: &str = "$OPERAND"; + +/// An ordinal number, for example "third" → `3rd`. +pub const ORDINAL: &str = "$ORDINAL"; + +/// A percentage value including the percent sign, for example +/// "ten point five percent" → `10.5%`. +pub const PERCENT: &str = "$PERCENT"; + +/// A postal code as used in the target locale, for example +/// "one zero zero one zero" → `10010`. +pub const POSTALCODE: &str = "$POSTALCODE"; + +/// A numbered day within a month, for example "the twenty third" → `23rd`. +pub const DAY: &str = "$DAY"; + +/// An amount of money with a currency unit name, for example +/// "forty three dollars" → `$43`. +pub const MONEY: &str = "$MONEY"; + +/// A named month in a year, for example "july" → `July`. Contextual phrases +/// like "2 months from now" are not supported. +pub const MONTH: &str = "$MONTH"; + +/// A numbered street name, for example "fifty first" → `51st`. +pub const STREET: &str = "$STREET"; + +/// A specific time of day, for example "ten thirty" → `10:30`. +pub const TIME: &str = "$TIME"; + +/// A year, for example "twenty ten" → `2010`. +pub const YEAR: &str = "$YEAR"; diff --git a/services/google-transcribe/src/client.rs b/services/google-transcribe/src/client.rs index 2959b26..beeed2e 100644 --- a/services/google-transcribe/src/client.rs +++ b/services/google-transcribe/src/client.rs @@ -5,13 +5,16 @@ use anyhow::Result; use async_stream::{stream, try_stream}; use futures::Stream; use tokio::sync::mpsc::UnboundedReceiver; -use tracing::debug; +use tracing::{debug, info}; use googleapis_tonic_google_cloud_speech_v2::google::cloud::speech::v2::recognition_config::DecodingConfig; +use googleapis_tonic_google_cloud_speech_v2::google::cloud::speech::v2::speech_adaptation::AdaptationPhraseSet; +use googleapis_tonic_google_cloud_speech_v2::google::cloud::speech::v2::speech_adaptation::adaptation_phrase_set::Value as AdaptationPhraseSetValue; use googleapis_tonic_google_cloud_speech_v2::google::cloud::speech::v2::{ - ExplicitDecodingConfig, RecognitionConfig, RecognitionFeatures, StreamingRecognitionConfig, - StreamingRecognitionFeatures, StreamingRecognizeRequest, StreamingRecognizeResponse, - SpeakerDiarizationConfig, + ExplicitDecodingConfig, PhraseSet, RecognitionConfig, RecognitionFeatures, + StreamingRecognitionConfig, StreamingRecognitionFeatures, StreamingRecognizeRequest, + StreamingRecognizeResponse, SpeakerDiarizationConfig, SpeechAdaptation, + phrase_set::Phrase, }; use googleapis_tonic_google_cloud_speech_v2::google::cloud::speech::v2::explicit_decoding_config; use googleapis_tonic_google_cloud_speech_v2::google::cloud::speech::v2::streaming_recognize_request::StreamingRequest; @@ -19,6 +22,8 @@ use googleapis_tonic_google_cloud_speech_v2::google::cloud::speech::v2::streamin use context_switch_core::AudioFormat; use context_switch_core::audio; +use crate::TranscribeParams; +use crate::class_tokens::OOV_CLASS_DIGIT_SEQUENCE; use crate::host::Client; /// A google transcribe client. Capable of streaming audio data in and transcribe results out. @@ -40,13 +45,19 @@ impl TranscribeClient { pub async fn transcribe<'a>( &mut self, - model: &str, - language_codes: &[String], - diarization: bool, + params: &TranscribeParams, interim_results: bool, audio_format: AudioFormat, mut audio_receiver: UnboundedReceiver>, ) -> Result> + 'a> { + let language_codes = params.languages()?; + let TranscribeParams { + model, + diarization, + numerals, + .. + } = params; + let model = model.as_str(); let decoding_config = ExplicitDecodingConfig { // We only support 16-bit signed little-endian PCM samples here for now. encoding: explicit_decoding_config::AudioEncoding::Linear16.into(), @@ -54,6 +65,28 @@ impl TranscribeClient { audio_channel_count: audio_format.channels as i32, }; + // Bias digit-sequence recognition so spoken numbers are transcribed as digits. + // Sent unconditionally when requested; token availability depends on the model and + // locale, and Google silently ignores unsupported tokens. + let adaptation = numerals.then(|| { + info!( + token = OOV_CLASS_DIGIT_SEQUENCE, + "Sending speech adaptation class token" + ); + SpeechAdaptation { + phrase_sets: vec![AdaptationPhraseSet { + value: Some(AdaptationPhraseSetValue::InlinePhraseSet(PhraseSet { + phrases: vec![Phrase { + value: OOV_CLASS_DIGIT_SEQUENCE.to_owned(), + boost: 0.0, + }], + ..Default::default() + })), + }], + custom_classes: vec![], + } + }); + let recognition_config = RecognitionConfig { // TODO: configure model: model.into(), @@ -65,7 +98,7 @@ impl TranscribeClient { }), ..Default::default() }), - adaptation: None, + adaptation, transcript_normalization: None, denoiser_config: None, translation_config: None, @@ -91,6 +124,7 @@ impl TranscribeClient { model = %model, language_codes = ?language_codes, diarization, + numerals, interim_results, "Starting Google streaming_recognize" ); diff --git a/services/google-transcribe/src/lib.rs b/services/google-transcribe/src/lib.rs index cc0e5c6..71d2139 100644 --- a/services/google-transcribe/src/lib.rs +++ b/services/google-transcribe/src/lib.rs @@ -1,6 +1,10 @@ //! A Google Speech to Text V2 service. +use anyhow::{Context, Result}; use serde::Deserialize; +use context_switch_core::language::Languages; + +pub mod class_tokens; mod client; mod host; pub mod transcribe; @@ -10,6 +14,20 @@ pub use transcribe::GoogleTranscribe; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Params { + /// Google Cloud location and API endpoint. Only `global`, `eu`, and `us` are supported. + /// Defaults to `global`. + #[serde(default)] + pub region: Region, + /// Recognition parameters passed through to the transcribe function. + #[serde(flatten)] + pub transcribe: TranscribeParams, +} + +/// The subset of `Params` that configures recognition and is passed through to the +/// transcribe function. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TranscribeParams { /// Google Cloud Speech-to-Text `V2` recognition model (for example, `latest_long`). pub model: String, /// One or more comma-separated BCP 47 locale codes sent as `language_codes`. @@ -18,10 +36,20 @@ pub struct Params { /// the selected model, language, and region. #[serde(default)] pub diarization: bool, - /// Google Cloud location and API endpoint. Only `global`, `eu`, and `us` are supported. - /// Defaults to `global`. + /// Bias recognition toward digit sequences so spoken numbers are transcribed as digits + /// (for example, "nine four one two" → `9412`). Implemented as a speech-adaptation hint + /// using the `$OOV_CLASS_DIGIT_SEQUENCE` class token; token availability depends on the + /// selected model and locale. #[serde(default)] - pub region: Region, + pub numerals: bool, +} + +impl TranscribeParams { + /// Extract the BCP 47 locale codes from the comma-separated `language` value. + pub fn languages(&self) -> Result { + Languages::from_csv(&self.language) + .context("language must contain at least one locale code") + } } #[derive(Debug, Clone, Copy, Default, Deserialize)] diff --git a/services/google-transcribe/src/transcribe.rs b/services/google-transcribe/src/transcribe.rs index e653e08..5624f84 100644 --- a/services/google-transcribe/src/transcribe.rs +++ b/services/google-transcribe/src/transcribe.rs @@ -12,16 +12,15 @@ use googleapis_tonic_google_cloud_speech_v2::google::cloud::speech::v2::{ }; use tonic::Code; -use context_switch_core::language::Languages; use context_switch_core::{ AudioFormat, AudioFrame, AudioProducer, BillingRecord, BillingSchedule, Conversation, ConversationOutput, Input, OutputModality, Service, }; use crate::Params; +use crate::TranscribeParams; use crate::client::TranscribeClient; use crate::host::Host; - #[derive(Debug)] pub struct GoogleTranscribe; @@ -43,8 +42,6 @@ impl Service for GoogleTranscribe { .output_modalities .iter() .any(|modality| matches!(modality, OutputModality::InterimText)); - let languages = Languages::from_csv(¶ms.language) - .context("language must contain at least one locale code")?; let host = Host::new(params.region.into()).await?; @@ -61,8 +58,7 @@ impl Service for GoogleTranscribe { let session_future = transcribe_and_process_stream_session( &mut client, - ¶ms, - &languages, + ¶ms.transcribe, interim_results, audio_format, audio_receiver, @@ -128,24 +124,16 @@ fn forward_audio_and_emit_billing( async fn transcribe_and_process_stream_session( client: &mut TranscribeClient, - params: &Params, - languages: &Languages, + params: &TranscribeParams, interim_results: bool, audio_format: AudioFormat, audio_receiver: UnboundedReceiver>, output: &ConversationOutput, ) -> Result { - let include_detected_language = languages.len() > 1; + let include_detected_language = params.languages()?.len() > 1; let response_stream = client - .transcribe( - ¶ms.model, - languages, - params.diarization, - interim_results, - audio_format, - audio_receiver, - ) + .transcribe(params, interim_results, audio_format, audio_receiver) .await?; process_stream_session( From 0e4433902add03a5646c385e2dd55d9ce36edf94 Mon Sep 17 00:00:00 2001 From: Armin Sander Date: Wed, 16 Sep 2026 14:38:38 +0200 Subject: [PATCH 2/4] google-transcribe: take first alternative, boost digit class token, document rejected re-ranking --- ...-google-transcribe-numerals-class-token.md | 22 ++++++++++ services/google-transcribe/src/client.rs | 12 +++-- services/google-transcribe/src/transcribe.rs | 44 +++++++------------ 3 files changed, 46 insertions(+), 32 deletions(-) diff --git a/docs/adr/0005-google-transcribe-numerals-class-token.md b/docs/adr/0005-google-transcribe-numerals-class-token.md index e829e8e..9c3ab80 100644 --- a/docs/adr/0005-google-transcribe-numerals-class-token.md +++ b/docs/adr/0005-google-transcribe-numerals-class-token.md @@ -32,6 +32,8 @@ numbers come out as digits. no model/locale filter: Google publishes no (model × locale) support matrix for class tokens (the class-tokens page is locale-only), and Google silently ignores tokens unsupported for the request's locale. +- The phrase carries a `boost` of 20.0 (the maximum), taken over from an + internal project that parameterized Google via FreeSWITCH. - The example CLI accepts `--numerals` for the Google provider. ## Consequences @@ -45,3 +47,23 @@ numbers come out as digits. - If a support filter is ever needed, per-locale availability data must be sourced fresh from Google's class-tokens page; it is not derivable from the API reference or the proto crate. + +## Rejected alternative: post-hoc alternative re-ranking + +An earlier iteration added a `digitBoost` parameter that added a +confidence bonus to final alternatives whose transcript was digit-only, +plus a `maxAlternatives` parameter (default 4) so Google would return +lower alternatives to boost. It was removed because it cannot work +reliably: + +- Google populates `confidence` only on the top alternative of a final + streaming result; every lower alternative carries `0.0`, which is the + documented sentinel for "not set", not a real score. Re-ranking by + confidence therefore compares unknown values against one known value. +- The only reliable ranking signal is Google's own ordering ("alternatives + are ordered in terms of accuracy, with the top (first) alternative being + the most probable, as ranked by the recognizer"), which the service now + follows directly: final results take the first alternative, and + `max_alternatives` is hardcoded to 1 in `client.rs`. +- Lower alternatives' confidences are still logged for observability, but + never used for selection. diff --git a/services/google-transcribe/src/client.rs b/services/google-transcribe/src/client.rs index beeed2e..d5e9ac1 100644 --- a/services/google-transcribe/src/client.rs +++ b/services/google-transcribe/src/client.rs @@ -78,7 +78,9 @@ impl TranscribeClient { value: Some(AdaptationPhraseSetValue::InlinePhraseSet(PhraseSet { phrases: vec![Phrase { value: OOV_CLASS_DIGIT_SEQUENCE.to_owned(), - boost: 0.0, + // This was taken over from the internal project that parameterized + // google via FreeSWITCH. Probably a good way to go for sure. + boost: 20.0, }], ..Default::default() })), @@ -91,11 +93,15 @@ impl TranscribeClient { // TODO: configure model: model.into(), language_codes: language_codes.to_vec(), - features: diarization.then_some(RecognitionFeatures { - diarization_config: Some(SpeakerDiarizationConfig { + features: Some(RecognitionFeatures { + diarization_config: diarization.then_some(SpeakerDiarizationConfig { min_speaker_count: 0, max_speaker_count: 0, }), + // We only ever emit the first alternative (see the selection comment in + // transcribe.rs); requesting more would only produce alternatives whose + // confidence is unset and cannot be compared. + max_alternatives: 1, ..Default::default() }), adaptation, diff --git a/services/google-transcribe/src/transcribe.rs b/services/google-transcribe/src/transcribe.rs index 5624f84..6e9e5db 100644 --- a/services/google-transcribe/src/transcribe.rs +++ b/services/google-transcribe/src/transcribe.rs @@ -7,8 +7,7 @@ use tokio::sync::mpsc::UnboundedReceiver; use tracing::{debug, info, warn}; use googleapis_tonic_google_cloud_speech_v2::google::cloud::speech::v2::{ - SpeechRecognitionAlternative, StreamingRecognizeResponse, WordInfo, - streaming_recognize_response::SpeechEventType, + StreamingRecognizeResponse, WordInfo, streaming_recognize_response::SpeechEventType, }; use tonic::Code; @@ -179,19 +178,20 @@ where // - For each result, alternatives are ordered by confidence, most confident first. // // Implementation detail: - // - For final results, we select the alternative with the highest confidence. - // Confidence is only populated on the top alternative; `0.0` is a sentinel for - // "not set", so ties (including all-unset) fall back to the first alternative. + // - For final results, we take the first alternative. Google's docs state that + // alternatives are "ordered in terms of accuracy, with the top (first) + // alternative being the most probable, as ranked by the recognizer", and that + // `confidence` "is set only for the top alternative" with `0.0` as the + // sentinel for "not set". Lower alternatives therefore carry no comparable + // score, so re-ranking them by confidence would compare unknown values; + // Google's own ordering is the only reliable signal, and it already puts the + // best hypothesis first. Their confidences are logged for observability only. // - For non-final responses, we concatenate transcripts from all results in the // current response as-is. match &response.results[..] { [] => continue, - [one] - if one.is_final - && let Some(alternative) = - alternative_with_max_confidence(&one.alternatives) => - { + [one] if one.is_final => { match one.alternatives.as_slice() { [only] => debug!( confidence = only.confidence, @@ -206,6 +206,11 @@ where "Final recognition alternatives" ), } + + let Some(alternative) = one.alternatives.first() else { + continue; + }; + // Sometimes there is whitespace at the beginning, so we trim. // // Intentionally allow empty final text. A non-final hypothesis may contain @@ -319,25 +324,6 @@ fn should_restart_for_stream_limit(code: Code, message: &str) -> bool { code == Code::Aborted && message.contains("max duration of 5 minutes reached for stream") } -/// Selects the alternative with the highest confidence. -/// -/// Google only populates `confidence` on the top alternative (`0.0` = not set), so -/// ties are common; on a tie the first alternative wins, matching Google's -/// documented "most confident first" ordering. -fn alternative_with_max_confidence( - alternatives: &[SpeechRecognitionAlternative], -) -> Option<&SpeechRecognitionAlternative> { - alternatives - .iter() - .enumerate() - .max_by(|(index_a, a), (index_b, b)| { - a.confidence - .total_cmp(&b.confidence) - .then(index_b.cmp(index_a)) - }) - .map(|(_, alternative)| alternative) -} - fn speaker_with_max_assigned_characters(words: &[WordInfo]) -> Option { let mut char_count_by_speaker = HashMap::<&str, usize>::new(); From 56bd6049deac143fa320e2ee5cf2622f1446c7d1 Mon Sep 17 00:00:00 2001 From: Armin Sander Date: Wed, 16 Sep 2026 14:55:05 +0200 Subject: [PATCH 3/4] google-transcribe: reword boost rationale, log only the single final alternative --- services/google-transcribe/src/client.rs | 4 ++-- services/google-transcribe/src/transcribe.rs | 21 +++++--------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/services/google-transcribe/src/client.rs b/services/google-transcribe/src/client.rs index d5e9ac1..40c19ae 100644 --- a/services/google-transcribe/src/client.rs +++ b/services/google-transcribe/src/client.rs @@ -78,8 +78,8 @@ impl TranscribeClient { value: Some(AdaptationPhraseSetValue::InlinePhraseSet(PhraseSet { phrases: vec![Phrase { value: OOV_CLASS_DIGIT_SEQUENCE.to_owned(), - // This was taken over from the internal project that parameterized - // google via FreeSWITCH. Probably a good way to go for sure. + // Maximum boost, taken over from the internal project that + // parameterized Google via FreeSWITCH (see ADR 0005). boost: 20.0, }], ..Default::default() diff --git a/services/google-transcribe/src/transcribe.rs b/services/google-transcribe/src/transcribe.rs index 6e9e5db..aeafc60 100644 --- a/services/google-transcribe/src/transcribe.rs +++ b/services/google-transcribe/src/transcribe.rs @@ -192,24 +192,13 @@ where match &response.results[..] { [] => continue, [one] if one.is_final => { - match one.alternatives.as_slice() { - [only] => debug!( - confidence = only.confidence, - "Final recognition alternative" - ), - _ => debug!( - alternatives = ?one - .alternatives - .iter() - .map(|a| (a.confidence, a.transcript.as_str())) - .collect::>(), - "Final recognition alternatives" - ), - } - - let Some(alternative) = one.alternatives.first() else { + let [alternative] = one.alternatives.as_slice() else { continue; }; + debug!( + confidence = alternative.confidence, + "Final recognition alternative" + ); // Sometimes there is whitespace at the beginning, so we trim. // From 4d8d02f34a00cd72b3c12633490823d4a580f9c8 Mon Sep 17 00:00:00 2001 From: Armin Sander Date: Wed, 16 Sep 2026 14:56:03 +0200 Subject: [PATCH 4/4] release: bump workspace version to 3.8.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f77e988..e70eb70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ members = [ ] [workspace.package] -version = "3.7.2" +version = "3.8.0" edition = "2024" license = "MIT" repository = "https://github.com/pragmatrix/context-switch"