From 8c33ca43d600a7a0d9fa226159c290bce6eb41e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Wed, 24 Jun 2026 23:10:29 +0800 Subject: [PATCH] fix: repair video ai production cli toolchain --- .../engines/ep01_shot03_production.py | 59 +++++++++++++++---- .../engines/multi-reference-video-adapter.py | 5 +- video-ai-system/engines/subtitle-renderer.py | 7 +-- 3 files changed, 56 insertions(+), 15 deletions(-) diff --git a/video-ai-system/engines/ep01_shot03_production.py b/video-ai-system/engines/ep01_shot03_production.py index 50892e5..0237ff3 100644 --- a/video-ai-system/engines/ep01_shot03_production.py +++ b/video-ai-system/engines/ep01_shot03_production.py @@ -32,6 +32,26 @@ import time PROJECT_ROOT = Path(__file__).parent.parent sys.path.insert(0, str(PROJECT_ROOT / "engines")) +DEFAULT_PYTHON = "/Users/bingshuolingdianyuanhe/.workbuddy/binaries/python/envs/default/bin/python3" + + +def resolve_python_bin(): + """Pick the Python that has the video AI dependencies installed.""" + candidates = [ + os.environ.get("PYTHON_BIN"), + os.environ.get("PYTHON"), + DEFAULT_PYTHON, + sys.executable, + "python3", + ] + for candidate in candidates: + if not candidate: + continue + candidate_path = Path(candidate) + if candidate_path.is_absolute() and not candidate_path.exists(): + continue + return str(candidate) + return sys.executable class EP01Shot03ProductionCLI: @@ -47,6 +67,7 @@ class EP01Shot03ProductionCLI: self.config_dir = PROJECT_ROOT / "plans" / "script-to-screen" self.output_dir = PROJECT_ROOT / "outputs" / "ep01" / "shot03" self.assets_dir = PROJECT_ROOT / "assets" + self.python_bin = resolve_python_bin() # 确保输出目录存在 self.output_dir.mkdir(parents=True, exist_ok=True) @@ -70,6 +91,7 @@ class EP01Shot03ProductionCLI: print(f" 镜头: {self.shot_id}") print(f" 输出: {self.output_dir}") print(f" Dry Run: {self.dry_run}") + print(f" Python: {self.python_bin}") def _load_config(self): """加载 E1-SHOT03 配置,从剧本原文+分镜文件读取真实台词""" @@ -241,7 +263,7 @@ class EP01Shot03ProductionCLI: print(f" 📤 生成资产包...") result = subprocess.run( [ - "python", str(PROJECT_ROOT / "engines" / "char-hero-design-packer" / "char-hero-design-packer.py"), + self.python_bin, str(PROJECT_ROOT / "engines" / "char-hero-design-packer" / "char-hero-design-packer.py"), "--character", self.config["character"], "--generate-all" ], @@ -287,7 +309,7 @@ class EP01Shot03ProductionCLI: if len(reference_images) >= 2: # 多参考图模式 cmd = [ - "python", str(PROJECT_ROOT / "engines" / "multi-reference-video-adapter" / "multi-reference-video-adapter.py"), + self.python_bin, str(PROJECT_ROOT / "engines" / "multi-reference-video-adapter.py"), "--prompt", self.config["prompt"], "--references"] + reference_images + [ "--output", str(output_path), @@ -333,7 +355,7 @@ class EP01Shot03ProductionCLI: output_with_plaque = self.output_dir / "video_with_plaque.mp4" # TODO: 实际调用 planar-tracker.py - # cmd = ["python", str(PROJECT_ROOT / "engines" / "planar-tracker.py"), ...] + # cmd = [self.python_bin, str(PROJECT_ROOT / "engines" / "planar-tracker.py"), ...] return { "output_path": str(output_with_plaque), @@ -349,7 +371,7 @@ class EP01Shot03ProductionCLI: output_path = self.output_dir / "dialogue.mp3" cmd = [ - "python", str(PROJECT_ROOT / "engines" / "voice-emotion-compiler" / "voice-emotion-compiler.py"), + self.python_bin, str(PROJECT_ROOT / "engines" / "voice-emotion-compiler.py"), "--text", self.config["dialogue"], "--emotion", self.config["emotion"], "--output", str(output_path) @@ -383,7 +405,7 @@ class EP01Shot03ProductionCLI: output_path = self.output_dir / "video_synced.mp4" cmd = [ - "python", str(PROJECT_ROOT / "engines" / "lipsync-adapter" / "lipsync-adapter.py"), + self.python_bin, str(PROJECT_ROOT / "engines" / "lipsync-adapter.py"), "--video", str(video_path), "--audio", str(audio_path), "--output", str(output_path) @@ -417,12 +439,14 @@ class EP01Shot03ProductionCLI: raise Exception(f"视频不存在: {video_path}") output_path = self.output_dir / "video_with_subtitles.mp4" + srt_path = self.output_dir / "dialogue.srt" + self._write_single_line_srt(srt_path, self.config["dialogue"], float(self.config.get("duration", 3))) # 调用 subtitle-renderer.py cmd = [ - "python", str(PROJECT_ROOT / "engines" / "subtitle-renderer.py"), - "--input", str(video_path), - "--text", self.config["dialogue"], + self.python_bin, str(PROJECT_ROOT / "engines" / "subtitle-renderer.py"), + "--srt", str(srt_path), + "--video", str(video_path), "--output", str(output_path) ] @@ -467,7 +491,7 @@ class EP01Shot03ProductionCLI: # 混音 print(f" 混音...") cmd = [ - "python", str(PROJECT_ROOT / "engines" / "audio-mixer" / "audio-mixer.py"), + self.python_bin, str(PROJECT_ROOT / "engines" / "audio-mixer.py"), "--dialogue", str(dialogue_path), "--output", str(self.output_dir / "mixed_audio.mp3") ] @@ -502,7 +526,7 @@ class EP01Shot03ProductionCLI: return {"skipped": True, "reason": "no video found"} cmd = [ - "python", str(PROJECT_ROOT / "engines" / "shot-qc-automation" / "shot-qc-automation.py"), + self.python_bin, str(PROJECT_ROOT / "engines" / "shot-qc-automation.py"), "--video", str(video_path), "--character", self.config["character"], "--output", str(self.output_dir / "qc_report.json") @@ -582,6 +606,21 @@ class EP01Shot03ProductionCLI: return True + def _write_single_line_srt(self, srt_path, text, duration): + """Write a one-line SRT from the locked source dialogue.""" + duration_ms = max(500, int(duration * 1000)) + end_seconds = duration_ms // 1000 + end_ms = duration_ms % 1000 + srt_path.parent.mkdir(parents=True, exist_ok=True) + content = ( + "1\n" + f"00:00:00,000 --> 00:00:{end_seconds:02d},{end_ms:03d}\n" + f"{text}\n" + ) + with open(srt_path, "w", encoding="utf-8") as f: + f.write(content) + return srt_path + def main(): parser = argparse.ArgumentParser(description="EP01-SHOT03-PRODUCTION-CLI") diff --git a/video-ai-system/engines/multi-reference-video-adapter.py b/video-ai-system/engines/multi-reference-video-adapter.py index 48e2a26..b8c89a0 100644 --- a/video-ai-system/engines/multi-reference-video-adapter.py +++ b/video-ai-system/engines/multi-reference-video-adapter.py @@ -34,10 +34,13 @@ def load_api_config(): """从仓库 .env、本机密钥文件和环境变量加载配置""" config = {} env_files = [ + Path(os.environ["VIDEO_AI_SECRETS_FILE"]) if os.environ.get("VIDEO_AI_SECRETS_FILE") else None, + Path("/Users/bingshuolingdianyuanhe/Documents/guanghulab-local-secrets/video-ai-system.env"), PROJECT_ROOT / ".env", - Path.home() / "guanghulab-local-secrets" / "video-ai-system.env", ] for env_file in env_files: + if env_file is None: + continue if not env_file.exists(): continue with open(env_file, "r", encoding="utf-8") as f: diff --git a/video-ai-system/engines/subtitle-renderer.py b/video-ai-system/engines/subtitle-renderer.py index b04be77..30dbdc8 100644 --- a/video-ai-system/engines/subtitle-renderer.py +++ b/video-ai-system/engines/subtitle-renderer.py @@ -5,7 +5,7 @@ Subtitle Renderer · 字幕渲染引擎 将 SRT 字幕文件渲染为 PNG 序列,再通过 FFmpeg 合成到视频中。 依赖: - pip install cairosvg Pillow pysrt + pip install Pillow pysrt 用法: # 基本用法(SRT → PNG 序列) @@ -50,11 +50,10 @@ except ImportError: sys.exit(1) try: - import cairosvg from PIL import Image, ImageDraw, ImageFont except ImportError: - print("[ERROR] 缺少依赖:cairosvg Pillow") - print("请先安装:pip install cairosvg Pillow") + print("[ERROR] 缺少依赖:Pillow") + print("请先安装:pip install Pillow") sys.exit(1)