ref: using AquaLyrics to drive lyrics processing

This commit is contained in:
alikia2x (寒寒) 2024-11-28 01:25:36 +08:00
parent e60baa0358
commit 0d60e9a094
Signed by: alikia2x
GPG Key ID: 56209E0CCD8420C6
13 changed files with 4 additions and 711 deletions

View File

@ -1,6 +1,6 @@
<script lang="ts">
import createSpring from '@core/graphics/spring';
import type { LyricWord, ScriptItem } from '@core/lyrics/type';
import type { LyricWord, ScriptItem } from '@alikia/aqualyrics';
import type { LyricPos } from './type';
import type { Spring } from '@core/graphics/spring/spring';
import userAdjustingProgress from '@core/state/userAdjustingProgress';

View File

@ -1,252 +0,0 @@
import {
alt_sc,
apply,
buildLexer,
expectEOF,
fail,
kleft,
kmid,
kright,
opt_sc,
type Parser,
rep,
rep_sc,
seq,
str,
tok,
type Token
} from 'typescript-parsec';
import type { LrcJsonData, ParsedLrc, ScriptItem } from '../type';
import type { IDTag } from './type';
interface ParserScriptItem {
start: number;
text: string;
translation?: string;
singer?: string;
}
function convertTimeToMs({
mins,
secs,
decimals
}: {
mins?: number | string;
secs?: number | string;
decimals?: string;
}) {
let result = 0;
if (mins) {
result += Number(mins) * 60 * 1000;
}
if (secs) {
result += Number(secs) * 1000;
}
if (decimals) {
const denom = Math.pow(10, decimals.length);
result += Number(decimals) / (denom / 1000);
}
return result;
}
const digit = Array.from({ length: 10 }, (_, i) => apply(str(i.toString()), (_) => i)).reduce(
(acc, cur) => alt_sc(cur, acc),
fail('no alternatives')
);
const numStr = apply(rep_sc(digit), (r) => r.join(''));
const num = apply(numStr, (r) => parseInt(r));
const alpha = alt_sc(
Array.from({ length: 26 }, (_, i) =>
apply(str(String.fromCharCode('a'.charCodeAt(0) + i)), (_) => String.fromCharCode('a'.charCodeAt(0) + i))
).reduce((acc, cur) => alt_sc(cur, acc), fail('no alternatives')),
Array.from({ length: 26 }, (_, i) =>
apply(str(String.fromCharCode('A'.charCodeAt(0) + i)), (_) => String.fromCharCode('A'.charCodeAt(0) + i))
).reduce((acc, cur) => alt_sc(cur, acc), fail('no alternatives'))
);
const alphaStr = apply(rep(alpha), (r) => r.join(''));
function spaces<K>(): Parser<K, Token<K>[]> {
return rep_sc(str(' '));
}
const unicodeStr = rep(tok('char'));
function trimmed<K, T>(p: Parser<K, Token<T>[]>): Parser<K, Token<T>[]> {
return apply(p, (r) => {
while (r.length > 0 && r[0].text.trim() === '') {
r.shift();
}
while (r.length > 0 && r[r.length - 1].text.trim() === '') {
r.pop();
}
return r;
});
}
function padded<K, T>(p: Parser<K, T>): Parser<K, T> {
return kmid(spaces(), p, spaces());
}
function anythingTyped(types: string[]) {
return types.map((t) => tok(t)).reduce((acc, cur) => alt_sc(cur, acc), fail('no alternatives'));
}
function lrcTimestamp<K, T>(delim: [Parser<K, Token<T>>, Parser<K, Token<T>>]) {
const innerTS = alt_sc(
apply(seq(num, str(':'), num, str('.'), numStr), (r) =>
convertTimeToMs({ mins: r[0], secs: r[2], decimals: r[4] })
),
apply(seq(num, str('.'), numStr), (r) => convertTimeToMs({ secs: r[0], decimals: r[2] })),
apply(seq(num, str(':'), num), (r) => convertTimeToMs({ mins: r[0], secs: r[2] })),
apply(num, (r) => convertTimeToMs({ secs: r }))
);
return kmid(delim[0], innerTS, delim[1]);
}
const squareTS = lrcTimestamp([tok('['), tok(']')]);
const angleTS = lrcTimestamp([tok('<'), tok('>')]);
const lrcTag = apply(
seq(
tok('['),
alphaStr,
str(':'),
tokenParserToText(trimmed(rep(anythingTyped(['char', '[', ']', '<', '>'])))),
tok(']')
),
(r) => ({
[r[1]]: r[3]
})
);
function joinTokens<T>(tokens: Token<T>[]) {
return tokens.map((t) => t.text).join('');
}
function tokenParserToText<K, T>(p: Parser<K, Token<T>> | Parser<K, Token<T>[]>): Parser<K, string> {
return apply(p, (r: Token<T> | Token<T>[]) => {
if (Array.isArray(r)) {
return joinTokens(r);
}
return r.text;
});
}
const singerIndicator = kleft(tok('char'), str(':'));
const translateParser = kright(tok('|'), unicodeStr);
function lrcLine(
wordDiv = ' ', legacy = false
): Parser<unknown, ['script_item', ParserScriptItem] | ['lrc_tag', IDTag] | ['comment', string] | ['empty', null]> {
return alt_sc(
legacy ? apply(seq(squareTS, trimmed(rep_sc(anythingTyped(['char', '[', ']', '<', '>'])))), (r) =>
['script_item', { start: r[0], text: joinTokens(r[1]) } as ParserScriptItem] // TODO: Complete this
) : apply(
seq(
squareTS,
opt_sc(padded(singerIndicator)),
rep_sc(
seq(
opt_sc(angleTS),
trimmed(rep_sc(anythingTyped(['char', '[', ']'])))
)
),
opt_sc(trimmed(translateParser))
), (r) => {
const start = r[0];
const singerPart = r[1];
const mainPart = r[2];
const translatePart = r[3];
const text = mainPart
.map((s) => joinTokens(s[1]))
.filter((s) => s.trim().length > 0)
.join(wordDiv);
const singer = singerPart?.text;
const translation = translatePart === undefined ? undefined : joinTokens(translatePart);
return ['script_item', { start, text, singer, translation } as ParserScriptItem];
}),
apply(lrcTag, (r) => ['lrc_tag', r as IDTag]),
apply(seq(spaces(), str('#'), unicodeStr), (cmt) => ['comment', cmt[2].join('')] as const),
apply(spaces(), (_) => ['empty', null] as const)
);
}
export function dumpToken<T>(t: Token<T> | undefined): string {
if (t === undefined) {
return '<EOF>';
}
return '`' + t.text + '` -> ' + dumpToken(t.next);
}
export function parseLRC(
input: string,
{ wordDiv, strict, legacy }: { wordDiv?: string; strict?: boolean; legacy?: boolean } = {}
): ParsedLrc {
const tokenizer = buildLexer([
[true, /^\[/gu, '['],
[true, /^\]/gu, ']'],
[true, /^\|/gu, '|'],
[true, /^./gu, 'char']
]);
const lines = input
.split(/\r\n|\r|\n/gu)
.filter((line) => line.trim().length > 0)
.map((line) => tokenizer.parse(line));
return lines
.map((line) => {
const res = expectEOF(lrcLine(wordDiv, legacy).parse(line));
if (!res.successful) {
if (strict) {
throw new Error('Failed to parse full line: ' + dumpToken(line));
} else {
console.error('Failed to parse full line: ' + dumpToken(line));
}
return null;
}
return res.candidates[0].result;
})
.filter((r) => r !== null)
.reduce((acc, cur) => {
switch (cur[0]) {
case 'lrc_tag':
Object.assign(acc, cur[1]);
return acc;
case 'script_item':
acc.scripts = acc.scripts || [];
acc.scripts.push(cur[1]);
return acc;
default:
return acc;
}
}, {} as ParsedLrc);
}
export default function lrcParser(lrc: string): LrcJsonData {
const parsedLrc = parseLRC(lrc, { wordDiv: '', strict: false });
if (parsedLrc.scripts === undefined) {
return parsedLrc as LrcJsonData;
}
let finalLrc: LrcJsonData = parsedLrc as LrcJsonData;
let lyrics: ScriptItem[] = [];
let i = 0;
while (i < parsedLrc.scripts.length - 1) {
let lyricLine = parsedLrc.scripts[i] as ScriptItem;
lyricLine.start/=1000;
lyricLine.end = parsedLrc.scripts[i+1].start / 1000;
if (parsedLrc.scripts[i+1].text.trim() === "") {
i+=2;
} else i++;
if (lyricLine.text.trim() !== "") {
lyrics.push(lyricLine);
}
}
finalLrc.scripts = lyrics;
return finalLrc;
}

View File

@ -1,3 +0,0 @@
export interface IDTag {
[key: string]: string;
}

View File

@ -1,12 +1,11 @@
import type { LrcJsonData } from "lrc-parser-ts";
import type { LyricData } from "@alikia/aqualyrics";
export default function createLyricsSearcher(lrc: LrcJsonData): (progress: number) => number {
export default function createLyricsSearcher(lrc: LyricData): (progress: number) => number {
if (!lrc || !lrc.scripts) return () => 0;
const startTimes: number[] = lrc.scripts.map(script => script.start);
const endTimes: number[] = lrc.scripts.map(script => script.end);
return function(progress: number): number {
// 使用二分查找定位 progress 对应的歌词索引
let left = 0;
let right = startTimes.length - 1;
@ -22,12 +21,10 @@ export default function createLyricsSearcher(lrc: LrcJsonData): (progress: numbe
}
}
// 循环结束后,检查 left 索引
if (left < startTimes.length && startTimes[left] > progress && (left === 0 || endTimes[left - 1] <= progress)) {
return left;
}
// 如果没有找到确切的 progress返回小于等于 progress 的最大索引
return Math.max(0, right);
};
}

View File

@ -1,32 +0,0 @@
import type { LrcJsonData } from '../type';
import { parseTTML as ttmlParser } from './parser';
import type { LyricLine } from './ttml-types';
export type * from './ttml-types';
export function parseTTML(text: string) {
let lyrics: LrcJsonData;
const lyricLines = ttmlParser(text).lyricLines;
lyrics = {
scripts: lyricLines.map((value: LyricLine, _index: number, _array: LyricLine[]) => {
let words = value.words.length == 0 ? undefined : value.words;
if (words) {
words = words.map((word) => {
let r = word;
r.startTime /= 1000;
r.endTime /= 1000;
return r;
});
}
return {
text: value.words.map((word) => word.word).join(''),
start: value.startTime / 1000,
end: value.endTime / 1000,
translation: value.translatedLyric || undefined,
singer: value.singer || undefined,
words: words
};
})
};
return lyrics;
}

View File

@ -1,172 +0,0 @@
/**
* @fileoverview
* Parser for TTML lyrics.
* Used to parse lyrics files from Apple Music,
* and extended to support translation and pronounciation of text.
* @see https://www.w3.org/TR/2018/REC-ttml1-20181108/
*/
import type {
LyricLine,
LyricWord,
TTMLLyric,
TTMLMetadata,
} from "./ttml-types";
const timeRegexp =
/^(((?<hour>[0-9]+):)?(?<min>[0-9]+):)?(?<sec>[0-9]+([.:]([0-9]+))?)/;
function parseTimespan(timeSpan: string): number {
const matches = timeRegexp.exec(timeSpan);
if (matches) {
const hour = Number(matches.groups?.hour || "0");
const min = Number(matches.groups?.min || "0");
const sec = Number(matches.groups?.sec.replace(/:/, ".") || "0");
return Math.floor((hour * 3600 + min * 60 + sec) * 1000);
}
throw new TypeError(`Failed to parse time stamp:${timeSpan}`);
}
export function parseTTML(ttmlText: string): TTMLLyric {
const domParser = new DOMParser();
const ttmlDoc: XMLDocument = domParser.parseFromString(
ttmlText,
"application/xml",
);
let mainAgentId = "v1";
const metadata: TTMLMetadata[] = [];
for (const meta of ttmlDoc.querySelectorAll("meta")) {
if (meta.tagName === "amll:meta") {
const key = meta.getAttribute("key");
if (key) {
const value = meta.getAttribute("value");
if (value) {
const existing = metadata.find((m) => m.key === key);
if (existing) {
existing.value.push(value);
} else {
metadata.push({
key,
value: [value],
});
}
}
}
}
}
for (const agent of ttmlDoc.querySelectorAll("ttm\\:agent")) {
if (agent.getAttribute("type") === "person") {
const id = agent.getAttribute("xml:id");
if (id) {
mainAgentId = id;
}
}
}
const lyricLines: LyricLine[] = [];
function parseParseLine(lineEl: Element, isBG = false, isDuet = false) {
const line: LyricLine = {
words: [],
translatedLyric: "",
romanLyric: "",
isBG,
isDuet:
!!lineEl.getAttribute("ttm:agent") &&
lineEl.getAttribute("ttm:agent") !== mainAgentId,
startTime: 0,
endTime: 0,
singer: ""
};
if (isBG) line.isDuet = isDuet;
let haveBg = false;
for (const wordNode of lineEl.childNodes) {
if (wordNode.nodeType === Node.TEXT_NODE) {
line.words?.push({
word: wordNode.textContent ?? "",
startTime: 0,
endTime: 0,
});
} else if (wordNode.nodeType === Node.ELEMENT_NODE) {
const wordEl = wordNode as Element;
const role = wordEl.getAttribute("ttm:role");
if (wordEl.nodeName === "span" && role) {
if (role === "x-bg") {
parseParseLine(wordEl, true, line.isDuet);
haveBg = true;
} else if (role === "x-translation") {
line.translatedLyric = wordEl.innerHTML;
} else if (role === "x-roman") {
line.romanLyric = wordEl.innerHTML;
} else if (role === "x-singer") {
line.singer = wordEl.innerHTML;
}
} else if (wordEl.hasAttribute("begin") && wordEl.hasAttribute("end")) {
const word: LyricWord = {
word: wordNode.textContent ?? "",
startTime: parseTimespan(wordEl.getAttribute("begin") ?? ""),
endTime: parseTimespan(wordEl.getAttribute("end") ?? ""),
};
const emptyBeat = wordEl.getAttribute("amll:empty-beat");
if (emptyBeat) {
word.emptyBeat = Number(emptyBeat);
}
line.words.push(word);
}
}
}
if (line.isBG) {
const firstWord = line.words?.[0];
if (firstWord?.word.startsWith("(")) {
firstWord.word = firstWord.word.substring(1);
if (firstWord.word.length === 0) {
line.words.shift();
}
}
const lastWord = line.words?.[line.words.length - 1];
if (lastWord?.word.endsWith(")")) {
lastWord.word = lastWord.word.substring(0, lastWord.word.length - 1);
if (lastWord.word.length === 0) {
line.words.pop();
}
}
}
const startTime = lineEl.getAttribute("begin");
const endTime = lineEl.getAttribute("end");
if (startTime && endTime) {
line.startTime = parseTimespan(startTime);
line.endTime = parseTimespan(endTime);
} else {
line.startTime = line.words
.filter((v) => v.word.trim().length > 0)
.reduce((pv, cv) => Math.min(pv, cv.startTime), Infinity);
line.endTime = line.words
.filter((v) => v.word.trim().length > 0)
.reduce((pv, cv) => Math.max(pv, cv.endTime), 0);
}
if (haveBg) {
const bgLine = lyricLines.pop();
lyricLines.push(line);
if (bgLine) lyricLines.push(bgLine);
} else {
lyricLines.push(line);
}
}
for (const lineEl of ttmlDoc.querySelectorAll("body p[begin][end]")) {
parseParseLine(lineEl);
}
return {
metadata,
lyricLines: lyricLines,
};
}

View File

@ -1,27 +0,0 @@
export interface TTMLMetadata {
key: string;
value: string[];
}
export interface TTMLLyric {
metadata: TTMLMetadata[];
lyricLines: LyricLine[];
}
export interface LyricWord {
startTime: number;
endTime: number;
word: string;
emptyBeat?: number;
}
export interface LyricLine {
words: LyricWord[];
translatedLyric: string;
romanLyric: string;
isBG: boolean;
isDuet: boolean;
startTime: number;
endTime: number;
singer: string;
}

View File

@ -1,33 +0,0 @@
export interface ScriptItem{
end: number;
chorus?: string;
start: number;
text: string;
words?: LyricWord[];
translation?: string;
singer?: string;
}
export interface LyricWord {
startTime: number;
endTime: number;
word: string;
emptyBeat?: number;
}
export interface LyricMetadata {
ar?: string;
ti?: string;
al?: string;
au?: string;
length?: string;
offset?: string;
tool?: string;
ve?: string;
}
export interface LyricData extends LyricMetadata {
scripts?: ScriptItem[];
[key: string]: any;
}

View File

@ -1,52 +0,0 @@
import { describe, expect, it } from 'vitest';
import fs from 'fs';
import { parseLRC } from '@core/lyrics/lrc/parser';
describe('LRC parser test', () => {
const test01Buffer = fs.readFileSync('./packages/core/test/lyrics/resources/test-01.lrc');
const test01Text = test01Buffer.toString('utf-8');
const test03Buffer = fs.readFileSync('./packages/core/test/lyrics/resources/test-03.lrc');
const test03Text = test03Buffer.toString('utf-8');
const lf_alternatives = ['\n', '\r\n', '\r'];
it('Parses test-01.lrc', () => {
for (const lf of lf_alternatives) {
const text = test01Text.replaceAll('\n', lf);
const result = parseLRC(text, { wordDiv: '', strict: true });
expect(result.ar).toBe("洛天依");
expect(result.ti).toBe("中华少女·终");
expect(result.al).toBe("中华少女");
expect(result["tool"]).toBe("歌词滚动姬 https://lrc-maker.github.io");
expect(result.scripts!![1].text).toBe("因果与恩怨牵杂等谁来诊断");
expect(result.scripts!![1].start).toBe(49000 + 588);
}
})
it('Parses test-03.lrc', () => {
const result = parseLRC(test03Text, { wordDiv: ' ', strict: true });
expect(result.scripts!![5].text).toBe("བྲོ་ར་འདི་ལ་བྲོ་ཅིག་འཁྲབ།");
expect(result.scripts!![5].translation).toBe("在舞池里舞一舞");
expect(result.scripts!![6].text).toBe("祝祷转过千年 五色经幡飘飞");
expect(result.scripts!![6].singer).toBe("a");
expect(result.scripts!![11].singer).toBeUndefined();
expect(result.scripts!![11].translation).toBe("我们在此相聚");
});
it('Accepts some weird but parsable LRCs', () => {
const cases = [
"[ti: []]",
"[ar: [<]]",
"[ar: <ar>]",
"[ar: a b c]",
"[00:00.00] <00:00.04> When the <00:00.16> the",
"[00:00.00] [00:00.04] When [00:00.16] the",
"[00:00.0000000] <00:00.04> When <00:00.16> the",
"[00:00.00] <00:00.04> [When] <00:00.16> the",
];
for (const c of cases) {
expect(() => parseLRC(c, { strict: false })).not.toThrow();
}
})
});

View File

@ -1,56 +0,0 @@
[ti: 中华少女·终]
[ar: 洛天依]
[al: 中华少女]
[tool: 歌词滚动姬 https://lrc-maker.github.io]
[00:46.706] 我想要仗剑天涯却陷入纷乱
[00:49.588] 因果与恩怨牵杂等谁来诊断
[00:52.284] 暗箭在身后是否该回身看
[00:55.073] 人心有了鬼心房便要过鬼门关
[00:57.875] 早已茫然染了谁的血的这抹长衫
[01:00.702] 独木桥上独目瞧的人一夫当关
[01:03.581] 复仇或是诅咒缠在身上的宿命
[01:06.591] 棋子在棋盘被固定移动不停转
[01:09.241] 仇恨与仇恨周而复始往返
[01:12.586] 酒楼深胭脂一点分隔光暗
[01:15.205] 数求问天涯海角血债偿还
[01:18.015] 终是神念迷茫只做旁观
[01:21.087] 是非恩怨三生纠葛轮转
[01:23.709] 回望从前苦笑将杯酒斟满
[01:26.573] 那时明月今日仍旧皎洁
[01:29.115] 只叹换拨人看
[01:31.024] 你可是这样的少年
[01:33.971] 梦想着穿越回从前
[01:36.554] 弦月下着青衫抚长剑
[01:42.341] 风起时以血绘长卷
[01:45.276] 三寸剑只手撼江山
[01:47.838] 拂衣去逍遥天地间
[01:52.991]
[02:16.707] 黄藓绿斑苔痕将岁月扒谱
[02:20.077] 望眼欲穿你何时寄来家书
[02:22.788] 踱步间院落飞絮聚散化作愁字
[02:25.601] 当泪水成河能否凫上一位游子
[02:28.050] 当庭间嫣红轻轻闻着雨声
[02:30.841] 电闪雷鸣院中青翠摇曳倚风
[02:33.362] 青丝落指尖模糊记忆更为清晰
[02:36.334] 那一朵英姿遮挡于一抹旌旗飘
[02:39.511] 厮杀与厮杀周而复始招摇
[02:42.576] 血渍滑落于枪尖映照宵小
[02:45.726] 城池下红莲飞溅绽放照耀
[02:48.509] 碧落黄泉再无叨扰
[02:51.338] 北风呼啸三生等待轮转
[02:53.660] 山崖古道思绪被光阴晕染
[02:56.895] 那时明月今日仍旧皎洁
[02:59.293] 只叹孤身人看
[03:01.335] 你可是这样的少年
[03:04.377] 梦想着穿越回从前
[03:06.924] 北风里铁衣冷槊光寒
[03:12.607] 一朝去大小三百战
[03:15.623] 岁月欺万里定江山
[03:18.126] 再与她同游天地间
[03:24.356] 说书人或许会留恋
[03:27.057] 但故事毕竟有终点
[03:29.590] 最好的惊堂木是时间
[03:35.157] 就让我合上这书卷
[03:38.242] 愿那些梦中的玩伴
[03:40.857] 梦醒后仍然是少年
[03:46.139]

View File

@ -1,77 +0,0 @@
[ti: 雪山之眼]
[ar: 洛天依 & 旦增益西]
[al: 游四方]
[tool: 歌词滚动姬 https://lrc-maker.github.io]
[length: 04:17.400]
[00:34.280] 浸透了经卷 记忆的呼喊
[00:37.800] 雪珠滚落山巅 栽下一个春天
[00:47.390] 松石敲响玲珑清脆的银花
[00:51.600] 穿过玛瑙的红霞
[00:54.430] 在她眼中结编 亘久诗篇
[01:05.440] a: བྲོ་ར་འདི་ལ་བྲོ་ཅིག་འཁྲབ། | 在舞池里舞一舞
[01:08.780] a: 祝祷转过千年 五色经幡飘飞
[01:12.040] 奏起悠扬巴叶 任岁月拨弦
[01:19.130] གཞས་ར་འདི་ལ་གཞས་གཅིག་བཏང་། 我在歌坛献首歌
[01:22.330] 宫殿 塔尖 彩绘 日月 同辉
[01:25.810] 那层厚重壁垒化身 蝉翼一片
[01:29.110] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། | 我们在此相聚
[01:30.790] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
[01:32.510] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
[01:34.120] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
[01:35.920] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
[01:37.630] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
[01:39.350] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
[01:41.050] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
[01:42.740] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
[01:44.630] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
[01:46.280] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
[01:48.010] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
[01:49.600] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
[01:51.380] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
[01:53.070] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
[01:54.820] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
[01:58.580] སྔོན་དང་པོ་གྲུབ་ཐོབ་ཐང་སྟོང་རྒྱལ་པོས་མཛད་པའི་མཛད་ཚུལ་དུ། དང་པོ་རྔོན་པའི་ས་སྦྱངས་ས་འདུལ། གཉིས་པ་རྒྱ་ལུའི་བྱིན་འབེབས། གསུམ་པ་ལྷ་མོའི་གླུ་གར་སོགས་རིན་ཆེན་གསུང་མགུར་གཞུང་བཟང་མང་པོ་འདུག་སྟེ། དེ་ཡང་མ་ཉུང་གི་ཚིག་ལ་དུམ་མཚམས་གཅིག་ཞུས་པ་བྱུང་བ་ཡིན་པ་ལགས་སོ། 如祖师唐东杰布所著,一有温巴净地,二有甲鲁祈福,三有仙女歌舞,所著繁多,在此简略献之。
[02:24.240] 浸透了经卷 记忆的呼喊
[02:27.450] 雪珠滚落山巅 栽下一个春天
[02:37.090] 松石敲响玲珑清脆的银花
[02:41.280] 穿过玛瑙的红霞
[02:44.010] 在她眼中结编 亘久诗篇
[02:55.250] བྲོ་ར་འདི་ལ་བྲོ་ཅིག་འཁྲབ། 在舞池里舞一舞
[02:58.410] 祝祷转过千年 五色经幡飘飞
[03:01.750] 奏起悠扬巴叶 任岁月拨弦
[03:08.840] གཞས་ར་འདི་ལ་གཞས་གཅིག་བཏང་། 我在歌坛献首歌
[03:12.050] 宫殿 塔尖 彩绘 日月 同辉
[03:15.400] 那层厚重壁垒化身 蝉翼一片
[03:18.850] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
[03:20.480] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
[03:22.210] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
[03:23.910] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
[03:25.662] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
[03:27.391] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
[03:29.096] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
[03:30.789] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
[03:32.496] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
[03:34.175] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
[03:35.876] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
[03:37.606] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
[03:39.290] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
[03:41.030] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
[03:42.679] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
[03:44.455] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
[03:46.176] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
[03:47.910] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
[03:49.625] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
[03:51.293] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
[03:53.005] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
[03:54.742] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
[03:56.479] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
[03:58.159] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
[03:59.859] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
[04:01.548] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
[04:03.312] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
[04:05.026] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
[04:06.721] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
[04:08.479] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
[04:10.175] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
[04:11.923] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
[04:17.400]

View File

@ -1,6 +1,6 @@
<script lang="ts">
import createSpring from '@core/graphics/spring';
import type { ScriptItem } from '@core/lyrics/type';
import type { ScriptItem } from '@alikia/aqualyrics';
import type { LyricPos } from './type';
import type { Spring } from '@core/graphics/spring/spring';