use voxora_traits::{AsrError, TranscribeOptions};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct MiniMaxParams {
pub language: Option<String>,
pub timestamp_level: TimestampLevel,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum TimestampLevel {
#[default]
Sentence,
Word,
}
impl TimestampLevel {
fn as_str(&self) -> &'static str {
match self {
Self::Sentence => "",
Self::Word => "word",
}
}
}
pub trait MiniMaxParamsApply {
fn language_header(&self) -> Option<&str>;
fn multipart_fields(&self) -> Vec<(&'static str, &str)>;
}
impl MiniMaxParamsApply for MiniMaxParams {
fn language_header(&self) -> Option<&str> {
self.language
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
}
fn multipart_fields(&self) -> Vec<(&'static str, &str)> {
vec![("timestamp_level", self.timestamp_level.as_str())]
}
}
pub fn apply(opts: &TranscribeOptions) -> Result<MiniMaxParams, AsrError> {
crate::language::validate_lang_bcp47(opts.language.as_deref())?;
if opts.translate {
return Err(AsrError::Unsupported(
"MiniMax does not support translation to English",
));
}
let timestamp_level = if opts.timestamps {
TimestampLevel::Word
} else {
TimestampLevel::Sentence
};
Ok(MiniMaxParams {
language: opts.language.clone(),
timestamp_level,
})
}
pub fn collect_result(
resp: crate::client::AsrResp,
opts: &TranscribeOptions,
) -> voxora_traits::TranscriptionResult {
use voxora_traits::{TranscriptionResult, TranscriptionSegment};
const ASSUMED_SAMPLE_RATE: u64 = 16_000;
let segments = resp
.segments
.into_iter()
.map(|seg| {
let start = (seg.start.max(0.0) * ASSUMED_SAMPLE_RATE as f64) as u64;
let end = (seg.end.max(0.0) * ASSUMED_SAMPLE_RATE as f64) as u64;
let text = match &seg.speaker {
Some(label) => format!("[{label}] {}", seg.text),
None => seg.text,
};
TranscriptionSegment::new(start, end, text)
})
.collect();
let language = opts.language.clone();
TranscriptionResult::with_segments(resp.text, language, segments)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::AsrResp;
#[test]
fn apply_defaults_to_auto_detect() {
let opts = TranscribeOptions::default();
let p = apply(&opts).expect("default opts should build");
assert!(p.language.is_none());
assert_eq!(p.timestamp_level, TimestampLevel::Sentence);
assert!(p.language_header().is_none());
}
#[test]
fn apply_with_language_keeps_it() {
let opts = TranscribeOptions::new(Some("en".into()), false, false);
let p = apply(&opts).expect("en is valid");
assert_eq!(p.language.as_deref(), Some("en"));
assert_eq!(p.language_header(), Some("en"));
}
#[test]
fn apply_rejects_unknown_language() {
let opts = TranscribeOptions::new(Some("klingon".into()), false, false);
let err = apply(&opts).expect_err("klingon should be rejected");
match err {
AsrError::InvalidInput(msg) => {
assert!(msg.contains("klingon"), "{msg}");
}
other => panic!("expected InvalidInput, got {other:?}"),
}
}
#[test]
fn apply_rejects_translate() {
let opts = TranscribeOptions::new(Some("en".into()), true, false);
let err = apply(&opts).expect_err("translate should be unsupported");
assert!(matches!(err, AsrError::Unsupported(_)));
}
#[test]
fn apply_with_timestamps_picks_word_level() {
let opts = TranscribeOptions::new(Some("en".into()), false, true);
let p = apply(&opts).expect("en is valid");
assert_eq!(p.timestamp_level, TimestampLevel::Word);
let fields = p.multipart_fields();
assert!(
fields
.iter()
.any(|(k, v)| *k == "timestamp_level" && *v == "word")
);
}
#[test]
fn apply_without_timestamps_picks_sentence_level() {
let opts = TranscribeOptions::new(Some("en".into()), false, false);
let p = apply(&opts).expect("en is valid");
assert_eq!(p.timestamp_level, TimestampLevel::Sentence);
let fields = p.multipart_fields();
assert!(
fields
.iter()
.any(|(k, v)| *k == "timestamp_level" && v.is_empty())
);
}
#[test]
fn language_header_trims_whitespace() {
let p = MiniMaxParams {
language: Some(" ".to_string()),
timestamp_level: TimestampLevel::Sentence,
};
assert!(
p.language_header().is_none(),
"whitespace must collapse to None"
);
}
#[test]
fn collect_result_folds_speaker_into_text() {
let resp = AsrResp {
text: "Hello".into(),
duration: Some(1.0),
n_speakers: Some(1),
segments: vec![crate::client::AsrSegment {
id: 0,
start: 0.0,
end: 1.0,
speaker: Some("S1".into()),
text: "Hello".into(),
}],
trace_id: None,
};
let opts = TranscribeOptions::default();
let result = collect_result(resp, &opts);
assert_eq!(result.text, "Hello");
assert_eq!(result.segments.len(), 1);
assert_eq!(result.segments[0].text, "[S1] Hello");
}
#[test]
fn collect_result_converts_seconds_to_samples_at_16khz() {
let resp = AsrResp {
text: "x".into(),
duration: Some(1.0),
n_speakers: None,
segments: vec![crate::client::AsrSegment {
id: 0,
start: 0.5,
end: 1.5,
speaker: None,
text: "x".into(),
}],
trace_id: None,
};
let opts = TranscribeOptions::default();
let result = collect_result(resp, &opts);
assert_eq!(result.segments[0].start_sample, 8000); assert_eq!(result.segments[0].end_sample, 24000); }
#[test]
fn collect_result_echoes_caller_language() {
let resp = AsrResp {
text: "Hello".into(),
duration: None,
n_speakers: None,
segments: vec![],
trace_id: None,
};
let opts = TranscribeOptions::new(Some("en".into()), false, false);
let result = collect_result(resp, &opts);
assert_eq!(result.language.as_deref(), Some("en"));
}
}