KaraokeTestudaiBackend/subtitle_generator.py

135 lines
6.2 KiB
Python
Raw Normal View History

2024-12-17 21:14:21 +10:30
# Substation Alpha (ASS) generation
# Colour values are &HAABBGGRR, &HBBGGRR, or &HAA.
# Alpha is actually inverted, i.e. transparency - FF is transparent, 00 is opaque.
ass_preamble = '''[Script Info]
ScriptType: v4.00+
WrapStyle: 0
ScaledBorderAndShadow: yes
YCbCr Matrix: TV.709
PlayResX: {PlayResX}
PlayResY: {PlayResY}
[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: Default,Arial,72,&H002A0A00,&H000019FF,&H00FFFFFF,&H00000000,0,0,0,0,100,100,0,0,1,2.5,0,8,30,30,25,1
Style: Kanji,Migu 1P,{KanjiSize},&H{KaraokeColourPast},&H{KaraokeColourFuture},&H00FFFFFF,&H00000000,0,0,0,0,100,100,0,0,1,4.0,0,2,30,30,25,1
Style: Furigana,Migu 1P,{FuriSize},&H{KaraokeColourPast},&H{KaraokeColourFuture},&H00FFFFFF,&H00000000,0,0,0,0,100,100,0,0,1,2.5,0,2,30,30,0,1
Style: Romaji,Migu 1P,{RomajiSize},&H{KaraokeColourPast},&H{KaraokeColourFuture},&H00FFFFFF,&H00000000,0,0,0,0,100,100,0,0,1,2.5,0,8,30,30,25,1
Style: Translation,Migu 1P,{TranslationSize},&H00FFFFFF,&H000019FF,&H00000000,&H00000000,0,1,0,0,100,100,0,0,1,1.0,3,8,30,30,25,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
'''
format_defaults = {
'PlayResX': 1280,
'PlayResY': 720,
'TranslationSize': 48,
'RomajiSize': 60,
'KanjiSize': 72,
'FuriSize': 36,
'KaraokeColourFuture': '000019FF',
'KaraokeColourPast': 'E02A0A00',
}
def seconds_to_timestamp(t: float) -> str:
minutes, seconds = divmod(t, 60)
hours, minutes = divmod(minutes, 60)
return f'{int(hours):02}:{int(minutes):02}:{seconds:05.2f}'
2024-12-17 21:14:21 +10:30
from format import LyricTrack
from japanese_converters import kana_to_syllable_list
2024-12-17 21:14:21 +10:30
def generate_ass(filename: str, lyric_track: LyricTrack, format_overloads: dict = None):
format_dict = format_defaults.copy()
if format_overloads:
format_dict.update(format_overloads)
preamble = ass_preamble.format(**format_dict)
2024-12-17 21:14:21 +10:30
# Kanji Furigana layout stuff
size_kanji_x = format_dict['KanjiSize'] # TODO: work out scaling factor for fullwidth from point size
size_furi_x = format_dict['FuriSize'] # TODO: work out scaling factor for fullwidth from point size
res_x = format_dict['PlayResX']
2024-12-17 21:14:21 +10:30
with open(filename, 'w') as file:
file.write(preamble)
t = 68.0 # placeholder
for line in lyric_track.lines:
t0 = seconds_to_timestamp(t)
timestamps = line.get_timestamps(0.5, t)
centiseconds = line.get_karaoke_centiseconds(0.5)
t = timestamps[-1] + 1.0 # placeholder
t1 = seconds_to_timestamp(t)
sub_preamble = f'Dialogue: 0,{t0},{t1}'
# Translation line is easy and static
file.write(f'{sub_preamble},Translation,,,,,,{line.translated_line}\n')
# Romaji line is also easy, just intersperse durations
romaji_line = f'{{\\k{centiseconds[0]}}}'
i = 0 # syllable counter
for syl in line.romaji_syllables:
if not syl.strip():
romaji_line += f'{{\\k0}}{syl}'
continue
romaji_line += f'{{\\K{centiseconds[i+1]-centiseconds[i]}}}{syl}'
i += 1
file.write(f'{sub_preamble},Romaji,,,,,,{romaji_line}\n')
# Now for the kanji and furi lines...
kanji_plain_str = ''.join([b.kanji for b in line.furi_blocks])
full_kanji_width = len(kanji_plain_str) * size_kanji_x
kanji_line = f'{{\\k{centiseconds[0]}}}'
kanji_line_progress = 0 # increment as we go, to track furi position
furi_lines = []
i = 0 # syllable counter
for furi_block in line.furi_blocks:
if len(furi_block.furi) == 0: # kana or punctuation, nice and simple!
syls = kana_to_syllable_list(furi_block.kanji)
for syl in syls:
if len(syl.strip()) == 0: # don't time spaces
kanji_line += f'{{\\k0}}{syl}'
kanji_line_progress += len(syl)
else:
kanji_line += f'{{\\K{centiseconds[i+1]-centiseconds[i]}}}{syl}'
kanji_line_progress += len(syl)
i += 1
else: # Kanji block
i0 = i # Store this to later calculate block time for the kanji
syls = kana_to_syllable_list(furi_block.furi)
furi_line = f'{{\\k{centiseconds[i]}}}'
furi_chars = 0
for syl in syls:
furi_line += f'{{\\K{centiseconds[i+1]-centiseconds[i]}}}{syl}'
furi_chars += len(syl)
i += 1
# Need to calculate kanji block position and span to typeset the furigana above it
k = furi_block.kanji
k_start = kanji_line_progress
kanji_line_progress += len(k)
k_end = kanji_line_progress
target_middle_x = (size_kanji_x * (k_end+k_start)/2) - (full_kanji_width/2) # x=0 at center
furi_width = furi_chars * size_furi_x
margin_l = 0 if target_middle_x < 0 else int(target_middle_x*1.57)
margin_r = 0 if target_middle_x > 0 else int(-target_middle_x*1.57)
furi_lines.append(f'{sub_preamble},Furigana,,{margin_l},{margin_r},,,{furi_line}\n')
kanji_line += f'{{\\K{centiseconds[i]-centiseconds[i0]}}}{k}'
file.write(f'{sub_preamble},Kanji,,,,,,{kanji_line}\n')
for line in furi_lines:
file.write(line)
2024-12-17 21:14:21 +10:30
example_layout = r'''
2024-12-17 21:14:21 +10:30
Dialogue: 0,0:01:08.00,0:01:26.00,Kanji,,,,,,{\k0}{\K100}{\K100}{\K100}{\K100}{\K100}{\K100}から{\K100}{\K100}って{\K100}{\K100}{\K100}{\K100} {\K100}{\K100}{\K100}{\K100}{\K100}{\K100}
Dialogue: 0,0:01:08.00,0:01:26.00,Furigana,, 0,1130,,,{\k0}{\K100}あめ
Dialogue: 0,0:01:08.00,0:01:26.00,Furigana,, 0, 900,,,{\k200}{\K100}ゆき
Dialogue: 0,0:01:08.00,0:01:26.00,Furigana,, 0, 700,,,{\k400}{\K100}てん
Dialogue: 0,0:01:08.00,0:01:26.00,Furigana,, 0, 370,,,{\k600}{\K100}
Dialogue: 0,0:01:08.00,0:01:26.00,Furigana,, 0, 0,,,{\k800}{\K100}
Dialogue: 0,0:01:08.00,0:01:26.00,Furigana,, 260, 0,,,{\k1000}{\K100}うるお
Dialogue: 0,0:01:08.00,0:01:26.00,Furigana,, 570, 0,,,{\k1200}{\K100}
Dialogue: 0,0:01:08.00,0:01:26.00,Furigana,, 800, 0,,,{\k1400}{\K100}
Dialogue: 0,0:01:08.00,0:01:26.00,Translation,,,,,,Rain and snow fall from the heavens. Moisten the earth and make it sprout
Dialogue: 0,0:01:08.00,0:01:26.00,Romaji,,,,,,{\K0}{\K100}ame {\K100}ya {\K100}yuki {\K100}ga {\K100}ten {\K100}kara {\K100}fu{\K100}tte {\K100}chi {\K100}wo {\K100}uruo{\K100}shi {\K100}me {\K100}wo {\K100}da{\K100}sa {\K100}se{\K100}ru
'''