1use anyhow::Result;
2use async_trait::async_trait;
3use bytes::Bytes;
4use futures::stream::BoxStream;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use tokio::sync::mpsc;
8
9mod aliyun;
10mod deepgram;
11mod tencent_cloud;
12mod tencent_cloud_basic;
13
14#[cfg(feature = "offline")]
15mod supertonic;
16
17pub use aliyun::AliyunTtsClient;
18pub use deepgram::DeepegramTtsClient;
19pub use tencent_cloud::TencentCloudTtsClient;
20pub use tencent_cloud_basic::TencentCloudTtsBasicClient;
21
22#[cfg(feature = "offline")]
23pub use supertonic::SupertonicTtsClient;
24
25#[derive(Clone, Default)]
26pub struct SynthesisCommand {
27 pub text: String,
28 pub speaker: Option<String>,
29 pub play_id: Option<String>,
30 pub streaming: bool,
31 pub end_of_stream: bool,
32 pub option: SynthesisOption,
33 pub base64: bool,
34 pub cache_key: Option<String>,
35 pub auto_hangup: Option<bool>,
36}
37pub type SynthesisCommandSender = mpsc::UnboundedSender<SynthesisCommand>;
38pub type SynthesisCommandReceiver = mpsc::UnboundedReceiver<SynthesisCommand>;
39
40#[derive(Debug, Clone, Serialize, Hash, Eq, PartialEq)]
41pub enum SynthesisType {
42 #[serde(rename = "tencent")]
43 TencentCloud,
44 #[serde(rename = "aliyun")]
45 Aliyun,
46 #[serde(rename = "deepgram")]
47 Deepgram,
48 #[cfg(feature = "offline")]
49 #[serde(rename = "supertonic")]
50 Supertonic,
51 #[serde(rename = "other")]
52 Other(String),
53}
54
55impl std::fmt::Display for SynthesisType {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 match self {
58 SynthesisType::TencentCloud => write!(f, "tencent"),
59 SynthesisType::Aliyun => write!(f, "aliyun"),
60 SynthesisType::Deepgram => write!(f, "deepgram"),
61 #[cfg(feature = "offline")]
62 SynthesisType::Supertonic => write!(f, "supertonic"),
63 SynthesisType::Other(provider) => write!(f, "{}", provider),
64 }
65 }
66}
67
68impl<'de> Deserialize<'de> for SynthesisType {
69 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
70 where
71 D: serde::Deserializer<'de>,
72 {
73 let value = String::deserialize(deserializer)?;
74 match value.as_str() {
75 "tencent" => Ok(SynthesisType::TencentCloud),
76 "aliyun" => Ok(SynthesisType::Aliyun),
77 "deepgram" => Ok(SynthesisType::Deepgram),
78 #[cfg(feature = "offline")]
79 "supertonic" => Ok(SynthesisType::Supertonic),
80 _ => Ok(SynthesisType::Other(value)),
81 }
82 }
83}
84
85#[cfg(test)]
86mod tests;
87#[derive(Debug, Clone, Deserialize, Serialize)]
88#[serde(rename_all = "camelCase")]
89#[serde(default)]
90pub struct SynthesisOption {
91 pub samplerate: Option<i32>,
92 pub provider: Option<SynthesisType>,
93 pub speed: Option<f32>,
94 pub app_id: Option<String>,
95 pub secret_id: Option<String>,
96 #[serde(alias = "apiKey")]
97 pub secret_key: Option<String>,
98 pub volume: Option<i32>,
99 pub speaker: Option<String>,
100 pub codec: Option<String>,
101 pub subtitle: Option<bool>,
102 #[serde(alias = "voice")]
103 pub model: Option<String>,
104 pub language: Option<String>,
105 pub emotion: Option<String>,
108 pub endpoint: Option<String>,
109 pub extra: Option<HashMap<String, String>>,
110 pub max_concurrent_tasks: Option<usize>,
111 pub session_id: Option<String>,
112}
113
114impl SynthesisOption {
115 pub fn merge_with(&self, option: Option<SynthesisOption>) -> Self {
116 if let Some(other) = option {
117 Self {
118 samplerate: other.samplerate.or(self.samplerate),
119 provider: other.provider.or(self.provider.clone()),
120 speed: other.speed.or(self.speed),
121 app_id: other.app_id.or(self.app_id.clone()),
122 secret_id: other.secret_id.or(self.secret_id.clone()),
123 secret_key: other.secret_key.or(self.secret_key.clone()),
124 volume: other.volume.or(self.volume),
125 speaker: other.speaker.or(self.speaker.clone()),
126 codec: other.codec.or(self.codec.clone()),
127 subtitle: other.subtitle.or(self.subtitle),
128 model: other.model.or(self.model.clone()),
129 language: other.language.or(self.language.clone()),
130 emotion: other.emotion.or(self.emotion.clone()),
131 endpoint: other.endpoint.or(self.endpoint.clone()),
132 extra: other.extra.or(self.extra.clone()),
133 max_concurrent_tasks: other.max_concurrent_tasks.or(self.max_concurrent_tasks),
134 session_id: other.session_id.or(self.session_id.clone()),
135 }
136 } else {
137 self.clone()
138 }
139 }
140}
141
142#[derive(Debug)]
143pub enum SynthesisEvent {
144 AudioChunk(Bytes),
146 Subtitles(Vec<Subtitle>),
148 Finished,
149}
150
151#[derive(Debug, Clone)]
152pub struct Subtitle {
153 pub text: String,
154 pub begin_time: u32,
155 pub end_time: u32,
156 pub begin_index: u32,
157 pub end_index: u32,
158}
159
160impl Subtitle {
161 pub fn new(
162 text: String,
163 begin_time: u32,
164 end_time: u32,
165 begin_index: u32,
166 end_index: u32,
167 ) -> Self {
168 Self {
169 text,
170 begin_time,
171 end_time,
172 begin_index,
173 end_index,
174 }
175 }
176}
177
178pub fn bytes_size_to_duration(bytes: usize, sample_rate: u32) -> u32 {
180 (500.0 * bytes as f32 / sample_rate as f32) as u32
181}
182
183#[async_trait]
184pub trait SynthesisClient: Send {
185 fn provider(&self) -> SynthesisType;
187
188 async fn start(
191 &mut self,
192 ) -> Result<BoxStream<'static, (Option<usize>, Result<SynthesisEvent>)>>;
193
194 async fn synthesize(
198 &mut self,
199 text: &str,
200 cmd_seq: Option<usize>,
201 option: Option<SynthesisOption>,
202 ) -> Result<()>;
203
204 async fn stop(&mut self) -> Result<()>;
205}
206
207impl Default for SynthesisOption {
208 fn default() -> Self {
209 Self {
210 samplerate: Some(16000),
211 provider: None,
212 speed: Some(1.0),
213 app_id: None,
214 secret_id: None,
215 secret_key: None,
216 volume: Some(5), speaker: None,
218 codec: Some("pcm".to_string()),
219 subtitle: None,
220 model: None,
221 language: None,
222 emotion: None,
223 endpoint: None,
224 extra: None,
225 max_concurrent_tasks: None,
226 session_id: None,
227 }
228 }
229}
230
231impl SynthesisOption {
232 pub fn check_default(&mut self) {
233 if let Some(provider) = &self.provider {
234 match provider.to_string().as_str() {
235 "tencent" | "tencent_basic" => {
236 if self.app_id.is_none() {
237 self.app_id = std::env::var("TENCENT_APPID").ok();
238 }
239 if self.secret_id.is_none() {
240 self.secret_id = std::env::var("TENCENT_SECRET_ID").ok();
241 }
242 if self.secret_key.is_none() {
243 self.secret_key = std::env::var("TENCENT_SECRET_KEY").ok();
244 }
245 }
246 "voiceapi" => {
247 if self.endpoint.is_none() {
249 self.endpoint = std::env::var("VOICEAPI_ENDPOINT")
250 .ok()
251 .or_else(|| Some("http://localhost:8000".to_string()));
252 }
253 if self.speaker.is_none() {
255 self.speaker = std::env::var("VOICEAPI_SPEAKER_ID")
256 .ok()
257 .or_else(|| Some("0".to_string()));
258 }
259 }
260 "aliyun" => {
261 if self.secret_key.is_none() {
262 self.secret_key = std::env::var("DASHSCOPE_API_KEY").ok();
263 }
264 }
265 "deepgram" => {
266 if self.secret_key.is_none() {
267 self.secret_key = std::env::var("DEEPGRAM_API_KEY").ok();
268 }
269 }
270 _ => {}
271 }
272 }
273 }
274}