native_whisperx/translation/
result.rs1use audio_analysis_transcription::TranscriptionPipelineResponse;
4use serde::Serialize;
5use std::{path::PathBuf, time::Instant};
6
7use super::{CuratedLanguage, TranslationLeg, TranslationPlan, TranslationPlanProvenance};
8use crate::workflow::{
9 CancellationHandle, FiniteCancellation, NoopTranscriptionProgressObserver,
10 TranscriptionProgressEvent, TranscriptionProgressObserver, TranscriptionProgressTask,
11};
12
13pub trait SegmentTranslationProvider {
19 fn provider_id(&self) -> &str {
21 "segment-translation-provider"
22 }
23
24 fn translate_segment(
26 &mut self,
27 leg: &TranslationLeg,
28 text: &str,
29 ) -> Result<String, TranslationModelError>;
30}
31
32#[derive(Debug)]
34pub enum TranslatedTranscriptionOutcome {
35 Completed(TranslatedTranscriptionResult),
37 Cancelled(FiniteCancellation),
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
43#[error("{message}")]
44pub struct TranslationModelError {
45 message: String,
46}
47
48impl TranslationModelError {
49 pub fn new(message: impl Into<String>) -> Self {
51 Self {
52 message: message.into(),
53 }
54 }
55
56 pub fn message(&self) -> &str {
58 &self.message
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Serialize)]
67#[serde(rename_all = "camelCase")]
68pub struct TranslatedTranscriptionResult {
69 transcript: text_transcripts::TranscriptionContract,
70 source_language: CuratedLanguage,
71 target_language: CuratedLanguage,
72 provenance: TranslationPlanProvenance,
73 legs: Vec<TranslationLeg>,
74}
75
76impl TranslatedTranscriptionResult {
77 pub fn transcript(&self) -> &text_transcripts::TranscriptionContract {
79 &self.transcript
80 }
81
82 pub fn into_transcript(self) -> text_transcripts::TranscriptionContract {
84 self.transcript
85 }
86
87 pub const fn source_language(&self) -> CuratedLanguage {
89 self.source_language
90 }
91
92 pub const fn target_language(&self) -> CuratedLanguage {
94 self.target_language
95 }
96
97 pub const fn provenance(&self) -> TranslationPlanProvenance {
99 self.provenance
100 }
101
102 pub fn legs(&self) -> &[TranslationLeg] {
104 &self.legs
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
110pub enum TranslationError {
111 #[error(
113 "translation leg {leg_index} ({leg_source}->{leg_target}, model `{model_id}`) failed for segment {segment_index}: {source}"
114 )]
115 LegFailed {
116 leg_index: usize,
118 segment_index: u64,
120 leg_source: CuratedLanguage,
122 leg_target: CuratedLanguage,
124 model_id: String,
126 #[source]
128 source: TranslationModelError,
129 },
130}
131
132pub fn translate_transcription(
139 source: &TranscriptionPipelineResponse,
140 plan: &TranslationPlan,
141 provider: &mut dyn SegmentTranslationProvider,
142) -> Result<TranslatedTranscriptionResult, TranslationError> {
143 let mut observer = NoopTranscriptionProgressObserver;
144 let cancellation = CancellationHandle::new();
145 match translate_transcription_with_control(
146 source,
147 plan,
148 provider,
149 0,
150 PathBuf::from("<translation>"),
151 &mut observer,
152 &cancellation,
153 )? {
154 TranslatedTranscriptionOutcome::Completed(result) => Ok(result),
155 TranslatedTranscriptionOutcome::Cancelled(_) => {
156 unreachable!("the compatibility translation entry point uses an uncancelled handle")
157 }
158 }
159}
160
161pub fn translate_transcription_with_control(
163 source: &TranscriptionPipelineResponse,
164 plan: &TranslationPlan,
165 provider: &mut dyn SegmentTranslationProvider,
166 file_index: usize,
167 input: PathBuf,
168 observer: &mut dyn TranscriptionProgressObserver,
169 cancellation: &CancellationHandle,
170) -> Result<TranslatedTranscriptionOutcome, TranslationError> {
171 let started = Instant::now();
172 if cancellation.is_cancelled() {
173 return Ok(cancelled_translation(file_index, input, observer, started));
174 }
175
176 let mut transcript = source.transcript.clone();
177 observer.observe(TranscriptionProgressEvent::TaskStart {
178 file_index,
179 task: TranscriptionProgressTask::Translation,
180 });
181
182 for (leg_index, leg) in plan.legs().iter().enumerate() {
183 if cancellation.is_cancelled() {
184 return Ok(cancelled_translation(file_index, input, observer, started));
185 }
186 let leg_started = Instant::now();
187 let provider_id = provider.provider_id().to_string();
188 observer.observe(TranscriptionProgressEvent::TranslationLegStart {
189 file_index,
190 leg_index,
191 total_legs: plan.legs().len(),
192 provenance: plan.provenance(),
193 source: leg.source(),
194 target: leg.target(),
195 provider: provider_id.clone(),
196 model_id: leg.model_id().to_string(),
197 });
198 if cancellation.is_cancelled() {
199 return Ok(cancelled_translation(file_index, input, observer, started));
200 }
201 for segment in &mut transcript.segments {
202 if cancellation.is_cancelled() {
203 return Ok(cancelled_translation(file_index, input, observer, started));
204 }
205 let source_text = segment.text.trim();
206 if source_text.is_empty() {
207 continue;
208 }
209 segment.text = match provider.translate_segment(leg, source_text) {
210 Ok(translated) => translated,
211 Err(source) => {
212 let error = TranslationError::LegFailed {
213 leg_index,
214 segment_index: segment.index,
215 leg_source: leg.source(),
216 leg_target: leg.target(),
217 model_id: leg.model_id().to_string(),
218 source,
219 };
220 observer.observe(TranscriptionProgressEvent::Failure {
221 file_index,
222 input: input.clone(),
223 task: Some(TranscriptionProgressTask::Translation),
224 duration_seconds: started.elapsed().as_secs_f64(),
225 message: error.to_string(),
226 });
227 return Err(error);
228 }
229 };
230 }
231 if cancellation.is_cancelled() {
232 return Ok(cancelled_translation(file_index, input, observer, started));
233 }
234 observer.observe(TranscriptionProgressEvent::TranslationLegEnd {
235 file_index,
236 leg_index,
237 total_legs: plan.legs().len(),
238 provenance: plan.provenance(),
239 source: leg.source(),
240 target: leg.target(),
241 provider: provider_id,
242 model_id: leg.model_id().to_string(),
243 duration_seconds: leg_started.elapsed().as_secs_f64(),
244 });
245 if cancellation.is_cancelled() {
246 return Ok(cancelled_translation(file_index, input, observer, started));
247 }
248 }
249
250 let target_language = plan.target().code().to_string();
251 for segment in &mut transcript.segments {
252 segment.language = Some(target_language.clone());
253 segment.words.clear();
254 segment.chars.clear();
255 }
256 transcript.language = Some(target_language);
257 transcript.text = Some(transcript.joined_text());
258
259 let result = TranslatedTranscriptionResult {
260 transcript,
261 source_language: plan.source(),
262 target_language: plan.target(),
263 provenance: plan.provenance(),
264 legs: plan.legs().to_vec(),
265 };
266 observer.observe(TranscriptionProgressEvent::TaskEnd {
267 file_index,
268 task: TranscriptionProgressTask::Translation,
269 duration_seconds: started.elapsed().as_secs_f64(),
270 });
271 Ok(TranslatedTranscriptionOutcome::Completed(result))
272}
273
274fn cancelled_translation(
275 file_index: usize,
276 input: PathBuf,
277 observer: &mut dyn TranscriptionProgressObserver,
278 started: Instant,
279) -> TranslatedTranscriptionOutcome {
280 let cancellation = FiniteCancellation::new(
281 file_index,
282 input.clone(),
283 Some(TranscriptionProgressTask::Translation),
284 );
285 observer.observe(TranscriptionProgressEvent::Cancelled {
286 file_index,
287 input,
288 task: Some(TranscriptionProgressTask::Translation),
289 duration_seconds: started.elapsed().as_secs_f64(),
290 });
291 TranslatedTranscriptionOutcome::Cancelled(cancellation)
292}