1pub mod adapters;
13
14use std::sync::Arc;
15
16use async_trait::async_trait;
17use futures::stream::{self, BoxStream};
18use futures::StreamExt;
19
20use crate::error::Result;
21use crate::processor::frame::{AudioFrame, Frame, LlmContext, StartParams, TurnParams, VadParams};
22use crate::types::{RealtimeEvent, RealtimeSetup, ToolDecl};
23
24pub type RealtimeServiceSetup = RealtimeSetup;
28
29pub type Tool = ToolDecl;
32
33#[async_trait]
40pub trait SttService: Send {
41 fn name(&self) -> &str;
42 async fn start(&mut self, params: &StartParams) -> Result<()>;
43 async fn run_stt(&mut self, audio: Arc<AudioFrame>) -> Result<Vec<Frame>>;
46 async fn set_muted(&mut self, muted: bool);
47}
48
49#[async_trait]
52pub trait TtsService: Send {
53 fn name(&self) -> &str;
54 fn sample_rate(&self) -> u32;
55 async fn start(&mut self, params: &StartParams) -> Result<()>;
56 async fn run_tts(&mut self, text: &str) -> Result<Vec<Frame>>;
57}
58
59#[async_trait]
62pub trait LlmService: Send {
63 fn name(&self) -> &str;
64 async fn start(&mut self, params: &StartParams) -> Result<()>;
65 async fn run_llm<'a>(&'a mut self, ctx: &'a LlmContext) -> Result<BoxStream<'a, Frame>>;
66 fn set_tools(&mut self, tools: Vec<Tool>);
67}
68
69#[async_trait]
83impl SttService for Box<dyn SttService> {
84 fn name(&self) -> &str {
85 (**self).name()
86 }
87 async fn start(&mut self, params: &StartParams) -> Result<()> {
88 (**self).start(params).await
89 }
90 async fn run_stt(&mut self, audio: Arc<AudioFrame>) -> Result<Vec<Frame>> {
91 (**self).run_stt(audio).await
92 }
93 async fn set_muted(&mut self, muted: bool) {
94 (**self).set_muted(muted).await
95 }
96}
97
98#[async_trait]
99impl TtsService for Box<dyn TtsService> {
100 fn name(&self) -> &str {
101 (**self).name()
102 }
103 fn sample_rate(&self) -> u32 {
104 (**self).sample_rate()
105 }
106 async fn start(&mut self, params: &StartParams) -> Result<()> {
107 (**self).start(params).await
108 }
109 async fn run_tts(&mut self, text: &str) -> Result<Vec<Frame>> {
110 (**self).run_tts(text).await
111 }
112}
113
114#[async_trait]
115impl LlmService for Box<dyn LlmService> {
116 fn name(&self) -> &str {
117 (**self).name()
118 }
119 async fn start(&mut self, params: &StartParams) -> Result<()> {
120 (**self).start(params).await
121 }
122 async fn run_llm<'a>(&'a mut self, ctx: &'a LlmContext) -> Result<BoxStream<'a, Frame>> {
123 (**self).run_llm(ctx).await
124 }
125 fn set_tools(&mut self, tools: Vec<Tool>) {
126 (**self).set_tools(tools)
127 }
128}
129
130#[async_trait]
133pub trait RealtimeLlmService: Send {
134 async fn connect(&mut self, setup: RealtimeServiceSetup) -> Result<()>;
135 async fn send_audio(&mut self, chunk: Arc<AudioFrame>) -> Result<()>;
136 async fn update_system(&mut self, prompt: String, tools: Vec<Tool>) -> Result<()>;
137 async fn send_tool_result(&mut self, id: String, result: serde_json::Value) -> Result<()>;
138 async fn next_event(&mut self) -> Option<RealtimeEvent>;
139
140 fn input_sample_rate(&self) -> u32 {
146 16_000
147 }
148
149 async fn kickoff(&mut self) -> Result<()> {
155 Ok(())
156 }
157
158 fn event_notify(&self) -> Option<Arc<tokio::sync::Notify>> {
166 None
167 }
168
169 async fn poll_event(&mut self) -> crate::realtime::PollEvent {
174 crate::realtime::PollEvent::Ready(self.next_event().await)
175 }
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub enum VadState {
185 Quiet,
186 Starting,
187 Speaking,
188 Stopping,
189}
190
191#[derive(Debug, Clone, Copy, PartialEq)]
193pub struct TurnPrediction {
194 pub is_complete: bool,
195 pub probability: f32,
196}
197
198pub trait VadAnalyzer: Send {
201 fn sample_rate(&self) -> u32;
202 fn analyze(&mut self, audio: &AudioFrame) -> VadState;
204 fn set_params(&mut self, params: VadParams);
205}
206
207pub trait TurnAnalyzer: Send {
210 fn analyze_turn(&mut self, audio: &AudioFrame, vad: VadState) -> TurnPrediction;
213 fn set_params(&mut self, params: TurnParams);
214}
215
216pub struct MockStt {
223 pub transcript: String,
224 pub user_id: Arc<str>,
225}
226
227impl MockStt {
228 pub fn new(transcript: impl Into<String>) -> Self {
229 Self {
230 transcript: transcript.into(),
231 user_id: Arc::from("user"),
232 }
233 }
234}
235
236#[async_trait]
237impl SttService for MockStt {
238 fn name(&self) -> &str {
239 "MockStt"
240 }
241 async fn start(&mut self, _params: &StartParams) -> Result<()> {
242 Ok(())
243 }
244 async fn run_stt(&mut self, _audio: Arc<AudioFrame>) -> Result<Vec<Frame>> {
245 Ok(vec![
246 Frame::InterimTranscription {
247 text: self.transcript.clone(),
248 user_id: self.user_id.clone(),
249 language: None,
250 },
251 Frame::Transcription {
252 text: self.transcript.clone(),
253 user_id: self.user_id.clone(),
254 language: None,
255 final_: true,
256 },
257 ])
258 }
259 async fn set_muted(&mut self, _muted: bool) {}
260}
261
262pub struct MockLlm {
265 pub prefix: String,
266 pub tools: Vec<Tool>,
267}
268
269impl MockLlm {
270 pub fn new(prefix: impl Into<String>) -> Self {
271 Self {
272 prefix: prefix.into(),
273 tools: Vec::new(),
274 }
275 }
276}
277
278#[async_trait]
279impl LlmService for MockLlm {
280 fn name(&self) -> &str {
281 "MockLlm"
282 }
283 async fn start(&mut self, _params: &StartParams) -> Result<()> {
284 Ok(())
285 }
286 async fn run_llm<'a>(&'a mut self, ctx: &'a LlmContext) -> Result<BoxStream<'a, Frame>> {
287 let user_text = ctx
289 .messages
290 .last()
291 .and_then(|m| m.get("content"))
292 .and_then(|c| c.as_str())
293 .unwrap_or("hello")
294 .to_string();
295 let reply = format!("{}{}", self.prefix, user_text);
296 let frames = vec![
297 Frame::LlmResponseStart,
298 Frame::LlmText(reply),
299 Frame::LlmResponseEnd,
300 ];
301 Ok(stream::iter(frames).boxed())
302 }
303 fn set_tools(&mut self, tools: Vec<Tool>) {
304 self.tools = tools;
305 }
306}
307
308pub struct MockTts {
312 pub rate: u32,
313}
314
315impl MockTts {
316 pub fn new(rate: u32) -> Self {
317 Self { rate }
318 }
319}
320
321#[async_trait]
322impl TtsService for MockTts {
323 fn name(&self) -> &str {
324 "MockTts"
325 }
326 fn sample_rate(&self) -> u32 {
327 self.rate
328 }
329 async fn start(&mut self, _params: &StartParams) -> Result<()> {
330 Ok(())
331 }
332 async fn run_tts(&mut self, text: &str) -> Result<Vec<Frame>> {
333 let pcm = vec![0i16; text.len().max(1)];
335 let audio = Arc::new(AudioFrame::mono(pcm, self.rate));
336 Ok(vec![
337 Frame::TtsStarted { context_id: None },
338 Frame::TtsAudio {
339 audio,
340 context_id: None,
341 },
342 Frame::TtsStopped { context_id: None },
343 ])
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350 use crate::observer::{FrameEvent, FrameObserver};
354 use crate::pipeline::{Pipeline, PipelineTask, PipelineTaskParams};
355 use crate::processor::frame::Frame;
356 use crate::service::adapters::{LlmProcessor, SttProcessor, TtsProcessor};
357 use std::sync::atomic::{AtomicBool, Ordering};
358 use std::sync::{Arc, Mutex};
359
360 #[derive(Default)]
362 struct Tap {
363 names: Mutex<Vec<&'static str>>,
364 saw_output_audio: AtomicBool,
365 }
366 #[async_trait]
367 impl FrameObserver for Tap {
368 async fn on_process(&self, e: &FrameEvent<'_>) {
369 self.names.lock().unwrap().push(e.frame.name());
370 if matches!(e.frame, Frame::OutputAudio(_)) {
371 self.saw_output_audio.store(true, Ordering::Relaxed);
372 }
373 }
374 }
375
376 #[tokio::test]
379 async fn mock_cascaded_pipeline_runs_a_turn_end_to_end() {
380 let stt = SttProcessor::new(MockStt::new("book a dentist appointment"));
381 let llm = LlmProcessor::new(MockLlm::new("you said: "));
382 let tts = TtsProcessor::new(MockTts::new(24_000));
383 let pipeline = Pipeline::new(vec![Box::new(stt), Box::new(llm), Box::new(tts)]);
384
385 let tap = Arc::new(Tap::default());
386 let task = PipelineTask::new(
387 pipeline,
388 PipelineTaskParams::default(),
389 vec![tap.clone() as Arc<dyn FrameObserver>],
390 );
391
392 let audio = Arc::new(AudioFrame::mono(vec![1, 2, 3, 4], 16_000));
394 task.queue_frame(Frame::InputAudio(audio)).await;
395 task.stop_when_done().await;
396 let _ = task.cancel_token();
397
398 tokio::time::timeout(std::time::Duration::from_secs(5), task.run())
400 .await
401 .expect("cascaded pipeline timed out")
402 .expect("run ok");
403
404 assert!(
406 tap.saw_output_audio.load(Ordering::Relaxed),
407 "mock cascaded pipeline must emit OutputAudio; saw {:?}",
408 tap.names.lock().unwrap()
409 );
410 let names = tap.names.lock().unwrap().clone();
411 assert!(
412 names.contains(&"Transcription"),
413 "STT must emit Transcription"
414 );
415 assert!(names.contains(&"LlmText"), "LLM must emit LlmText");
416 }
417}