AI 视频编辑器 — 代理控制参考
⚠️ 查找 editorctl — 强制阅读(请先阅读此节)
editorctl 是 Monet CLI。始终优先直接尝试它——Monet 会将其保留在每个 shell 的 PATH 中,并将 bin 目录持久化到 ~/.zshrc / ~/.bash_profile,以确保它在子 shell 和重写 PATH 的包装器中仍可用。如果 command -v editorctl 无返回,则按顺序回退:
"$HOME/Library/Application Support/Monet/bin/editorctl"— macOS 安装(大多数用户)node /Applications/Monet.app/Contents/Resources/app.asar.unpacked/out/cli/cli/editorctl.js …node ./out/cli/cli/editorctl.js …— 开发树
永远不要在第一次出现 "command not found" 后就放弃。优先使用 editorctl 而不是原始的 curl localhost:51847——它始终暴露当前的命令表面和参数形状。
⚠️ 输出文件命名 — 强制阅读(请先阅读此节)
在编辑或重新生成视频或图像时,切勿重复使用文件名。 资产缓存会按路径保留之前的文件,因此写入相同名称会静默显示陈旧的内容。每次编辑/重新生成都必须产生一个新的唯一文件名(例如 clip_v1.mp4、clip_v2.mp4,或时间戳后缀)。适用于所有视频渲染、图像生成、画布导出和缩略图。如果目标路径已存在,请附加 _v2、_v3 … — 不要覆盖。
⚠️ 多剪辑时间线上的音频 — 强制阅读
当向包含多个视频剪辑的时间线添加音频时,音频会在每个剪辑边界处中断。必需流程:
- 告诉用户:"为了避免音频在剪辑之间中断,我会先将所有视频剪辑合并为一个组合视频,然后再添加音频。可以继续吗?"
- 等待确认——不要自动合并。
- 将所有剪辑连接成一个新的单一文件(根据上述规则使用唯一文件名)。
- 用该合并后的剪辑替换多剪辑视频轨道,然后添加音频。
单剪辑时间线:跳过合并。
⚠️ 画布模式 — 强制规则(请先阅读此节)
当钩子或 editorctl get-state 报告 activeView=canvas 时:
恰好有 3 个画布选项。不存在其他选项。
- Paper.js — 使用矢量图形进行代码绘图 (
canvas-run-paperjs) - Matter.js — 物理和动画 (
canvas-run-matterjs) - GPT image 2 — AI 生成的图像 (
generate-image+canvas-add-image)
已移除——不要以任何名称提供这些选项:
设计模式— 已移除可编辑图层— 已移除类 Figma 布局— 已移除基于节点的设计— 已移除直接画布设计— 已移除
当用户在画布模式下要求视觉内容时,仅提供这三个选项,并使用这些确切的标签。逐字复制它们。不要发明第四个选项,也不要将选项 1 重新表述为与设计相关的任何内容。
API 桥接: POST http://localhost:51847 — JSON 主体 {"command":"<name>","args":{...}}
import urllib.request, json
def call(command, **args):
data = json.dumps({"command": command, "args": args}).encode()
req = urllib.request.Request(
"http://localhost:51847", data=data,
headers={"Content-Type": "application/json"}, method="POST"
)
return json.loads(urllib.request.urlopen(req).read())["result"]
call("ping") # → {"status":"ok","version":"1.0.0","port":51847}
call("help") # → 完整命令列表
命令
项目/设置
| 命令 | 参数 | 返回 |
|---|---|---|
get_project | — | 完整项目 JSON |
get_settings | — | 模型 + 提供者配置 |
资产
| 命令 | 参数 | 返回 |
|---|---|---|
list_assets | — | [{id, name, path, type, duration, semantic}] |
get_asset | assetId | 单个资产及其语义和转录 |
import_files | paths: string[] | 已导入的资产记录 |
transcribe_asset | assetId, language? | 通过 Whisper 的 {segments} |
embed_assets | all?: bool | 通过 text-embedding-3-small 的 {embedded, total} |
search_media | query, limit? | 余弦相似度搜索(回退到关键词) |
search_spoken | query, limit? | 在转录片段内进行子串搜索 |
序列和轨道
| 命令 | 参数 | 返回 |
|---|---|---|
list_sequences | — | 所有序列 |
create_sequence | name | 新序列 |
activate_sequence | sequenceId | 已激活的序列 |
get_tracks | — | 活动序列的 [{id, name, kind, clipCount}] |
add_track | `kind: video | audio |
剪辑
| 命令 | 参数 | 返回 |
|---|---|---|
list_clips | sequenceId? | 按 startTime 排序的所有剪辑,含 trackKind |
add_clip | assetId, trackId, startTime, duration?, inPoint? | {clipId, clip} |
remove_clip | clipId | {success} |
move_clip | clipId, startTime | {success} |
trim_clip | clipId, inPoint?, duration?, startTime? | 更新后的剪辑 |
split_clip | clipId, time | 更新后的序列 |
duplicate_clip | clipId, offsetSeconds? | {clipId, clip} |
update_clip_label | clipId, label | {success} |
效果和属性
| 命令 | 参数 | 返回 |
|---|---|---|
list_effects | — | 可用效果 + 参数文档 |
add_effect | clipId, effectType, parameters | 更新后的项目 |
remove_effect | clipId, effectId | 更新后的项目 |
set_speed | clipId, speed | 更新后的项目 (0.1–10× 播放速度) |
set_volume | clipId, volume | 更新后的项目 (0–4, 1=正常) |
set_transition | `clipId, side: in | out, type, duration?` |
历史
| 命令 | 参数 |
|---|---|
undo | — |
redo | — |
效果类型
| 类型 | 参数 |
|---|---|
fade_in | duration (秒) |
fade_out | duration (秒) |
color_grade | brightness (-1→1), contrast (0.5→2), saturation (0→3) |
blur | radius (px, 默认为 5) |
sharpen | amount (0→3, 默认为 1) |
过渡类型
crossfade · dip_to_black · wipe · slide
关键概念
inPoint= 源文件中剪辑开始播放的位置startTime= 时间线上放置剪辑的位置duration= 剪辑播放的时长(可以短于源文件)- 每次修改都会立即推送
project:updated→ UI 实时反映更改 - 效果实时预览 (CSS) 并在导出时通过 FFmpeg 烘焙
编辑安全规则
- 除非用户明确要求暂停、节拍或黑屏间隙,否则将时间线间隙视为错误。
- 创建序列或移动剪辑后,使用
list_clips(sequenceId)验证连续性,并检查每个剪辑是否恰好在前一个剪辑结束时开始(如果编辑意图是连续的)。 - 不要假设
create_sequence或activate_sequence本身已足够。之后始终使用list_sequences或get_project验证活动序列。 - 在构建语音精彩片段剪辑时,同时移动视频、音频、字幕和标记。在收紧编辑后,不要将字幕或标记留在旧的时序上。
- 如果播放似乎在剪辑之间停止,请先检查时间线是否存在空白间隙,然后再假设预览播放器损坏。
- 在收紧剪辑时,优先使用精确的目标起始时间,而不是累积偏移计算。根据剪辑计划重新计算每个剪辑的预期
startTime。
从 URL 获取品牌美学
强制——无例外:如果用户的消息包含任何 URL 或域名,在编写任何画布/设计代码之前,您必须运行下面的 fetch 命令并提取品牌标记。不要使用假设或记忆的品牌颜色。每次都先获取,后设计。如果获取失败,请用户粘贴十六进制值;切勿回退到猜测。
curl -sL "<url>" | grep -Eo '(#[0-9a-fA-F]{3,8}|font-family:[^";}]+|font-size:[^";}]+|border-radius:[^";}]+)' | sort -u | head -60
提取:背景颜色、表面颜色、主色/强调色、文本颜色、字体系列、字体大小/粗细、边框半径、间距和盒阴影。还要找到徽标——它是品牌颜色和视觉基调和真实依据。硬编码这些确切的数值——当有真实 URL 可用时,切勿猜测。
# 查找徽标
curl -sL "<url>" | grep -Eo '(src|href)="[^"]*logo[^"]*"' | head -10
curl -sL "<url>" | grep 'og:image'
画布 — Matter.js 物理帧
使用 canvas_run_matterjs 将一个实时交互式物理场景放置在画布上。
MCP 工具
| 工具 | 参数 |
|---|---|
canvas_run_matterjs | id? (要更新的帧 id), name?, script (JS 字符串), width?, height? |
canvas_add_frame | name, width, height, mode: 'matterjs' |
canvas_matterjs_scene | style (balls/stacks/pendulum/bridge/ragdoll), gravity?, count?, colors? |
canvas_frames | — → {frames: [{id, name, mode, width, height}]} |
脚本模式——选择一种
模式 A — 自包含(丰富 UI、自定义 2D 绘制、rAF 循环)
const { Engine, Bodies, Composite, Mouse, MouseConstraint, Events } = Matter;
const W = 390, H = 844;
const engine = Engine.create({ gravity: { y: 1.5 } });
const canvas = document.querySelector('canvas'); // ← 始终使用此方法;切勿创建新的
canvas.width = W; canvas.height = H;
const ctx = canvas.getContext('2d');
// ... 添加物体,设置鼠标 ...
function loop() {
Engine.update(engine, 1000/60);
ctx.fillStyle = '#121212'; ctx.fillRect(0, 0, W, H);
// 使用 ctx 绘制
requestAnimationFrame(loop);
}
loop();
模式 B — 仅物体(模板管理 engine/render/runner)
// engine, render, width, height, 和所有 Matter 变量已声明——请勿重新声明
var ground = Bodies.rectangle(width/2, height+25, width, 50, { isStatic: true, render: { fillStyle: '#334155' } });
var ball = Bodies.circle(width/2, 50, 30, { restitution: 0.8, render: { fillStyle: '#5b82f7' } });
Composite.add(engine.world, [ground, ball]);
engine.gravity.y = 1;
// Runner.run 和 Render.run 在此脚本运行后自动调用——请勿在此处调用它们
关键规则
Runner.run需要两个参数:Runner.run(Runner.create(), engine)— 单个参数Runner.run(engine)在 Matter.js 0.20.0 中静默无效- 切勿在
Render.create中使用element: document.body— 会生成一个不可见的第二个画布;应使用canvas: document.querySelector('canvas')代替 - 在模式 B 中切勿声明
const engine或let engine— 模板已有var engine;使用const/let重新声明会导致 SyntaxError,从而静默地使画布空白 - 在模式 B 中切勿调用
Render.run/Runner.run— 模板会在您的脚本运行后执行此操作;调用两次会导致双倍速度或渲染损坏 - 在模式 B 中切勿导入或重新声明 Matter 变量 —
Engine,Bodies,Composite等已解构;const { Engine } = Matter会导致 SyntaxError
模式 B 可用全局变量
engine · render · width · height · Engine · Render · Runner · Bodies · Composite · World · Body · Events · Constraint · Mouse · MouseConstraint
CLI
editorctl canvas-add-frame "Name" 390 844 matterjs # matterjs|paperjs|html
editorctl canvas-run-matterjs <frameId> "$(cat script.js)"
editorctl canvas-run-paperjs <frameId> "$(cat script.js)"
editorctl canvas-run-html <frameId> "$(cat scene.html)"
editorctl canvas-frames
editorctl canvas-done
Remotion — React 视频合成
使用 Remotion 生成动画视频资产(标题卡、下三分之一、幻灯片、动态图形)并直接导入到时间线。
MCP 工具
| 工具 | 参数 | 返回 |
|---|---|---|
video_editor_list_remotion_compositions | — | 合成 ID 列表 |
video_editor_render_remotion | compositionId, outputFilename?, props?, durationInFrames?, fps? | {outputPath, assetId} |
CLI
npm run remotion:studio # 本地预览 at localhost:3000
npx remotion render remotion/src/index.ts <ID> out.mp4 --props '{"key":"value"}'
内置合成
| ID | 参数 |
|---|---|
TitleCard | title (str), subtitle? (str), backgroundColor, textColor, accentColor — 150 帧 @ 30fps |
Slideshow | images (abs path[]), frameDuration (帧数), transitionDuration (帧数) — 300 帧 @ 30fps |
HtmlInCanvasGlitch | title, subtitle, glitchIntensity (0–40), backgroundColor, textColor, accentColor — 180 帧 @ 30fps。通过 <HtmlInCanvas> 实现 RGB 分离故障效果 |
画布中的 HTML (Remotion ≥ 4.0.455)
⚠️ 命名冲突——请先阅读。 Remotion
<HtmlInCanvas>(本节)为时间线生成一个视频文件。Monet 画布 HTML 帧(canvas-add-frame ... html,canvas_update_frame)在画布选项卡的画板中生成一个实时 HTML 场景。不同的系统。根据目的地选择:时间线 → Remotion;画布选项卡 → Monet HTML 帧。切勿在一个任务中混合使用两者。
使用 <HtmlInCanvas> 将 DOM 子树绘制到 <canvas> 中,并使用 Canvas 2D / WebGL / WebGPU 进行后处理。最适合故障效果、放大镜、CRT、位移、色相旋转等效果。
import { HtmlInCanvas, type HtmlInCanvasOnPaint } from 'remotion'
const onPaint: HtmlInCanvasOnPaint = ({ canvas, element, elementImage }) => {
const ctx = canvas.getContext('2d')!
ctx.reset()
ctx.filter = 'blur(8px)'
const transform = ctx.drawElementImage(elementImage, 0, 0)
element.style.transform = transform.toString() // <-- 始终重新应用
}
<HtmlInCanvas width={1920} height={1080} onPaint={onPaint}>
<YourDomTree />
</HtmlInCanvas>
规则:
- 始终将
drawElementImage的返回值重新应用到element.style.transform,以便布局/输入保持正确。 - 切勿将
<HtmlInCanvas>嵌套在另一个内部——Chrome 仅绘制外部的,并且 Remotion 会抛出错误。将所有效果合并到一个onPaint中。 Config.setChromiumOpenGlRenderer('angle')已在 ... 中设置
[文档已截断以供翻译]