use std::fmt::Write as _;
use super::AlignedLyrics;
use crate::lineage::LineageContext;
use crate::model::Clip;
use crate::tag::TrackMetadata;
use crate::textfmt::{format_duration, to_single_line};
pub fn render_clip_lyrics(clip: &Clip) -> Option<String> {
if clip.lyrics.trim().is_empty() {
return None;
}
Some(format!("{}\n", clip.lyrics.trim_end()))
}
pub fn render_clip_lrc(clip: &Clip, lineage: &LineageContext) -> Option<String> {
if clip.lyrics.trim().is_empty() {
return None;
}
let mut out = lrc_headers(clip, lineage);
for line in clip.lyrics.trim_end().lines() {
let _ = writeln!(out, "{line}");
}
Some(out)
}
pub fn render_synced_lrc(
clip: &Clip,
lineage: &LineageContext,
aligned: &AlignedLyrics,
) -> Option<String> {
let body = aligned.lrc_body();
if body.is_empty() {
return None;
}
let mut out = lrc_headers(clip, lineage);
out.push_str(&body);
Some(out)
}
fn lrc_headers(clip: &Clip, lineage: &LineageContext) -> String {
let meta = TrackMetadata::from_clip(clip, lineage);
let length = format_duration(clip.duration);
let headers: [(&str, &str); 5] = [
("ti", &meta.title),
("ar", &meta.artist),
("al", &meta.album),
("length", &length),
("re", "rs-suno"),
];
let mut out = String::new();
for (tag, value) in headers {
if value.is_empty() {
continue;
}
let _ = writeln!(out, "[{tag}:{}]", to_single_line(value));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testutil::{full_clip, full_lineage};
#[test]
fn lyrics_render_verbatim_with_one_trailing_newline() {
let clip = Clip {
lyrics: "line one\nline two".to_owned(),
..Clip::default()
};
assert_eq!(
render_clip_lyrics(&clip),
Some("line one\nline two\n".to_owned())
);
}
#[test]
fn lyrics_normalise_trailing_whitespace_to_one_newline() {
let clip = Clip {
lyrics: "verse\n\n\n".to_owned(),
..Clip::default()
};
assert_eq!(render_clip_lyrics(&clip), Some("verse\n".to_owned()));
}
#[test]
fn lyrics_none_when_empty_or_whitespace_only() {
assert_eq!(render_clip_lyrics(&Clip::default()), None);
let clip = Clip {
lyrics: " \n\t \n".to_owned(),
..Clip::default()
};
assert_eq!(render_clip_lyrics(&clip), None);
}
#[test]
fn lyrics_use_clip_lyrics_not_prompt() {
let clip = Clip {
prompt: "the generation prompt".to_owned(),
lyrics: "the actual sung words".to_owned(),
..Clip::default()
};
let rendered = render_clip_lyrics(&clip).unwrap();
assert!(rendered.contains("the actual sung words"));
assert!(!rendered.contains("the generation prompt"));
}
#[test]
fn lrc_none_when_lyrics_blank() {
let empty = Clip::default();
assert_eq!(
render_clip_lrc(&empty, &LineageContext::own_root(&empty)),
None
);
let clip = Clip {
lyrics: " \n\t \n".to_owned(),
..Clip::default()
};
assert_eq!(
render_clip_lrc(&clip, &LineageContext::own_root(&clip)),
None
);
}
#[test]
fn lrc_renders_untimed_body_with_headers() {
let rendered = render_clip_lrc(&full_clip(), &full_lineage()).unwrap();
let expected = "[ti:Electric Storm]\n\
[ar:alice]\n\
[al:Weather Series]\n\
[length:3:32]\n\
[re:rs-suno]\n\
thunder rolls\n\
over the plains\n";
assert_eq!(rendered, expected);
assert!(!rendered.contains("[00:"));
}
#[test]
fn lrc_omits_unknown_headers() {
let clip = Clip {
title: "Bare".to_owned(),
lyrics: "one line".to_owned(),
..Clip::default()
};
let rendered = render_clip_lrc(&clip, &LineageContext::own_root(&clip)).unwrap();
assert!(!rendered.contains("[length:"));
assert!(rendered.contains("[ti:Bare]\n"));
assert!(rendered.contains("[re:rs-suno]\n"));
assert!(rendered.ends_with("one line\n"));
}
fn sample_aligned() -> crate::lyrics::AlignedLyrics {
crate::lyrics::AlignedLyrics {
lines: vec![crate::lyrics::AlignedLine {
text: "thunder rolls".to_owned(),
start_s: 1.5,
end_s: 2.4,
section: "Verse 1".to_owned(),
words: vec![
crate::lyrics::AlignedLineWord {
text: "thunder".to_owned(),
start_s: 1.5,
end_s: 2.0,
},
crate::lyrics::AlignedLineWord {
text: "rolls".to_owned(),
start_s: 2.1,
end_s: 2.4,
},
],
}],
..Default::default()
}
}
#[test]
fn synced_lrc_has_headers_then_line_stamps() {
let rendered = render_synced_lrc(&full_clip(), &full_lineage(), &sample_aligned()).unwrap();
let expected = "[ti:Electric Storm]\n\
[ar:alice]\n\
[al:Weather Series]\n\
[length:3:32]\n\
[re:rs-suno]\n\
[00:01.50]thunder rolls\n";
assert_eq!(rendered, expected);
}
#[test]
fn synced_lrc_is_none_for_empty_alignment() {
let empty = crate::lyrics::AlignedLyrics::default();
assert_eq!(
render_synced_lrc(&full_clip(), &full_lineage(), &empty),
None
);
}
}