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| Segment {
70 start: finite_or_zero(s.start),
71 end: finite_or_zero(s.end),
72 text: s.text.clone(),
73 })
74 .collect(),
75 normalization_warnings: warnings.to_vec(),
76 request_id: None,
77 }
78 }
79
80 pub fn to_json_pretty(&self) -> Result<String, TranscriptionError> {
82 serde_json::to_string_pretty(self)
83 .map_err(|e| TranscriptionError::internal(format!("STT DTO serialize failed: {e}")))
84 }
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
89pub struct TtsMetaDto {
90 pub schema_version: u32,
91 pub sample_rate_hz: u32,
92 pub channels: u16,
93 pub backend_kind: String,
94 pub provider: String,
95 pub model: String,
96 pub voice: String,
97 pub language: String,
98 pub duration_ms: u64,
99 pub text_chars: usize,
100 pub text_truncated: bool,
101 pub chunk_count: usize,
102 pub synthesized_chars: usize,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub adapter: Option<String>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub trust: Option<String>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub provenance: Option<String>,
112}
113
114#[cfg(feature = "tts")]
115impl TtsMetaDto {
116 pub fn from_result(r: &crate::tts::SynthesisResult) -> Self {
117 Self {
118 schema_version: TTS_META_SCHEMA_VERSION,
119 sample_rate_hz: r.sample_rate_hz,
120 channels: r.channels,
121 backend_kind: match r.backend_kind {
122 crate::tts::BackendKind::Local => "local".into(),
123 },
124 provider: r.provider.clone(),
125 model: r.model.clone(),
126 voice: r.voice.clone(),
127 language: r.language.clone(),
128 duration_ms: r.duration_ms,
129 text_chars: r.text_chars,
130 text_truncated: r.text_truncated,
131 chunk_count: r.chunk_count,
132 synthesized_chars: r.synthesized_chars,
133 adapter: r.adapter.clone(),
134 trust: r.trust.clone(),
135 provenance: r.provenance.clone(),
136 }
137 }
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
142pub struct ErrorDto {
143 pub schema_version: u32,
144 pub category: String,
145 pub retryable: bool,
146 pub message: String,
147 pub exit_code: i32,
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub operation: Option<String>,
150}
151
152impl ErrorDto {
153 pub fn from_error(err: &TranscriptionError) -> Self {
154 Self {
155 schema_version: ERROR_SCHEMA_VERSION,
156 category: err.error_category().as_str().into(),
157 retryable: err.retryable(),
158 message: err.to_string(),
159 exit_code: err.exit_code(),
160 operation: None,
161 }
162 }
163}
164
165fn finite_or_zero(v: f64) -> f64 {
166 if v.is_finite() {
167 v
168 } else {
169 0.0
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn stt_dto_roundtrip_fields() {
179 let r = TranscriptionResult::local(
180 "hi".into(),
181 vec![Segment {
182 start: 0.0,
183 end: 1.0,
184 text: "hi".into(),
185 }],
186 Some("en".into()),
187 "base".into(),
188 1.0,
189 );
190 let dto = SttResultDto::from_result(&r);
191 let json = dto.to_json_pretty().unwrap();
192 assert!(json.contains("schema_version"));
193 assert!(!json.contains("NaN"));
194 let back: SttResultDto = serde_json::from_str(&json).unwrap();
195 assert_eq!(back.text, "hi");
196 assert_eq!(back.schema_version, 1);
197 }
198
199 #[test]
200 fn nan_duration_sanitized() {
201 let mut r = TranscriptionResult::local("x".into(), vec![], None, "m".into(), f64::NAN);
202 r.duration_secs = f64::NAN;
203 let dto = SttResultDto::from_result(&r);
204 assert_eq!(dto.duration_secs, 0.0);
205 }
206}