Skip to main content

vox_rtc_server/
types.rs

1use serde::{Deserialize, Serialize};
2use serde_json::{Map, Value};
3
4pub const EVENT_CLIENT_EVENT: &str = "client.event";
5pub const EVENT_BROWSER_EVENT: &str = "browser.event";
6pub const EVENT_RTC_CLIENT_DISCONNECTED: &str = "rtc.client.disconnected";
7pub const EVENT_ERROR: &str = "error";
8pub const EVENT_INTERRUPTION_DETECTED: &str = "interruption.detected";
9pub const EVENT_INTERRUPTION_FALSE_POSITIVE: &str = "interruption.false_positive";
10pub const EVENT_RESPONSE_AUDIO_CLEAR: &str = "response.audio.clear";
11pub const EVENT_RESPONSE_CANCELLED: &str = "response.cancelled";
12pub const EVENT_RESPONSE_COMMITTED: &str = "response.committed";
13pub const EVENT_RESPONSE_CREATED: &str = "response.created";
14pub const EVENT_RESPONSE_DONE: &str = "response.done";
15pub const EVENT_RTC_SESSION_ATTACHED: &str = "rtc.session.attached";
16pub const EVENT_RTC_SIGNALING_ERROR: &str = "rtc.signaling_error";
17pub const EVENT_SESSION_CREATED: &str = "session.created";
18pub const EVENT_TRANSCRIPT_COMPLETED: &str =
19    "conversation.item.input_audio_transcription.completed";
20pub const EVENT_TURN_STATE_CHANGED: &str = "turn.state_changed";
21pub const EVENT_SPEECH_STARTED: &str = "input_audio_buffer.speech_started";
22pub const EVENT_SPEECH_STOPPED: &str = "input_audio_buffer.speech_stopped";
23pub const EVENT_TRANSCRIPT_DELTA: &str = "conversation.item.input_audio_transcription.delta";
24pub const EVENT_TURN_EOU_PREDICTED: &str = "turn.eou.predicted";
25
26pub const ERROR_CODE_RESPONSE_REJECTED_TURN_STATE: &str = "response_rejected_turn_state";
27pub const ERROR_CODE_RESPONSE_REJECTED_USER_SPEECH: &str = "response_rejected_user_speech";
28pub const ERROR_CODE_RESPONSE_STALE_GENERATION: &str = "response_stale_generation";
29pub const ERROR_CODE_RESPONSE_ALREADY_ACTIVE: &str = "response_already_active";
30pub const ERROR_CODE_RESPONSE_FAILED: &str = "response_failed";
31pub const ERROR_CODE_COMMAND_INVALID: &str = "command_invalid";
32pub const ERROR_CODE_SESSION_FAILED: &str = "session_failed";
33
34pub type EventData = Map<String, Value>;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum ConnectionState {
38    Disconnected,
39    Connecting,
40    Connected,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum ChannelState {
45    Idle,
46    Joining,
47    Joined,
48    Closed,
49    Declined,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
53pub struct RtcIceServer {
54    pub urls: Value,
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub username: Option<String>,
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub credential: Option<String>,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
62pub struct SessionBootstrap {
63    pub session_id: String,
64    pub expires_at: String,
65    #[serde(default)]
66    pub attach_ttl_seconds: u64,
67    #[serde(default)]
68    pub ice_servers: Vec<RtcIceServer>,
69}
70
71#[derive(Debug, Clone, Default)]
72pub struct SessionConfig {
73    pub stt_model: Option<String>,
74    pub tts_model: Option<String>,
75    pub voice: Option<String>,
76    pub turn_profile: Option<String>,
77    pub vad_backend: Option<String>,
78    pub turn_detector: Option<String>,
79    pub speech_context: Option<bool>,
80    pub extra: EventData,
81}
82
83#[derive(Debug, Clone, Default)]
84pub struct ResponseOptions {
85    pub allow_interruptions: Option<bool>,
86    pub generation_id: Option<String>,
87    pub output: Option<ResponseOutputOptions>,
88}
89
90#[derive(Debug, Clone, Default)]
91pub struct ResponseOutputOptions {
92    pub model: Option<String>,
93    pub voice: Option<String>,
94    pub language: Option<String>,
95    pub speed: Option<f64>,
96    pub params: Option<EventData>,
97}
98
99#[derive(Debug, Clone, PartialEq)]
100pub struct ResponseOutput {
101    pub model: String,
102    pub voice: Option<String>,
103    pub language: String,
104    pub speed: f64,
105    pub params: EventData,
106}
107
108#[derive(Debug, Clone)]
109pub struct ClientEventEnvelope {
110    pub event: String,
111    pub payload: Value,
112}
113
114#[derive(Debug, Clone)]
115pub struct WireEvent {
116    pub r#type: String,
117    pub data: EventData,
118    pub session_id: String,
119    pub channel_name: String,
120}
121
122#[derive(Debug, Clone)]
123pub struct SessionAttachedEvent {
124    pub session_id: String,
125    pub channel_name: String,
126    pub data: EventData,
127}
128
129#[derive(Debug, Clone)]
130pub struct SessionCreatedEvent {
131    pub session_id: String,
132    pub channel_name: String,
133    pub data: EventData,
134    pub session: Option<EventData>,
135}
136
137#[derive(Debug, Clone)]
138pub struct TranscriptEvent {
139    pub session_id: String,
140    pub channel_name: String,
141    pub data: EventData,
142    pub transcript: String,
143    pub language: Option<String>,
144    pub start_ms: Option<f64>,
145    pub end_ms: Option<f64>,
146    pub eou_probability: Option<f64>,
147    pub topics: Option<Vec<String>>,
148    pub entities: Vec<TranscriptEntity>,
149    pub words: Vec<TranscriptWord>,
150    pub speech_context: Option<SpeechContext>,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct TranscriptEntity {
155    pub r#type: String,
156    pub text: String,
157    pub start_char: u64,
158    pub end_char: u64,
159}
160
161#[derive(Debug, Clone, PartialEq)]
162pub struct TranscriptWord {
163    pub word: String,
164    pub start_ms: f64,
165    pub end_ms: f64,
166    pub confidence: Option<f64>,
167}
168
169#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
170#[serde(rename_all = "snake_case")]
171pub enum SpeechContextStatus {
172    Complete,
173    Partial,
174    Failed,
175}
176
177#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
178#[serde(rename_all = "snake_case")]
179pub enum SpeechContextTrack {
180    Speaker,
181    Sounds,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
185pub struct SpeechContextSpan {
186    pub label: String,
187    pub start_ms: u64,
188    pub end_ms: u64,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
192pub struct SpeechContextSoundSpan {
193    #[serde(flatten)]
194    pub span: SpeechContextSpan,
195    pub score: f64,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
199pub struct SpeechContext {
200    pub schema_version: u8,
201    pub status: SpeechContextStatus,
202    #[serde(default)]
203    pub emotions: Option<Vec<SpeechContextSpan>>,
204    #[serde(default)]
205    pub vocal: Option<Vec<SpeechContextSpan>>,
206    #[serde(default)]
207    pub sounds: Option<Vec<SpeechContextSoundSpan>>,
208    #[serde(default)]
209    pub unavailable: Option<Vec<SpeechContextTrack>>,
210}
211
212impl SpeechContext {
213    pub(crate) fn is_valid(&self) -> bool {
214        if self.schema_version != 2
215            || !self
216                .emotions
217                .iter()
218                .flatten()
219                .all(|span| !span.label.is_empty() && span.end_ms > span.start_ms)
220            || !self
221                .vocal
222                .iter()
223                .flatten()
224                .all(|span| !span.label.is_empty() && span.end_ms > span.start_ms)
225            || !self.sounds.iter().flatten().all(|sound| {
226                !sound.span.label.is_empty()
227                    && sound.span.end_ms > sound.span.start_ms
228                    && sound.score.is_finite()
229                    && (0.0..=1.0).contains(&sound.score)
230            })
231        {
232            return false;
233        }
234        let unavailable = self.unavailable.as_deref().unwrap_or_default();
235        let unique = unavailable
236            .iter()
237            .enumerate()
238            .all(|(index, track)| !unavailable[..index].contains(track));
239        if !unique {
240            return false;
241        }
242        let speaker_unavailable = unavailable.contains(&SpeechContextTrack::Speaker);
243        let sounds_unavailable = unavailable.contains(&SpeechContextTrack::Sounds);
244        match self.status {
245            SpeechContextStatus::Complete => {
246                self.emotions.is_some()
247                    && self.vocal.is_some()
248                    && self.sounds.is_some()
249                    && self.unavailable.is_none()
250            }
251            SpeechContextStatus::Partial => {
252                unavailable.len() == 1
253                    && speaker_unavailable == self.emotions.is_none()
254                    && speaker_unavailable == self.vocal.is_none()
255                    && sounds_unavailable == self.sounds.is_none()
256            }
257            SpeechContextStatus::Failed => {
258                unavailable.len() == 2
259                    && speaker_unavailable
260                    && sounds_unavailable
261                    && self.emotions.is_none()
262                    && self.vocal.is_none()
263                    && self.sounds.is_none()
264            }
265        }
266    }
267}
268
269#[derive(Debug, Clone)]
270pub struct TurnStateEvent {
271    pub session_id: String,
272    pub channel_name: String,
273    pub data: EventData,
274    pub state: String,
275    pub previous_state: Option<String>,
276}
277
278#[derive(Debug, Clone)]
279pub struct SpeechStartedEvent {
280    pub session_id: String,
281    pub channel_name: String,
282    pub data: EventData,
283    pub timestamp_ms: Option<f64>,
284}
285
286#[derive(Debug, Clone)]
287pub struct SpeechStoppedEvent {
288    pub session_id: String,
289    pub channel_name: String,
290    pub data: EventData,
291    pub timestamp_ms: Option<f64>,
292}
293
294#[derive(Debug, Clone)]
295pub struct TranscriptDeltaEvent {
296    pub session_id: String,
297    pub channel_name: String,
298    pub data: EventData,
299    pub delta: String,
300    pub start_ms: Option<f64>,
301    pub end_ms: Option<f64>,
302}
303
304#[derive(Debug, Clone)]
305pub struct TurnEouPredictedEvent {
306    pub session_id: String,
307    pub channel_name: String,
308    pub data: EventData,
309    pub probability: Option<f64>,
310    pub threshold: Option<f64>,
311    pub delay_ms: Option<f64>,
312    pub start_ms: Option<f64>,
313    pub end_ms: Option<f64>,
314    pub decision: Option<String>,
315    pub action: Option<String>,
316    pub turn_detector: Option<String>,
317}
318
319#[derive(Debug, Clone)]
320pub struct ResponseEvent {
321    pub session_id: String,
322    pub channel_name: String,
323    pub data: EventData,
324    pub response_id: Option<String>,
325    pub generation_id: Option<String>,
326    pub output: Option<ResponseOutput>,
327}
328
329#[derive(Debug, Clone)]
330pub struct InterruptionEvent {
331    pub response: ResponseEvent,
332    pub vad_active_ms: Option<f64>,
333    pub partial_transcript: Option<String>,
334    pub reason: Option<String>,
335}
336
337#[derive(Debug, Clone)]
338pub struct BrowserEvent {
339    pub session_id: String,
340    pub channel_name: String,
341    pub data: EventData,
342    pub event: String,
343    pub payload: Value,
344}
345
346#[derive(Debug, Clone)]
347pub struct CloseEvent {
348    pub session_id: String,
349    pub channel_name: String,
350    pub data: EventData,
351    pub reason: String,
352    pub connection_state: Option<String>,
353    pub ice_connection_state: Option<String>,
354    pub data_channel_state: Option<String>,
355}
356
357#[derive(Debug, Clone)]
358pub struct ErrorEvent {
359    pub session_id: String,
360    pub channel_name: String,
361    pub data: EventData,
362    pub message: Option<String>,
363    pub code: Option<String>,
364    pub recoverable: bool,
365    pub generation_id: Option<String>,
366}
367
368#[derive(Debug, Clone)]
369pub struct SignalingErrorEvent {
370    pub session_id: String,
371    pub channel_name: String,
372    pub data: EventData,
373    pub message: Option<String>,
374    pub generation: Option<i64>,
375}
376
377#[derive(Debug, Clone)]
378pub struct StartAck {
379    pub accepted: bool,
380    pub generation_id: String,
381    pub response_id: Option<String>,
382    pub output: Option<ResponseOutput>,
383    pub error_code: Option<String>,
384    pub error_message: Option<String>,
385    pub recoverable: bool,
386}
387
388pub(crate) fn optional_string(data: &EventData, key: &str) -> Option<String> {
389    data.get(key).and_then(Value::as_str).map(ToOwned::to_owned)
390}
391
392pub(crate) fn required_string(data: &EventData, key: &str, fallback: &str) -> String {
393    optional_string(data, key)
394        .filter(|s| !s.is_empty())
395        .unwrap_or_else(|| fallback.to_owned())
396}
397
398pub(crate) fn optional_number(data: &EventData, key: &str) -> Option<f64> {
399    data.get(key).and_then(Value::as_f64)
400}
401
402pub(crate) fn optional_i64(data: &EventData, key: &str) -> Option<i64> {
403    data.get(key).and_then(Value::as_i64)
404}
405
406pub(crate) fn optional_nonempty_string(data: &EventData, key: &str) -> Option<String> {
407    optional_string(data, key).filter(|s| !s.is_empty())
408}
409
410pub(crate) fn recoverable_flag(data: &EventData) -> bool {
411    data.get("recoverable").and_then(Value::as_bool).unwrap_or(true)
412}
413
414pub(crate) fn optional_string_vec(data: &EventData, key: &str) -> Option<Vec<String>> {
415    data.get(key).and_then(Value::as_array).and_then(|items| {
416        items
417            .iter()
418            .map(|item| item.as_str().map(ToOwned::to_owned))
419            .collect()
420    })
421}
422
423pub(crate) fn transcript_entities(data: &EventData) -> Vec<TranscriptEntity> {
424    data.get("entities")
425        .and_then(Value::as_array)
426        .map(|items| items.iter().filter_map(transcript_entity).collect())
427        .unwrap_or_default()
428}
429
430fn transcript_entity(value: &Value) -> Option<TranscriptEntity> {
431    let object = value.as_object()?;
432    Some(TranscriptEntity {
433        r#type: optional_string(object, "type").unwrap_or_default(),
434        text: optional_string(object, "text").unwrap_or_default(),
435        start_char: object.get("start_char").and_then(Value::as_u64).unwrap_or(0),
436        end_char: object.get("end_char").and_then(Value::as_u64).unwrap_or(0),
437    })
438}
439
440pub(crate) fn transcript_words(data: &EventData) -> Vec<TranscriptWord> {
441    data.get("words")
442        .and_then(Value::as_array)
443        .map(|items| items.iter().filter_map(transcript_word).collect())
444        .unwrap_or_default()
445}
446
447fn transcript_word(value: &Value) -> Option<TranscriptWord> {
448    let object = value.as_object()?;
449    Some(TranscriptWord {
450        word: optional_string(object, "word").unwrap_or_default(),
451        start_ms: optional_number(object, "start_ms").unwrap_or(0.0),
452        end_ms: optional_number(object, "end_ms").unwrap_or(0.0),
453        confidence: optional_number(object, "confidence"),
454    })
455}