active_call/synthesis/
supertonic.rs1use crate::offline::get_offline_models;
2use crate::synthesis::{SynthesisClient, SynthesisEvent, SynthesisOption, SynthesisType};
3use anyhow::{Result, anyhow};
4use async_trait::async_trait;
5use audio_codec::BoxedResampler;
6use bytes::Bytes;
7use futures::stream::BoxStream;
8use tokio::sync::mpsc;
9use tokio_stream::wrappers::UnboundedReceiverStream;
10use tokio_util::sync::CancellationToken;
11use tracing::{debug, warn};
12
13pub struct SupertonicTtsClient {
14 voice_style: String,
15 speed: f32,
16 target_rate: i32,
17 tx: Option<mpsc::UnboundedSender<(Option<usize>, Result<SynthesisEvent>)>>,
18 token: CancellationToken,
19}
20
21impl SupertonicTtsClient {
22 pub fn create(_streaming: bool, option: &SynthesisOption) -> Result<Box<dyn SynthesisClient>> {
23 let voice_style = option.speaker.clone().unwrap_or_else(|| "M1".to_string());
24 let speed = option.speed.unwrap_or(1.0);
25 let target_rate = option.samplerate.unwrap_or(16000);
26
27 Ok(Box::new(Self {
28 voice_style,
29 speed,
30 target_rate,
31 tx: None,
32 token: CancellationToken::new(),
33 }))
34 }
35
36 fn ensure_models_initialized() -> Result<()> {
37 if get_offline_models().is_none() {
38 anyhow::bail!(
39 "Offline models not initialized. Please call init_offline_models() first."
40 );
41 }
42 Ok(())
43 }
44
45 async fn synthesize_text(&self, text: String, cmd_seq: Option<usize>) -> Result<()> {
46 let Some(tx) = self.tx.as_ref() else {
47 return Ok(());
48 };
49
50 let models =
51 get_offline_models().ok_or_else(|| anyhow!("offline models not initialized"))?;
52
53 let voice_style = self.voice_style.clone();
54 let speed = self.speed;
55 let target_rate = self.target_rate;
56 let tx_clone = tx.clone();
57 let language = "en".to_string();
59
60 let tts_arc = models.get_supertonic().await?;
61
62 tokio::task::spawn_blocking(move || {
64 let mut guard = match tts_arc.try_write() {
67 Ok(g) => g,
68 Err(_) => {
69 warn!("Supertonic TTS write lock unavailable, skipping synthesis");
70 let _ = tx_clone.send((cmd_seq, Err(anyhow!("TTS write lock unavailable"))));
71 return;
72 }
73 };
74
75 if let Some(tts) = guard.as_mut() {
76 debug!(
77 text = %text,
78 voice = %voice_style,
79 speed = speed,
80 target_rate = target_rate,
81 "Calling Supertonic TTS synthesis"
82 );
83
84 match tts.synthesize(&text, &language, Some(&voice_style), Some(speed)) {
85 Ok(samples) => {
86 if !samples.is_empty() {
87 let mut samples_i16: Vec<i16> = samples
88 .iter()
89 .map(|&s| (s * 32768.0).max(-32768.0).min(32767.0) as i16)
90 .collect();
91
92 if tts.sample_rate() != target_rate {
94 let mut resampler = BoxedResampler::new(
95 tts.sample_rate() as usize,
96 target_rate as usize,
97 )
98 .expect("invalid sample rate");
99 samples_i16 = resampler.resample(&samples_i16);
100 }
101
102 let mut bytes = Vec::with_capacity(samples_i16.len() * 2);
104 for s in samples_i16 {
105 bytes.extend_from_slice(&s.to_le_bytes());
106 }
107
108 let _ = tx_clone.send((
110 cmd_seq,
111 Ok(SynthesisEvent::AudioChunk(Bytes::from(bytes))),
112 ));
113
114 let _ = tx_clone.send((cmd_seq, Ok(SynthesisEvent::Finished)));
116 } else {
117 warn!("Supertonic produced empty audio");
118 let _ = tx_clone.send((cmd_seq, Ok(SynthesisEvent::Finished)));
119 }
120 }
121 Err(e) => {
122 warn!(error = %e, "Supertonic inference failed");
123 let _ = tx_clone.send((cmd_seq, Err(anyhow!("Synthesis failed: {}", e))));
124 }
125 }
126 } else {
127 warn!("Supertonic TTS not initialized");
128 let _ = tx_clone.send((cmd_seq, Err(anyhow!("TTS not initialized"))));
129 }
130 })
131 .await
132 .map_err(|e| anyhow!("task join error: {}", e))?;
133
134 Ok(())
135 }
136}
137
138#[async_trait]
139impl SynthesisClient for SupertonicTtsClient {
140 fn provider(&self) -> SynthesisType {
141 SynthesisType::Supertonic
142 }
143
144 async fn start(
145 &mut self,
146 ) -> Result<BoxStream<'static, (Option<usize>, Result<SynthesisEvent>)>> {
147 Self::ensure_models_initialized()?;
148
149 let (tx, rx) = mpsc::unbounded_channel();
150 self.tx = Some(tx);
151
152 let models =
154 get_offline_models().ok_or_else(|| anyhow!("offline models not initialized"))?;
155 models.init_supertonic().await?;
156
157 debug!(
158 "SupertonicTtsClient started with voice: {}",
159 self.voice_style
160 );
161
162 Ok(Box::pin(UnboundedReceiverStream::new(rx)))
163 }
164
165 async fn synthesize(
166 &mut self,
167 text: &str,
168 cmd_seq: Option<usize>,
169 _option: Option<SynthesisOption>,
170 ) -> Result<()> {
171 self.synthesize_text(text.to_string(), cmd_seq).await
172 }
173
174 async fn stop(&mut self) -> Result<()> {
175 self.token.cancel();
176 self.tx = None;
177 Ok(())
178 }
179}