主路
Rendi
TOBATSU · SHARED SKILL · DRAFT
pip-compose-rendi浮窗画中画 / 上下分屏 / 左右分屏. 走 Rendi, 本机 ffmpeg 兜底, 直推 R2. 跟 subtitle-burn-rendi 同套路, 不解析自然语言 — agent 把意图翻成 payload, skill 只接结构化字段.
定位
划清边界, 让 skill 单一职责, 别成"啥都管"的怪物
默认布局
本地 ffmpeg 跑了一遍默认 filter graph 的实际帧, 跟 prototype 02 的视觉一致
配置 · 输出
布局
layout 字段决定走哪条 — 其他字段只对当前 layout 生效
overlay_x + overlay_y, 互斥校验 fail 直接 invalid_layout_param.入参
必填两个 URL, 其他全部可省, 缺啥补啥
| 字段 | 类型 | 说明 |
|---|---|---|
main_video_urlREQ | url | 主视频, 公网可下 |
pip_video_urlREQ | url | 副视频 (通常是数字人 / 解说画面) |
| 字段 | 类型 | 取值 · 默认 |
|---|---|---|
layout | enum | overlay / split_v / split_h overlay |
overlay_position | enum | tl / tr / bl / br / custom tr |
overlay_size | float | 0.12 - 0.80 (短边占比) 0.34 |
overlay_shape | enum | circle / square / vrect / hrect circle |
overlay_margin | float | 0 - 0.10 0.02 |
overlay_x / overlay_y | float | 0 - 1.0, custom 时必填 |
split_pip_side | enum | first / second second |
split_pip_ratio | float | 0.20 - 0.50 0.40 |
| 字段 | 类型 | 取值 · 默认 |
|---|---|---|
main_framing | object \| null | {offset_x:-1..1, offset_y:-1..1, zoom:0.30..3.20} null = cover |
pip_framing | object \| null | 同上 |
main_volume | float | 0 - 1.0 1.0 |
pip_volume | float | 0 - 1.0 1.0 |
处理
从 payload 到 R2 URL, 10 步
position=custom 必须有 x/y)inputs/, 3 次重试, 全程加 User-Agentprovider_failedtobatsu-agent output upload, 推两份: 视频 + filter-graph.txt渲染
三种 layout 对应三套 filter chain, debug 时会落盘成 filter-graph.txt
ffmpeg# 主 1080x1920, PIP 占短边 34% = 367x367 圆, 右上角距边 22px [0:v]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,setsar=1[main]; [1:v]scale=367:367:force_original_aspect_ratio=increase,crop=367:367, format=yuva420p, geq=lum='p(X,Y)':a='clip((183-hypot(X-183,Y-183))*255,0,255)'[pip]; [main][pip]overlay=x=691:y=22[v]; [0:a]volume=1.0[a0];[1:a]volume=1.0[a1];[a0][a1]amix=inputs=2:duration=first[a]
geq 那行用 (R - distance) × 255 clip 到 0-255 = 抗锯齿圆边 (距离边缘 1px 内做渐变). 不用 if(lt(...),255,0) 那种硬边.
ffmpeg[0:v]scale=1080:1152:force_original_aspect_ratio=increase,crop=1080:1152[top]; [1:v]scale=1080:768:force_original_aspect_ratio=increase,crop=1080:768[bot]; [top][bot]vstack=inputs=2[v]; [0:a]volume=1.0[a0];[1:a]volume=1.0[a1];[a0][a1]amix=inputs=2:duration=first[a]
ffmpeg[0:v]scale=648:1920:force_original_aspect_ratio=increase,crop=648:1920[left]; [1:v]scale=432:1920:force_original_aspect_ratio=increase,crop=432:1920[right]; [left][right]hstack=inputs=2[v]; ...
出参
两份产物 + 一份元数据, 上游拿 URL 直接渲染
json{ "status": "ok", "provider": "rendi", "fallback_used": false, "rendi_command_id": "f004b6a5-...", "output_video_url": "https://cdn-tobatsu/.../composed-video.mp4", "filter_graph_url": "https://cdn-tobatsu/.../filter-graph.txt", "output_width": 1080, "output_height": 1920, "output_duration_seconds": 59.20, "layout_resolved": { "layout": "overlay", "canvas": { "w": 1080, "h": 1920 }, "main_box": { "x": 0, "y": 0, "w": 1080, "h": 1920 }, "pip_box": { "x": 691, "y": 22, "w": 367, "h": 367 }, "pip_shape": "circle" }, "config_used": { /* merge 后完整 config */ } }
filter-graph.txt 是 debug 用, 把实际下发到 ffmpeg 的 filter 字符串落盘, 出问题能复现.
失败
跟 subtitle skill 一致: fail 前写 evidence 到 outputs/.logs/compose-error.json, 再 tobatsu-agent fail
| reason | 触发 |
|---|---|
invalid_payload | 字段缺 / 类型错 / JSON 解析失败 |
invalid_layout_param | 数值超范围 / 互斥字段冲突 |
download_failed | 任一视频拉不到 (3 次重试后) |
unreadable_video | ffprobe 失败 / 无视频流 |
provider_failed | Rendi + 本机 ffmpeg 都跑挂 |
upload_failed | R2 上传 3 次重试都挂 |
对比
同套路 ≠ 同输入. 两个 skill 共用基础能力, 但解决的事不一样
| subtitle-burn-rendi | pip-compose-rendi | |
|---|---|---|
| 解决 | 一路视频 + SRT/ASS → 烧字幕 | 两路视频 → 合成一路 |
| 模式 | Mode A / B / C | 单一: 默认 + payload 覆盖 |
| 字体 | 装 8 个 (Montserrat + Noto 系) | 无 |
| Python venv | 需要 (pysubs2) | 不需要 (标准库) |
| 共用 | Rendi 主 + 本机兜底 / 扁平 zip / 10s poll / 600s cap / User-Agent / fail evidence / tobatsu-agent output upload / Python 3.10 兼容 | |
ffmpeg + RENDI_API_KEY 就行. 之前 magi3 已经装过这两样, 这个 skill 上线不需要再动 worker.清单
skill 包的目录长这样