基于 SDK 新模块(前两个 commit)的 screencast 工具能力升级:
- runner.py:
· 新增 activate_window: bool = True 参数,录屏前自动调
activate_and_maximize_jiangchang_window,避免开头几秒录到 PowerShell/桌面
· 新增 monitor_index: Optional[int] = None 参数,透传给 ScreenRecorder
· 自带 SDK 路径兜底 _ensure_sdk_on_path,让 record_screencast.py 直接 python
跑时(不走 pytest conftest)也能 import jiangchang_desktop_sdk.window_win32
- recorder.py:
· ScreenRecorder.__init__ 加 monitor_index 参数
· _capture_loop 用 idx 选 monitor,越界兜底到 0
· None = 旧行为(monitors[0] 所有屏合并),1 = 主屏,2+ = 第 N 屏
· 多显示器场景推荐显式传 monitor_index=1 避免录出半边空白
- subtitle.py:
· SubtitleEngine.process_line 支持二元组(兼容旧脚本)和三元组(每条独立 duration)
· 长等待场景可以让某条字幕停留更久(如 30s)覆盖中间无日志的"字幕真空"
所有改动向后兼容,全部新增参数有默认值。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
95 lines
3.6 KiB
Python
95 lines
3.6 KiB
Python
"""字幕引擎:根据 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],
|
||
default_duration: float = 5.0,
|
||
):
|
||
"""
|
||
script: [(关键词, 字幕文案) | (关键词, 字幕文案, 显示时长秒), ...]
|
||
- 二元组:用 default_duration(向后兼容)
|
||
- 三元组:本条字幕独立指定显示秒数(用于长等待场景延长字幕停留)
|
||
关键词大小写不敏感,每个关键词只触发一次。
|
||
"""
|
||
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 entry_tuple in self._script:
|
||
if len(entry_tuple) == 2:
|
||
keyword, text = entry_tuple
|
||
duration = self._default_duration
|
||
elif len(entry_tuple) == 3:
|
||
keyword, text, duration = entry_tuple
|
||
else:
|
||
continue
|
||
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=float(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")
|