Placeholder timing lyrics output
This commit is contained in:
parent
af3ab162d4
commit
eed5178493
|
@ -16,6 +16,7 @@ const btn_download_subtitles = document.getElementById('btn_download_subtitles')
|
||||||
const req_tokenization_url = './tokenize';
|
const req_tokenization_url = './tokenize';
|
||||||
// let has_lyrics_changed = false
|
// let has_lyrics_changed = false
|
||||||
let tokenized_lyric_lines = [];
|
let tokenized_lyric_lines = [];
|
||||||
|
let arrangement_line_numbers = [];
|
||||||
let lyric_section_ranges = {}; // [start, end) line indices
|
let lyric_section_ranges = {}; // [start, end) line indices
|
||||||
const re_lyric_section = /\[(.+)\]/;
|
const re_lyric_section = /\[(.+)\]/;
|
||||||
|
|
||||||
|
@ -84,26 +85,24 @@ function on_receive_tokenized_lyrics(data) {
|
||||||
|
|
||||||
function update_arrangement_output() {
|
function update_arrangement_output() {
|
||||||
console.log('Updating arrangement output');
|
console.log('Updating arrangement output');
|
||||||
const tl_lines = lyrics_tl_input_element.value.split('\n');
|
arrangement_line_numbers = []
|
||||||
const arrangement_str = arrangement_input_element.value;
|
arrangement_input_element.value
|
||||||
var html = '';
|
.split(',')
|
||||||
arrangement_str.split(',').forEach(section => {
|
.map(s=>lyric_section_ranges[s.trim()])
|
||||||
const s = section.trim();
|
.filter(x=>x)
|
||||||
if (s in lyric_section_ranges) {
|
.forEach(([i0, i1])=>{
|
||||||
console.log(`Adding section [${s}]`);
|
for (let i=i0; i<i1; i++) arrangement_line_numbers.push(i);
|
||||||
|
});
|
||||||
|
|
||||||
// html += `[${s}]<br>`; // Section name is already in the line range :)
|
const tl_lines = lyrics_tl_input_element.value.split('\n');
|
||||||
const [i0, i1] = lyric_section_ranges[s];
|
var html = '';
|
||||||
for (let i = i0; i < i1; i++) {
|
for (const i of arrangement_line_numbers) {
|
||||||
if ((typeof tokenized_lyric_lines[i]) != "string") {
|
const tokenized_line = tokenized_lyric_lines[i];
|
||||||
if (i < tl_lines.length) html += `<span class="lyrics-tl">${tl_lines[i]}<br></span>`;
|
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]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`Cannot add section [${s}]`);
|
|
||||||
}
|
}
|
||||||
})
|
html += format_parsed_line(tokenized_line);
|
||||||
|
}
|
||||||
arrangement_output_element.innerHTML = html;
|
arrangement_output_element.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,7 +230,24 @@ btn_download_subtitles.addEventListener('click', () => {
|
||||||
import generate_subtitles_from_data from './subtitle_generator.js'
|
import generate_subtitles_from_data from './subtitle_generator.js'
|
||||||
function generate_subtitles() {
|
function generate_subtitles() {
|
||||||
console.log('Attempting to 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({
|
const subtitles = generate_subtitles_from_data({
|
||||||
song_title: document.getElementById('song_title').value,
|
song_title: document.getElementById('song_title').value,
|
||||||
song_title_en: document.getElementById('song_title_en').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: document.getElementById('song_composer').value,
|
||||||
song_composer_en: document.getElementById('song_composer_en').value,
|
song_composer_en: document.getElementById('song_composer_en').value,
|
||||||
title_fade_duration_ms: 2500,
|
title_fade_duration_ms: 2500,
|
||||||
|
lines: lines,
|
||||||
});
|
});
|
||||||
subtitle_editor_textarea.value = subtitles;
|
subtitle_editor_textarea.value = subtitles;
|
||||||
subtitle_editor_textarea.dispatchEvent(new Event('change', {})); // This triggers a reload in the player
|
subtitle_editor_textarea.dispatchEvent(new Event('change', {})); // This triggers a reload in the player
|
||||||
|
|
|
@ -3,6 +3,26 @@ function timecode(s) {
|
||||||
return new Date(s*1000).toISOString().substring(11, 22); // This can overflow at 24 hours. Who cares?
|
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) {
|
export default function generate_subtitles_from_data(data) {
|
||||||
const format_defaults = {
|
const format_defaults = {
|
||||||
'PlayResX': 1280,
|
'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
|
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]
|
[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
|
// Title card
|
||||||
const t0 = 0.0;
|
const t0 = 1.0;
|
||||||
const t1 = 80.0; // TODO: tie this to the first line time
|
const t1 = (data.lines?.[0]?.t0 ?? 11.0) - 1.0; // tie this to the first line time
|
||||||
const t0s = timecode(t0);
|
const t0s = timecode(t0);
|
||||||
const t1s = timecode(t1);
|
const t1s = timecode(t1);
|
||||||
const duration_seconds = t1-t0;
|
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_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})}`;
|
const fade = `{\\fade(255,0,255,0,${fade_duration_ms},${duration_ms-fade_duration_ms},${duration_ms})}`;
|
||||||
s += `
|
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_composer}
|
||||||
Dialogue: 0,${t0s},${t1s},TitleJP,,,,,,${fade}作詞 ${data.song_lyricist}
|
Dialogue: 0,${t0s},${t1s},TitleJP,,,,,,${fade}作詞 ${data.song_lyricist}
|
||||||
Dialogue: 0,${t0s},${t1s},TitleJP,,,,,,${fade}曲名 ${data.song_title}
|
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}Music: ${data.song_composer_en}
|
||||||
Dialogue: 0,${t0s},${t1s},Title,,,,,,${fade}Lyrics: ${data.song_lyricist_en}
|
Dialogue: 0,${t0s},${t1s},Title,,,,,,${fade}Lyrics: ${data.song_lyricist_en}
|
||||||
Dialogue: 0,${t0s},${t1s},Title,,,,,,${fade}${data.song_title_en}`;
|
Dialogue: 0,${t0s},${t1s},Title,,,,,,${fade}${data.song_title_en}
|
||||||
// TODO: add the lines
|
`;
|
||||||
|
|
||||||
|
// 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;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue