1use anyhow::Result;
2use rsipstack::dialog::{authenticate::Credential, invitation::InviteOption};
3use serde::{Deserialize, Serialize};
4use serde_with::skip_serializing_none;
5use std::collections::HashMap;
6
7use crate::{
8 media::{
9 agc::AGCOption,
10 ambiance::AmbianceOption,
11 recorder::RecorderOption,
12 track::media_pass::MediaPassOption,
13 vad::VADOption,
14 },
15 synthesis::SynthesisOption,
16 transcription::TranscriptionOption,
17};
18
19pub mod app;
20pub mod call;
21pub mod callrecord;
22pub mod config;
23pub mod event;
24pub mod handler;
25pub mod locator;
26pub mod media;
27pub mod net_tool;
28pub mod main_builder;
29
30#[cfg(feature = "offline")]
31pub mod offline;
32
33pub mod playbook;
34pub mod synthesis;
35pub mod transcription;
36pub mod useragent;
37
38#[derive(Debug, Deserialize, Serialize, Default, Clone)]
39#[serde(default)]
40pub struct SipOption {
41 pub username: Option<String>,
42 pub password: Option<String>,
43 pub realm: Option<String>,
44 pub contact: Option<String>,
45 pub headers: Option<HashMap<String, String>>,
46 pub hangup_headers: Option<HashMap<String, String>>,
47 pub extract_headers: Option<Vec<String>>,
48 pub enable_srtp: Option<bool>,
49}
50
51#[skip_serializing_none]
52#[derive(Debug, Deserialize, Serialize, Clone)]
53#[serde(rename_all = "camelCase")]
54pub struct CallOption {
55 pub denoise: Option<bool>,
56 pub agc: Option<AGCOption>,
57 pub offer: Option<String>,
58 pub callee: Option<String>,
59 pub caller: Option<String>,
60 pub recorder: Option<RecorderOption>,
61 pub vad: Option<VADOption>,
62 pub asr: Option<TranscriptionOption>,
63 pub tts: Option<SynthesisOption>,
64 pub media_pass: Option<MediaPassOption>,
65 pub handshake_timeout: Option<u64>,
67 pub enable_ipv6: Option<bool>,
68 pub inactivity_timeout: Option<u64>, pub sip: Option<SipOption>,
70 pub extra: Option<HashMap<String, String>>,
71 pub codec: Option<String>, pub ambiance: Option<AmbianceOption>,
73 pub eou: Option<EouOption>,
74 pub realtime: Option<RealtimeOption>,
75 pub subscribe: Option<bool>,
76 pub enable_ice_lite: Option<bool>,
77 pub ringback_detection: Option<RingbackDetectionOption>,
78}
79
80impl Default for CallOption {
81 fn default() -> Self {
82 Self {
83 denoise: None,
84 agc: None,
85 offer: None,
86 callee: None,
87 caller: None,
88 recorder: None,
89 asr: None,
90 vad: None,
91 tts: None,
92 media_pass: None,
93 handshake_timeout: None,
94 inactivity_timeout: Some(50), enable_ipv6: None,
96 sip: None,
97 extra: None,
98 codec: None,
99 ambiance: None,
100 eou: None,
101 realtime: None,
102 subscribe: None,
103 enable_ice_lite: None,
104 ringback_detection: None,
105 }
106 }
107}
108
109impl CallOption {
110 pub fn check_default(&mut self) {
111 if let Some(tts) = &mut self.tts {
112 tts.check_default();
113 }
114 if let Some(asr) = &mut self.asr {
115 asr.check_default();
116 }
117 if let Some(realtime) = &mut self.realtime {
118 realtime.check_default();
119 }
120 }
121
122 pub fn build_invite_option(&self) -> Result<InviteOption> {
123 let mut invite_option = InviteOption::default();
124 if let Some(offer) = &self.offer {
125 invite_option.offer = Some(offer.clone().into());
126 }
127 if let Some(callee) = &self.callee {
128 invite_option.callee = callee.clone().try_into()?;
129 }
130 let caller_uri = if let Some(caller) = &self.caller {
131 if caller.starts_with("sip:") || caller.starts_with("sips:") {
133 caller.clone()
134 } else {
135 format!("sip:{}", caller)
136 }
137 } else if let Some(username) = self.sip.as_ref().and_then(|sip| sip.username.as_ref()) {
138 let domain = self
141 .sip
142 .as_ref()
143 .and_then(|sip| sip.realm.as_ref())
144 .map(|s| s.as_str())
145 .unwrap_or("127.0.0.1");
146 format!("sip:{}@{}", username, domain)
147 } else {
148 "sip:active-call@127.0.0.1".to_string()
150 };
151 invite_option.caller = caller_uri.try_into()?;
152
153 if let Some(sip) = &self.sip {
154 invite_option.credential = Some(Credential {
155 username: sip.username.clone().unwrap_or_default(),
156 password: sip.password.clone().unwrap_or_default(),
157 realm: sip.realm.clone(),
158 });
159 invite_option.headers = sip.headers.as_ref().map(|h| {
160 h.iter()
161 .map(|(k, v)| rsipstack::rsip::Header::Other(k.clone(), v.clone()))
162 .collect::<Vec<_>>()
163 });
164 sip.contact.as_ref().map(|c| match c.clone().try_into() {
165 Ok(u) => {
166 invite_option.contact = u;
167 }
168 Err(_) => {}
169 });
170 }
171 Ok(invite_option)
172 }
173}
174
175#[skip_serializing_none]
176#[derive(Debug, Deserialize, Serialize, Clone)]
177#[serde(rename_all = "camelCase")]
178pub struct ReferOption {
179 pub denoise: Option<bool>,
180 pub agc: Option<AGCOption>,
181 pub timeout: Option<u32>,
182 pub moh: Option<String>,
183 pub vad: Option<VADOption>,
184 pub asr: Option<TranscriptionOption>,
185 pub auto_hangup: Option<bool>,
187 pub sip: Option<SipOption>,
188 pub call_id: Option<String>,
189 pub pause_parent_asr: Option<bool>,
191 pub forward_dtmf: Option<bool>,
193}
194
195#[skip_serializing_none]
196#[derive(Clone, Debug, Deserialize, Serialize, Default)]
197#[serde(rename_all = "camelCase")]
198pub struct EouOption {
199 pub r#type: Option<String>,
200 pub endpoint: Option<String>,
201 #[serde(alias = "apiKey")]
202 pub secret_key: Option<String>,
203 pub secret_id: Option<String>,
204 pub timeout: Option<u32>,
206 pub extra: Option<HashMap<String, String>>,
207}
208
209#[skip_serializing_none]
210#[derive(Clone, Debug, Deserialize, Serialize, Default)]
211#[serde(rename_all = "camelCase")]
212pub struct RingbackDetectionOption {
213 pub enabled: Option<bool>,
214 pub model_weights_path: Option<String>,
216 pub min_buffer_secs: Option<f32>,
218 pub detection_interval_secs: Option<f32>,
220 pub confidence_threshold: Option<f32>,
222 pub on_state_change_only: Option<bool>,
224 pub sliding_window_size: Option<usize>,
226 pub final_confidence_threshold: Option<f32>,
228}
229
230#[derive(Debug, Clone, Serialize, Hash, Eq, PartialEq)]
231pub enum RealtimeType {
232 #[serde(rename = "openai")]
233 OpenAI,
234 #[serde(rename = "azure")]
235 Azure,
236 Other(String),
237}
238
239impl<'de> Deserialize<'de> for RealtimeType {
240 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
241 where
242 D: serde::Deserializer<'de>,
243 {
244 let value = String::deserialize(deserializer)?;
245 match value.as_str() {
246 "openai" => Ok(RealtimeType::OpenAI),
247 "azure" => Ok(RealtimeType::Azure),
248 _ => Ok(RealtimeType::Other(value)),
249 }
250 }
251}
252
253#[skip_serializing_none]
254#[derive(Clone, Debug, Deserialize, Serialize, Default)]
255#[serde(rename_all = "camelCase")]
256pub struct RealtimeOption {
257 pub provider: Option<RealtimeType>,
258 pub model: Option<String>,
259 #[serde(alias = "apiKey")]
260 pub secret_key: Option<String>,
261 pub secret_id: Option<String>,
262 pub endpoint: Option<String>,
263 pub turn_detection: Option<serde_json::Value>,
264 pub tools: Option<Vec<serde_json::Value>>,
265 pub extra: Option<HashMap<String, String>>,
266}
267
268impl RealtimeOption {
269 pub fn check_default(&mut self) {
270 if self.secret_key.is_none() {
271 self.secret_key = std::env::var("OPENAI_API_KEY").ok();
272 }
273 }
274}
275
276pub type Spawner = fn(
277 std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>,
278) -> tokio::task::JoinHandle<()>;
279static EXTERNAL_SPAWNER: std::sync::OnceLock<Spawner> = std::sync::OnceLock::new();
280
281pub fn set_spawner(spawner: Spawner) -> Result<(), Spawner> {
282 EXTERNAL_SPAWNER.set(spawner)
283}
284
285pub fn spawn<F>(future: F) -> tokio::task::JoinHandle<()>
286where
287 F: std::future::Future<Output = ()> + Send + 'static,
288{
289 if let Some(spawner) = EXTERNAL_SPAWNER.get() {
290 spawner(Box::pin(future))
291 } else {
292 tokio::spawn(future)
293 }
294}