分享一个字幕时间轴生成器源码

分享一个字幕时间轴生成器源码
分享一个字幕时间轴生成器源码

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 位参与者

阅读完整话题

来源: LinuxDo 最新话题查看原文