use crate::tokenizer::special_tokens;
use crate::Segment;
#[cfg(test)]
mod tests;
pub const MAX_TIMESTAMP_SECONDS: f32 = 30.0;
pub const TIMESTAMP_RESOLUTION: f32 = 0.02;
pub const MAX_TIMESTAMP_TOKENS: u32 = 1500;
struct SegmentExtractor {
segments: Vec<Segment>,
current_start: Option<f32>,
current_tokens: Vec<u32>,
}
impl SegmentExtractor {
fn new() -> Self {
Self {
segments: Vec::new(),
current_start: None,
current_tokens: Vec::new(),
}
}
fn try_finalize_segment<F>(&mut self, end_time: f32, tokenizer_decode: &mut F)
where
F: FnMut(&[u32]) -> Option<String>,
{
let Some(start) = self.current_start else {
return;
};
if let Some(segment) = self.create_segment(start, end_time, tokenizer_decode) {
self.segments.push(segment);
}
self.current_tokens.clear();
}
fn create_segment<F>(&self, start: f32, end: f32, tokenizer_decode: &mut F) -> Option<Segment>
where
F: FnMut(&[u32]) -> Option<String>,
{
if self.current_tokens.is_empty() {
return None;
}
let text = tokenizer_decode(&self.current_tokens)
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty())?;
Some(Segment {
start,
end,
text,
tokens: self.current_tokens.clone(),
})
}
fn handle_timestamp<F>(&mut self, time: f32, tokenizer_decode: &mut F)
where
F: FnMut(&[u32]) -> Option<String>,
{
self.try_finalize_segment(time, tokenizer_decode);
self.current_start = Some(time);
}
fn finalize_remaining<F>(&mut self, tokenizer_decode: &mut F)
where
F: FnMut(&[u32]) -> Option<String>,
{
let Some(start) = self.current_start else {
return;
};
let estimated_duration = (self.current_tokens.len() as f32) * 0.06;
let end = start + estimated_duration;
if let Some(segment) = self.create_segment(start, end, tokenizer_decode) {
self.segments.push(segment);
}
}
}
pub fn extract_segments<F>(tokens: &[u32], mut tokenizer_decode: F) -> Vec<Segment>
where
F: FnMut(&[u32]) -> Option<String>,
{
let mut extractor = SegmentExtractor::new();
for &token in tokens {
if is_control_token(token) {
continue;
}
if special_tokens::is_timestamp(token) {
let time = special_tokens::timestamp_to_seconds(token).unwrap_or(0.0);
extractor.handle_timestamp(time, &mut tokenizer_decode);
} else {
extractor.current_tokens.push(token);
}
}
extractor.finalize_remaining(&mut tokenizer_decode);
extractor.segments
}
pub fn is_control_token(token: u32) -> bool {
token == special_tokens::SOT
|| token == special_tokens::EOT
|| token == special_tokens::TRANSCRIBE
|| token == special_tokens::TRANSLATE
|| token == special_tokens::NO_TIMESTAMPS
|| token == special_tokens::NO_SPEECH
|| is_language_token(token)
}
fn is_language_token(token: u32) -> bool {
(special_tokens::LANG_BASE..special_tokens::TRANSCRIBE).contains(&token)
}
#[must_use]
pub fn is_timestamp(token: u32) -> bool {
special_tokens::is_timestamp(token)
}
#[must_use]
pub fn timestamp_to_seconds(token: u32) -> Option<f32> {
special_tokens::timestamp_to_seconds(token)
}
pub fn parse_timestamp_pairs(tokens: &[u32]) -> Vec<(f32, f32)> {
let timestamps: Vec<f32> = tokens
.iter()
.filter_map(|&token| special_tokens::timestamp_to_seconds(token))
.collect();
timestamps
.windows(2)
.filter(|w| w[1] > w[0])
.map(|w| (w[0], w[1]))
.collect()
}
#[must_use]
pub fn seconds_to_timestamp_token(seconds: f32) -> u32 {
let clamped = seconds.clamp(0.0, MAX_TIMESTAMP_SECONDS);
let offset = (clamped / TIMESTAMP_RESOLUTION).round() as u32;
special_tokens::TIMESTAMP_BASE + offset.min(MAX_TIMESTAMP_TOKENS)
}
pub fn get_timestamps(tokens: &[u32]) -> Vec<(usize, f32)> {
tokens
.iter()
.enumerate()
.filter_map(|(idx, &token)| {
special_tokens::timestamp_to_seconds(token).map(|time| (idx, time))
})
.collect()
}
#[must_use]
pub fn has_timestamps(tokens: &[u32]) -> bool {
tokens.iter().any(|&t| special_tokens::is_timestamp(t))
}
#[must_use]
pub fn count_text_tokens(tokens: &[u32]) -> usize {
tokens
.iter()
.filter(|&&t| !special_tokens::is_timestamp(t) && !is_control_token(t))
.count()
}
#[must_use]
pub fn estimate_duration_from_tokens(token_count: usize) -> f32 {
(token_count as f32) * 0.06
}
pub fn merge_segments(segments: &[Segment], max_gap: f32) -> Vec<Segment> {
if segments.is_empty() {
return Vec::new();
}
let mut merged = Vec::new();
let mut current = segments[0].clone();
for segment in segments.iter().skip(1) {
if segment.start - current.end <= max_gap {
current.end = segment.end;
current.text.push(' ');
current.text.push_str(&segment.text);
current.tokens.extend_from_slice(&segment.tokens);
} else {
merged.push(current);
current = segment.clone();
}
}
merged.push(current);
merged
}
pub fn split_long_segments(segments: &[Segment], max_duration: f32) -> Vec<Segment> {
let mut result = Vec::new();
for segment in segments {
if segment.end - segment.start <= max_duration {
result.push(segment.clone());
} else {
let sentences = split_sentences(&segment.text);
if sentences.len() > 1 {
let total_duration = segment.end - segment.start;
let total_chars: usize = sentences.iter().map(|s| s.len()).sum();
let mut current_time = segment.start;
for sentence in sentences {
let sentence_duration =
(sentence.len() as f32 / total_chars as f32) * total_duration;
result.push(Segment {
start: current_time,
end: current_time + sentence_duration,
text: sentence,
tokens: vec![], });
current_time += sentence_duration;
}
} else {
result.push(segment.clone());
}
}
}
result
}
fn split_sentences(text: &str) -> Vec<String> {
let mut sentences = Vec::new();
let mut current = String::new();
for ch in text.chars() {
current.push(ch);
if ch == '.' || ch == '!' || ch == '?' {
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
sentences.push(trimmed);
}
current.clear();
}
}
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
sentences.push(trimmed);
}
sentences
}