Python版本
"""
基于音节/字数精确分配时间
"""
import re
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
# ==================== 核心逻辑 ====================
def count_english_syllables(word):
w = word.strip('.,;:!?"\'()[]{}').lower()
if not w:
return 0
vowels = set("aeiouy")
count = 0
prev_is_vowel = False
for ch in w:
is_vowel = ch in vowels
if is_vowel and not prev_is_vowel:
count += 1
prev_is_vowel = is_vowel
if w.endswith("e") and not w.endswith("le") and count > 1:
count -= 1
if w.endswith("le") and len(w) > 2 and w[-3] not in vowels:
count += 1
return max(count, 1)
def count_pronunciation_units(text):
total = 0
tokens = re.findall(r'[\u4e00-\u9fff]|[a-zA-Z]+|\d+%?|[^\s]', text)
for token in tokens:
if re.match(r'[\u4e00-\u9fff]', token):
total += 1
elif re.match(r'[a-zA-Z]+', token):
total += count_english_syllables(token)
elif re.match(r'\d+%?', token):
digits = token.replace('%', '')
total += min(len(digits) + 1, 5)
return total
def split_sentences(text):
parts = re.split(r'(?<=[.,;])\s+', text)
return [p.strip() for p in parts if p.strip()]
def ms_to_parts(ms):
total_sec = ms / 1000
m = int(total_sec // 60)
s = total_sec - m * 60
return m, s
def fmt_lyrics3(ms):
m, s = ms_to_parts(ms)
return f"[{m:02d}:{s:05.2f}]"
def fmt_srt(ms):
m, s = ms_to_parts(ms)
return f"{m:02d}:{s:05.2f}".replace(".", ",")
def fmt_ass(ms):
m, s = ms_to_parts(ms)
h = m // 60
m = m % 60
return f"{h}:{m:02d}:{s:05.2f}"
def fmt_vtt(ms):
m, s = ms_to_parts(ms)
return f"{m:02d}:{s:06.3f}"
def generate_subtitles(text, total_seconds, fmt="lyrics3"):
total_ms = float(total_seconds) * 1000
sentences = split_sentences(text)
if not sentences:
return "", 0
units_per_sentence = [count_pronunciation_units(s) for s in sentences]
total_units = sum(units_per_sentence)
if total_units == 0:
return "", 0
ms_per_unit = total_ms / total_units
timings = []
cur = 0.0
for i, s in enumerate(sentences):
dur = units_per_sentence[i] * ms_per_unit
timings.append((cur, cur + dur, s))
cur += dur
formatters = {"lyrics3": fmt_lyrics3, "srt": fmt_srt, "ass": fmt_ass, "vtt": fmt_vtt}
f = formatters.get(fmt, fmt_lyrics3)
lines = []
if fmt == "vtt":
lines.append("WEBVTT\n")
elif fmt == "ass":
lines.append("[Script Info]")
lines.append("ScriptType: v4.00+")
lines.append("PlayResX: 384")
lines.append("PlayResY: 288\n")
lines.append("[V4+ Styles]")
lines.append("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding")
lines.append("Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1\n")
lines.append("[Events]")
lines.append("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text")
for i, (start, end, sent) in enumerate(timings):
if fmt == "srt":
lines.append(str(i + 1))
lines.append(f"{f(start)} --> {f(end)}")
lines.append(sent)
lines.append("")
elif fmt == "ass":
lines.append(f"Dialogue: 0,{f(start)},{f(end)},Default,,0,0,0,,{sent}")
elif fmt == "vtt":
lines.append(f"{f(start)} --> {f(end)}")
lines.append(sent)
lines.append("")
else:
lines.append(f"{f(start)}{sent}")
return "\n".join(lines), len(sentences)
# ==================== GUI ====================
class SubtitleApp:
def __init__(self, root):
self.root = root
self.root.title("字幕时间轴生成器")
self.root.geometry("900x720")
self.root.configure(bg="#1e1e2e")
self.root.minsize(700, 550)
# 样式
style = ttk.Style()
style.theme_use("clam")
style.configure("TFrame", background="#1e1e2e")
style.configure("TLabel", background="#1e1e2e", foreground="#cdd6f4", font=("Segoe UI", 11))
style.configure("TButton", background="#45475a", foreground="#cdd6f4", font=("Segoe UI", 10), borderwidth=0, padding=8)
style.map("TButton", background=[("active", "#585b70")])
style.configure("TEntry", fieldbackground="#313244", foreground="#cdd6f4", font=("Segoe UI", 11), padding=6)
style.configure("TCombobox", fieldbackground="#313244", foreground="#cdd6f4", font=("Segoe UI", 11), padding=4)
self.build_ui()
def build_ui(self):
# 主容器
main = ttk.Frame(self.root, padding=20)
main.pack(fill="both", expand=True)
# 标题
title = tk.Label(main, text="🎬 字幕时间轴生成器", font=("Segoe UI", 20, "bold"),
bg="#1e1e2e", fg="#cba6f7")
title.pack(pady=(0, 5))
subtitle = tk.Label(main, text="基于音节/字数精确分配时间 · 支持中英混合",
font=("Segoe UI", 10), bg="#1e1e2e", fg="#6c7086")
subtitle.pack(pady=(0, 20))
# 输入区域
input_frame = ttk.Frame(main)
input_frame.pack(fill="both", expand=True)
lbl_input = tk.Label(input_frame, text="📝 输入文本(按句号/逗号/分号自动分句)",
font=("Segoe UI", 11, "bold"), bg="#1e1e2e", fg="#a6adc8")
lbl_input.pack(anchor="w")
self.text_input = scrolledtext.ScrolledText(
input_frame, height=10, font=("Segoe UI", 11),
bg="#313244", fg="#cdd6f4", insertbackground="#cdd6f4",
relief="flat", borderwidth=0, padx=12, pady=12,
wrap=tk.WORD, highlightthickness=1, highlightbackground="#45475a"
)
self.text_input.pack(fill="both", expand=True, pady=(5, 15))
# 设置行
settings_frame = ttk.Frame(main)
settings_frame.pack(fill="x", pady=(0, 15))
# 时长
dur_frame = ttk.Frame(settings_frame)
dur_frame.pack(side="left", padx=(0, 30))
ttk.Label(dur_frame, text="⏱ 总时长(秒)").pack(side="left", padx=(0, 8))
self.duration_var = tk.StringVar(value="73")
self.duration_entry = ttk.Entry(dur_frame, textvariable=self.duration_var, width=10)
self.duration_entry.pack(side="left")
# 格式
fmt_frame = ttk.Frame(settings_frame)
fmt_frame.pack(side="left", padx=(0, 30))
ttk.Label(fmt_frame, text="📄 输出格式").pack(side="left", padx=(0, 8))
self.format_var = tk.StringVar(value="lyrics3")
fmt_combo = ttk.Combobox(fmt_frame, textvariable=self.format_var,
values=["lyrics3", "srt", "ass", "vtt"],
state="readonly", width=10)
fmt_combo.pack(side="left")
# 统计
self.stats_var = tk.StringVar(value="分句: 0 句 | 总发音单位: 0")
stats_label = tk.Label(settings_frame, textvariable=self.stats_var,
font=("Segoe UI", 10), bg="#1e1e2e", fg="#6c7086")
stats_label.pack(side="right")
# 按钮
btn_frame = ttk.Frame(main)
btn_frame.pack(fill="x", pady=(0, 15))
self.btn_generate = tk.Button(
btn_frame, text="✨ 生成字幕", font=("Segoe UI", 12, "bold"),
bg="#cba6f7", fg="#1e1e2e", activebackground="#b4befe",
activeforeground="#1e1e2e", relief="flat", borderwidth=0,
padx=24, pady=10, cursor="hand2", command=self.on_generate
)
self.btn_generate.pack(side="left", padx=(0, 10))
self.btn_copy = tk.Button(
btn_frame, text="📋 复制全部", font=("Segoe UI", 11),
bg="#45475a", fg="#cdd6f4", activebackground="#585b70",
activeforeground="#cdd6f4", relief="flat", borderwidth=0,
padx=20, pady=10, cursor="hand2", command=self.on_copy
)
self.btn_copy.pack(side="left", padx=(0, 10))
self.btn_save = tk.Button(
btn_frame, text="💾 保存文件", font=("Segoe UI", 11),
bg="#45475a", fg="#cdd6f4", activebackground="#585b70",
activeforeground="#cdd6f4", relief="flat", borderwidth=0,
padx=20, pady=10, cursor="hand2", command=self.on_save
)
self.btn_save.pack(side="left")
# 输出区域
output_frame = ttk.Frame(main)
output_frame.pack(fill="both", expand=True)
lbl_output = tk.Label(output_frame, text="📤 生成结果",
font=("Segoe UI", 11, "bold"), bg="#1e1e2e", fg="#a6adc8")
lbl_output.pack(anchor="w")
self.text_output = scrolledtext.ScrolledText(
output_frame, height=12, font=("Cascadia Code", 10),
bg="#11111b", fg="#a6e3a1", insertbackground="#a6e3a1",
relief="flat", borderwidth=0, padx=12, pady=12,
wrap=tk.NONE, highlightthickness=1, highlightbackground="#45475a"
)
self.text_output.pack(fill="both", expand=True, pady=(5, 0))
def on_generate(self):
text = self.text_input.get("1.0", "end-1c").strip()
if not text:
messagebox.showwarning("提示", "请先输入文本。")
return
try:
duration = float(self.duration_var.get())
if duration <= 0:
raise ValueError
except ValueError:
messagebox.showwarning("提示", "请输入有效的正数时长(秒)。")
return
fmt = self.format_var.get()
result, count = generate_subtitles(text, duration, fmt)
self.text_output.delete("1.0", "end")
if result:
self.text_output.insert("1.0", result)
total_units = sum(count_pronunciation_units(s) for s in split_sentences(text))
self.stats_var.set(f"分句: {count} 句 | 总发音单位: {total_units}")
def on_copy(self):
result = self.text_output.get("1.0", "end-1c").strip()
if result:
self.root.clipboard_clear()
self.root.clipboard_append(result)
messagebox.showinfo("提示", "已复制到剪贴板!")
def on_save(self):
result = self.text_output.get("1.0", "end-1c").strip()
if not result:
messagebox.showwarning("提示", "没有可保存的内容,请先生成字幕。")
return
fmt = self.format_var.get()
exts = {"lyrics3": ".txt", "srt": ".srt", "ass": ".ass", "vtt": ".vtt"}
ext = exts.get(fmt, ".txt")
path = filedialog.asksaveasfilename(
defaultextension=ext,
filetypes=[(f"{fmt.upper()} 字幕", f"*{ext}"), ("所有文件", "*.*")]
)
if path:
with open(path, "w", encoding="utf-8") as f:
f.write(result)
messagebox.showinfo("提示", f"已保存到:{path}")
if __name__ == "__main__":
root = tk.Tk()
app = SubtitleApp(root)
root.mainloop()
HTML版本
3 个帖子 - 3 位参与者