pub mod adapters;
use std::sync::Arc;
use async_trait::async_trait;
use futures::stream::{self, BoxStream};
use futures::StreamExt;
use crate::error::Result;
use crate::processor::frame::{AudioFrame, Frame, LlmContext, StartParams, TurnParams, VadParams};
use crate::types::{RealtimeEvent, RealtimeSetup, ToolDecl};
pub type RealtimeServiceSetup = RealtimeSetup;
pub type Tool = ToolDecl;
#[async_trait]
pub trait SttService: Send {
fn name(&self) -> &str;
async fn start(&mut self, params: &StartParams) -> Result<()>;
async fn run_stt(&mut self, audio: Arc<AudioFrame>) -> Result<Vec<Frame>>;
async fn set_muted(&mut self, muted: bool);
}
#[async_trait]
pub trait TtsService: Send {
fn name(&self) -> &str;
fn sample_rate(&self) -> u32;
async fn start(&mut self, params: &StartParams) -> Result<()>;
async fn run_tts(&mut self, text: &str) -> Result<Vec<Frame>>;
}
#[async_trait]
pub trait LlmService: Send {
fn name(&self) -> &str;
async fn start(&mut self, params: &StartParams) -> Result<()>;
async fn run_llm<'a>(&'a mut self, ctx: &'a LlmContext) -> Result<BoxStream<'a, Frame>>;
fn set_tools(&mut self, tools: Vec<Tool>);
}
#[async_trait]
impl SttService for Box<dyn SttService> {
fn name(&self) -> &str {
(**self).name()
}
async fn start(&mut self, params: &StartParams) -> Result<()> {
(**self).start(params).await
}
async fn run_stt(&mut self, audio: Arc<AudioFrame>) -> Result<Vec<Frame>> {
(**self).run_stt(audio).await
}
async fn set_muted(&mut self, muted: bool) {
(**self).set_muted(muted).await
}
}
#[async_trait]
impl TtsService for Box<dyn TtsService> {
fn name(&self) -> &str {
(**self).name()
}
fn sample_rate(&self) -> u32 {
(**self).sample_rate()
}
async fn start(&mut self, params: &StartParams) -> Result<()> {
(**self).start(params).await
}
async fn run_tts(&mut self, text: &str) -> Result<Vec<Frame>> {
(**self).run_tts(text).await
}
}
#[async_trait]
impl LlmService for Box<dyn LlmService> {
fn name(&self) -> &str {
(**self).name()
}
async fn start(&mut self, params: &StartParams) -> Result<()> {
(**self).start(params).await
}
async fn run_llm<'a>(&'a mut self, ctx: &'a LlmContext) -> Result<BoxStream<'a, Frame>> {
(**self).run_llm(ctx).await
}
fn set_tools(&mut self, tools: Vec<Tool>) {
(**self).set_tools(tools)
}
}
#[async_trait]
pub trait RealtimeLlmService: Send {
async fn connect(&mut self, setup: RealtimeServiceSetup) -> Result<()>;
async fn send_audio(&mut self, chunk: Arc<AudioFrame>) -> Result<()>;
async fn update_system(&mut self, prompt: String, tools: Vec<Tool>) -> Result<()>;
async fn send_tool_result(&mut self, id: String, result: serde_json::Value) -> Result<()>;
async fn next_event(&mut self) -> Option<RealtimeEvent>;
fn input_sample_rate(&self) -> u32 {
16_000
}
async fn kickoff(&mut self) -> Result<()> {
Ok(())
}
fn event_notify(&self) -> Option<Arc<tokio::sync::Notify>> {
None
}
async fn poll_event(&mut self) -> crate::realtime::PollEvent {
crate::realtime::PollEvent::Ready(self.next_event().await)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VadState {
Quiet,
Starting,
Speaking,
Stopping,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TurnPrediction {
pub is_complete: bool,
pub probability: f32,
}
pub trait VadAnalyzer: Send {
fn sample_rate(&self) -> u32;
fn analyze(&mut self, audio: &AudioFrame) -> VadState;
fn set_params(&mut self, params: VadParams);
}
pub trait TurnAnalyzer: Send {
fn analyze_turn(&mut self, audio: &AudioFrame, vad: VadState) -> TurnPrediction;
fn set_params(&mut self, params: TurnParams);
}
pub struct MockStt {
pub transcript: String,
pub user_id: Arc<str>,
}
impl MockStt {
pub fn new(transcript: impl Into<String>) -> Self {
Self {
transcript: transcript.into(),
user_id: Arc::from("user"),
}
}
}
#[async_trait]
impl SttService for MockStt {
fn name(&self) -> &str {
"MockStt"
}
async fn start(&mut self, _params: &StartParams) -> Result<()> {
Ok(())
}
async fn run_stt(&mut self, _audio: Arc<AudioFrame>) -> Result<Vec<Frame>> {
Ok(vec![
Frame::InterimTranscription {
text: self.transcript.clone(),
user_id: self.user_id.clone(),
language: None,
},
Frame::Transcription {
text: self.transcript.clone(),
user_id: self.user_id.clone(),
language: None,
final_: true,
},
])
}
async fn set_muted(&mut self, _muted: bool) {}
}
pub struct MockLlm {
pub prefix: String,
pub tools: Vec<Tool>,
}
impl MockLlm {
pub fn new(prefix: impl Into<String>) -> Self {
Self {
prefix: prefix.into(),
tools: Vec::new(),
}
}
}
#[async_trait]
impl LlmService for MockLlm {
fn name(&self) -> &str {
"MockLlm"
}
async fn start(&mut self, _params: &StartParams) -> Result<()> {
Ok(())
}
async fn run_llm<'a>(&'a mut self, ctx: &'a LlmContext) -> Result<BoxStream<'a, Frame>> {
let user_text = ctx
.messages
.last()
.and_then(|m| m.get("content"))
.and_then(|c| c.as_str())
.unwrap_or("hello")
.to_string();
let reply = format!("{}{}", self.prefix, user_text);
let frames = vec![
Frame::LlmResponseStart,
Frame::LlmText(reply),
Frame::LlmResponseEnd,
];
Ok(stream::iter(frames).boxed())
}
fn set_tools(&mut self, tools: Vec<Tool>) {
self.tools = tools;
}
}
pub struct MockTts {
pub rate: u32,
}
impl MockTts {
pub fn new(rate: u32) -> Self {
Self { rate }
}
}
#[async_trait]
impl TtsService for MockTts {
fn name(&self) -> &str {
"MockTts"
}
fn sample_rate(&self) -> u32 {
self.rate
}
async fn start(&mut self, _params: &StartParams) -> Result<()> {
Ok(())
}
async fn run_tts(&mut self, text: &str) -> Result<Vec<Frame>> {
let pcm = vec![0i16; text.len().max(1)];
let audio = Arc::new(AudioFrame::mono(pcm, self.rate));
Ok(vec![
Frame::TtsStarted { context_id: None },
Frame::TtsAudio {
audio,
context_id: None,
},
Frame::TtsStopped { context_id: None },
])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::observer::{FrameEvent, FrameObserver};
use crate::pipeline::{Pipeline, PipelineTask, PipelineTaskParams};
use crate::processor::frame::Frame;
use crate::service::adapters::{LlmProcessor, SttProcessor, TtsProcessor};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
#[derive(Default)]
struct Tap {
names: Mutex<Vec<&'static str>>,
saw_output_audio: AtomicBool,
}
#[async_trait]
impl FrameObserver for Tap {
async fn on_process(&self, e: &FrameEvent<'_>) {
self.names.lock().unwrap().push(e.frame.name());
if matches!(e.frame, Frame::OutputAudio(_)) {
self.saw_output_audio.store(true, Ordering::Relaxed);
}
}
}
#[tokio::test]
async fn mock_cascaded_pipeline_runs_a_turn_end_to_end() {
let stt = SttProcessor::new(MockStt::new("book a dentist appointment"));
let llm = LlmProcessor::new(MockLlm::new("you said: "));
let tts = TtsProcessor::new(MockTts::new(24_000));
let pipeline = Pipeline::new(vec![Box::new(stt), Box::new(llm), Box::new(tts)]);
let tap = Arc::new(Tap::default());
let task = PipelineTask::new(
pipeline,
PipelineTaskParams::default(),
vec![tap.clone() as Arc<dyn FrameObserver>],
);
let audio = Arc::new(AudioFrame::mono(vec![1, 2, 3, 4], 16_000));
task.queue_frame(Frame::InputAudio(audio)).await;
task.stop_when_done().await;
let _ = task.cancel_token();
tokio::time::timeout(std::time::Duration::from_secs(5), task.run())
.await
.expect("cascaded pipeline timed out")
.expect("run ok");
assert!(
tap.saw_output_audio.load(Ordering::Relaxed),
"mock cascaded pipeline must emit OutputAudio; saw {:?}",
tap.names.lock().unwrap()
);
let names = tap.names.lock().unwrap().clone();
assert!(
names.contains(&"Transcription"),
"STT must emit Transcription"
);
assert!(names.contains(&"LlmText"), "LLM must emit LlmText");
}
}