piper-phoneme-streaming 0.1.0

A high-performance Rust library for streaming Text-to-Phoneme (G2P) conversion.
Documentation
use crate::text_expand::{ExpandResult, ExpandTask, ExpandUnit};
use std::collections::VecDeque;

use super::{number_to_words_en, ones_en};

pub struct EnDecimalExpandTask;

impl ExpandTask for EnDecimalExpandTask {
    fn expand(&self, queue: &VecDeque<ExpandUnit>) -> Option<ExpandResult> {
        if queue.len() < 3 {
            if let Some(ExpandUnit::Number(_)) = queue.front() {
                if queue.len() == 1 {
                    return Some(ExpandResult::Maybe);
                }
                if let Some(ExpandUnit::Mark('.')) = queue.get(1) {
                    return Some(ExpandResult::Maybe);
                }
            }
            return None;
        }

        match (queue.front(), queue.get(1), queue.get(2)) {
            (
                Some(ExpandUnit::Number(int_part)),
                Some(ExpandUnit::Mark('.')),
                Some(ExpandUnit::Number(frac_part)),
            ) => {
                let int_val = int_part.parse::<u64>().ok()?;
                // Expand the integer part using number_to_words_en
                let mut units: Vec<ExpandUnit> = number_to_words_en(int_val)
                    .into_iter()
                    .map(|w| ExpandUnit::Word(w.into()))
                    .collect();
                units.push(ExpandUnit::Word("point".into()));
                // Expand the fractional part digit by digit
                for ch in frac_part.chars() {
                    let digit = ch.to_digit(10)? as u64;
                    let word = ones_en(digit);
                    if !word.is_empty() {
                        units.push(ExpandUnit::Word(word.into()));
                    } else {
                        // digit == 0
                        units.push(ExpandUnit::Word("zero".into()));
                    }
                }
                Some(ExpandResult::Replace(3, units))
            }
            _ => None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_en_decimal_expansion() {
        let task = EnDecimalExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Number("3".into()));
        queue.push_back(ExpandUnit::Mark('.'));
        queue.push_back(ExpandUnit::Number("14".into()));

        let res = task.expand(&queue).unwrap();
        if let ExpandResult::Replace(n, units) = res {
            assert_eq!(n, 3);
            assert_eq!(
                units,
                vec![
                    ExpandUnit::Word("three".into()),
                    ExpandUnit::Word("point".into()),
                    ExpandUnit::Word("one".into()),
                    ExpandUnit::Word("four".into()),
                ]
            );
        } else {
            panic!("Expected Replace result");
        }
    }

    #[test]
    fn test_en_decimal_zero_fractional() {
        // "1.0" — fractional digit is zero, should expand to "one point zero"
        let task = EnDecimalExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Number("1".into()));
        queue.push_back(ExpandUnit::Mark('.'));
        queue.push_back(ExpandUnit::Number("0".into()));

        let res = task.expand(&queue).unwrap();
        if let ExpandResult::Replace(n, units) = res {
            assert_eq!(n, 3);
            assert_eq!(
                units,
                vec![
                    ExpandUnit::Word("one".into()),
                    ExpandUnit::Word("point".into()),
                    ExpandUnit::Word("zero".into()),
                ]
            );
        } else {
            panic!("Expected Replace result for 1.0");
        }
    }

    #[test]
    fn test_en_decimal_maybe_single_number() {
        let task = EnDecimalExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Number("3".into()));

        let res = task.expand(&queue);
        assert!(
            matches!(res, Some(ExpandResult::Maybe)),
            "Single number should return Maybe"
        );
    }

    #[test]
    fn test_en_decimal_maybe_number_dot() {
        let task = EnDecimalExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Number("3".into()));
        queue.push_back(ExpandUnit::Mark('.'));

        let res = task.expand(&queue);
        assert!(
            matches!(res, Some(ExpandResult::Maybe)),
            "Number + '.' should return Maybe"
        );
    }

    #[test]
    fn test_en_decimal_non_decimal_returns_none() {
        let task = EnDecimalExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Word("hello".into()));

        let res = task.expand(&queue);
        assert!(res.is_none(), "Non-decimal input should return None");
    }
}