use std::fmt::Write as _;
#[derive(Debug, Clone, PartialEq)]
pub struct AlignedWord {
pub word: String,
pub success: bool,
pub start_s: f64,
pub end_s: f64,
pub p_align: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AlignedLineWord {
pub text: String,
pub start_s: f64,
pub end_s: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AlignedLine {
pub text: String,
pub start_s: f64,
pub end_s: f64,
pub section: String,
pub words: Vec<AlignedLineWord>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct AlignedLyrics {
pub words: Vec<AlignedWord>,
pub lines: Vec<AlignedLine>,
pub waveform_data: Vec<f64>,
pub hoot_cer: Option<f64>,
pub is_streamed: Option<bool>,
}
impl AlignedLyrics {
pub fn is_empty(&self) -> bool {
self.lines.is_empty() && self.words.is_empty()
}
pub fn plain_text(&self) -> String {
if !self.lines.is_empty() {
return self
.lines
.iter()
.map(|line| line.text.trim_end())
.collect::<Vec<_>>()
.join("\n");
}
self.words
.iter()
.map(|word| word.word.as_str())
.collect::<Vec<_>>()
.join(" ")
}
pub fn lrc_body(&self) -> String {
let mut out = String::new();
for line in &self.lines {
let text = if line.text.trim().is_empty() {
line.words
.iter()
.map(|w| w.text.trim())
.filter(|t| !t.is_empty())
.collect::<Vec<_>>()
.join(" ")
} else {
line.text.trim().to_owned()
};
let _ = writeln!(out, "[{}]{text}", lrc_stamp(line.start_s));
}
out
}
pub fn sylt_entries(&self) -> Vec<(u32, String)> {
let mut entries = Vec::new();
for (line_index, line) in self.lines.iter().enumerate() {
let words: Vec<&AlignedLineWord> = line
.words
.iter()
.filter(|w| !w.text.trim().is_empty())
.collect();
let prefix = if line_index == 0 { "" } else { "\n" };
if words.is_empty() {
let text = line.text.trim();
if !text.is_empty() {
entries.push((to_ms(line.start_s), format!("{prefix}{text}")));
}
continue;
}
for (word_index, word) in words.iter().enumerate() {
let text = word.text.trim();
let segment = if word_index == 0 {
format!("{prefix}{text}")
} else {
format!(" {text}")
};
entries.push((to_ms(word.start_s), segment));
}
}
entries
}
}
fn to_ms(secs: f64) -> u32 {
if !secs.is_finite() || secs <= 0.0 {
return 0;
}
(secs * 1000.0).round() as u32
}
fn lrc_stamp(secs: f64) -> String {
let cs = centiseconds(secs);
format!("{:02}:{:02}.{:02}", cs / 6000, (cs / 100) % 60, cs % 100)
}
fn centiseconds(secs: f64) -> u64 {
if !secs.is_finite() || secs <= 0.0 {
return 0;
}
(secs * 100.0).round() as u64
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_aligned() -> AlignedLyrics {
AlignedLyrics {
words: vec![
AlignedWord {
word: "Hello".to_owned(),
success: true,
start_s: 0.5,
end_s: 0.9,
p_align: 0.99,
},
AlignedWord {
word: "world".to_owned(),
success: true,
start_s: 1.0,
end_s: 1.4,
p_align: 0.98,
},
AlignedWord {
word: "again".to_owned(),
success: true,
start_s: 61.2,
end_s: 61.8,
p_align: 0.97,
},
],
lines: vec![
AlignedLine {
text: "Hello world".to_owned(),
start_s: 0.5,
end_s: 1.4,
section: "Verse 1".to_owned(),
words: vec![
AlignedLineWord {
text: "Hello".to_owned(),
start_s: 0.5,
end_s: 0.9,
},
AlignedLineWord {
text: "world".to_owned(),
start_s: 1.0,
end_s: 1.4,
},
],
},
AlignedLine {
text: "[Chorus]".to_owned(),
start_s: 60.0,
end_s: 60.0,
section: "Chorus".to_owned(),
words: vec![],
},
AlignedLine {
text: "again".to_owned(),
start_s: 61.2,
end_s: 61.8,
section: "Chorus".to_owned(),
words: vec![AlignedLineWord {
text: "again".to_owned(),
start_s: 61.2,
end_s: 61.8,
}],
},
],
..Default::default()
}
}
#[test]
fn lrc_body_has_line_level_stamps() {
let aligned = sample_aligned();
let body = aligned.lrc_body();
let expected = "[00:00.50]Hello world\n\
[01:00.00][Chorus]\n\
[01:01.20]again\n";
assert_eq!(body, expected);
}
#[test]
fn plain_text_joins_line_text() {
let aligned = sample_aligned();
assert_eq!(aligned.plain_text(), "Hello world\n[Chorus]\nagain");
}
#[test]
fn sylt_entries_are_word_level_with_line_breaks() {
let aligned = sample_aligned();
let entries = aligned.sylt_entries();
assert_eq!(
entries,
vec![
(500, "Hello".to_owned()),
(1000, " world".to_owned()),
(60000, "\n[Chorus]".to_owned()),
(61200, "\nagain".to_owned()),
]
);
}
#[test]
fn stamps_round_and_do_not_wrap_minutes() {
assert_eq!(lrc_stamp(61.2), "01:01.20");
assert_eq!(lrc_stamp(3661.0), "61:01.00");
assert_eq!(to_ms(1.2346), 1235);
assert_eq!(to_ms(-1.0), 0);
}
}