Skip to content

Migration 0.5

ChouChiu edited this page Sep 23, 2026 · 1 revision

从 0.4 升级到 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));

QRC 与 KRC 保留行头的行时长(行为变化)

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 生成器「间隔很大才补结束时间戳」的判断也会用上真实的行结束时间。

QRC 音节切分不再用正则

(.*?)\((\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) 计时——这是格式本身的读法,不要把 ( 并进后面的单词。

qrc_parser::parse_lyrics_line 不再返回 Option

扫描式切分对任何输入都能给出一行,签名从 -> 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)"));

格式分发改走 parser_for / generator_for

每种格式现在都有一个零大小类型实现核心 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};。

解密器实现 LyricsDecrypter

新增 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));

搜索层暴露的 reqwest 类型升到 0.13

SearchError::Http(reqwest::Error) 与 base_api 再导出的 Method / Response / StatusCode 现在是 reqwest 0.13 的类型。只用 Display 或按变体匹配的代码不受影响;把它们交给自己依赖的 reqwest 0.12 时会类型不匹配,需要同步升级。

Searchers 新增 AmllTtmlDb

新增的 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」一节。

TTML 行内译文不再当作歌词(行为变化)

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 这样的形式。

Clone this wiki locally