From 3ea0efad159daeef5a7c898c505b576fdd724b94 Mon Sep 17 00:00:00 2001 From: ComputelessComputer <63365510+ComputelessComputer@users.noreply.github.com> Date: Mon, 18 May 2026 14:46:03 +0900 Subject: [PATCH] fix(stt): pass speaker count hints Infer participant speaker counts for live and batch STT requests, pass them through Hyprnote proxy URLs, and map them to supported provider diarization parameters. --- apps/desktop/src/stt/useRunBatch.test.ts | 39 ++++++++++- apps/desktop/src/stt/useRunBatch.ts | 42 ++++++++++- .../src/actors/listener/adapters.rs | 33 +++------ .../src/adapter/assemblyai/live.rs | 69 +++++++++++++------ .../src/adapter/elevenlabs/batch.rs | 28 ++++++++ .../src/adapter/gladia/batch.rs | 53 ++++++++++++++ .../src/adapter/hyprnote/batch.rs | 9 +++ .../src/adapter/hyprnote/live.rs | 28 ++++++++ crates/owhisper-interface/src/openapi.rs | 9 +++ crates/transcribe-proxy/src/query_params.rs | 16 +++++ .../transcribe-proxy/src/routes/batch/mod.rs | 26 +++++++ .../src/routes/streaming/hyprnote.rs | 26 +++++++ 12 files changed, 332 insertions(+), 46 deletions(-) diff --git a/apps/desktop/src/stt/useRunBatch.test.ts b/apps/desktop/src/stt/useRunBatch.test.ts index 571f43365b..1dedc50c60 100644 --- a/apps/desktop/src/stt/useRunBatch.test.ts +++ b/apps/desktop/src/stt/useRunBatch.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "vitest"; -import { getBatchProvider } from "./useRunBatch"; +import { getBatchProvider, getSessionSpeakerCount } from "./useRunBatch"; describe("getBatchProvider", () => { test("maps pyannote to the batch transcription provider", () => { @@ -19,3 +19,40 @@ describe("getBatchProvider", () => { ); }); }); + +describe("getSessionSpeakerCount", () => { + test("counts distinct session participants plus the current user", () => { + const rows = new Map([ + ["mapping-1", { session_id: "session-1", human_id: "human-a" }], + ["mapping-2", { session_id: "session-1", human_id: "human-a" }], + ["mapping-3", { session_id: "session-1", human_id: "human-b" }], + ["mapping-4", { session_id: "other-session", human_id: "human-c" }], + ]); + const store = { + forEachRow: (_table: string, callback: (rowId: string) => void) => { + for (const rowId of rows.keys()) callback(rowId); + }, + getCell: (_table: string, rowId: string, cellId: string) => + rows.get(rowId)?.[cellId as "session_id" | "human_id"], + }; + + expect(getSessionSpeakerCount(store as any, "session-1", "self")).toBe(3); + }); + + test("returns undefined until at least two speakers are known", () => { + const rows = new Map([ + ["mapping-1", { session_id: "session-1", human_id: "human-a" }], + ]); + const store = { + forEachRow: (_table: string, callback: (rowId: string) => void) => { + for (const rowId of rows.keys()) callback(rowId); + }, + getCell: (_table: string, rowId: string, cellId: string) => + rows.get(rowId)?.[cellId as "session_id" | "human_id"], + }; + + expect(getSessionSpeakerCount(store as any, "session-1", null)).toBe( + undefined, + ); + }); +}); diff --git a/apps/desktop/src/stt/useRunBatch.ts b/apps/desktop/src/stt/useRunBatch.ts index 1e6f4894d0..40476ebb10 100644 --- a/apps/desktop/src/stt/useRunBatch.ts +++ b/apps/desktop/src/stt/useRunBatch.ts @@ -32,6 +32,8 @@ type RunOptions = { maxSpeakers?: number; }; +type Store = NonNullable>; + const DIRECT_BATCH_PROVIDERS: Set = new Set([ "deepgram", "soniox", @@ -81,6 +83,38 @@ export function isStoppedTranscriptionError(error: unknown) { ); } +export function getSessionSpeakerCount( + store: Store, + sessionId: string, + selfHumanId?: string | null, +): number | undefined { + const humanIds = new Set(); + + store.forEachRow("mapping_session_participant", (mappingId, _forEachCell) => { + const sid = store.getCell( + "mapping_session_participant", + mappingId, + "session_id", + ); + if (sid !== sessionId) return; + + const humanId = store.getCell( + "mapping_session_participant", + mappingId, + "human_id", + ); + if (typeof humanId === "string" && humanId) { + humanIds.add(humanId); + } + }); + + if (typeof selfHumanId === "string" && selfHumanId) { + humanIds.add(selfHumanId); + } + + return humanIds.size > 1 ? humanIds.size : undefined; +} + export const useRunBatch = (sessionId: string) => { const store = main.UI.useStore(main.STORE_ID); const indexes = main.UI.useIndexes(main.STORE_ID); @@ -114,6 +148,12 @@ export const useRunBatch = (sessionId: string) => { const createdAt = new Date().toISOString(); const memoMd = store.getCell("sessions", sessionId, "raw_md"); let transcriptId: string | null = null; + const inferredNumSpeakers = + options?.numSpeakers === undefined && + options?.minSpeakers === undefined && + options?.maxSpeakers === undefined + ? getSessionSpeakerCount(store, sessionId, user_id) + : undefined; const handlePersist: BatchPersistCallback | undefined = options?.handlePersist; @@ -232,7 +272,7 @@ export const useRunBatch = (sessionId: string) => { languages: options?.languages ?? getTranscriptionLanguages(aiLanguage, spokenLanguages), - num_speakers: options?.numSpeakers, + num_speakers: options?.numSpeakers ?? inferredNumSpeakers, min_speakers: options?.minSpeakers, max_speakers: options?.maxSpeakers, }; diff --git a/crates/listener-core/src/actors/listener/adapters.rs b/crates/listener-core/src/actors/listener/adapters.rs index 8546347732..ee359ba52e 100644 --- a/crates/listener-core/src/actors/listener/adapters.rs +++ b/crates/listener-core/src/actors/listener/adapters.rs @@ -410,32 +410,25 @@ fn i16_bytes_to_f32(bytes: &Bytes) -> Vec { } fn build_listen_params(args: &ListenerArgs) -> owhisper_interface::ListenParams { - let adapter_kind = - AdapterKind::from_url_and_languages(&args.base_url, &args.languages, Some(&args.model)); let redemption_time_ms = if args.onboarding { "60" } else { "400" }; - let mut custom_query = std::collections::HashMap::from([( + let custom_query = std::collections::HashMap::from([( "redemption_time_ms".to_string(), redemption_time_ms.to_string(), )]); - - if adapter_kind == AdapterKind::AssemblyAI - && let Some(expected_speakers) = assemblyai_expected_speakers(args) - { - custom_query.insert("speaker_labels".to_string(), "true".to_string()); - custom_query.insert("max_speakers".to_string(), expected_speakers.to_string()); - } + let num_speakers = expected_speakers(args); owhisper_interface::ListenParams { model: Some(args.model.clone()), languages: args.languages.clone(), sample_rate: super::super::SAMPLE_RATE, keywords: args.keywords.clone(), + num_speakers, custom_query: Some(custom_query), ..Default::default() } } -fn assemblyai_expected_speakers(args: &ListenerArgs) -> Option { +fn expected_speakers(args: &ListenerArgs) -> Option { let mut participants = args.participant_human_ids.clone(); if let Some(self_human_id) = &args.self_human_id @@ -655,16 +648,16 @@ mod tests { } #[test] - fn assemblyai_expected_speakers_counts_distinct_participants() { + fn expected_speakers_counts_distinct_participants() { let mut args = listener_args("https://api.assemblyai.com", "u3-rt-pro"); args.participant_human_ids = vec!["remote".to_string(), "self".to_string()]; args.self_human_id = Some("self".to_string()); - assert_eq!(assemblyai_expected_speakers(&args), Some(2)); + assert_eq!(expected_speakers(&args), Some(2)); } #[test] - fn build_listen_params_adds_assemblyai_diarization_hints() { + fn build_listen_params_sets_num_speakers_without_assemblyai_custom_query() { let mut args = listener_args("https://api.assemblyai.com", "u3-rt-pro"); args.participant_human_ids = vec!["remote".to_string()]; args.self_human_id = Some("self".to_string()); @@ -672,14 +665,9 @@ mod tests { let params = build_listen_params(&args); let custom_query = params.custom_query.expect("custom query"); - assert_eq!( - custom_query.get("speaker_labels").map(String::as_str), - Some("true") - ); - assert_eq!( - custom_query.get("max_speakers").map(String::as_str), - Some("2") - ); + assert_eq!(params.num_speakers, Some(2)); + assert!(!custom_query.contains_key("speaker_labels")); + assert!(!custom_query.contains_key("max_speakers")); } #[test] @@ -691,6 +679,7 @@ mod tests { let params = build_listen_params(&args); let custom_query = params.custom_query.expect("custom query"); + assert_eq!(params.num_speakers, Some(2)); assert!(!custom_query.contains_key("speaker_labels")); assert!(!custom_query.contains_key("max_speakers")); } diff --git a/crates/owhisper-client/src/adapter/assemblyai/live.rs b/crates/owhisper-client/src/adapter/assemblyai/live.rs index 9eab800843..2dbecdaa2c 100644 --- a/crates/owhisper-client/src/adapter/assemblyai/live.rs +++ b/crates/owhisper-client/src/adapter/assemblyai/live.rs @@ -57,18 +57,13 @@ impl RealtimeSttAdapter for AssemblyAIAdapter { query_pairs.append_pair("max_turn_silence", max_silence); } - if matches!(resolved_model, ResolvedLiveModel::U3RtPro) - && let Some(custom) = ¶ms.custom_query - { - if custom - .get("speaker_labels") - .is_some_and(|value| value == "true") - { + if matches!(resolved_model, ResolvedLiveModel::U3RtPro) { + if Self::streaming_speaker_labels_enabled(params) { query_pairs.append_pair("speaker_labels", "true"); } - if let Some(max_speakers) = custom.get("max_speakers") { - query_pairs.append_pair("max_speakers", max_speakers); + if let Some(max_speakers) = Self::streaming_max_speakers(params) { + query_pairs.append_pair("max_speakers", &max_speakers.to_string()); } } @@ -232,6 +227,27 @@ impl AssemblyAIAdapter { } } + fn streaming_speaker_labels_enabled(params: &ListenParams) -> bool { + params.num_speakers.is_some() + || params.min_speakers.is_some() + || params.max_speakers.is_some() + || params + .custom_query + .as_ref() + .and_then(|custom| custom.get("speaker_labels")) + .is_some_and(|value| value == "true") + } + + fn streaming_max_speakers(params: &ListenParams) -> Option { + params.max_speakers.or(params.num_speakers).or_else(|| { + params + .custom_query + .as_ref() + .and_then(|custom| custom.get("max_speakers")) + .and_then(|value| value.parse().ok()) + }) + } + fn parse_speaker_label(label: Option<&str>) -> Option { let label = label?.trim(); if label.is_empty() || label.eq_ignore_ascii_case("unknown") { @@ -339,8 +355,6 @@ impl ResolvedLiveModel { #[cfg(test)] mod tests { - use std::collections::HashMap; - use hypr_language::ISO639; use owhisper_interface::ListenParams; use owhisper_interface::stream::StreamResponse; @@ -424,10 +438,7 @@ mod tests { API_BASE, &owhisper_interface::ListenParams { model: Some("u3-rt-pro".to_string()), - custom_query: Some(HashMap::from([ - ("speaker_labels".to_string(), "true".to_string()), - ("max_speakers".to_string(), "3".to_string()), - ])), + num_speakers: Some(3), ..Default::default() }, 1, @@ -439,14 +450,28 @@ mod tests { } #[test] - fn test_whisper_fallback_omits_streaming_diarization_hints() { + fn test_streaming_min_speakers_enables_diarization() { + let url = AssemblyAIAdapter.build_ws_url( + API_BASE, + &owhisper_interface::ListenParams { + model: Some("u3-rt-pro".to_string()), + min_speakers: Some(2), + ..Default::default() + }, + 1, + ); + + let query = url.query().expect("query string"); + assert!(query.contains("speaker_labels=true")); + assert!(!query.contains("max_speakers")); + } + + #[test] + fn test_streaming_diarization_hints_skip_whisper_fallback() { let url = AssemblyAIAdapter.build_ws_url( API_BASE, &owhisper_interface::ListenParams { - custom_query: Some(HashMap::from([ - ("speaker_labels".to_string(), "true".to_string()), - ("max_speakers".to_string(), "3".to_string()), - ])), + num_speakers: Some(3), languages: vec![ISO639::Ko.into()], ..Default::default() }, @@ -455,8 +480,8 @@ mod tests { let query = url.query().expect("query string"); assert!(query.contains("speech_model=whisper-rt")); - assert!(!query.contains("speaker_labels=true")); - assert!(!query.contains("max_speakers=3")); + assert!(!query.contains("speaker_labels")); + assert!(!query.contains("max_speakers")); } #[test] diff --git a/crates/owhisper-client/src/adapter/elevenlabs/batch.rs b/crates/owhisper-client/src/adapter/elevenlabs/batch.rs index 0c9e2d0395..46bb71af85 100644 --- a/crates/owhisper-client/src/adapter/elevenlabs/batch.rs +++ b/crates/owhisper-client/src/adapter/elevenlabs/batch.rs @@ -86,6 +86,10 @@ impl ElevenLabsAdapter { .text("diarize", "true") .text("timestamps_granularity", "word"); + if let Some(num_speakers) = Self::num_speakers_hint(params) { + form = form.text("num_speakers", num_speakers.to_string()); + } + if let Some(lang) = params.languages.first() { form = form.text("language_code", lang.iso639().code().to_string()); } @@ -116,6 +120,10 @@ impl ElevenLabsAdapter { Ok(Self::convert_to_batch_response(transcript)) } + fn num_speakers_hint(params: &ListenParams) -> Option { + params.num_speakers.or(params.max_speakers) + } + fn convert_to_batch_response(response: TranscriptResponse) -> BatchResponse { let words: Vec = response .words @@ -164,6 +172,26 @@ mod tests { use super::*; use crate::http_client::create_client; + #[test] + fn num_speakers_hint_prefers_exact_count_then_max() { + let exact = ListenParams { + num_speakers: Some(3), + max_speakers: Some(5), + ..Default::default() + }; + let ranged = ListenParams { + max_speakers: Some(5), + ..Default::default() + }; + + assert_eq!(ElevenLabsAdapter::num_speakers_hint(&exact), Some(3)); + assert_eq!(ElevenLabsAdapter::num_speakers_hint(&ranged), Some(5)); + assert_eq!( + ElevenLabsAdapter::num_speakers_hint(&ListenParams::default()), + None + ); + } + #[test] fn speaker_labeled_words_use_mixed_capture_channel() { let response = TranscriptResponse { diff --git a/crates/owhisper-client/src/adapter/gladia/batch.rs b/crates/owhisper-client/src/adapter/gladia/batch.rs index fa739ecf27..c644a1a190 100644 --- a/crates/owhisper-client/src/adapter/gladia/batch.rs +++ b/crates/owhisper-client/src/adapter/gladia/batch.rs @@ -53,11 +53,23 @@ struct TranscriptRequest<'a> { #[serde(skip_serializing_if = "Option::is_none")] diarization: Option, #[serde(skip_serializing_if = "Option::is_none")] + diarization_config: Option, + #[serde(skip_serializing_if = "Option::is_none")] custom_vocabulary: Option>, #[serde(skip_serializing_if = "Option::is_none")] name_consistency: Option, } +#[derive(Debug, PartialEq, Serialize)] +struct DiarizationConfig { + #[serde(skip_serializing_if = "Option::is_none")] + number_of_speakers: Option, + #[serde(skip_serializing_if = "Option::is_none")] + min_speakers: Option, + #[serde(skip_serializing_if = "Option::is_none")] + max_speakers: Option, +} + #[derive(Debug, Serialize)] struct LanguageConfig { #[serde(skip_serializing_if = "Vec::is_empty")] @@ -226,6 +238,7 @@ impl GladiaAdapter { model, language_config, diarization: Some(true), + diarization_config: Self::diarization_config(params), custom_vocabulary, name_consistency: Some(true), }; @@ -297,6 +310,19 @@ impl GladiaAdapter { .await } + fn diarization_config(params: &ListenParams) -> Option { + let config = DiarizationConfig { + number_of_speakers: params.num_speakers, + min_speakers: params.min_speakers, + max_speakers: params.max_speakers, + }; + + (config.number_of_speakers.is_some() + || config.min_speakers.is_some() + || config.max_speakers.is_some()) + .then_some(config) + } + fn convert_to_batch_response(response: TranscriptResponse) -> BatchResponse { let result = response.result.unwrap_or_default(); let transcription = result.transcription.unwrap_or_default(); @@ -357,6 +383,33 @@ mod tests { use super::*; use crate::http_client::create_client; + #[test] + fn diarization_config_uses_speaker_count_hints() { + let params = ListenParams { + num_speakers: Some(3), + min_speakers: Some(2), + max_speakers: Some(4), + ..Default::default() + }; + + assert_eq!( + GladiaAdapter::diarization_config(¶ms), + Some(DiarizationConfig { + number_of_speakers: Some(3), + min_speakers: Some(2), + max_speakers: Some(4), + }) + ); + } + + #[test] + fn diarization_config_is_omitted_without_speaker_hints() { + assert_eq!( + GladiaAdapter::diarization_config(&ListenParams::default()), + None + ); + } + #[tokio::test] #[ignore] async fn test_gladia_batch_transcription() { diff --git a/crates/owhisper-client/src/adapter/hyprnote/batch.rs b/crates/owhisper-client/src/adapter/hyprnote/batch.rs index 608a809f03..4c9e8c761c 100644 --- a/crates/owhisper-client/src/adapter/hyprnote/batch.rs +++ b/crates/owhisper-client/src/adapter/hyprnote/batch.rs @@ -56,6 +56,15 @@ async fn do_transcribe_file( for kw in ¶ms.keywords { q.append_pair("keyword", kw); } + if let Some(num_speakers) = params.num_speakers { + q.append_pair("num_speakers", &num_speakers.to_string()); + } + if let Some(min_speakers) = params.min_speakers { + q.append_pair("min_speakers", &min_speakers.to_string()); + } + if let Some(max_speakers) = params.max_speakers { + q.append_pair("max_speakers", &max_speakers.to_string()); + } if let Some(custom) = ¶ms.custom_query { for (key, value) in custom { q.append_pair(key, value); diff --git a/crates/owhisper-client/src/adapter/hyprnote/live.rs b/crates/owhisper-client/src/adapter/hyprnote/live.rs index 8165b7278e..4aaf623b30 100644 --- a/crates/owhisper-client/src/adapter/hyprnote/live.rs +++ b/crates/owhisper-client/src/adapter/hyprnote/live.rs @@ -46,6 +46,16 @@ impl RealtimeSttAdapter for HyprnoteAdapter { query.append_pair("keyword", keyword); } + if let Some(num_speakers) = params.num_speakers { + query.append_pair("num_speakers", &num_speakers.to_string()); + } + if let Some(min_speakers) = params.min_speakers { + query.append_pair("min_speakers", &min_speakers.to_string()); + } + if let Some(max_speakers) = params.max_speakers { + query.append_pair("max_speakers", &max_speakers.to_string()); + } + if let Some(custom) = ¶ms.custom_query { for (key, value) in custom { query.append_pair(key, value); @@ -137,6 +147,24 @@ mod tests { assert!(url_str.contains("keyword=transcription")); } + #[test] + fn test_url_with_speaker_counts() { + let adapter = HyprnoteAdapter::default(); + let params = owhisper_interface::ListenParams { + num_speakers: Some(3), + min_speakers: Some(2), + max_speakers: Some(4), + ..Default::default() + }; + + let url = adapter.build_ws_url(API_BASE, ¶ms, 1); + let url_str = url.as_str(); + + assert!(url_str.contains("num_speakers=3")); + assert!(url_str.contains("min_speakers=2")); + assert!(url_str.contains("max_speakers=4")); + } + #[test] fn test_url_with_custom_query() { let adapter = HyprnoteAdapter::default(); diff --git a/crates/owhisper-interface/src/openapi.rs b/crates/owhisper-interface/src/openapi.rs index b460f206d4..3862e38dd0 100644 --- a/crates/owhisper-interface/src/openapi.rs +++ b/crates/owhisper-interface/src/openapi.rs @@ -16,6 +16,15 @@ pub struct CommonListenParams { /// Keyword boosting. Comma-separated or repeated query params #[allow(dead_code)] keywords: Option, + /// Expected exact number of speakers, when supported by the selected provider + #[allow(dead_code)] + num_speakers: Option, + /// Minimum expected number of speakers, when supported by the selected provider + #[allow(dead_code)] + min_speakers: Option, + /// Maximum expected number of speakers, when supported by the selected provider + #[allow(dead_code)] + max_speakers: Option, } #[derive(utoipa::IntoParams)] diff --git a/crates/transcribe-proxy/src/query_params.rs b/crates/transcribe-proxy/src/query_params.rs index 085c2aa0a2..13326f2e4d 100644 --- a/crates/transcribe-proxy/src/query_params.rs +++ b/crates/transcribe-proxy/src/query_params.rs @@ -89,6 +89,12 @@ impl QueryParams { }) .unwrap_or_default() } + + pub fn parse_optional_u32(&self, key: &str) -> Option { + self.get_first(key) + .and_then(|value| value.parse::().ok()) + .filter(|value| *value > 0) + } } impl Deref for QueryParams { @@ -277,4 +283,14 @@ mod tests { assert_eq!(languages[0].iso639(), ISO639::En); assert_eq!(languages[1].iso639(), ISO639::Ko); } + + #[test] + fn parse_optional_u32_ignores_missing_invalid_and_zero_values() { + let params = parse_query("?num_speakers=3&min_speakers=0&max_speakers=nope"); + + assert_eq!(params.parse_optional_u32("num_speakers"), Some(3)); + assert_eq!(params.parse_optional_u32("min_speakers"), None); + assert_eq!(params.parse_optional_u32("max_speakers"), None); + assert_eq!(params.parse_optional_u32("missing"), None); + } } diff --git a/crates/transcribe-proxy/src/routes/batch/mod.rs b/crates/transcribe-proxy/src/routes/batch/mod.rs index 85a0605144..89a2f7d53a 100644 --- a/crates/transcribe-proxy/src/routes/batch/mod.rs +++ b/crates/transcribe-proxy/src/routes/batch/mod.rs @@ -105,6 +105,9 @@ pub(super) fn build_listen_params(params: &QueryParams) -> ListenParams { model: params.get_first("model").map(|s| s.to_string()), languages: params.get_languages(), keywords: params.parse_keywords(), + num_speakers: params.parse_optional_u32("num_speakers"), + min_speakers: params.parse_optional_u32("min_speakers"), + max_speakers: params.parse_optional_u32("max_speakers"), ..Default::default() }) } @@ -152,4 +155,27 @@ mod tests { assert_eq!(listen_params.languages[1].iso639(), ISO639::Ko); assert_eq!(listen_params.languages[1].region(), Some("KR")); } + + #[test] + fn test_build_listen_params_with_speaker_counts() { + let mut params = QueryParams::default(); + params.insert( + "num_speakers".to_string(), + QueryValue::Single("3".to_string()), + ); + params.insert( + "min_speakers".to_string(), + QueryValue::Single("2".to_string()), + ); + params.insert( + "max_speakers".to_string(), + QueryValue::Single("4".to_string()), + ); + + let listen_params = build_listen_params(¶ms); + + assert_eq!(listen_params.num_speakers, Some(3)); + assert_eq!(listen_params.min_speakers, Some(2)); + assert_eq!(listen_params.max_speakers, Some(4)); + } } diff --git a/crates/transcribe-proxy/src/routes/streaming/hyprnote.rs b/crates/transcribe-proxy/src/routes/streaming/hyprnote.rs index b54740431d..ecee9b7a27 100644 --- a/crates/transcribe-proxy/src/routes/streaming/hyprnote.rs +++ b/crates/transcribe-proxy/src/routes/streaming/hyprnote.rs @@ -24,6 +24,9 @@ fn build_listen_params(params: &QueryParams) -> ListenParams { sample_rate: parse_param(params, "sample_rate", 16000), channels: parse_param(params, "channels", 1), keywords: params.parse_keywords(), + num_speakers: params.parse_optional_u32("num_speakers"), + min_speakers: params.parse_optional_u32("min_speakers"), + max_speakers: params.parse_optional_u32("max_speakers"), ..Default::default() }) } @@ -293,6 +296,29 @@ mod tests { assert_eq!(listen_params.channels, 1); } + #[test] + fn test_build_listen_params_with_speaker_counts() { + let mut params = QueryParams::default(); + params.insert( + "num_speakers".to_string(), + QueryValue::Single("3".to_string()), + ); + params.insert( + "min_speakers".to_string(), + QueryValue::Single("2".to_string()), + ); + params.insert( + "max_speakers".to_string(), + QueryValue::Single("4".to_string()), + ); + + let listen_params = build_listen_params(¶ms); + + assert_eq!(listen_params.num_speakers, Some(3)); + assert_eq!(listen_params.min_speakers, Some(2)); + assert_eq!(listen_params.max_speakers, Some(4)); + } + #[test] fn test_build_listen_params_with_keywords() { let mut params = QueryParams::default();