fork(fix): Clone AMLL 并修复 BUG

- 将AMLL Clone到本以地进行修复和优化(emm虽然这很不优雅但是暂时无时间做子模块和Fork)
- 修复在当前播放歌词行不可见的视口Seek会出现滚动偏移的问题
This commit is contained in:
lqtmcstudio
2026-06-07 00:02:14 +08:00
parent 783d2c3dee
commit 72f4510dc8
458 changed files with 86075 additions and 1665 deletions

View File

@@ -0,0 +1,109 @@
/**
* @fileoverview ASS 字幕格式导出。
* 注意导出会损失 10ms 以内精度(按厘秒四舍五入)。
*
* 格式示例:
* Dialogue: 0,0:00:12.34, 0:00:15.67, Default, v1,0,0,0,,{\k20}Hello{\k15} world
* Dialogue: 0,0:00:12.34, 0:00:15.67, Default, v1-trans,0,0,0,,你好 世界
* Dialogue: 0,0:00:12.34, 0:00:15.67, Default, v1-roman,0,0,0,,ni hao shi jie
*/
import type { LyricLine } from "../types";
import { normalizeTimestamp } from "../utils";
function writeASSTimestamp(ms: number): string {
const normalized = normalizeTimestamp(ms);
const milli = Math.round(normalized) % 1000;
const secTotal = Math.floor(Math.round(normalized) / 1000);
const sec = secTotal % 60;
const minTotal = Math.floor(secTotal / 60);
const hour = Math.floor(minTotal / 60);
return `${hour}:${String(minTotal % 60).padStart(2, "0")}:${String(sec).padStart(2, "0")}.${String(Math.floor(milli / 10)).padStart(2, "0")}`;
}
function getSpeakerName(line: LyricLine): string {
let name = line.isDuet ? "v2" : "v1";
if (line.isBG) name += "-bg";
return name;
}
function writeLyricDialogue(
result: string[],
startTime: number,
endTime: number,
name: string,
text: string,
) {
result.push(
`Dialogue: 0,${writeASSTimestamp(startTime)}, ${writeASSTimestamp(endTime)}, Default, ${name},0,0,0,,${text}`,
);
}
/**
* 将歌词数组转换为 ASS 字幕格式字符串
* @param lines 歌词数组
* @returns ASS 字幕格式字符串
*/
export function stringifyAss(lines: LyricLine[]): string {
const result: string[] = [
"[Script Info]",
"[Events]",
"Formats: Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text",
];
for (const line of lines) {
const timedWords = line.words
.map((w) => ({
...w,
startTime: normalizeTimestamp(w.startTime),
endTime: normalizeTimestamp(w.endTime),
}))
.filter((w) => w.endTime > w.startTime);
const startTime = Math.min(...timedWords.map((w) => w.startTime));
const endTime = Math.max(...timedWords.map((w) => w.endTime));
if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) continue;
let lyricText = "";
let previousWordEndTime = startTime;
for (const word of line.words) {
const wordStart = normalizeTimestamp(word.startTime);
const wordEnd = normalizeTimestamp(word.endTime);
if (wordStart >= wordEnd) {
lyricText += word.word;
continue;
}
if (wordStart > previousWordEndTime) {
const gapDurationCS = Math.floor(
(wordStart - previousWordEndTime + 5) / 10,
);
if (gapDurationCS > 0) lyricText += `{\\k${gapDurationCS}}`;
}
const wordDurationCS = Math.floor((wordEnd - wordStart + 5) / 10);
if (wordDurationCS > 0) lyricText += `{\\k${wordDurationCS}}`;
lyricText += word.word;
previousWordEndTime = wordEnd;
}
const speaker = getSpeakerName(line);
writeLyricDialogue(result, startTime, endTime, speaker, lyricText);
if (line.translatedLyric)
writeLyricDialogue(
result,
startTime,
endTime,
`${speaker}-trans`,
line.translatedLyric,
);
if (line.romanLyric)
writeLyricDialogue(
result,
startTime,
endTime,
`${speaker}-roman`,
line.romanLyric,
);
}
return `${result.join("\n")}\n`;
}