#[cfg(test)]
mod tests;
use crate::audio::{
MelFilterbank, ProcessorState, StreamingConfig as AudioStreamingConfig, StreamingProcessor,
};
use crate::error::{WhisperError, WhisperResult};
use crate::TranscriptionResult;
#[derive(Debug, Clone)]
pub struct StreamingConfig {
pub audio: AudioStreamingConfig,
pub max_tokens_per_chunk: usize,
pub overlap_tokens: usize,
pub temperature: f32,
pub return_partial: bool,
}
impl Default for StreamingConfig {
fn default() -> Self {
Self {
audio: AudioStreamingConfig::default(),
max_tokens_per_chunk: 224, overlap_tokens: 10,
temperature: 0.0,
return_partial: true,
}
}
}
impl StreamingConfig {
#[must_use]
pub fn with_sample_rate(sample_rate: u32) -> Self {
Self {
audio: AudioStreamingConfig::with_sample_rate(sample_rate),
..Default::default()
}
}
#[must_use]
pub fn without_vad(mut self) -> Self {
self.audio = self.audio.without_vad();
self
}
#[must_use]
pub const fn with_partial_results(mut self, enable: bool) -> Self {
self.return_partial = enable;
self
}
}
#[derive(Debug, Clone)]
pub struct StreamingResult {
pub text: String,
pub is_final: bool,
pub confidence: f32,
pub chunk_index: usize,
pub latency_ms: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TranscriberState {
Ready,
Processing,
Finalized,
}
pub struct StreamingTranscriber {
processor: StreamingProcessor,
mel: MelFilterbank,
config: StreamingConfig,
state: TranscriberState,
accumulated_text: String,
chunk_index: usize,
previous_tokens: Vec<u32>,
}
impl StreamingTranscriber {
#[must_use]
pub fn new(config: StreamingConfig) -> Self {
Self {
processor: StreamingProcessor::new(config.audio.clone()),
mel: MelFilterbank::new(&aprender::audio::MelConfig::whisper()),
config,
state: TranscriberState::Ready,
accumulated_text: String::new(),
chunk_index: 0,
previous_tokens: Vec::new(),
}
}
#[must_use]
pub fn with_sample_rate(sample_rate: u32) -> Self {
Self::new(StreamingConfig::with_sample_rate(sample_rate))
}
#[must_use]
pub const fn state(&self) -> TranscriberState {
self.state
}
#[must_use]
pub fn text(&self) -> &str {
&self.accumulated_text
}
#[must_use]
pub const fn chunk_index(&self) -> usize {
self.chunk_index
}
#[must_use]
pub fn chunk_progress(&self) -> f32 {
self.processor.chunk_progress()
}
pub fn push_audio(&mut self, samples: &[f32]) {
if self.state == TranscriberState::Finalized {
return;
}
self.processor.push_audio(samples);
}
pub fn process(&mut self) -> WhisperResult<Option<StreamingResult>> {
if self.state == TranscriberState::Finalized {
return Ok(None);
}
self.processor.process();
if self.processor.state() != ProcessorState::ChunkReady {
if self.config.return_partial && self.processor.chunk_progress() > 0.3 {
return Ok(Some(self.create_partial_result()));
}
return Ok(None);
}
let Some(chunk) = self.processor.get_chunk() else {
return Ok(None);
};
self.state = TranscriberState::Processing;
let mel_spec = self
.mel
.compute(&chunk)
.map_err(|e| WhisperError::Audio(e.to_string()))?;
let chunk_result = self.transcribe_chunk(&mel_spec)?;
self.state = TranscriberState::Ready;
self.chunk_index += 1;
if !chunk_result.text.is_empty() {
if !self.accumulated_text.is_empty() {
self.accumulated_text.push(' ');
}
self.accumulated_text.push_str(&chunk_result.text);
}
Ok(Some(chunk_result))
}
fn create_partial_result(&self) -> StreamingResult {
StreamingResult {
text: String::from("[listening...]"),
is_final: false,
confidence: 0.0,
chunk_index: self.chunk_index,
latency_ms: (self.processor.chunk_progress() * 30000.0) as u32,
}
}
#[allow(clippy::unnecessary_wraps)]
fn transcribe_chunk(&self, mel_spec: &[f32]) -> WhisperResult<StreamingResult> {
let _ = mel_spec;
let result = StreamingResult {
text: String::new(), is_final: true,
confidence: 1.0,
chunk_index: self.chunk_index,
latency_ms: 0,
};
Ok(result)
}
pub fn finalize(&mut self) -> WhisperResult<TranscriptionResult> {
if let Some(chunk) = self.processor.flush() {
if !chunk.is_empty() {
let mel_spec = self
.mel
.compute(&chunk)
.map_err(|e| WhisperError::Audio(e.to_string()))?;
let chunk_result = self.transcribe_chunk(&mel_spec)?;
if !chunk_result.text.is_empty() {
if !self.accumulated_text.is_empty() {
self.accumulated_text.push(' ');
}
self.accumulated_text.push_str(&chunk_result.text);
}
}
}
self.state = TranscriberState::Finalized;
Ok(TranscriptionResult {
text: self.accumulated_text.clone(),
language: "en".into(),
segments: vec![],
profiling: None,
})
}
pub fn reset(&mut self) {
self.processor = StreamingProcessor::new(self.config.audio.clone());
self.state = TranscriberState::Ready;
self.accumulated_text.clear();
self.chunk_index = 0;
self.previous_tokens.clear();
}
#[must_use]
pub fn stats(&self) -> StreamingStats {
let processor_stats = self.processor.stats();
StreamingStats {
chunks_processed: self.chunk_index,
samples_processed: processor_stats.samples_processed,
buffer_fill: processor_stats.buffer_fill(),
total_text_length: self.accumulated_text.len(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct StreamingStats {
pub chunks_processed: usize,
pub samples_processed: u64,
pub buffer_fill: f32,
pub total_text_length: usize,
}