aurum_core/tts/provider.rs
1//! TTS provider trait and shared result types.
2
3use crate::error::Result;
4use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6
7// Deserialize remains for BackendKind / options enums only; SynthesisResult is
8// Serialize-only (JOE-1614).
9
10/// Default sample rate produced by the KittenTTS nano engine.
11pub const DEFAULT_SAMPLE_RATE_HZ: u32 = 24_000;
12
13/// Backend classification for honesty JSON.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum BackendKind {
17 /// On-device ONNX inference.
18 Local,
19}
20
21/// Options controlling a single synthesis request.
22#[derive(Debug, Clone)]
23pub struct SynthesisOptions {
24 pub model: String,
25 pub voice: String,
26 pub language: String,
27 /// Optional requested sample rate. Must equal the adapter native rate when set;
28 /// non-native values are rejected (no metadata-only relabeling, no resampler).
29 pub sample_rate_hz: Option<u32>,
30 /// Speaking rate multiplier (must be finite and within engine range).
31 pub speaking_rate: f32,
32 /// Wall-clock timeout in milliseconds (enforced by the local provider).
33 pub timeout_ms: u64,
34 /// Optional cooperative cancel flag.
35 pub cancel: Option<crate::cancel::CancelFlag>,
36 /// When true, never hit the network for missing voice packs.
37 pub local_only: bool,
38 /// Optional local model-pack directory (JOE-1619). When set, loads artifacts
39 /// from the pack manifest instead of the built-in catalogue cache. Requires a
40 /// known adapter; bare ONNX paths are rejected by pack load.
41 pub pack_dir: Option<std::path::PathBuf>,
42 /// Allow `local_unverified` trust for [`Self::pack_dir`] (explicit opt-in).
43 pub allow_unverified: bool,
44}
45
46impl Default for SynthesisOptions {
47 fn default() -> Self {
48 Self {
49 model: crate::tts::DEFAULT_TTS_MODEL.to_string(),
50 voice: crate::tts::DEFAULT_TTS_VOICE.to_string(),
51 language: "en".to_string(),
52 sample_rate_hz: None,
53 speaking_rate: 1.0,
54 timeout_ms: crate::tts::DEFAULT_TIMEOUT_MS,
55 cancel: None,
56 local_only: false,
57 pack_dir: None,
58 allow_unverified: false,
59 }
60 }
61}
62
63/// Normalized result returned by every TTS provider.
64///
65/// **Not** deserializable as a whole: PCM must not be reconstructed from JSON
66/// metadata (JOE-1614). Use [`crate::dto::TtsMetaDto`] for external contracts.
67#[derive(Debug, Clone, Serialize)]
68pub struct SynthesisResult {
69 /// Mono PCM samples (signed 16-bit).
70 #[serde(skip)]
71 pub pcm_i16_mono: Vec<i16>,
72 /// Actual sample rate of [`Self::pcm_i16_mono`] (always the adapter native rate).
73 pub sample_rate_hz: u32,
74 /// Always 1 for MVP.
75 pub channels: u16,
76 pub backend_kind: BackendKind,
77 pub provider: String,
78 /// Canonical model id actually used.
79 pub model: String,
80 /// Canonical voice id actually used.
81 pub voice: String,
82 pub language: String,
83 /// Duration derived from final PCM length and actual sample rate.
84 pub duration_ms: u64,
85 /// Character count of the synthesized (complete) text.
86 pub text_chars: usize,
87 /// Always `false` under complete-or-error policy.
88 pub text_truncated: bool,
89 /// Number of model-safe chunks synthesized and concatenated.
90 pub chunk_count: usize,
91 /// Characters of source text actually synthesized (equals `text_chars`).
92 pub synthesized_chars: usize,
93 /// Adapter id that executed synthesis (JOE-1576).
94 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub adapter: Option<String>,
96 /// Trust mode: builtin | verified | local_unverified.
97 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub trust: Option<String>,
99 /// Provenance: builtin | custom | local_pack.
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub provenance: Option<String>,
102}
103
104/// Provider trait for local (and future) TTS backends.
105#[async_trait]
106pub trait SynthesisProvider: Send + Sync {
107 fn name(&self) -> &'static str;
108
109 async fn synthesize(&self, text: &str, opts: &SynthesisOptions) -> Result<SynthesisResult>;
110
111 async fn preload(&self, model: &str, voice: &str) -> Result<()>;
112}