use serde::{Deserialize, Serialize};
use super::PRED_HIDDEN;
use super::audio;
#[cfg(feature = "diarization")]
use super::diarization::StreamingDiarizationState;
use super::features::MelSpectrogram;
use super::now_timestamp;
#[non_exhaustive]
pub struct DecoderState {
pub h: Vec<f32>,
pub c: Vec<f32>,
pub prev_token: i64,
pub consecutive_blanks: usize,
}
impl DecoderState {
pub fn new(blank_id: usize) -> Self {
Self {
h: vec![0.0; PRED_HIDDEN],
c: vec![0.0; PRED_HIDDEN],
prev_token: blank_id as i64,
consecutive_blanks: 0,
}
}
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct WordInfo {
pub word: String,
pub start: f64,
pub end: f64,
pub confidence: f32,
#[serde(skip_serializing_if = "Option::is_none")]
pub speaker: Option<u32>,
}
impl WordInfo {
pub fn new(
word: impl Into<String>,
start: f64,
end: f64,
confidence: f32,
speaker: Option<u32>,
) -> Self {
Self {
word: word.into(),
start,
end,
confidence,
speaker,
}
}
}
pub(crate) fn aggregate_confidence(words: &[WordInfo]) -> Option<f32> {
if words.is_empty() {
return None;
}
let mut weighted_sum = 0.0_f64;
let mut total_weight = 0.0_f64;
for w in words {
let weight = (w.end - w.start).max(0.0);
weighted_sum += f64::from(w.confidence) * weight;
total_weight += weight;
}
let mean = if total_weight > 0.0 {
weighted_sum / total_weight
} else {
words.iter().map(|w| f64::from(w.confidence)).sum::<f64>() / words.len() as f64
};
Some(mean as f32)
}
#[non_exhaustive]
pub struct StreamingState {
pub decoder: DecoderState,
pub audio_buffer: Vec<f32>,
pub assembler: TranscriptAssembler,
pub window_start_samples: usize,
pub context_samples: usize,
pub pending_samples: usize,
pub resampler: Option<rubato::Async<f32>>,
pub mel_fft_input: Vec<rustfft::num_complex::Complex<f32>>,
pub mel_power: Vec<f32>,
pub mel_output: Vec<f32>,
pub resample_output_buf: Vec<f32>,
pub vad_endpointer: Option<crate::vad::VadEndpointer>,
pub punctuation: Option<bool>,
pub itn: Option<bool>,
pub endpoint_mode: EndpointMode,
#[cfg(feature = "diarization")]
pub diarization_state: Option<StreamingDiarizationState>,
}
pub struct FeatureExtractor {
mel: MelSpectrogram,
}
impl Default for FeatureExtractor {
fn default() -> Self {
Self::new()
}
}
impl FeatureExtractor {
pub fn new() -> Self {
Self {
mel: MelSpectrogram::new(),
}
}
pub fn prepare_buffer(&self, samples: &[f32], audio_buffer: &mut Vec<f32>) -> Option<usize> {
audio::prepare_audio_buffer(samples, audio_buffer)
}
pub fn compute_mel(
&self,
samples: &[f32],
fft_buf: &mut Vec<rustfft::num_complex::Complex<f32>>,
power_buf: &mut Vec<f32>,
output_buf: &mut Vec<f32>,
) -> usize {
self.mel
.compute_with_buffers(samples, fft_buf, power_buf, output_buf)
}
pub fn compute(&self, samples: &[f32]) -> (Vec<f32>, usize) {
self.mel.compute(samples)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum EndpointReason {
Vad,
Blank,
Stop,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum EndpointMode {
#[default]
Auto,
Assistant,
Manual,
}
impl EndpointMode {
pub fn parse_token(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"auto" => Some(Self::Auto),
"assistant" => Some(Self::Assistant),
"manual" => Some(Self::Manual),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Auto => "auto",
Self::Assistant => "assistant",
Self::Manual => "manual",
}
}
}
pub struct TranscriptAssembler {
committed_text: String,
committed_words: Vec<WordInfo>,
text: String,
words: Vec<WordInfo>,
}
impl Default for TranscriptAssembler {
fn default() -> Self {
Self::new()
}
}
impl TranscriptAssembler {
pub fn new() -> Self {
Self {
committed_text: String::new(),
committed_words: Vec::new(),
text: String::new(),
words: Vec::new(),
}
}
pub fn append(&mut self, new_words: Vec<WordInfo>) {
for w in &new_words {
if !self.text.is_empty() {
self.text.push(' ');
}
self.text.push_str(&w.word);
}
self.words.extend(new_words);
}
pub fn set_words(&mut self, words: Vec<WordInfo>) {
self.text = words
.iter()
.map(|w| w.word.as_str())
.collect::<Vec<_>>()
.join(" ");
self.words = words;
}
pub fn commit_live(&mut self) {
if self.words.is_empty() {
return;
}
if !self.committed_text.is_empty() && !self.text.is_empty() {
self.committed_text.push(' ');
}
self.committed_text.push_str(&self.text);
self.committed_words.append(&mut self.words);
self.text.clear();
}
fn full_text(&self) -> String {
if self.committed_text.is_empty() {
self.text.clone()
} else if self.text.is_empty() {
self.committed_text.clone()
} else {
format!("{} {}", self.committed_text, self.text)
}
}
fn full_words(&self) -> Vec<WordInfo> {
let mut out = Vec::with_capacity(self.committed_words.len() + self.words.len());
out.extend_from_slice(&self.committed_words);
out.extend_from_slice(&self.words);
out
}
pub fn finalize(&mut self, timestamp: f64) -> TranscriptSegment {
self.finalize_with_reason(timestamp, EndpointReason::Stop)
}
pub fn finalize_with_reason(
&mut self,
timestamp: f64,
reason: EndpointReason,
) -> TranscriptSegment {
self.commit_live();
let words = std::mem::take(&mut self.committed_words);
let text = std::mem::take(&mut self.committed_text);
let confidence = aggregate_confidence(&words);
self.text.clear();
self.words.clear();
TranscriptSegment {
text,
words,
is_final: true,
speech_final: true,
endpoint_reason: Some(reason),
timestamp,
confidence,
}
}
pub fn partial(&self, timestamp: f64) -> TranscriptSegment {
let words = self.full_words();
TranscriptSegment {
text: self.full_text(),
words: words.clone(),
is_final: false,
speech_final: false,
endpoint_reason: None,
timestamp,
confidence: aggregate_confidence(&words),
}
}
pub fn is_empty(&self) -> bool {
self.committed_text.is_empty() && self.text.is_empty()
}
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct TranscriptSegment {
pub text: String,
pub words: Vec<WordInfo>,
pub is_final: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub speech_final: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub endpoint_reason: Option<EndpointReason>,
pub timestamp: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub confidence: Option<f32>,
}
impl TranscriptSegment {
pub fn empty_final() -> Self {
Self {
text: String::new(),
words: vec![],
is_final: true,
speech_final: true,
endpoint_reason: Some(EndpointReason::Stop),
timestamp: now_timestamp(),
confidence: None,
}
}
}