Placeholder timing lyrics output

This commit is contained in:
Luke Hubmayer-Werner 2024-12-19 21:57:51 +10:30
parent af3ab162d4
commit eed5178493
2 changed files with 149 additions and 24 deletions

View File

@ -16,6 +16,7 @@ const btn_download_subtitles = document.getElementById('btn_download_subtitles')
const req_tokenization_url = './tokenize';
// let has_lyrics_changed = false
let tokenized_lyric_lines = [];
let arrangement_line_numbers = [];
let lyric_section_ranges = {}; // [start, end) line indices
const re_lyric_section = /\[(.+)\]/;
@ -84,26 +85,24 @@ function on_receive_tokenized_lyrics(data) {
function update_arrangement_output() {
console.log('Updating arrangement output');
const tl_lines = lyrics_tl_input_element.value.split('\n');
const arrangement_str = arrangement_input_element.value;
var html = '';
arrangement_str.split(',').forEach(section => {
const s = section.trim();
if (s in lyric_section_ranges) {
console.log(`Adding section [${s}]`);
arrangement_line_numbers = []
arrangement_input_element.value
.split(',')
.map(s=>lyric_section_ranges[s.trim()])
.filter(x=>x)
.forEach(([i0, i1])=>{
for (let i=i0; i<i1; i++) arrangement_line_numbers.push(i);
});
// html += `[${s}]<br>`; // Section name is already in the line range :)
const [i0, i1] = lyric_section_ranges[s];
for (let i = i0; i < i1; i++) {
if ((typeof tokenized_lyric_lines[i]) != "string") {
if (i < tl_lines.length) html += `<span class="lyrics-tl">${tl_lines[i]}<br></span>`;
const tl_lines = lyrics_tl_input_element.value.split('\n');
var html = '';
for (const i of arrangement_line_numbers) {
const tokenized_line = tokenized_lyric_lines[i];
if ((typeof tokenized_line) != "string" && i < tl_lines.length) {
html += `<span class="lyrics-tl">${tl_lines[i]}<br></span>`;
}
html += format_parsed_line(tokenized_lyric_lines[i]);
html += format_parsed_line(tokenized_line);
}
} else {
console.log(`Cannot add section [${s}]`);
}
})
arrangement_output_element.innerHTML = html;
}
@ -231,7 +230,24 @@ btn_download_subtitles.addEventListener('click', () => {
import generate_subtitles_from_data from './subtitle_generator.js'
function generate_subtitles() {
console.log('Attempting to generate subtitles');
// TODO: Do something to generate subtitles
let lines = [];
let t0 = 72.0; // TODO: add line timing data
const seconds_per_line = 10.0;
const tl_lines = lyrics_tl_input_element.value.split('\n');
for (const i of arrangement_line_numbers) {
const tokenized_line = tokenized_lyric_lines[i];
if ((typeof tokenized_line) == "string") continue;
lines.push(Object.assign({
t0: t0,
t1: t0+seconds_per_line,
translated_line: tl_lines?.[i] ?? '',
}, tokenized_line));
t0 += seconds_per_line;
}
const subtitles = generate_subtitles_from_data({
song_title: document.getElementById('song_title').value,
song_title_en: document.getElementById('song_title_en').value,
@ -240,6 +256,7 @@ function generate_subtitles() {
song_composer: document.getElementById('song_composer').value,
song_composer_en: document.getElementById('song_composer_en').value,
title_fade_duration_ms: 2500,
lines: lines,
});
subtitle_editor_textarea.value = subtitles;
subtitle_editor_textarea.dispatchEvent(new Event('change', {})); // This triggers a reload in the player

View File

@ -3,6 +3,26 @@ function timecode(s) {
return new Date(s*1000).toISOString().substring(11, 22); // This can overflow at 24 hours. Who cares?
}
const kana_merge_previous_syllable = new Set('ゃゅょぁぃぇぉぅゎんャュョァィェォゥヮン'.split(''));
const kana_merge_next_syllable = new Set('っッ'.split(''));
function kana_to_syllable_list(kana) {
let a = [];
const len = kana.length;
for (let i = 0; i < len; i++) {
const k = kana[i];
if (kana_merge_next_syllable.has(k)) {
i++;
a.push(k + kana[i]);
} else if (kana_merge_previous_syllable.has(k)) {
a.push(a.pop() + k);
} else {
a.push(k);
}
}
console.log(a);
return a;
}
export default function generate_subtitles_from_data(data) {
const format_defaults = {
'PlayResX': 1280,
@ -39,10 +59,11 @@ Style: Title,Droid Sans,72,&HFFFFFF,&H0019FF,&H000000,&H000000,0,0,0,0,100,100,0
Style: TitleJP,Droid Sans Japanese,72,&HFFFFFF,&H0019FF,&H000000,&H000000,0,0,0,0,100,100,0,0,1,2.5,0,1,30,30,25,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text`;
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
`;
// Title card
const t0 = 0.0;
const t1 = 80.0; // TODO: tie this to the first line time
const t0 = 1.0;
const t1 = (data.lines?.[0]?.t0 ?? 11.0) - 1.0; // tie this to the first line time
const t0s = timecode(t0);
const t1s = timecode(t1);
const duration_seconds = t1-t0;
@ -50,13 +71,100 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text`
const fade_duration_ms = data.title_fade_duration_ms ?? 1000;
const fade = `{\\fade(255,0,255,0,${fade_duration_ms},${duration_ms-fade_duration_ms},${duration_ms})}`;
s += `
Comment: 0,00:00:00.00,00:01:20.00,,,,,,,Title Card
Comment: 0,${t0s},${t1s},,,,,,,Title Card
Dialogue: 0,${t0s},${t1s},TitleJP,,,,,,${fade}作曲 ${data.song_composer}
Dialogue: 0,${t0s},${t1s},TitleJP,,,,,,${fade}作詞 ${data.song_lyricist}
Dialogue: 0,${t0s},${t1s},TitleJP,,,,,,${fade}曲名 ${data.song_title}
Dialogue: 0,${t0s},${t1s},Title,,,,,,${fade}Music: ${data.song_composer_en}
Dialogue: 0,${t0s},${t1s},Title,,,,,,${fade}Lyrics: ${data.song_lyricist_en}
Dialogue: 0,${t0s},${t1s},Title,,,,,,${fade}${data.song_title_en}`;
// TODO: add the lines
Dialogue: 0,${t0s},${t1s},Title,,,,,,${fade}${data.song_title_en}
`;
// Kanji Furigana layout stuff
const pt_to_fullwidth = 55.0/72.0; // This seems right for DroidSansJapanese, probably different for each font
const size_kanji_x = format_defaults.KanjiSize * pt_to_fullwidth;
const size_furi_x = format_defaults.FuriSize * pt_to_fullwidth;
const res_x = format_defaults.PlayResX;
const res_xh = res_x/2;
// Add the lines
data.lines?.forEach(line => {
const t0 = line.t0;
const t1 = line.t1;
const t0s = timecode(t0);
const t1s = timecode(t1);
const duration_seconds = t1-t0;
const num_syllables = line.romaji_syllables.filter(x=>x.trim()).length;
const duration_cs = Math.floor(duration_seconds * 100); // For karaoke syllable fallback
const fallback_syl_duration_cs = duration_cs/num_syllables;
const centiseconds = line.arr_syllables_cs ?? Array(num_syllables+1).fill().map((_,i) => Math.floor(i*fallback_syl_duration_cs));
const sub_preamble = `Dialogue: 0,${t0s},${t1s}`;
const furi_preamble = `Dialogue: 1,${t0s},${t1s}`; // Different layer to avoid kanji collision detection
// Translation line is easy and static
s += `${sub_preamble},Translation,,,,,,${line.translated_line}\n`;
// Romaji line is also easy, just intersperse durations
let romaji_line = `{\\k${centiseconds[0]}}`;
let i = 0; // syllable counter
for (const syl of line.romaji_syllables) {
if (syl.trim()){ // not whtespace
romaji_line += `{\\K${centiseconds[i+1]-centiseconds[i]}}${syl}`;
i++;
} else { // whitespace
romaji_line += `{\\k0}${syl}`;
}
}
s += `${sub_preamble},Romaji,,,,,,${romaji_line}\n`;
// Now for the kanji and furi lines...
const kanji_plain_str = line.furi_blocks.map(b => b[0]).join('');
const full_kanji_width = kanji_plain_str.length * size_kanji_x;
let kanji_line = `{\\k${centiseconds[0]}}`;
let kanji_line_progress = 0; // increment as we go, to track furi position
let furi_lines = [];
i = 0; // syllable counter
for (const furi_block of line.furi_blocks) {
if (furi_block[1].length == 0) { // kana or punctuation, nice and simple!
let syls = kana_to_syllable_list(furi_block[0]);
for (const syl of syls) {
if (syl.trim().length == 0) { // don't time spaces
kanji_line += `{\\k0}${syl}`;
kanji_line_progress += syl.length;
} else {
kanji_line += `{\\K${centiseconds[i+1]-centiseconds[i]}}${syl}`;
kanji_line_progress += syl.length;
i++;
}
}
} else { // Kanji block
const i0 = i; // Store this to later calculate block time for the kanji
let syls = kana_to_syllable_list(furi_block[1]);
let furi_line = `{\\k${centiseconds[i]}}`;
let furi_chars = 0;
for (const syl of syls) {
furi_line += `{\\K${centiseconds[i+1]-centiseconds[i]}}${syl}`;
furi_chars += syl.length;
i++;
}
// Need to calculate kanji block position and span to typeset the furigana above it
const k = furi_block[0];
const k_start = kanji_line_progress;
kanji_line_progress += k.length;
const k_end = kanji_line_progress;
const target_middle_x = (size_kanji_x * (k_end+k_start)/2) - (full_kanji_width/2); // x=0 at center
const furi_width = furi_chars * size_furi_x;
const margin_l = Math.floor(res_xh+target_middle_x);
const margin_r = Math.floor(res_xh-target_middle_x);
furi_lines.push(`${furi_preamble},Furigana,,${margin_l},${margin_r},,,${furi_line}\n`);
kanji_line += `{\\K${centiseconds[i]-centiseconds[i0]}}${k}`;
}
};
s += `${sub_preamble},Kanji,,,,,,${kanji_line}\n`;
for (line of furi_lines) {
s += line;
}
});
return s;
}