Files
ai-agent-book/chapter5/video-edit/video_editor.py
T
liqiang b119135836
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
2026-08-20 13:12:50 +00:00

127 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
视频剪辑执行层(双后端)。
书中实验 5-6 的核心是"代码生成"Proposer Agent 生成一段调用 Blender Python API
(bpy)的脚本来完成剪辑。因此本层有两个后端:
- blender:把剪辑计划翻译成 bpy 脚本,用 `blender --background --python` 无头渲染
(见 blender_editor.py)——书中原方案,需安装 Blender;
- ffmpeg :用 ffmpeg 完成等价的裁剪/字幕/慢动作,单二进制、CI 友好,本机已验证。
apply_edit(..., backend=) 统一入口:backend="auto" 时装了 Blender 走 bpy,否则回退
ffmpeg。**无论走哪个后端,都会把 Proposer 生成的 bpy 脚本落盘(output/edit.py**
体现"生成 Blender Python API 代码"这一核心,便于人工核对或换机执行。
支持的操作:
- trim 裁剪 [start, end] 片段
- subtitle 在片段上叠加字幕
- slowmo 慢动作(放慢到 factor 倍时长)
所有操作最终产出一个标准 mp4(H.264 + AAC)。
"""
import os
from blender_editor import blender_available, render_with_blender, write_bpy_script
from ffmpeg_utils import find_font, run
def _esc(text: str) -> str:
"""转义 drawtext/subtitles 文本中的特殊字符。"""
return text.replace("\\", "\\\\").replace(":", r"\:").replace("'", r"\'")
def apply_edit(source: str, plan: dict, out_path: str,
backend: str = "auto", script_path: str = None) -> str:
"""
按剪辑计划 plan 生成成片。
plan 结构(由 Proposer Agent 产出):
{
"start": float, # 目标片段起点(秒)
"end": float, # 目标片段终点(秒)
"effects": [ # 可选特效列表
{"type": "subtitle", "text": "..."},
{"type": "slowmo", "factor": 2.0}
]
}
backend:
"auto" 装了 blender 用 bpy,否则回退 ffmpeg(默认)。
"blender" 强制用 Blender bpy 无头渲染(未装 blender 则报错)。
"ffmpeg" 强制用 ffmpeg 剪辑。
无论哪个后端,都会把 Proposer 生成的 bpy 脚本写到 script_path
(默认 out 同目录 edit.py),作为"生成 Blender API 代码"的产物。
返回成片路径。
"""
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
if script_path is None:
script_path = os.path.join(os.path.dirname(out_path) or ".", "edit.py")
# 始终产出 bpy 脚本:这正是书中"Proposer 生成 Blender 脚本"的落地产物。
write_bpy_script(source, plan, out_path, script_path)
use_blender = backend == "blender" or (backend == "auto" and blender_available())
if use_blender:
return render_with_blender(source, plan, out_path, script_path)
if backend == "blender": # 显式要求 Blender 却没装
return render_with_blender(source, plan, out_path, script_path) # 抛清晰错误
return _apply_edit_ffmpeg(source, plan, out_path)
def _apply_edit_ffmpeg(source: str, plan: dict, out_path: str) -> str:
"""ffmpeg 后端:与 bpy 脚本等价的裁剪/字幕/慢动作,产出 H.264+AAC 的 mp4。"""
start, end = float(plan["start"]), float(plan["end"])
if end <= start:
raise ValueError(f"剪辑区间非法:start={start} >= end={end}")
effects = plan.get("effects", []) or []
vf_chain = [] # 视频滤镜链
af_chain = [] # 音频滤镜链
font = find_font()
for eff in effects:
etype = eff.get("type")
if etype == "subtitle":
txt = _esc(eff.get("text", ""))
opts = [f"text='{txt}'", "fontsize=52", "fontcolor=white",
"x=(w-text_w)/2", "y=h-text_h-50",
"box=1", "boxcolor=black@0.6", "boxborderw=16"]
if font:
opts.insert(0, f"fontfile={font}")
vf_chain.append("drawtext=" + ":".join(opts))
elif etype == "slowmo":
raw = eff.get("factor", 2.0)
if raw is None:
continue
factor = float(raw)
if factor <= 0:
continue
vf_chain.append(f"setpts={factor}*PTS")
# atempo 只支持 0.5~2.0,用 1/factor 放慢音频。
af_chain.append(f"atempo={max(0.5, min(2.0, 1.0 / factor))}")
cmd = ["ffmpeg", "-y", "-ss", f"{start:.3f}", "-to", f"{end:.3f}", "-i", source]
if vf_chain:
cmd += ["-vf", ",".join(vf_chain)]
if af_chain:
cmd += ["-af", ",".join(af_chain)]
cmd += ["-c:v", "libx264", "-c:a", "aac", "-pix_fmt", "yuv420p", out_path]
out_dir = os.path.dirname(out_path)
if out_dir:
os.makedirs(out_dir, exist_ok=True)
run(cmd, desc="ffmpeg 剪辑")
return out_path
# --------------------------------------------------------------------------- #
# Blender 后端说明
# --------------------------------------------------------------------------- #
# 真实的 Blender bpy 脚本生成与无头渲染已实现在 blender_editor.py
# - generate_bpy_script():把剪辑计划翻译成一段 bpy 脚本(new_movie / 裁剪 /
# TEXT / SPEED / render)——即书中"Proposer 生成 Blender 脚本"
# - render_with_blender():用 `blender --background --python edit.py` 无头执行。
# apply_edit(backend="blender") 即走这条路径;backend="auto"(默认)在未装 Blender 时
# 回退到本文件的 ffmpeg 后端。核心的"两步 Vision 定位 + 提议者-审核者"与执行层解耦,
# 两个后端共用同一份剪辑计划(plan),agents.py / demo.py 无需为切换后端改动逻辑。