Skip to content

Commit d80d939

Browse files
authored
Postpone ASS generation (#64)
1 parent 895d438 commit d80d939

File tree

15 files changed

+217
-105
lines changed

15 files changed

+217
-105
lines changed

config/example.jsonc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@
4848
// -ac 1 -y "${output}"`.
4949
"script": ""
5050
},
51+
"subtitle": {
52+
// The style of subtitles. It can be one of the `traditional` or `karaoke`.
53+
//
54+
// * `traditional`: render subtitles by lines.
55+
// * `karaoke`: render subtitles by words. Certain lyrics without karaoke
56+
// format support will fallback to `traditional` rendering.
57+
//
58+
// If it is unspecified, `karaoke` will be used by default.
59+
"style": "karaoke"
60+
},
5161
"providers": {
5262
"mv": {
5363
"bilibili": {

server/components/downloader.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,13 @@ class Downloader {
7676
this.downloading = true;
7777
entry.onDownload();
7878
const lyrics = entry.lyrics();
79-
const lyricsPath = `${this.location}/${lyrics.id()}.ass`;
79+
const lyricsPath = `${this.location}/${lyrics.id()}.json`;
8080
if (!fs.existsSync(lyricsPath)) {
8181
try {
82-
fs.writeFileSync(lyricsPath, await lyrics.formattedLyrics());
82+
fs.writeFileSync(
83+
lyricsPath,
84+
JSON.stringify(await lyrics.formattedLyrics())
85+
);
8386
} catch (e) {
8487
console.error(e);
8588
// Clean up.

server/components/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export { default as Database } from "./database";
22
export { default as Downloader } from "./downloader";
33
export { default as Encoder } from "./encoder";
44
export { default as Player } from "./player";
5+
export { default as Subtitler } from "./subtitler";

server/components/subtitler.js

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { Style } from "../models/lyrics/common/lyrics";
2+
import { padStart } from "../utils";
3+
4+
const compileWord = (word) => {
5+
const duration = Math.round((word["endTime"] - word["startTime"]) * 100);
6+
return `{\\K${duration}}${word["word"]}`;
7+
};
8+
9+
const formatTime = (time) => {
10+
const hour = parseInt(String(time / 3600));
11+
const min = parseInt(String((time - 3600 * hour) / 60));
12+
const sec = parseInt(String(time - 3600 * hour - 60 * min));
13+
const mil = Math.min(
14+
Math.round((time - 3600 * hour - 60 * min - sec) * 100),
15+
99
16+
);
17+
return `${hour}:${padStart(min, 2)}:${padStart(sec, 2)}.${padStart(mil, 2)}`;
18+
};
19+
20+
const formatColor = (color) => {
21+
return color.toUpperCase().replace("#", "&H");
22+
};
23+
24+
class Subtitler {
25+
ASS_STYLES = ["K1", "K2"];
26+
27+
style = Style.Karaoke;
28+
// TODO: These constants should be configurable.
29+
ADVANCE = 5;
30+
DELAY = 1;
31+
FONTSIZE = 24;
32+
PRIMARY_COLOR = "#000000FF";
33+
SECONDARY_COLOR = "#00FFFFFF";
34+
OUTLINE_COLOR = "#00000000";
35+
BACKGROUND_COLOR = "#00000000";
36+
BOLD = false;
37+
OUTLINE = 2;
38+
SHADOW = 0;
39+
40+
constructor(style) {
41+
this.style = style;
42+
}
43+
44+
compile = (lines, lyrics) => {
45+
const style = this.bestStyle(lyrics.style());
46+
const dialogues = this.dialogues(lines, style);
47+
const header = this.header(lyrics.title());
48+
return `${header}\n${dialogues}`;
49+
};
50+
51+
header = (title) => {
52+
// prettier-ignore
53+
return `[Script Info]
54+
Title: ${title}
55+
ScriptType: v4.00+
56+
57+
[V4+ Styles]
58+
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
59+
Style: K1,Source Han Serif,${this.FONTSIZE},${formatColor(this.PRIMARY_COLOR)},${formatColor(this.SECONDARY_COLOR)},${formatColor(this.OUTLINE_COLOR)},${formatColor(this.BACKGROUND_COLOR)},${this.BOLD ? 1 : 0},0,0,0,100,100,0,0,1,${this.OUTLINE},${this.SHADOW},1,60,30,80,1
60+
Style: K2,Source Han Serif,${this.FONTSIZE},${formatColor(this.PRIMARY_COLOR)},${formatColor(this.SECONDARY_COLOR)},${formatColor(this.OUTLINE_COLOR)},${formatColor(this.BACKGROUND_COLOR)},${this.BOLD ? 1 : 0},0,0,0,100,100,0,0,1,${this.OUTLINE},${this.SHADOW},3,30,60,40,1
61+
[Events]
62+
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
63+
`;
64+
};
65+
66+
dialogue = (line, style, assStyle, advance) => {
67+
let words;
68+
switch (style) {
69+
case Style.Traditional:
70+
words = `{\\K${Math.round(advance * 100)}}{\\K0}`;
71+
words += line["line"];
72+
break;
73+
case Style.Karaoke:
74+
words = `{\\K${Math.round(advance * 100)}}`;
75+
words += line["words"].map(compileWord).join("");
76+
break;
77+
default:
78+
throw new Error(`unexpected style "${style}"`);
79+
}
80+
return `Dialogue: 0,${formatTime(line["startTime"] - advance)},${formatTime(
81+
line["endTime"] + this.DELAY
82+
)},${assStyle},,0,0,0,,${words}`;
83+
};
84+
85+
dialogues = (lines, style) => {
86+
let result = [];
87+
let displays = new Array(this.ASS_STYLES.length).fill(0);
88+
for (const line of lines) {
89+
// Escape empty lines.
90+
if (!line["line"]) {
91+
continue;
92+
}
93+
94+
// Identify new paragraphs.
95+
let newParagraph = false;
96+
const lastEndTime = Math.max(...displays);
97+
if (lastEndTime < line["startTime"] - this.ADVANCE) {
98+
// Assert there is a new paragraph if there are more than `advance`
99+
// blank.
100+
newParagraph = true;
101+
}
102+
103+
// Calculate lyrics show in advance time.
104+
const priorEndTime = Math.min(...displays);
105+
let index = 0;
106+
if (!newParagraph) {
107+
index = displays.indexOf(priorEndTime);
108+
}
109+
let advance = Math.max(line["startTime"] - priorEndTime, 0);
110+
if (newParagraph) {
111+
advance = Math.min(advance, this.ADVANCE);
112+
}
113+
114+
const assStyle = this.ASS_STYLES[index];
115+
result.push(this.dialogue(line, style, assStyle, advance));
116+
117+
// Clean up.
118+
if (newParagraph) {
119+
displays.fill(line["startTime"] - advance);
120+
}
121+
displays[index] = line["endTime"] + this.DELAY;
122+
}
123+
return result.join("\n");
124+
};
125+
126+
bestStyle = (style) => {
127+
switch (style) {
128+
case Style.Traditional:
129+
switch (this.style) {
130+
case Style.Traditional:
131+
case Style.Karaoke:
132+
return Style.Traditional;
133+
default:
134+
throw new Error(`unexpected style "${this.style}"`);
135+
}
136+
case Style.Karaoke:
137+
switch (this.style) {
138+
case Style.Traditional:
139+
return Style.Traditional;
140+
case Style.Karaoke:
141+
return Style.Karaoke;
142+
default:
143+
throw new Error(`unexpected style "${this.style}"`);
144+
}
145+
default:
146+
throw new Error(`unexpected style "${style}"`);
147+
}
148+
};
149+
}
150+
151+
export default Subtitler;

server/models/lyrics/common/lyrics.js

Lines changed: 13 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,15 @@ class Word {
1414
this.endTime = endTime;
1515
}
1616

17-
compile = () => {
18-
const duration = Math.round((this.endTime - this.startTime) * 100);
19-
return `{\\K${duration}}${this.word}`;
17+
format = () => {
18+
return {
19+
word: this.word,
20+
startTime: this.startTime,
21+
endTime: this.endTime,
22+
};
2023
};
2124
}
2225

23-
const padTime = (timeComponent) => {
24-
return String(timeComponent).padStart(2, "0");
25-
};
26-
27-
const convertTime = (time) => {
28-
const hour = parseInt(String(time / 3600));
29-
const min = parseInt(String((time - 3600 * hour) / 60));
30-
const sec = parseInt(String(time - 3600 * hour - 60 * min));
31-
const mil = Math.min(
32-
Math.round((time - 3600 * hour - 60 * min - sec) * 100),
33-
99
34-
);
35-
return `${hour}:${padTime(min)}:${padTime(sec)}.${padTime(mil)}`;
36-
};
37-
3826
class Line {
3927
line;
4028
startTime;
@@ -47,25 +35,13 @@ class Line {
4735
this.endTime = endTime;
4836
}
4937

50-
isEmpty = () => {
51-
return this.line.trim().length === 0;
52-
};
53-
54-
compile = (style, assStyle, advance, delay) => {
55-
let words = this.line;
56-
switch (style) {
57-
case Style.Traditional:
58-
break;
59-
case Style.Karaoke:
60-
words = `{\\K${Math.round(advance * 100)}}`;
61-
words += this.words.map((value) => value.compile()).join("");
62-
break;
63-
default:
64-
throw new Error(`unexpected style "${style}"`);
65-
}
66-
return `Dialogue: 0,${convertTime(this.startTime - advance)},${convertTime(
67-
this.endTime + delay
68-
)},${assStyle},,0,0,0,,${words}`;
38+
format = () => {
39+
return {
40+
line: this.line,
41+
startTime: this.startTime,
42+
endTime: this.endTime,
43+
words: this.words.map((value) => value.format()),
44+
};
6945
};
7046
}
7147

server/models/lyrics/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
export { Style } from "./common";
2+
13
export { Provider as PetitLyricsProvider } from "./petit-lyrics";

server/models/lyrics/petit-lyrics/entry.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import fetch from "node-fetch";
22
import { parseStringPromise as parseXMLString } from "xml2js";
33
import { Line, Style, Word } from "../common";
4-
import { compile, header } from "../utils";
54

65
const NAME = "petit_lyrics";
76

@@ -74,7 +73,6 @@ class Entry {
7473
};
7574

7675
formattedLyrics = async () => {
77-
const h = header(this.title_);
7876
const rawLyrics = await this.rawLyrics();
7977
const text = await parseXMLString(rawLyrics);
8078

@@ -96,8 +94,7 @@ class Entry {
9694
}
9795
ls.push(l);
9896
}
99-
const result = compile(this.style(), ls);
100-
return `${h}${result}`;
97+
return ls.map((value) => value.format());
10198
};
10299

103100
rawLyrics = async () => {

server/models/lyrics/utils/compile.js

Lines changed: 0 additions & 43 deletions
This file was deleted.

server/models/lyrics/utils/header.js

Lines changed: 0 additions & 16 deletions
This file was deleted.

server/models/lyrics/utils/index.js

Lines changed: 0 additions & 2 deletions
This file was deleted.

0 commit comments

Comments
 (0)