Clipping / routers /subtitle_generator.py
aliSaac510's picture
save fix
7af5aaa
import os
from schemas import SubtitlePreset, WordTiming, SentenceTiming
def rgb_to_ass(hex_color):
"""ุชุญูˆูŠู„ ุฃู„ูˆุงู† Hex ุฅู„ู‰ ุตูŠุบุฉ ASS BGR"""
hex_color = hex_color.lstrip('#')
if len(hex_color) == 6:
r, g, b = hex_color[0:2], hex_color[2:4], hex_color[4:6]
return f"&H00{b}{g}{r}"
# Default to White if invalid
return "&H00FFFFFF"
def format_ass_time(seconds):
"""ุชุญูˆูŠู„ ุงู„ุซูˆุงู†ูŠ ุฅู„ู‰ ุชูˆู‚ูŠุช ASS ุฏู‚ูŠู‚ (H:MM:SS.cc)"""
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = seconds % 60
centiseconds = int(round((secs - int(secs)) * 100))
if centiseconds == 100:
secs += 1
centiseconds = 0
return f"{hours}:{minutes:02d}:{int(secs):02d}.{centiseconds:02d}"
def generate_pro_ass(transcription, preset, output_path):
"""ุชูˆู„ูŠุฏ ู…ู„ู ASS ุงุญุชุฑุงููŠ ู…ุน ุฏุนู… ูˆุถุน ุงู„ุฌู…ู„ ูˆุงู„ูƒู„ู…ุงุช"""
primary = rgb_to_ass(preset.primary_color)
secondary = rgb_to_ass(preset.secondary_color)
outline = rgb_to_ass(preset.outline_color)
# ุชุญูˆูŠู„ ุงู„ุดูุงููŠุฉ (0.0 - 1.0) ุฅู„ู‰ ุตูŠุบุฉ ASS (00 - FF)
# ู…ู„ุงุญุธุฉ: ููŠ ASSุŒ ุงู„ู€ 00 ู‡ูˆ ู…ุนุชู… ุชู…ุงู…ุงู‹ ูˆ FF ู‡ูˆ ุดูุงู ุชู…ุงู…ุงู‹
back_alpha = int((1.0 - getattr(preset, 'background_opacity', 1.0)) * 255)
# ุงุณุชุฎุฏุงู… ู„ูˆู† ุงู„ุฎู„ููŠุฉ ู…ู† ุงู„ู€ preset ุจุฏู„ุงู‹ ู…ู† ุงู„ุฃุณูˆุฏ ุงู„ุซุงุจุช
raw_back_color = rgb_to_ass(preset.back_color).replace("&H00", "")
back_color = f"&H{back_alpha:02X}{raw_back_color}"
spacing = getattr(preset, 'letter_spacing', 0.0)
angle = getattr(preset, 'rotation_angle', 0.0)
margin_h = getattr(preset, 'margin_h', 20)
header = f"""[Script Info]
ScriptType: v4.00+
PlayResX: 1080
PlayResY: 1920
ScaledBorderAndShadow: yes
[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: Main,{preset.font_name},{preset.font_size},{primary},{secondary},{outline},&H00000000,-1,0,0,0,100,100,{spacing},{angle},1,{preset.outline_width},{preset.shadow_depth},{preset.alignment},{margin_h},{margin_h},{preset.margin_v},1
Style: BackBox,{preset.font_name},{preset.font_size},{secondary},{secondary},&H00000000,{back_color},-1,0,0,0,100,100,{spacing},{angle},3,{getattr(preset, 'box_rounding', 10)},0,{preset.alignment},{margin_h},{margin_h},{preset.margin_v},1
"""
events = "\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
# ุชุญูˆูŠู„ ุงู„ุจูŠุงู†ุงุช ุฅู„ู‰ ุชู†ุณูŠู‚ ู…ูˆุญุฏ (SentenceTiming)
sentences = []
if transcription and isinstance(transcription[0], (dict, SentenceTiming)) and ('words' in transcription[0] or hasattr(transcription[0], 'words')):
for s in transcription:
sentences.append(s if hasattr(s, 'text') else SentenceTiming(**s))
else:
# ุฅุฐุง ูƒุงู†ุช ู‚ุงุฆู…ุฉ ูƒู„ู…ุงุช ูู‚ุทุŒ ู†ู‚ูˆู… ุจุชุฌู…ูŠุนู‡ุง ุจู†ุงุกู‹ ุนู„ู‰ max_words_per_line
current_words = []
for item in transcription:
word_data = item if hasattr(item, 'word') else WordTiming(**item)
current_words.append(word_data)
if len(current_words) >= preset.max_words_per_line:
sentences.append(SentenceTiming(
text=" ".join([w.word for w in current_words]),
start=current_words[0].start,
end=current_words[-1].end,
words=current_words
))
current_words = []
if current_words:
sentences.append(SentenceTiming(
text=" ".join([w.word for w in current_words]),
start=current_words[0].start,
end=current_words[-1].end,
words=current_words
))
for sentence in sentences:
s_start = format_ass_time(sentence.start)
s_end = format_ass_time(sentence.end)
if preset.display_mode == "word":
# ุงู„ูˆุถุน ุงู„ุชู‚ู„ูŠุฏูŠ: ูƒู„ู…ุฉ ุจูƒู„ู…ุฉ
for i, word_data in enumerate(sentence.words):
word_text = word_data.word.upper() if preset.uppercase else word_data.word
start_t = format_ass_time(word_data.start)
end_t = format_ass_time(word_data.end)
duration = word_data.end - word_data.start
t1 = min(60, int(duration * 150))
t2 = t1 + 80
scale = int(preset.pop_up_scale * 100)
# ุฅุถุงูุฉ ุชุฃุซูŠุฑ ุงู„ุชูˆู‡ุฌ (Blur) ุฅุฐุง ูƒุงู† ู…ูุนู„ุงู‹
glow_val = getattr(preset, 'glow_intensity', 0)
blur_tag = f"\\be{glow_val}" if glow_val > 0 else ""
# ุงู†ูŠู…ูŠุดู† ุงุญุชุฑุงููŠ ู…ุน ุฏูˆุฑุงู† ุฏูŠู†ุงู…ูŠูƒูŠ ุฃูˆ ุซุงุจุช
if angle != 0:
rotation = f"\\frz{angle}"
else:
rotation = "\\frz-2" if i % 2 == 0 else "\\frz2"
anim = f"{{\\fscx80\\fscy80{rotation}{blur_tag}\\t(0,{t1},0.3,\\fscx{scale}\\fscy{scale})\\t({t1},{t2},1.5,\\fscx100\\fscy100)}}"
if preset.back_box_enabled:
events += f"Dialogue: 0,{start_t},{end_t},BackBox,,0,0,0,,{{\\bord5\\1a&H40&}}{anim}{word_text}\n"
events += f"Dialogue: 1,{start_t},{end_t},Main,,0,0,0,,{anim}{word_text}\n"
else:
# ูˆุถุน ุงู„ุฌู…ู„ุฉ: ุชุธู‡ุฑ ุงู„ุฌู…ู„ุฉ ูƒุงู…ู„ุฉ ูˆุงู„ูƒู„ู…ุฉ ุงู„ู†ุดุทุฉ ุชุจุฑุฒ
# ู‚ู…ู†ุง ุจุชุนุฏูŠู„ ุงู„ู…ู†ุทู‚ ู„ุฅุฎูุงุก ุงู„ูƒู„ู…ุฉ ุงู„ุฃุตู„ูŠุฉ ููŠ ุงู„ุฎู„ููŠุฉ ุฃุซู†ุงุก ุธู‡ูˆุฑ ุงู„ู€ Popup
for i, target_word in enumerate(sentence.words):
w_start = format_ass_time(target_word.start)
w_end = format_ass_time(target_word.end)
duration = target_word.end - target_word.start
# 1. ุทุจู‚ุฉ ุงู„ุฎู„ููŠุฉ (ุงู„ุฌู…ู„ุฉ ูƒุงู…ู„ุฉ ู…ุน ุฅุฎูุงุก ุงู„ูƒู„ู…ุฉ ุงู„ู†ุดุทุฉ ุญุงู„ูŠุงู‹)
words_list_base = [w.word.upper() if preset.uppercase else w.word for w in sentence.words]
prefix_base = " ".join(words_list_base[:i]) + (" " if i > 0 else "")
suffix_base = (" " if i < len(words_list_base)-1 else "") + " ".join(words_list_base[i+1:])
target_word_base = words_list_base[i]
# ุฌุนู„ ุงู„ู†ุต ุงู„ุฃุณุงุณูŠ ุดูุงู ู‚ู„ูŠู„ุงู‹ (dimmed) ูˆุฅุฎูุงุก ุงู„ูƒู„ู…ุฉ ุงู„ู†ุดุทุฉ ุจู€ \alpha&HFF&
dim_alpha = "\\1a&H80&"
box_highlight_type = getattr(preset, 'box_highlight_type', 'word')
# ุฅุฐุง ูƒุงู† ุงู„ู†ูˆุน 'sentence'ุŒ ู†ุธู‡ุฑ ุงู„ุตู†ุฏูˆู‚ ุญูˆู„ ุงู„ุฌู…ู„ุฉ ูƒู„ู‡ุง ููŠ ุทุจู‚ุฉ ุงู„ุฎู„ููŠุฉ
if preset.back_box_enabled and box_highlight_type == "sentence":
box_hide = "" # ู„ุง ู†ุฎููŠ ุงู„ุจูˆูƒุณ ุนู† ุงู„ูƒู„ู…ุฉ ุงู„ู†ุดุทุฉ ููŠ ุทุจู‚ุฉ ุงู„ุฎู„ููŠุฉ ู„ุฃู†ู‡ ู„ู„ุฌู…ู„ุฉ ูƒู„ู‡ุง
base_display_text = f"{{{dim_alpha}}}{prefix_base}{{\\alpha&HFF&}}{target_word_base}{{\\alpha&H00&}}{{{dim_alpha}}}{suffix_base}"
events += f"Dialogue: 0,{w_start},{w_end},BackBox,,0,0,0,,{base_display_text}\n"
else:
# ุงู„ู†ู…ุท ุงู„ุงูุชุฑุงุถูŠ ุฃูˆ 'word': ุงู„ุตู†ุฏูˆู‚ ูŠุฎุชููŠ ู…ู† ุงู„ุฎู„ููŠุฉ ู„ูŠุธู‡ุฑ ููŠ ุทุจู‚ุฉ ุงู„ู‡ุงูŠู„ุงูŠุช ูู‚ุท
box_hide = "\\4a&HFF&"
base_display_text = f"{{{dim_alpha}}}{prefix_base}{{\\alpha&HFF&{box_hide}}}{target_word_base}{{\\alpha&H00&}}{{{dim_alpha}}}{suffix_base}"
events += f"Dialogue: 0,{w_start},{w_end},Main,,0,0,0,,{base_display_text}\n"
# 2. ุทุจู‚ุฉ ุงู„ูƒู„ู…ุฉ ุงู„ู†ุดุทุฉ (Highlight)
prefix_hl = " ".join(words_list_base[:i]) + (" " if i > 0 else "")
suffix_hl = (" " if i < len(words_list_base)-1 else "") + " ".join(words_list_base[i+1:])
target_word_hl = words_list_base[i]
t1 = min(60, int(duration * 150))
t2 = t1 + 80
# ุฅุถุงูุฉ ุชุฃุซูŠุฑ ุงู„ุชูˆู‡ุฌ (Blur)
glow_val = getattr(preset, 'glow_intensity', 0)
blur_tag = f"\\be{glow_val}" if glow_val > 0 else ""
if angle != 0:
rotation = f"\\frz{angle}"
else:
rotation = "\\frz-2" if i % 2 == 0 else "\\frz2"
# ุฅุนุฏุงุฏุงุช ุงู„ุญุฑูƒุฉ ูˆุงู„ู„ูˆู†
scale = int(preset.pop_up_scale * 100)
if preset.back_box_enabled:
if box_highlight_type == "word":
# ุงู„ุตู†ุฏูˆู‚ ุญูˆู„ ุงู„ูƒู„ู…ุฉ ูู‚ุท: ู†ุณุชุฎุฏู… BackBox ู…ุน ุฅุธู‡ุงุฑ ุงู„ุตู†ุฏูˆู‚ ูˆุงู„ูƒู„ู…ุฉ ุจูˆุถูˆุญ (ุดูุงููŠุฉ 00)
# \1c=ู†ุตุŒ \4c=ุตู†ุฏูˆู‚ุŒ \1a=ุดูุงููŠุฉ ู†ุตุŒ \4a=ุดูุงููŠุฉ ุตู†ุฏูˆู‚
color_tags = f"\\1c{primary}\\4c{secondary}\\1a&H00&\\4a&H00&"
anim = f"{{{color_tags}\\fscx{scale}\\fscy{scale}{rotation}{blur_tag}}}"
# ุฅุฎูุงุก ุงู„ู†ุต ูˆุงู„ุตู†ุฏูˆู‚ ููŠ ุงู„ุฃุฌุฒุงุก ุบูŠุฑ ุงู„ู†ุดุทุฉ
display_text_hl = f"{{\\alpha&HFF&\\4a&HFF&}}{prefix_hl}{anim}{target_word_hl}{{\\alpha&HFF&\\4a&HFF&}}{suffix_hl}"
events += f"Dialogue: 1,{w_start},{w_end},BackBox,,0,0,0,,{display_text_hl}\n"
else:
# ุงู„ุตู†ุฏูˆู‚ ุญูˆู„ ุงู„ุฌู…ู„ุฉ: ุงู„ู‡ุงูŠู„ุงูŠุช ู‡ูˆ ุชุบูŠูŠุฑ ู„ูˆู† ุงู„ู†ุต ู…ุน ุชุฃุซูŠุฑ Pop-up
color_tags = f"\\1c{secondary}\\1a&H00&"
anim = f"{{{color_tags}\\fscx{scale}\\fscy{scale}{rotation}{blur_tag}}}"
display_text_hl = f"{{\\alpha&HFF&}}{prefix_hl}{anim}{target_word_hl}{{\\alpha&HFF&}}{suffix_hl}"
events += f"Dialogue: 1,{w_start},{w_end},Main,,0,0,0,,{display_text_hl}\n"
else:
# ุจุฏูˆู† ุตู†ุงุฏูŠู‚ ู†ู‡ุงุฆูŠุงู‹: ู‡ุงูŠู„ุงูŠุช ู†ุตูŠ ูู‚ุท
color_tags = f"\\1c{secondary}\\1a&H00&"
anim = f"{{{color_tags}\\fscx{scale}\\fscy{scale}{rotation}{blur_tag}}}"
display_text_hl = f"{{\\alpha&HFF&}}{prefix_hl}{anim}{target_word_hl}{{\\alpha&HFF&}}{suffix_hl}"
events += f"Dialogue: 1,{w_start},{w_end},Main,,0,0,0,,{display_text_hl}\n"
with open(output_path, "w", encoding="utf-8-sig") as f:
f.write(header + events)