use std::str::FromStr;
use serde::{Deserialize, Serialize};
use super::options::Language;
use crate::error::{Error, Result};
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Word {
#[serde(default)]
word: String,
#[serde(default)]
start: f32,
#[serde(default)]
end: f32,
#[serde(default)]
probability: f32,
}
impl Word {
#[inline]
#[must_use]
pub fn word(&self) -> &str {
&self.word
}
#[inline]
#[must_use]
pub const fn start(&self) -> f32 {
self.start
}
#[inline]
#[must_use]
pub const fn end(&self) -> f32 {
self.end
}
#[inline]
#[must_use]
pub const fn probability(&self) -> f32 {
self.probability
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Transcript {
#[serde(default)]
text: String,
#[serde(default)]
language: String,
#[serde(default)]
words: Vec<Word>,
#[serde(default)]
ttft_ms: f32,
#[serde(default)]
decode_tps: f32,
}
impl Transcript {
pub(crate) fn from_parts(
text: String,
language: String,
words: Vec<Word>,
ttft_ms: f32,
decode_tps: f32,
) -> Self {
Transcript {
text,
language,
words,
ttft_ms,
decode_tps,
}
}
#[inline]
#[must_use]
pub fn text(&self) -> &str {
&self.text
}
#[must_use]
pub fn language(&self) -> Option<Language> {
self.language.parse().ok()
}
#[inline]
#[must_use]
pub fn language_code(&self) -> &str {
&self.language
}
#[inline]
#[must_use]
pub fn words(&self) -> &[Word] {
&self.words
}
#[inline]
#[must_use]
pub const fn ttft_ms(&self) -> f32 {
self.ttft_ms
}
#[inline]
#[must_use]
pub const fn decode_tps(&self) -> f32 {
self.decode_tps
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.text.trim().is_empty()
}
}
impl FromStr for Transcript {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
serde_json::from_str(s).map_err(|error| Error::envelope(s, error))
}
}
#[cfg(test)]
mod tests {
use super::*;
const SILENCE: &str = r#"{"text":"","language":"","words":[],"ttft_ms":0.0,"decode_tps":0.0}"#;
const LIGHTS: &str = concat!(
r#"{"text":"Turn off the kitchen lights.","language":"en","words":["#,
r#"{"word":"Turn","start":0.24,"end":0.56,"probability":0.887},"#,
r#"{"word":"off","start":0.56,"end":0.96,"probability":0.985},"#,
r#"{"word":"the","start":0.96,"end":1.36,"probability":0.993},"#,
r#"{"word":"kitchen","start":1.36,"end":1.84,"probability":0.928},"#,
r#"{"word":"lights.","start":1.84,"end":2.56,"probability":0.993}],"#,
r#""ttft_ms":53.6,"decode_tps":203.9}"#,
);
#[test]
fn silence_parses_to_an_empty_transcript() {
let transcript: Transcript = SILENCE.parse().expect("the silence envelope parses");
assert!(transcript.is_empty());
assert_eq!(transcript.text(), "");
assert_eq!(transcript.language(), None);
assert_eq!(transcript.language_code(), "");
assert!(transcript.words().is_empty());
assert_eq!(transcript.ttft_ms(), 0.0);
assert_eq!(transcript.decode_tps(), 0.0);
}
#[test]
fn a_timestamped_envelope_parses() {
let transcript: Transcript = LIGHTS.parse().expect("the sample envelope parses");
assert_eq!(transcript.text(), "Turn off the kitchen lights.");
assert_eq!(transcript.language(), Some(Language::En));
assert_eq!(transcript.ttft_ms(), 53.6);
assert_eq!(transcript.decode_tps(), 203.9);
let words = transcript.words();
assert_eq!(words.len(), 5);
assert_eq!(words[3].word(), "kitchen");
assert_eq!(words[3].start(), 1.36);
assert_eq!(words[3].end(), 1.84);
assert_eq!(words[3].probability(), 0.928);
let joined: Vec<&str> = words.iter().map(Word::word).collect();
assert_eq!(joined.join(" "), transcript.text());
assert!(
words
.windows(2)
.all(|pair| pair[0].end() <= pair[1].start())
);
}
#[test]
fn an_unknown_language_keeps_its_code() {
let transcript: Transcript = r#"{"text":"Hej.","language":"sv"}"#
.parse()
.expect("an unknown code is not a parse failure");
assert_eq!(transcript.language(), None);
assert_eq!(transcript.language_code(), "sv");
}
#[test]
fn a_missing_field_is_not_a_parse_failure() {
let transcript: Transcript = "{}".parse().expect("defaults fill in");
assert_eq!(transcript, Transcript::default());
assert!(transcript.is_empty());
}
#[test]
fn junk_is_reported_with_its_text() {
let error = "not json"
.parse::<Transcript>()
.expect_err("junk is not a transcript");
let Error::Envelope { text, .. } = &error else {
panic!("expected Error::Envelope");
};
assert_eq!(text, "not json");
}
#[test]
fn a_transcript_survives_serde() {
let transcript: Transcript = LIGHTS.parse().expect("the sample envelope parses");
let wire = serde_json::to_string(&transcript).expect("serialises");
let back: Transcript = wire.parse().expect("parses back");
assert_eq!(back, transcript);
}
}