fix: repair video ai production cli toolchain
This commit is contained in:
parent
a77bc6b884
commit
8c33ca43d6
@ -32,6 +32,26 @@ import time
|
|||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).parent.parent
|
PROJECT_ROOT = Path(__file__).parent.parent
|
||||||
sys.path.insert(0, str(PROJECT_ROOT / "engines"))
|
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:
|
class EP01Shot03ProductionCLI:
|
||||||
@ -47,6 +67,7 @@ class EP01Shot03ProductionCLI:
|
|||||||
self.config_dir = PROJECT_ROOT / "plans" / "script-to-screen"
|
self.config_dir = PROJECT_ROOT / "plans" / "script-to-screen"
|
||||||
self.output_dir = PROJECT_ROOT / "outputs" / "ep01" / "shot03"
|
self.output_dir = PROJECT_ROOT / "outputs" / "ep01" / "shot03"
|
||||||
self.assets_dir = PROJECT_ROOT / "assets"
|
self.assets_dir = PROJECT_ROOT / "assets"
|
||||||
|
self.python_bin = resolve_python_bin()
|
||||||
|
|
||||||
# 确保输出目录存在
|
# 确保输出目录存在
|
||||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@ -70,6 +91,7 @@ class EP01Shot03ProductionCLI:
|
|||||||
print(f" 镜头: {self.shot_id}")
|
print(f" 镜头: {self.shot_id}")
|
||||||
print(f" 输出: {self.output_dir}")
|
print(f" 输出: {self.output_dir}")
|
||||||
print(f" Dry Run: {self.dry_run}")
|
print(f" Dry Run: {self.dry_run}")
|
||||||
|
print(f" Python: {self.python_bin}")
|
||||||
|
|
||||||
def _load_config(self):
|
def _load_config(self):
|
||||||
"""加载 E1-SHOT03 配置,从剧本原文+分镜文件读取真实台词"""
|
"""加载 E1-SHOT03 配置,从剧本原文+分镜文件读取真实台词"""
|
||||||
@ -241,7 +263,7 @@ class EP01Shot03ProductionCLI:
|
|||||||
print(f" 📤 生成资产包...")
|
print(f" 📤 生成资产包...")
|
||||||
result = subprocess.run(
|
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"],
|
"--character", self.config["character"],
|
||||||
"--generate-all"
|
"--generate-all"
|
||||||
],
|
],
|
||||||
@ -287,7 +309,7 @@ class EP01Shot03ProductionCLI:
|
|||||||
if len(reference_images) >= 2:
|
if len(reference_images) >= 2:
|
||||||
# 多参考图模式
|
# 多参考图模式
|
||||||
cmd = [
|
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"],
|
"--prompt", self.config["prompt"],
|
||||||
"--references"] + reference_images + [
|
"--references"] + reference_images + [
|
||||||
"--output", str(output_path),
|
"--output", str(output_path),
|
||||||
@ -333,7 +355,7 @@ class EP01Shot03ProductionCLI:
|
|||||||
output_with_plaque = self.output_dir / "video_with_plaque.mp4"
|
output_with_plaque = self.output_dir / "video_with_plaque.mp4"
|
||||||
|
|
||||||
# TODO: 实际调用 planar-tracker.py
|
# 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 {
|
return {
|
||||||
"output_path": str(output_with_plaque),
|
"output_path": str(output_with_plaque),
|
||||||
@ -349,7 +371,7 @@ class EP01Shot03ProductionCLI:
|
|||||||
output_path = self.output_dir / "dialogue.mp3"
|
output_path = self.output_dir / "dialogue.mp3"
|
||||||
|
|
||||||
cmd = [
|
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"],
|
"--text", self.config["dialogue"],
|
||||||
"--emotion", self.config["emotion"],
|
"--emotion", self.config["emotion"],
|
||||||
"--output", str(output_path)
|
"--output", str(output_path)
|
||||||
@ -383,7 +405,7 @@ class EP01Shot03ProductionCLI:
|
|||||||
output_path = self.output_dir / "video_synced.mp4"
|
output_path = self.output_dir / "video_synced.mp4"
|
||||||
|
|
||||||
cmd = [
|
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),
|
"--video", str(video_path),
|
||||||
"--audio", str(audio_path),
|
"--audio", str(audio_path),
|
||||||
"--output", str(output_path)
|
"--output", str(output_path)
|
||||||
@ -417,12 +439,14 @@ class EP01Shot03ProductionCLI:
|
|||||||
raise Exception(f"视频不存在: {video_path}")
|
raise Exception(f"视频不存在: {video_path}")
|
||||||
|
|
||||||
output_path = self.output_dir / "video_with_subtitles.mp4"
|
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
|
# 调用 subtitle-renderer.py
|
||||||
cmd = [
|
cmd = [
|
||||||
"python", str(PROJECT_ROOT / "engines" / "subtitle-renderer.py"),
|
self.python_bin, str(PROJECT_ROOT / "engines" / "subtitle-renderer.py"),
|
||||||
"--input", str(video_path),
|
"--srt", str(srt_path),
|
||||||
"--text", self.config["dialogue"],
|
"--video", str(video_path),
|
||||||
"--output", str(output_path)
|
"--output", str(output_path)
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -467,7 +491,7 @@ class EP01Shot03ProductionCLI:
|
|||||||
# 混音
|
# 混音
|
||||||
print(f" 混音...")
|
print(f" 混音...")
|
||||||
cmd = [
|
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),
|
"--dialogue", str(dialogue_path),
|
||||||
"--output", str(self.output_dir / "mixed_audio.mp3")
|
"--output", str(self.output_dir / "mixed_audio.mp3")
|
||||||
]
|
]
|
||||||
@ -502,7 +526,7 @@ class EP01Shot03ProductionCLI:
|
|||||||
return {"skipped": True, "reason": "no video found"}
|
return {"skipped": True, "reason": "no video found"}
|
||||||
|
|
||||||
cmd = [
|
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),
|
"--video", str(video_path),
|
||||||
"--character", self.config["character"],
|
"--character", self.config["character"],
|
||||||
"--output", str(self.output_dir / "qc_report.json")
|
"--output", str(self.output_dir / "qc_report.json")
|
||||||
@ -582,6 +606,21 @@ class EP01Shot03ProductionCLI:
|
|||||||
|
|
||||||
return True
|
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():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="EP01-SHOT03-PRODUCTION-CLI")
|
parser = argparse.ArgumentParser(description="EP01-SHOT03-PRODUCTION-CLI")
|
||||||
|
|||||||
@ -34,10 +34,13 @@ def load_api_config():
|
|||||||
"""从仓库 .env、本机密钥文件和环境变量加载配置"""
|
"""从仓库 .env、本机密钥文件和环境变量加载配置"""
|
||||||
config = {}
|
config = {}
|
||||||
env_files = [
|
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",
|
PROJECT_ROOT / ".env",
|
||||||
Path.home() / "guanghulab-local-secrets" / "video-ai-system.env",
|
|
||||||
]
|
]
|
||||||
for env_file in env_files:
|
for env_file in env_files:
|
||||||
|
if env_file is None:
|
||||||
|
continue
|
||||||
if not env_file.exists():
|
if not env_file.exists():
|
||||||
continue
|
continue
|
||||||
with open(env_file, "r", encoding="utf-8") as f:
|
with open(env_file, "r", encoding="utf-8") as f:
|
||||||
|
|||||||
@ -5,7 +5,7 @@ Subtitle Renderer · 字幕渲染引擎
|
|||||||
将 SRT 字幕文件渲染为 PNG 序列,再通过 FFmpeg 合成到视频中。
|
将 SRT 字幕文件渲染为 PNG 序列,再通过 FFmpeg 合成到视频中。
|
||||||
|
|
||||||
依赖:
|
依赖:
|
||||||
pip install cairosvg Pillow pysrt
|
pip install Pillow pysrt
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
# 基本用法(SRT → PNG 序列)
|
# 基本用法(SRT → PNG 序列)
|
||||||
@ -50,11 +50,10 @@ except ImportError:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import cairosvg
|
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("[ERROR] 缺少依赖:cairosvg Pillow")
|
print("[ERROR] 缺少依赖:Pillow")
|
||||||
print("请先安装:pip install cairosvg Pillow")
|
print("请先安装:pip install Pillow")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user