Repository navigation
Migration 0.5
0.5.0 把「整行该显示多久」变成歌词模型里的一等数据:QRC 与 KRC 行头 [开始时间,时长] 写的行时长不再被丢掉,LineInfo 的音节行因此带上自己的首尾时间。其余破坏性改动都是路径与签名层面的:xml_utils 与 add_offset_to_syllable_items 换了位置、qrc_parser::parse_lyrics_line 不再返回 Option、搜索层暴露的 reqwest 类型升到 0.13、Searchers 多了一个变体。另外新增了 AMLL TTML DB 歌词源、书写约定与跨文档逐词计时。
六个 crate 统一为 0.5.0。因为升的是 minor 位,写作 "0.4.0" 的依赖不会被自动升上来,需要显式改:
[dependencies]
lyrics-helper = "0.5.0"仅需离线解析时禁用默认 feature:
[dependencies]
lyrics-helper = { version = "0.5.0", default-features = false }Note
search feature 下的 reqwest 升到 0.13,TLS 改用 rustls feature(aws-lc-rs 实现),构建时需要 C 工具链与 cmake。离线构建(default-features = false)不受影响。
LineInfo::Syllable 与 LineInfo::FullSyllable 各新增两个字段:
| 变体 | 新字段 | 语义 |
|---|---|---|
Syllable |
start_time: Option<i32>、end_time: Option<i32>
|
行自身的首尾时间(毫秒);为 None 时分别回退到首个音节的开始时间与末尾音节的结束时间 |
FullSyllable |
同上 | 同上 |
为什么需要它们:QRC 的行头写的是整行该显示多久,末个音节唱完不等于这行该消失。行时间与音节时间相互独立,把末个音节的 end_time 拉长来冒充行时长会带偏逐字高亮。
use lyrics_helper::{LyricsRawTypes, parse};
let data = parse(
"[170720,23942]Oh (170720,1578)you (172978,685)need(173663,2800)",
LyricsRawTypes::Qrc,
)
.unwrap();
let line = &data.lines.as_ref().unwrap()[0];
assert_eq!(line.start_time(), Some(170_720));
assert_eq!(line.end_time(), Some(194_662)); // 行头 170720 + 23942
assert_eq!(
line.syllables().unwrap().last().unwrap().end_time(),
176_463
); // 音节时间没有被行时间改写用结构体字面量构造这两个变体要补字段;用 .. 匹配的代码不用改。
use lyrics_helper::{LineInfo, LyricsAlignment, SyllableItem};
let syllables: Vec<SyllableItem> = Vec::new();
// 0.4:LineInfo::Syllable { syllables, alignment, sub_line }
let line = LineInfo::Syllable {
syllables,
start_time: None,
end_time: None,
alignment: LyricsAlignment::Unspecified,
sub_line: None,
};
assert!(line.is_syllable());Important
LineInfo::new_syllable() 与 LineInfo::new_full_syllable() 的签名不变,产生的行时间都是 None(即 0.4 的行为)。要直接给定行时间,用新增的构造器:
use lyrics_helper::{LineInfo, SyllableInfo, to_syllable_items};
let line = LineInfo::new_syllable_with_time(
to_syllable_items(vec![SyllableInfo::new("你好".to_string(), 0, 500)]),
Some(0),
Some(2000),
);
assert_eq!(line.duration(), Some(2000));start_time() / end_time() 的取值顺序是「行时间优先,为 None 才取首尾音节」,duration() 等派生方法跟着它走。add_offset() 会把行时间与音节时间一起平移:
use lyrics_helper::{LineInfo, SyllableInfo, helpers, to_syllable_items};
let mut lines = vec![LineInfo::new_syllable_with_time(
to_syllable_items(vec![SyllableInfo::new("你好".to_string(), 0, 500)]),
Some(0),
Some(2000),
)];
helpers::offset_helper::add_offset(&mut lines, 200);
assert_eq!(lines[0].start_time(), Some(-200));
assert_eq!(lines[0].end_time(), Some(1800));0.4 及以前,QRC 与 KRC 行头 [开始时间,时长] 里的时长在解析时被丢掉,行时间只能由首尾音节反推。0.5 起行头写进行时间,QRC / KRC / YRC 三种生成器的行头也都改为写回行时间,所以「解析 QRC 再生成 QRC」不再丢行时长:
use lyrics_helper::{LyricsRawTypes, LyricsTypes, generate_string, parse};
let data = parse(
"[0,4390]Stop(0,274) (274,274)And(548,274) (822,274)Stare(1096,274)",
LyricsRawTypes::Qrc,
)
.unwrap();
let qrc = generate_string(&data, LyricsTypes::Qrc).unwrap();
// 末个音节到 1370 结束,行头写的却是 4390
assert!(qrc.contains("[0,4390]"));KRC 同理,只是音节时间相对行首:
use lyrics_helper::{LyricsRawTypes, parse};
let data = parse("[1000,3000]<0,500,0>Hello<500,500,0> world", LyricsRawTypes::Krc).unwrap();
let line = &data.lines.as_ref().unwrap()[0];
assert_eq!(line.end_time(), Some(4000)); // 行头 1000 + 3000
assert_eq!(line.syllables().unwrap().last().unwrap().end_time(), 2000);KRC 行头缺少时长时,行结束时间回退到末个音节。YRC 解析器不读行头时长,行时间仍由音节推导。LRC 生成器「间隔很大才补结束时间戳」的判断也会用上真实的行结束时间。
(.*?)\((\d+),(\d+)\) 换成一次从左到右的扫描,可读的差别只有一处:末个时间戳之后的文本(以及未闭合 ( 之后的文本)不再被丢掉,而是并入末个音节,这样音节拼起来始终等于整行。
use lyrics_helper::{LyricsRawTypes, parse};
let data = parse("[17456,1000]Hello(17456,500) world", LyricsRawTypes::Qrc).unwrap();
let line = &data.lines.as_ref().unwrap()[0];
let spelled: String = line
.syllables()
.unwrap()
.iter()
.map(|syllable| syllable.text())
.collect();
assert_eq!(spelled, "Hello world");Note
括号内容不是 开始ms,时长ms 时按正文处理,归给紧随其后的那个词。QRC 的时间戳写在它所计的文本之后,真实载荷 (108,7)((115,7)Remix(122,36))(158,7) 里 (、Remix、) 各有一个时间戳,( 由 (115,7) 计时——这是格式本身的读法,不要把 ( 并进后面的单词。
扫描式切分对任何输入都能给出一行,签名从 -> Option<LineInfo> 改为 -> LineInfo,调用处去掉 ? / unwrap() 即可。行头读不出来时行时间为 None:
use lyrics_helper::parsers::parsers::qrc_parser::parse_lyrics_line;
// 0.4:parse_lyrics_line(line) -> Option<LineInfo>
let line = parse_lyrics_line("[0,1000]Hi(0,500)");
assert_eq!(line.end_time(), Some(1000));
let no_header = parse_lyrics_line("Hi(0,500)");
assert_eq!(no_header.end_time(), Some(500)); // 回退到末个音节KRC 的 krc_parser::parse_lyrics_line 仍返回 Option<LineInfo>(行头读不出开始时间时为 None)。
| 0.4 路径 | 0.5 路径 | 说明 |
|---|---|---|
lyrics_helper::decrypter::decrypter::qrc::xml_utils |
lyrics_helper::parsers::xml_utils |
QRC XML 信封的清洗与查找属于解析,不属于解密;lyrics-crypto 因此不再依赖 regex 与 quick-xml
|
lyrics_helper::add_offset_to_syllable_items(models::syllable_info) |
lyrics_helper::helpers::offset_helper::add_offset_to_syllable_items |
所有平移时间的函数收进 offset_helper
|
use lyrics_helper::helpers::offset_helper::add_offset_to_syllable_items;
use lyrics_helper::parsers::xml_utils;
use lyrics_helper::{SyllableInfo, to_syllable_items};
let mut items = to_syllable_items(vec![SyllableInfo::new("a".to_string(), 1000, 1500)]);
add_offset_to_syllable_items(&mut items, 200);
assert_eq!(items[0].start_time(), 800);
let root = xml_utils::create(r#"<Lyric_1 LyricContent="[0,500]a(0,500)"/>"#).unwrap();
assert_eq!(root.attribute("LyricContent"), Some("[0,500]a(0,500)"));每种格式现在都有一个零大小类型实现核心 trait(LrcParser、QrcParser … 在 lyrics_helper::parsers::parsers,LrcGenerator、QrcGenerator … 在 lyrics_helper::generators),parse / parse_auto / generate_string 改为查表转发,行为不变。新增的两个查表函数提到了门面顶层:
use lyrics_helper::{LyricsRawTypes, LyricsTypes, generator_for, parser_for};
let parser = parser_for(LyricsRawTypes::Lrc).unwrap();
let data = parser.parse("[00:01.00]Hello").unwrap();
let generator = generator_for(LyricsTypes::Lrc).unwrap();
assert!(generator.generate(&data).unwrap().contains("Hello"));
// 信封与只能解析、不能生成的格式没有对应项
assert!(parser_for(LyricsRawTypes::QrcFull).is_none());
assert!(generator_for(LyricsTypes::Ttml).is_none());调用 .parse() / .generate() 需要 trait 在作用域内;上面走的是 &dyn trait 对象所以不用导入,直接用具体类型时要 use lyrics_helper::traits::{LyricsParser, LyricsGenerator};。
新增 QrcDecrypter,与已有的 KrcDecrypter 一起提到门面顶层。DecryptError 现在是 Copy + Eq + Display + Error,能直接放进 ? 链或 Box<dyn Error>。自由函数 decrypt_qrc / decrypt_krc 不变,仍返回 Option<String>:
use lyrics_helper::traits::decrypter::{DecryptError, LyricsDecrypter};
use lyrics_helper::{KrcDecrypter, QrcDecrypter};
assert_eq!(QrcDecrypter.decrypt("not hex"), Err(DecryptError::InvalidInput));
assert_eq!(KrcDecrypter.decrypt("!!!"), Err(DecryptError::InvalidInput));SearchError::Http(reqwest::Error) 与 base_api 再导出的 Method / Response / StatusCode 现在是 reqwest 0.13 的类型。只用 Display 或按变体匹配的代码不受影响;把它们交给自己依赖的 reqwest 0.12 时会类型不匹配,需要同步升级。
新增的 AMLL TTML DB 歌词源对应 Searchers::AmllTtmlDb。对 Searchers 做穷举 match 的代码要补一个分支:
use lyrics_helper::searchers::Searchers;
fn label(searcher: Searchers) -> &'static str {
match searcher {
Searchers::AmllTtmlDb => "AMLL TTML DB",
_ => "其他平台",
}
}
assert_eq!(label(Searchers::AmllTtmlDb), "AMLL TTML DB");用法见搜索功能的「AMLL TTML DB」一节。
AMLL 把译文与音译写成 <p> 里的 <span ttm:role="x-translation" xml:lang="…"> 与 <span ttm:role="x-roman">。0.4 把它们当成正文拼进了行文本;0.5 起它们进入该行(或背景人声子行)的 translations 与 pronunciation。<head> 里的 Apple 风格译文仍然优先。
use lyrics_helper::{LyricsRawTypes, parse};
let ttml = r#"<tt xmlns="http://www.w3.org/ns/ttml" xmlns:ttm="http://www.w3.org/ns/ttml#metadata">
<body><div>
<p begin="0.0" end="1.0">Hello<span ttm:role="x-translation" xml:lang="zh-CN">你好</span></p>
</div></body>
</tt>"#;
let data = parse(ttml, LyricsRawTypes::Ttml).unwrap();
let line = &data.lines.as_ref().unwrap()[0];
assert_eq!(line.text_from_any(), "Hello");
assert_eq!(line.chinese_translation(), Some("你好"));以下都是新增,不影响已有代码:
-
AMLL TTML DB 歌词源 --
providers::web::amll_ttml_db与AmllTtmlDbSearcher,见搜索功能 -
书写约定 --
helpers::conventions从转录文本里读出对唱分边、背景和声与跨行续句,见辅助工具 -
跨文档逐词计时 --
helpers::word_timing::apply_word_timings把 YRC 等逐词文档的时间读到行级转录的文字上,见辅助工具 -
SyllableItem::parts_mut()-- 可变地取出音节项的子音节(普通音节为长度 1 的切片)
六个 crate 从仓库根目录移到了 crates/ 下。从 crates.io 或 git 依赖的项目不受影响(cargo 会在仓库里按包名找到它);用本地 path 依赖某个子 crate 的,要把路径改成 crates/lyrics-helper 这样的形式。