"""主编排器:录屏 + 运行 pytest + 生成字幕 + FFmpeg 合成。""" from __future__ import annotations import os import subprocess import sys import tempfile import time from datetime import datetime from pathlib import Path from typing import List, Optional, Tuple from .composer import compose_video from .recorder import ScreenRecorder from .subtitle import SubtitleEngine def run_screencast( skill_slug: str, subtitle_script: List[Tuple[str, str]], pytest_args: List[str], output_dir: str, media_assets_root: Optional[str] = None, music_subdir: str = "music", fps: int = 10, ) -> str: """ 录制一次技能桌面 E2E 演示视频。 Args: skill_slug: 技能唯一标识,如 "query-balance-icbc" subtitle_script: [(关键词, 字幕文案), ...] 按顺序匹配 pytest stdout pytest_args: 传递给 pytest 的参数列表 output_dir: 最终 MP4 输出目录 media_assets_root: media-assets 仓库本地路径; 不传时读 MEDIA_ASSETS_ROOT 环境变量, 再不存在则用默认值 D:\\OpenClaw\\client-commons\\media-assets music_subdir: music_assets_root 下音乐子目录,默认 "music" fps: 截帧帧率,默认 10 Returns: 输出 MP4 的绝对路径 """ if media_assets_root is None: media_assets_root = os.environ.get( "MEDIA_ASSETS_ROOT", r"D:\OpenClaw\client-commons\media-assets", ) music_dir = str(Path(media_assets_root) / music_subdir) output_dir_path = Path(output_dir) output_dir_path.mkdir(parents=True, exist_ok=True) date_str = datetime.now().strftime("%Y%m%d_%H%M%S") base_name = f"{skill_slug}_{date_str}" output_mp4 = output_dir_path / f"{base_name}.mp4" subtitle_file = output_dir_path / f"{base_name}.srt" engine = SubtitleEngine(subtitle_script) with tempfile.TemporaryDirectory(prefix="screencast_frames_") as frames_dir: recorder = ScreenRecorder(frames_dir, fps=fps) print(f"[screencast] 启动录制 → {output_mp4.name}") recorder.start() 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, stderr=subprocess.STDOUT, text=True, encoding="utf-8", errors="replace", env=env, ) assert proc.stdout is not None for line in proc.stdout: sys.stdout.write(line) sys.stdout.flush() engine.process_line(line) exit_code = proc.wait() if exit_code != 0: print(f"[screencast] pytest 退出码 {exit_code},录制仍继续合成") finally: recorder.stop() print(f"[screencast] 录制停止,共 {recorder.frame_count} 帧") engine.generate_srt(str(subtitle_file)) print(f"[screencast] 字幕 → {subtitle_file.name}") compose_video( frames_dir=frames_dir, fps=fps, subtitle_path=str(subtitle_file), music_dir=music_dir, output_path=str(output_mp4), ) return str(output_mp4)