"""字幕引擎:根据 stdout 关键词打时间戳,生成 SRT 字幕文件。""" from __future__ import annotations import time from dataclasses import dataclass from pathlib import Path from typing import List, Tuple @dataclass class _Entry: start: float # 距录制开始的秒数 text: str duration: float = 5.0 # 每条字幕默认显示 5 秒 class SubtitleEngine: def __init__(self, script: List[Tuple[str, str]], default_duration: float = 5.0): """ script: [(关键词, 字幕文案), ...] 关键词大小写不敏感,每个关键词只触发一次。 """ self._script = script self._default_duration = default_duration self._entries: List[_Entry] = [] self._matched: set[str] = set() self._t0: float = 0.0 def set_start_time(self, t: float) -> None: self._t0 = t def process_line(self, line: str) -> None: lower = line.lower() for keyword, text in self._script: if keyword in self._matched: continue if keyword.lower() in lower: elapsed = time.time() - self._t0 self._entries.append(_Entry(start=elapsed, text=text, duration=self._default_duration)) self._matched.add(keyword) def generate_srt(self, output_path: str) -> None: """生成 SRT 文件,自动处理字幕重叠。 字幕重叠修复策略(避免屏幕上同时显示 2+ 条叠成两行): - 字幕按 start 时间升序排序后; - 相邻字幕保留 GAP=0.1s 间隔; - 每条字幕保证至少 MIN_DURATION=1.0s 显示时间(短到这个值仍读不完是字幕脚本设计问题); - 若当前条让位给下一条后剩余时间 < MIN_DURATION,则把下一条 start 往后推。 这样任意时刻屏幕上最多 1 条字幕。 """ def _fmt(s: float) -> str: h = int(s // 3600) m = int((s % 3600) // 60) sec = int(s % 60) ms = int((s % 1) * 1000) return f"{h:02d}:{m:02d}:{sec:02d},{ms:03d}" # 防重叠 post-process MIN_DURATION = 1.0 GAP = 0.1 entries = sorted(self._entries, key=lambda e: e.start) for i in range(len(entries) - 1): cur, nxt = entries[i], entries[i + 1] max_end = nxt.start - GAP new_dur = max_end - cur.start if new_dur < MIN_DURATION: # 太挤:cur 至少撑 MIN_DURATION 秒,把 nxt 推迟 cur.duration = MIN_DURATION nxt.start = cur.start + MIN_DURATION + GAP else: # 正常让位:cur 显示到 (nxt.start - GAP) cur.duration = min(cur.duration, new_dur) # 最后一条不需要让位,保持原 duration Path(output_path).parent.mkdir(parents=True, exist_ok=True) with open(output_path, "w", encoding="utf-8") as f: for i, e in enumerate(entries, 1): f.write(f"{i}\n") f.write(f"{_fmt(e.start)} --> {_fmt(e.start + e.duration)}\n") f.write(f"{e.text}\n\n")