Skip to content

Commit

Permalink
Postpone ASS generation (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
zhxie authored Aug 5, 2022
1 parent 895d438 commit d80d939
Show file tree
Hide file tree
Showing 15 changed files with 217 additions and 105 deletions.
10 changes: 10 additions & 0 deletions config/example.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@
// -ac 1 -y "${output}"`.
"script": ""
},
"subtitle": {
// The style of subtitles. It can be one of the `traditional` or `karaoke`.
//
// * `traditional`: render subtitles by lines.
// * `karaoke`: render subtitles by words. Certain lyrics without karaoke
// format support will fallback to `traditional` rendering.
//
// If it is unspecified, `karaoke` will be used by default.
"style": "karaoke"
},
"providers": {
"mv": {
"bilibili": {
Expand Down
7 changes: 5 additions & 2 deletions server/components/downloader.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,13 @@ class Downloader {
this.downloading = true;
entry.onDownload();
const lyrics = entry.lyrics();
const lyricsPath = `${this.location}/${lyrics.id()}.ass`;
const lyricsPath = `${this.location}/${lyrics.id()}.json`;
if (!fs.existsSync(lyricsPath)) {
try {
fs.writeFileSync(lyricsPath, await lyrics.formattedLyrics());
fs.writeFileSync(
lyricsPath,
JSON.stringify(await lyrics.formattedLyrics())
);
} catch (e) {
console.error(e);
// Clean up.
Expand Down
1 change: 1 addition & 0 deletions server/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { default as Database } from "./database";
export { default as Downloader } from "./downloader";
export { default as Encoder } from "./encoder";
export { default as Player } from "./player";
export { default as Subtitler } from "./subtitler";
151 changes: 151 additions & 0 deletions server/components/subtitler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { Style } from "../models/lyrics/common/lyrics";
import { padStart } from "../utils";

const compileWord = (word) => {
const duration = Math.round((word["endTime"] - word["startTime"]) * 100);
return `{\\K${duration}}${word["word"]}`;
};

const formatTime = (time) => {
const hour = parseInt(String(time / 3600));
const min = parseInt(String((time - 3600 * hour) / 60));
const sec = parseInt(String(time - 3600 * hour - 60 * min));
const mil = Math.min(
Math.round((time - 3600 * hour - 60 * min - sec) * 100),
99
);
return `${hour}:${padStart(min, 2)}:${padStart(sec, 2)}.${padStart(mil, 2)}`;
};

const formatColor = (color) => {
return color.toUpperCase().replace("#", "&H");
};

class Subtitler {
ASS_STYLES = ["K1", "K2"];

style = Style.Karaoke;
// TODO: These constants should be configurable.
ADVANCE = 5;
DELAY = 1;
FONTSIZE = 24;
PRIMARY_COLOR = "#000000FF";
SECONDARY_COLOR = "#00FFFFFF";
OUTLINE_COLOR = "#00000000";
BACKGROUND_COLOR = "#00000000";
BOLD = false;
OUTLINE = 2;
SHADOW = 0;

constructor(style) {
this.style = style;
}

compile = (lines, lyrics) => {
const style = this.bestStyle(lyrics.style());
const dialogues = this.dialogues(lines, style);
const header = this.header(lyrics.title());
return `${header}\n${dialogues}`;
};

header = (title) => {
// prettier-ignore
return `[Script Info]
Title: ${title}
ScriptType: v4.00+
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
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
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
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
`;
};

dialogue = (line, style, assStyle, advance) => {
let words;
switch (style) {
case Style.Traditional:
words = `{\\K${Math.round(advance * 100)}}{\\K0}`;
words += line["line"];
break;
case Style.Karaoke:
words = `{\\K${Math.round(advance * 100)}}`;
words += line["words"].map(compileWord).join("");
break;
default:
throw new Error(`unexpected style "${style}"`);
}
return `Dialogue: 0,${formatTime(line["startTime"] - advance)},${formatTime(
line["endTime"] + this.DELAY
)},${assStyle},,0,0,0,,${words}`;
};

dialogues = (lines, style) => {
let result = [];
let displays = new Array(this.ASS_STYLES.length).fill(0);
for (const line of lines) {
// Escape empty lines.
if (!line["line"]) {
continue;
}

// Identify new paragraphs.
let newParagraph = false;
const lastEndTime = Math.max(...displays);
if (lastEndTime < line["startTime"] - this.ADVANCE) {
// Assert there is a new paragraph if there are more than `advance`
// blank.
newParagraph = true;
}

// Calculate lyrics show in advance time.
const priorEndTime = Math.min(...displays);
let index = 0;
if (!newParagraph) {
index = displays.indexOf(priorEndTime);
}
let advance = Math.max(line["startTime"] - priorEndTime, 0);
if (newParagraph) {
advance = Math.min(advance, this.ADVANCE);
}

const assStyle = this.ASS_STYLES[index];
result.push(this.dialogue(line, style, assStyle, advance));

// Clean up.
if (newParagraph) {
displays.fill(line["startTime"] - advance);
}
displays[index] = line["endTime"] + this.DELAY;
}
return result.join("\n");
};

bestStyle = (style) => {
switch (style) {
case Style.Traditional:
switch (this.style) {
case Style.Traditional:
case Style.Karaoke:
return Style.Traditional;
default:
throw new Error(`unexpected style "${this.style}"`);
}
case Style.Karaoke:
switch (this.style) {
case Style.Traditional:
return Style.Traditional;
case Style.Karaoke:
return Style.Karaoke;
default:
throw new Error(`unexpected style "${this.style}"`);
}
default:
throw new Error(`unexpected style "${style}"`);
}
};
}

export default Subtitler;
50 changes: 13 additions & 37 deletions server/models/lyrics/common/lyrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,15 @@ class Word {
this.endTime = endTime;
}

compile = () => {
const duration = Math.round((this.endTime - this.startTime) * 100);
return `{\\K${duration}}${this.word}`;
format = () => {
return {
word: this.word,
startTime: this.startTime,
endTime: this.endTime,
};
};
}

const padTime = (timeComponent) => {
return String(timeComponent).padStart(2, "0");
};

const convertTime = (time) => {
const hour = parseInt(String(time / 3600));
const min = parseInt(String((time - 3600 * hour) / 60));
const sec = parseInt(String(time - 3600 * hour - 60 * min));
const mil = Math.min(
Math.round((time - 3600 * hour - 60 * min - sec) * 100),
99
);
return `${hour}:${padTime(min)}:${padTime(sec)}.${padTime(mil)}`;
};

class Line {
line;
startTime;
Expand All @@ -47,25 +35,13 @@ class Line {
this.endTime = endTime;
}

isEmpty = () => {
return this.line.trim().length === 0;
};

compile = (style, assStyle, advance, delay) => {
let words = this.line;
switch (style) {
case Style.Traditional:
break;
case Style.Karaoke:
words = `{\\K${Math.round(advance * 100)}}`;
words += this.words.map((value) => value.compile()).join("");
break;
default:
throw new Error(`unexpected style "${style}"`);
}
return `Dialogue: 0,${convertTime(this.startTime - advance)},${convertTime(
this.endTime + delay
)},${assStyle},,0,0,0,,${words}`;
format = () => {
return {
line: this.line,
startTime: this.startTime,
endTime: this.endTime,
words: this.words.map((value) => value.format()),
};
};
}

Expand Down
2 changes: 2 additions & 0 deletions server/models/lyrics/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { Style } from "./common";

export { Provider as PetitLyricsProvider } from "./petit-lyrics";
5 changes: 1 addition & 4 deletions server/models/lyrics/petit-lyrics/entry.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import fetch from "node-fetch";
import { parseStringPromise as parseXMLString } from "xml2js";
import { Line, Style, Word } from "../common";
import { compile, header } from "../utils";

const NAME = "petit_lyrics";

Expand Down Expand Up @@ -74,7 +73,6 @@ class Entry {
};

formattedLyrics = async () => {
const h = header(this.title_);
const rawLyrics = await this.rawLyrics();
const text = await parseXMLString(rawLyrics);

Expand All @@ -96,8 +94,7 @@ class Entry {
}
ls.push(l);
}
const result = compile(this.style(), ls);
return `${h}${result}`;
return ls.map((value) => value.format());
};

rawLyrics = async () => {
Expand Down
43 changes: 0 additions & 43 deletions server/models/lyrics/utils/compile.js

This file was deleted.

16 changes: 0 additions & 16 deletions server/models/lyrics/utils/header.js

This file was deleted.

2 changes: 0 additions & 2 deletions server/models/lyrics/utils/index.js

This file was deleted.

Loading

0 comments on commit d80d939

Please sign in to comment.