1use core::time::Duration;
9
10pub use ort::session::builder::GraphOptimizationLevel;
11#[cfg(feature = "serde")]
12use serde::{Deserialize, Serialize};
13
14#[cfg(feature = "serde")]
15mod graph_optimization_level {
16 use serde::*;
17
18 use super::GraphOptimizationLevel;
19
20 #[derive(
21 Debug, Default, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize,
22 )]
23 #[serde(rename_all = "snake_case")]
24 enum OptimizationLevel {
25 Disable,
26 Level1,
27 Level2,
28 #[default]
29 Level3,
30 All,
31 }
32
33 impl From<GraphOptimizationLevel> for OptimizationLevel {
34 #[inline]
35 fn from(value: GraphOptimizationLevel) -> Self {
36 match value {
37 GraphOptimizationLevel::Disable => Self::Disable,
38 GraphOptimizationLevel::Level1 => Self::Level1,
39 GraphOptimizationLevel::Level2 => Self::Level2,
40 GraphOptimizationLevel::Level3 => Self::Level3,
41 GraphOptimizationLevel::All => Self::All,
42 }
43 }
44 }
45
46 impl From<OptimizationLevel> for GraphOptimizationLevel {
47 #[inline]
48 fn from(value: OptimizationLevel) -> Self {
49 match value {
50 OptimizationLevel::Disable => Self::Disable,
51 OptimizationLevel::Level1 => Self::Level1,
52 OptimizationLevel::Level2 => Self::Level2,
53 OptimizationLevel::Level3 => Self::Level3,
54 OptimizationLevel::All => Self::All,
55 }
56 }
57 }
58
59 #[cfg_attr(not(tarpaulin), inline(always))]
60 pub fn serialize<S>(level: &GraphOptimizationLevel, serializer: S) -> Result<S::Ok, S::Error>
61 where
62 S: Serializer,
63 {
64 OptimizationLevel::from(*level).serialize(serializer)
65 }
66
67 #[cfg_attr(not(tarpaulin), inline(always))]
68 pub fn deserialize<'de, D>(deserializer: D) -> Result<GraphOptimizationLevel, D::Error>
69 where
70 D: Deserializer<'de>,
71 {
72 OptimizationLevel::deserialize(deserializer).map(Into::into)
73 }
74
75 #[cfg_attr(not(tarpaulin), inline(always))]
76 pub const fn default() -> GraphOptimizationLevel {
77 GraphOptimizationLevel::Disable
78 }
79}
80
81#[derive(Debug, Clone)]
88#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
89pub struct SessionOptions {
90 #[cfg_attr(
91 feature = "serde",
92 serde(
93 default = "graph_optimization_level::default",
94 with = "graph_optimization_level"
95 )
96 )]
97 optimization_level: GraphOptimizationLevel,
98}
99
100impl Default for SessionOptions {
101 #[inline]
102 fn default() -> Self {
103 Self::new()
104 }
105}
106
107impl SessionOptions {
108 #[cfg_attr(not(tarpaulin), inline(always))]
110 pub const fn new() -> Self {
111 Self {
112 optimization_level: GraphOptimizationLevel::Level3,
113 }
114 }
115
116 #[cfg_attr(not(tarpaulin), inline(always))]
118 pub const fn optimization_level(&self) -> GraphOptimizationLevel {
119 self.optimization_level
120 }
121
122 #[cfg_attr(not(tarpaulin), inline(always))]
124 pub const fn set_optimization_level(&mut self, level: GraphOptimizationLevel) -> &mut Self {
125 self.optimization_level = level;
126 self
127 }
128
129 #[cfg_attr(not(tarpaulin), inline(always))]
131 #[must_use]
132 pub const fn with_optimization_level(mut self, level: GraphOptimizationLevel) -> Self {
133 self.optimization_level = level;
134 self
135 }
136}
137
138pub(crate) const FRAME_SHIFT_MS: u128 = 10;
140
141#[cfg_attr(not(tarpaulin), inline(always))]
143pub(crate) const fn duration_to_frames(d: Duration) -> u32 {
144 let frames = d.as_millis() / FRAME_SHIFT_MS;
145 if frames > u32::MAX as u128 {
146 u32::MAX
147 } else {
148 frames as u32
149 }
150}
151
152#[cfg_attr(not(tarpaulin), inline(always))]
154pub(crate) const fn sanitize_probability(value: f32) -> f32 {
155 if value.is_finite() {
156 value.clamp(0.0, 1.0)
157 } else {
158 0.0
159 }
160}
161
162#[cfg_attr(not(tarpaulin), inline(always))]
163const fn default_smooth_window_size() -> u32 {
164 5
165}
166
167#[cfg_attr(not(tarpaulin), inline(always))]
168const fn default_speech_threshold() -> f32 {
169 0.5
170}
171
172#[cfg_attr(not(tarpaulin), inline(always))]
173const fn default_pad_start() -> Duration {
174 Duration::from_millis(50)
175}
176
177#[cfg_attr(not(tarpaulin), inline(always))]
178const fn default_min_speech_duration() -> Duration {
179 Duration::from_millis(80)
180}
181
182#[cfg_attr(not(tarpaulin), inline(always))]
183const fn default_min_silence_duration() -> Duration {
184 Duration::from_millis(200)
185}
186
187#[cfg_attr(not(tarpaulin), inline(always))]
188const fn default_max_speech_duration() -> Option<Duration> {
189 Some(Duration::from_secs(20))
190}
191
192#[derive(Debug, Clone)]
199#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
200pub struct VadOptions {
201 #[cfg_attr(feature = "serde", serde(default = "default_smooth_window_size"))]
202 smooth_window_size: u32,
203
204 #[cfg_attr(feature = "serde", serde(default = "default_speech_threshold"))]
205 speech_threshold: f32,
206
207 #[cfg_attr(
208 feature = "serde",
209 serde(default = "default_pad_start", with = "humantime_serde")
210 )]
211 pad_start: Duration,
212
213 #[cfg_attr(
214 feature = "serde",
215 serde(default = "default_min_speech_duration", with = "humantime_serde")
216 )]
217 min_speech_duration: Duration,
218
219 #[cfg_attr(
220 feature = "serde",
221 serde(default = "default_min_silence_duration", with = "humantime_serde")
222 )]
223 min_silence_duration: Duration,
224
225 #[cfg_attr(
226 feature = "serde",
227 serde(
228 default = "default_max_speech_duration",
229 with = "humantime_serde::option"
230 )
231 )]
232 max_speech_duration: Option<Duration>,
233
234 #[cfg_attr(feature = "serde", serde(default))]
235 session_options: SessionOptions,
236}
237
238impl Default for VadOptions {
239 #[cfg_attr(not(tarpaulin), inline(always))]
240 fn default() -> Self {
241 Self::new()
242 }
243}
244
245impl VadOptions {
246 #[cfg_attr(not(tarpaulin), inline(always))]
248 pub const fn new() -> Self {
249 Self {
250 smooth_window_size: default_smooth_window_size(),
251 speech_threshold: default_speech_threshold(),
252 pad_start: default_pad_start(),
253 min_speech_duration: default_min_speech_duration(),
254 min_silence_duration: default_min_silence_duration(),
255 max_speech_duration: default_max_speech_duration(),
256 session_options: SessionOptions::new(),
257 }
258 }
259
260 #[cfg_attr(not(tarpaulin), inline(always))]
262 pub const fn smooth_window_size(&self) -> u32 {
263 self.smooth_window_size
264 }
265
266 #[cfg_attr(not(tarpaulin), inline(always))]
268 pub const fn set_smooth_window_size(&mut self, size: u32) -> &mut Self {
269 self.smooth_window_size = size;
270 self
271 }
272
273 #[cfg_attr(not(tarpaulin), inline(always))]
275 #[must_use]
276 pub const fn with_smooth_window_size(mut self, size: u32) -> Self {
277 self.smooth_window_size = size;
278 self
279 }
280
281 #[cfg_attr(not(tarpaulin), inline(always))]
283 pub const fn speech_threshold(&self) -> f32 {
284 self.speech_threshold
285 }
286
287 #[cfg_attr(not(tarpaulin), inline(always))]
289 pub const fn set_speech_threshold(&mut self, t: f32) -> &mut Self {
290 self.speech_threshold = sanitize_probability(t);
291 self
292 }
293
294 #[cfg_attr(not(tarpaulin), inline(always))]
296 #[must_use]
297 pub const fn with_speech_threshold(mut self, t: f32) -> Self {
298 self.speech_threshold = sanitize_probability(t);
299 self
300 }
301
302 #[cfg_attr(not(tarpaulin), inline(always))]
304 pub const fn pad_start(&self) -> Duration {
305 self.pad_start
306 }
307
308 #[cfg_attr(not(tarpaulin), inline(always))]
310 pub const fn set_pad_start(&mut self, d: Duration) -> &mut Self {
311 self.pad_start = d;
312 self
313 }
314
315 #[cfg_attr(not(tarpaulin), inline(always))]
317 #[must_use]
318 pub const fn with_pad_start(mut self, d: Duration) -> Self {
319 self.pad_start = d;
320 self
321 }
322
323 #[cfg_attr(not(tarpaulin), inline(always))]
325 pub const fn min_speech_duration(&self) -> Duration {
326 self.min_speech_duration
327 }
328
329 #[cfg_attr(not(tarpaulin), inline(always))]
331 pub const fn set_min_speech_duration(&mut self, d: Duration) -> &mut Self {
332 self.min_speech_duration = d;
333 self
334 }
335
336 #[cfg_attr(not(tarpaulin), inline(always))]
338 #[must_use]
339 pub const fn with_min_speech_duration(mut self, d: Duration) -> Self {
340 self.min_speech_duration = d;
341 self
342 }
343
344 #[cfg_attr(not(tarpaulin), inline(always))]
346 pub const fn max_speech_duration(&self) -> Option<Duration> {
347 self.max_speech_duration
348 }
349
350 #[cfg_attr(not(tarpaulin), inline(always))]
352 pub const fn set_max_speech_duration(&mut self, d: Duration) -> &mut Self {
353 self.max_speech_duration = Some(d);
354 self
355 }
356
357 #[cfg_attr(not(tarpaulin), inline(always))]
359 #[must_use]
360 pub const fn with_max_speech_duration(mut self, d: Duration) -> Self {
361 self.max_speech_duration = Some(d);
362 self
363 }
364
365 #[cfg_attr(not(tarpaulin), inline(always))]
367 #[must_use]
368 pub const fn clear_max_speech_duration(mut self) -> Self {
369 self.max_speech_duration = None;
370 self
371 }
372
373 #[cfg_attr(not(tarpaulin), inline(always))]
375 pub const fn min_silence_duration(&self) -> Duration {
376 self.min_silence_duration
377 }
378
379 #[cfg_attr(not(tarpaulin), inline(always))]
381 pub const fn set_min_silence_duration(&mut self, d: Duration) -> &mut Self {
382 self.min_silence_duration = d;
383 self
384 }
385
386 #[cfg_attr(not(tarpaulin), inline(always))]
388 #[must_use]
389 pub const fn with_min_silence_duration(mut self, d: Duration) -> Self {
390 self.min_silence_duration = d;
391 self
392 }
393
394 #[cfg_attr(not(tarpaulin), inline(always))]
396 pub const fn session_options(&self) -> &SessionOptions {
397 &self.session_options
398 }
399
400 #[cfg_attr(not(tarpaulin), inline(always))]
402 pub const fn set_session_options(&mut self, opts: SessionOptions) -> &mut Self {
403 self.session_options = opts;
404 self
405 }
406
407 #[cfg_attr(not(tarpaulin), inline(always))]
409 #[must_use]
410 pub const fn with_session_options(mut self, opts: SessionOptions) -> Self {
411 self.session_options = opts;
412 self
413 }
414
415 #[cfg_attr(not(tarpaulin), inline(always))]
418 pub(crate) const fn smooth_window_size_frames(&self) -> u32 {
419 self.smooth_window_size
420 }
421
422 #[cfg_attr(not(tarpaulin), inline(always))]
425 pub(crate) fn pad_start_frames(&self) -> u32 {
426 let raw = duration_to_frames(self.pad_start);
427 raw.max(self.smooth_window_size)
428 }
429
430 #[cfg_attr(not(tarpaulin), inline(always))]
432 pub(crate) fn min_speech_frames(&self) -> u32 {
433 duration_to_frames(self.min_speech_duration)
434 }
435
436 #[cfg_attr(not(tarpaulin), inline(always))]
438 pub(crate) fn min_silence_frames(&self) -> u32 {
439 duration_to_frames(self.min_silence_duration)
440 }
441
442 #[cfg_attr(not(tarpaulin), inline(always))]
444 pub(crate) fn max_speech_frames(&self) -> Option<u32> {
445 self.max_speech_duration.map(duration_to_frames)
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452
453 #[test]
454 fn session_options_default_optimizes_at_level_3() {
455 let opts = SessionOptions::default();
456 assert!(matches!(
457 opts.optimization_level(),
458 GraphOptimizationLevel::Level3
459 ));
460 }
461
462 #[test]
463 fn session_options_with_optimization_level_overrides() {
464 let opts = SessionOptions::new().with_optimization_level(GraphOptimizationLevel::Level1);
465 assert!(matches!(
466 opts.optimization_level(),
467 GraphOptimizationLevel::Level1
468 ));
469 }
470
471 #[test]
472 fn vad_options_default_match_upstream_firered_stream_vad_config() {
473 let opts = VadOptions::default();
474 assert_eq!(opts.smooth_window_size(), 5);
475 assert!((opts.speech_threshold() - 0.5).abs() < f32::EPSILON);
476 assert_eq!(opts.pad_start(), Duration::from_millis(50));
477 assert_eq!(opts.min_speech_duration(), Duration::from_millis(80));
478 assert_eq!(opts.max_speech_duration(), Some(Duration::from_secs(20)));
479 assert_eq!(opts.min_silence_duration(), Duration::from_millis(200));
480 }
481
482 #[test]
483 fn vad_options_speech_threshold_clamps_into_unit_interval() {
484 let mut opts = VadOptions::new();
485 opts.set_speech_threshold(2.5);
486 assert!((opts.speech_threshold() - 1.0).abs() < f32::EPSILON);
487 opts.set_speech_threshold(-0.3);
488 assert!((opts.speech_threshold() - 0.0).abs() < f32::EPSILON);
489 opts.set_speech_threshold(f32::NAN);
490 assert!((opts.speech_threshold() - 0.0).abs() < f32::EPSILON);
491 }
492
493 #[test]
494 fn vad_options_clear_max_speech_duration_disables_force_split() {
495 let opts = VadOptions::new()
496 .with_max_speech_duration(Duration::from_secs(5))
497 .clear_max_speech_duration();
498 assert_eq!(opts.max_speech_duration(), None);
499 assert_eq!(opts.max_speech_frames(), None);
500 }
501
502 #[test]
503 fn pad_start_frames_is_clamped_to_smooth_window_size() {
504 let opts = VadOptions::new()
505 .with_smooth_window_size(8)
506 .with_pad_start(Duration::from_millis(30)); assert_eq!(opts.pad_start_frames(), 8);
508 }
509
510 #[test]
511 fn duration_to_frames_truncates_partial_frames() {
512 assert_eq!(duration_to_frames(Duration::from_millis(15)), 1);
513 assert_eq!(duration_to_frames(Duration::from_millis(20)), 2);
514 assert_eq!(duration_to_frames(Duration::ZERO), 0);
515 assert_eq!(duration_to_frames(Duration::from_nanos(1)), 0);
517 assert_eq!(duration_to_frames(Duration::from_millis(9)), 0);
518 assert_eq!(duration_to_frames(Duration::from_millis(10)), 1);
520 assert_eq!(duration_to_frames(Duration::MAX), u32::MAX);
524 }
525
526 #[cfg(feature = "serde")]
527 #[test]
528 fn vad_options_round_trip_through_humantime_serde() {
529 let opts = VadOptions::new()
530 .with_min_silence_duration(Duration::from_millis(250))
531 .with_max_speech_duration(Duration::from_secs(15));
532 let serialized = serde_json::to_string(&opts).expect("serialize");
533 assert!(serialized.contains("250ms"));
534 assert!(serialized.contains("15s"));
535 let restored: VadOptions = serde_json::from_str(&serialized).expect("deserialize");
536 assert_eq!(restored.min_silence_duration(), opts.min_silence_duration());
537 assert_eq!(restored.max_speech_duration(), opts.max_speech_duration());
538 }
539}