use tokio::time::{Duration, Instant};
pub struct TtsChunkGuard {
buf: String,
last_flush: Instant,
min_chars: usize, max_chars: usize, max_wait: Duration, }
impl Default for TtsChunkGuard {
fn default() -> Self {
Self {
buf: String::new(),
last_flush: Instant::now(),
min_chars: 90, max_chars: 260, max_wait: Duration::from_millis(320),
}
}
}
impl TtsChunkGuard {
pub fn new() -> Self {
Self::default()
}
pub fn with_limits(min_chars: usize, max_chars: usize, max_wait: Duration) -> Self {
Self {
buf: String::new(),
last_flush: Instant::now(),
min_chars,
max_chars,
max_wait,
}
}
pub fn push(&mut self, chunk: &str) -> Option<String> {
if chunk.is_empty() {
return None;
}
self.buf.push_str(chunk);
let waited = self.last_flush.elapsed();
let len = self.buf.chars().count();
if len >= self.max_chars {
let out = self.flush_at_best_boundary_or_hard();
self.last_flush = Instant::now();
return Some(out);
}
if waited >= self.max_wait && len >= self.min_chars {
let out = self.flush_at_best_boundary_or_hard();
self.last_flush = Instant::now();
return Some(out);
}
if len >= self.min_chars && ends_with_boundary(&self.buf) {
let out = std::mem::take(&mut self.buf);
self.last_flush = Instant::now();
return Some(out);
}
None
}
pub fn finish(&mut self) -> Option<String> {
if self.buf.trim().is_empty() {
self.buf.clear();
return None;
}
self.last_flush = Instant::now();
Some(std::mem::take(&mut self.buf))
}
fn flush_at_best_boundary_or_hard(&mut self) -> String {
let s = self.buf.as_str();
if let Some(idx) = find_last_reasonable_boundary(s) {
let out = s[..idx].to_string();
let remain = s[idx..].to_string();
self.buf = remain;
return out;
}
std::mem::take(&mut self.buf)
}
}
fn ends_with_boundary(s: &str) -> bool {
let t = s.trim_end();
t.ends_with('.')
|| t.ends_with('?')
|| t.ends_with('!')
|| t.ends_with('\n')
|| t.ends_with(',')
|| t.ends_with(':')
|| t.ends_with(';')
}
fn find_last_reasonable_boundary(s: &str) -> Option<usize> {
let start = s.len().saturating_sub(320);
let window = &s[start..];
let mut best: Option<usize> = None;
for (i, ch) in window.char_indices() {
let abs = start + i;
if matches!(ch, '.' | '?' | '!' | '\n') {
best = Some(abs + ch.len_utf8());
}
}
if best.is_some() {
return best;
}
for (i, ch) in window.char_indices() {
let abs = start + i;
if matches!(ch, ',' | ':' | ';') {
best = Some(abs + ch.len_utf8());
}
}
best
}