piper-phoneme-streaming 0.1.1

A high-performance Rust library for streaming Text-to-Phoneme (G2P) conversion.
Documentation
use crate::G2pToken;
use crate::error::Result;
use crate::phoneme::PhonemeData;
use crate::semantic::{Language, SentenceUnit};
use std::sync::Arc;

use super::{Entry, Renderer, load_phoneme_data, promote_clauses, stable_cut_index};

pub struct StreamingSentencePhonemeUpgrade {
    language: Language,
    phdata: Arc<PhonemeData>,
}

impl StreamingSentencePhonemeUpgrade {
    pub fn new(language: Language) -> Result<Self> {
        let phdata = load_phoneme_data(language)?;

        Ok(Self { language, phdata })
    }

    pub fn new_session(&self) -> StreamingSentencePhonemeUpgradeSession {
        StreamingSentencePhonemeUpgradeSession {
            language: self.language,
            phdata: Arc::clone(&self.phdata),
            pending: Vec::new(),
            renderer: Renderer::new(self.language),
            clause_has_primary_stress: false,
        }
    }
}

pub struct StreamingSentencePhonemeUpgradeSession {
    language: Language,
    phdata: Arc<PhonemeData>,
    pending: Vec<Entry>,
    renderer: Renderer,
    /// Whether the current (open) clause has already had a primary-stressed word
    /// emitted.  Used to prevent the fallback promotion rule in `promote_clause`
    /// from incorrectly assigning primary stress to a trailing unstressed word
    /// after the stressed word has already left the pending buffer.
    clause_has_primary_stress: bool,
}

impl StreamingSentencePhonemeUpgradeSession {
    pub fn push(&mut self, unit: SentenceUnit) -> Vec<G2pToken> {
        let output = self.push_units(unit);
        self.renderer.render_partial(&output, &self.phdata)
    }

    pub fn finish(&mut self) -> Vec<G2pToken> {
        let output = self.flush_pending();
        self.renderer.render(&output, &self.phdata)
    }

    fn push_units(&mut self, unit: SentenceUnit) -> Vec<SentenceUnit> {
        if matches!(unit, SentenceUnit::ClauseBoundary(_)) {
            let mut output = self.flush_pending();
            output.push(unit);
            self.clause_has_primary_stress = false;
            return output;
        }

        self.pending.push(Entry::from_sentence_unit(&unit));

        // Always use false for mid-clause promote_clauses: we have the full pending
        // window available and must not change the stream/batch parity for this path.
        let cutoff = stable_cut_index(&self.pending, false);

        let mut working = self.pending.clone();
        promote_clauses(&mut working, &self.phdata, false);
        let mut output = Vec::new();
        for entry in working.drain(0..cutoff) {
            // Track whether any emitted entry has primary stress so that
            // flush_pending can suppress the fallback promotion on the remaining words.
            if entry.has_primary_stress() {
                self.clause_has_primary_stress = true;
            }
            output.push(entry.convert_to_sentence_unit(self.language, &self.phdata));
        }

        self.pending.drain(0..cutoff);
        output
    }

    fn flush_pending(&mut self) -> Vec<SentenceUnit> {
        if self.pending.is_empty() {
            return Vec::new();
        }

        let mut working = self.pending.clone();
        promote_clauses(&mut working, &self.phdata, self.clause_has_primary_stress);

        let mut output = Vec::with_capacity(self.pending.len());
        for entry in working {
            output.push(entry.convert_to_sentence_unit(self.language, &self.phdata));
        }

        self.pending.clear();
        output
    }
}