use audio_analysis_transcription::TranscriptionPipelineResponse;
use serde::Serialize;
use std::{path::PathBuf, time::Instant};
use super::{CuratedLanguage, TranslationLeg, TranslationPlan, TranslationPlanProvenance};
use crate::workflow::{
CancellationHandle, FiniteCancellation, NoopTranscriptionProgressObserver,
TranscriptionProgressEvent, TranscriptionProgressObserver, TranscriptionProgressTask,
};
pub trait SegmentTranslationProvider {
fn provider_id(&self) -> &str {
"segment-translation-provider"
}
fn translate_segment(
&mut self,
leg: &TranslationLeg,
text: &str,
) -> Result<String, TranslationModelError>;
}
#[derive(Debug)]
pub enum TranslatedTranscriptionOutcome {
Completed(TranslatedTranscriptionResult),
Cancelled(FiniteCancellation),
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("{message}")]
pub struct TranslationModelError {
message: String,
}
impl TranslationModelError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
pub fn message(&self) -> &str {
&self.message
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TranslatedTranscriptionResult {
transcript: text_transcripts::TranscriptionContract,
source_language: CuratedLanguage,
target_language: CuratedLanguage,
provenance: TranslationPlanProvenance,
legs: Vec<TranslationLeg>,
}
impl TranslatedTranscriptionResult {
pub fn transcript(&self) -> &text_transcripts::TranscriptionContract {
&self.transcript
}
pub fn into_transcript(self) -> text_transcripts::TranscriptionContract {
self.transcript
}
pub const fn source_language(&self) -> CuratedLanguage {
self.source_language
}
pub const fn target_language(&self) -> CuratedLanguage {
self.target_language
}
pub const fn provenance(&self) -> TranslationPlanProvenance {
self.provenance
}
pub fn legs(&self) -> &[TranslationLeg] {
&self.legs
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum TranslationError {
#[error(
"translation leg {leg_index} ({leg_source}->{leg_target}, model `{model_id}`) failed for segment {segment_index}: {source}"
)]
LegFailed {
leg_index: usize,
segment_index: u64,
leg_source: CuratedLanguage,
leg_target: CuratedLanguage,
model_id: String,
#[source]
source: TranslationModelError,
},
}
pub fn translate_transcription(
source: &TranscriptionPipelineResponse,
plan: &TranslationPlan,
provider: &mut dyn SegmentTranslationProvider,
) -> Result<TranslatedTranscriptionResult, TranslationError> {
let mut observer = NoopTranscriptionProgressObserver;
let cancellation = CancellationHandle::new();
match translate_transcription_with_control(
source,
plan,
provider,
0,
PathBuf::from("<translation>"),
&mut observer,
&cancellation,
)? {
TranslatedTranscriptionOutcome::Completed(result) => Ok(result),
TranslatedTranscriptionOutcome::Cancelled(_) => {
unreachable!("the compatibility translation entry point uses an uncancelled handle")
}
}
}
pub fn translate_transcription_with_control(
source: &TranscriptionPipelineResponse,
plan: &TranslationPlan,
provider: &mut dyn SegmentTranslationProvider,
file_index: usize,
input: PathBuf,
observer: &mut dyn TranscriptionProgressObserver,
cancellation: &CancellationHandle,
) -> Result<TranslatedTranscriptionOutcome, TranslationError> {
let started = Instant::now();
if cancellation.is_cancelled() {
return Ok(cancelled_translation(file_index, input, observer, started));
}
let mut transcript = source.transcript.clone();
observer.observe(TranscriptionProgressEvent::TaskStart {
file_index,
task: TranscriptionProgressTask::Translation,
});
for (leg_index, leg) in plan.legs().iter().enumerate() {
if cancellation.is_cancelled() {
return Ok(cancelled_translation(file_index, input, observer, started));
}
let leg_started = Instant::now();
let provider_id = provider.provider_id().to_string();
observer.observe(TranscriptionProgressEvent::TranslationLegStart {
file_index,
leg_index,
total_legs: plan.legs().len(),
provenance: plan.provenance(),
source: leg.source(),
target: leg.target(),
provider: provider_id.clone(),
model_id: leg.model_id().to_string(),
});
if cancellation.is_cancelled() {
return Ok(cancelled_translation(file_index, input, observer, started));
}
for segment in &mut transcript.segments {
if cancellation.is_cancelled() {
return Ok(cancelled_translation(file_index, input, observer, started));
}
let source_text = segment.text.trim();
if source_text.is_empty() {
continue;
}
segment.text = match provider.translate_segment(leg, source_text) {
Ok(translated) => translated,
Err(source) => {
let error = TranslationError::LegFailed {
leg_index,
segment_index: segment.index,
leg_source: leg.source(),
leg_target: leg.target(),
model_id: leg.model_id().to_string(),
source,
};
observer.observe(TranscriptionProgressEvent::Failure {
file_index,
input: input.clone(),
task: Some(TranscriptionProgressTask::Translation),
duration_seconds: started.elapsed().as_secs_f64(),
message: error.to_string(),
});
return Err(error);
}
};
}
if cancellation.is_cancelled() {
return Ok(cancelled_translation(file_index, input, observer, started));
}
observer.observe(TranscriptionProgressEvent::TranslationLegEnd {
file_index,
leg_index,
total_legs: plan.legs().len(),
provenance: plan.provenance(),
source: leg.source(),
target: leg.target(),
provider: provider_id,
model_id: leg.model_id().to_string(),
duration_seconds: leg_started.elapsed().as_secs_f64(),
});
if cancellation.is_cancelled() {
return Ok(cancelled_translation(file_index, input, observer, started));
}
}
let target_language = plan.target().code().to_string();
for segment in &mut transcript.segments {
segment.language = Some(target_language.clone());
segment.words.clear();
segment.chars.clear();
}
transcript.language = Some(target_language);
transcript.text = Some(transcript.joined_text());
let result = TranslatedTranscriptionResult {
transcript,
source_language: plan.source(),
target_language: plan.target(),
provenance: plan.provenance(),
legs: plan.legs().to_vec(),
};
observer.observe(TranscriptionProgressEvent::TaskEnd {
file_index,
task: TranscriptionProgressTask::Translation,
duration_seconds: started.elapsed().as_secs_f64(),
});
Ok(TranslatedTranscriptionOutcome::Completed(result))
}
fn cancelled_translation(
file_index: usize,
input: PathBuf,
observer: &mut dyn TranscriptionProgressObserver,
started: Instant,
) -> TranslatedTranscriptionOutcome {
let cancellation = FiniteCancellation::new(
file_index,
input.clone(),
Some(TranscriptionProgressTask::Translation),
);
observer.observe(TranscriptionProgressEvent::Cancelled {
file_index,
input,
task: Some(TranscriptionProgressTask::Translation),
duration_seconds: started.elapsed().as_secs_f64(),
});
TranslatedTranscriptionOutcome::Cancelled(cancellation)
}