1use crate::cleanup::{CleanupProviderKind, CleanupStyle, SegmentCleanupPolicy};
7use crate::error::TranscriptionError;
8use crate::providers::{BackendKind, Segment, TranscriptionResult};
9use serde::{Deserialize, Serialize};
10
11pub const STT_RESULT_SCHEMA_VERSION: u32 = 1;
13pub const TTS_META_SCHEMA_VERSION: u32 = 1;
15pub const ERROR_SCHEMA_VERSION: u32 = 1;
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
20pub struct SttResultDto {
21 pub schema_version: u32,
22 pub text: String,
23 pub language: Option<String>,
24 pub model: String,
25 pub provider: String,
26 pub duration_secs: f64,
27 pub backend_kind: BackendKind,
28 pub timestamps_reliable: bool,
29 pub cleanup_style: CleanupStyle,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub cleanup_provider: Option<CleanupProviderKind>,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub original_text: Option<String>,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub original_segments: Option<Vec<Segment>>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub cleanup_segment_policy: Option<SegmentCleanupPolicy>,
38 pub segments: Vec<Segment>,
39 #[serde(default, skip_serializing_if = "Vec::is_empty")]
41 pub normalization_warnings: Vec<String>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub request_id: Option<u64>,
44}
45
46impl SttResultDto {
47 pub fn from_result(result: &TranscriptionResult) -> Self {
48 Self::from_result_with_warnings(result, &[])
49 }
50
51 pub fn from_result_with_warnings(result: &TranscriptionResult, warnings: &[String]) -> Self {
52 Self {
53 schema_version: STT_RESULT_SCHEMA_VERSION,
54 text: result.text.clone(),
55 language: result.language.clone(),
56 model: result.model.clone(),
57 provider: result.provider.clone(),
58 duration_secs: finite_or_zero(result.duration_secs),
59 backend_kind: result.backend_kind,
60 timestamps_reliable: result.timestamps_reliable,
61 cleanup_style: result.cleanup_style,
62 cleanup_provider: result.cleanup_provider,
63 original_text: result.original_text.clone(),
64 original_segments: result.original_segments.clone(),
65 cleanup_segment_policy: result.cleanup_segment_policy,
66 segments: result
67 .segments
68 .iter()
69 .map(|s| {
70 Segment::from_parts_unchecked(
71 finite_or_zero(s.start()),
72 finite_or_zero(s.end()),
73 s.text().to_string(),
74 )
75 })
76 .collect(),
77 normalization_warnings: warnings.to_vec(),
78 request_id: None,
79 }
80 }
81
82 pub fn to_json_pretty(&self) -> Result<String, TranscriptionError> {
84 serde_json::to_string_pretty(self)
85 .map_err(|e| TranscriptionError::internal(format!("STT DTO serialize failed: {e}")))
86 }
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
91pub struct TtsMetaDto {
92 pub schema_version: u32,
93 pub sample_rate_hz: u32,
94 pub channels: u16,
95 pub backend_kind: String,
96 pub provider: String,
97 pub model: String,
98 pub voice: String,
99 pub language: String,
100 pub duration_ms: u64,
101 pub text_chars: usize,
102 pub text_truncated: bool,
103 pub chunk_count: usize,
104 pub synthesized_chars: usize,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub adapter: Option<String>,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub trust: Option<String>,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub provenance: Option<String>,
114}
115
116#[cfg(feature = "tts")]
117impl TtsMetaDto {
118 pub fn from_result(r: &crate::tts::SynthesisResult) -> Self {
119 Self {
120 schema_version: TTS_META_SCHEMA_VERSION,
121 sample_rate_hz: r.sample_rate_hz,
122 channels: r.channels,
123 backend_kind: match r.backend_kind {
124 crate::tts::BackendKind::Local => "local".into(),
125 },
126 provider: r.provider.clone(),
127 model: r.model.clone(),
128 voice: r.voice.clone(),
129 language: r.language.clone(),
130 duration_ms: r.duration_ms,
131 text_chars: r.text_chars,
132 text_truncated: r.text_truncated,
133 chunk_count: r.chunk_count,
134 synthesized_chars: r.synthesized_chars,
135 adapter: r.adapter.clone(),
136 trust: r.trust.clone(),
137 provenance: r.provenance.clone(),
138 }
139 }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
144pub struct ErrorDto {
145 pub schema_version: u32,
146 pub category: String,
147 pub retryable: bool,
148 pub message: String,
149 pub exit_code: i32,
150 #[serde(skip_serializing_if = "Option::is_none")]
151 pub operation: Option<String>,
152}
153
154impl ErrorDto {
155 pub fn from_error(err: &TranscriptionError) -> Self {
156 Self {
157 schema_version: ERROR_SCHEMA_VERSION,
158 category: err.error_category().as_str().into(),
159 retryable: err.retryable(),
160 message: err.to_string(),
161 exit_code: err.exit_code(),
162 operation: None,
163 }
164 }
165}
166
167fn finite_or_zero(v: f64) -> f64 {
168 if v.is_finite() {
169 v
170 } else {
171 0.0
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn stt_dto_roundtrip_fields() {
181 let r = TranscriptionResult::local(
182 "hi".into(),
183 vec![Segment::from_parts_unchecked(0.0, 1.0, "hi".to_string())],
184 Some("en".into()),
185 "base".into(),
186 1.0,
187 );
188 let dto = SttResultDto::from_result(&r);
189 let json = dto.to_json_pretty().unwrap();
190 assert!(json.contains("schema_version"));
191 assert!(!json.contains("NaN"));
192 let back: SttResultDto = serde_json::from_str(&json).unwrap();
193 assert_eq!(back.text, "hi");
194 assert_eq!(back.schema_version, 1);
195 }
196
197 #[test]
198 fn nan_duration_sanitized() {
199 let mut r = TranscriptionResult::local("x".into(), vec![], None, "m".into(), f64::NAN);
200 r.duration_secs = f64::NAN;
201 let dto = SttResultDto::from_result(&r);
202 assert_eq!(dto.duration_secs, 0.0);
203 }
204}