feat(screencast): 统一字幕样式 / 修字幕重叠 / 修子进程乱码
- composer.py:抽出 _JIANGCHANG_SUBTITLE_STYLE 常量,FontSize 28→14 / Outline 2→2(保持),1080p 屏幕约占 1.3% 高度,演示视频留足画面呼吸感 - subtitle.py:generate_srt 加 post-process 排队,相邻字幕互相让位 (MIN_DURATION=1.0s / GAP=0.1s),任意时刻屏幕上最多 1 条字幕, 避免开头几秒密集触发的字幕叠成两行 - runner.py:subprocess.Popen 注入 PYTHONIOENCODING=utf-8 + PYTHONUTF8=1, 让 Windows 子进程 Python 强制 UTF-8 输出,runner 端 UTF-8 解码即对齐, 修中文乱码 所有 skill 录屏受益,向后兼容(非 Windows 系统 UTF-8 注入无副作用)。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 混用报错
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user