diff --git a/tools/screencast/composer.py b/tools/screencast/composer.py index cea3c45..4ea95b9 100644 --- a/tools/screencast/composer.py +++ b/tools/screencast/composer.py @@ -7,6 +7,17 @@ import sys from pathlib import Path +# 匠厂统一字幕样式 —— 所有 skill 录屏共用,做品牌一致性 +# FontSize 14:1080p 屏幕约占 1.3% 高度,演示视频留足画面呼吸感 +# Outline 2:14 号字配 Outline 3 会显胖,2 已足够在浅色背景上保持可读 +# 修改请评估对所有 skill 录屏的影响。 +_JIANGCHANG_SUBTITLE_STYLE = ( + "FontName=Arial,FontSize=14," + "PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000," + "Outline=2,Shadow=1,Alignment=2" +) + + def compose_video( frames_dir: str, fps: int, @@ -32,9 +43,7 @@ def compose_video( subtitle_filter = ( f"subtitles='{srt_path_str}'" - ":force_style='FontName=Arial,FontSize=28," - "PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000," - "Outline=2,Shadow=1,Alignment=2'" + f":force_style='{_JIANGCHANG_SUBTITLE_STYLE}'" ) # 视频和音频都放进 filter_complex,避免 -vf 与 -filter_complex 混用报错 diff --git a/tools/screencast/runner.py b/tools/screencast/runner.py index 477781c..13ef29f 100644 --- a/tools/screencast/runner.py +++ b/tools/screencast/runner.py @@ -66,6 +66,13 @@ def run_screencast( engine.set_start_time(time.time()) try: + # Windows 子进程 stdout 默认走系统 ANSI code page(中文 Windows 是 cp936/GBK), + # 这边用 encoding="utf-8" 解码会乱码。注入 PYTHONIOENCODING + PYTHONUTF8 让 + # 子进程 Python 强制以 UTF-8 输出,两端编码对齐。非 Windows 默认就 UTF-8,无副作用。 + env = os.environ.copy() + env["PYTHONIOENCODING"] = "utf-8" + env["PYTHONUTF8"] = "1" + proc = subprocess.Popen( [sys.executable, "-m", "pytest", *pytest_args], stdout=subprocess.PIPE, @@ -73,6 +80,7 @@ def run_screencast( text=True, encoding="utf-8", errors="replace", + env=env, ) assert proc.stdout is not None for line in proc.stdout: diff --git a/tools/screencast/subtitle.py b/tools/screencast/subtitle.py index 122eb6c..a8b7ae8 100644 --- a/tools/screencast/subtitle.py +++ b/tools/screencast/subtitle.py @@ -40,6 +40,15 @@ class SubtitleEngine: 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) @@ -47,9 +56,26 @@ class SubtitleEngine: 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(self._entries, 1): + 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")