
Chanjing One Click Video Creation
- 58 installs
- 18 repo stars
- Updated March 28, 2026
- chanjing-ai/chan-skills
Generates a complete short video from a topic or workflow by orchestrating Chanjing TTS, digital-human synthesis, AI clips, and ffmpeg packaging.
About
A one-click renderer that turns a topic or workflow into a finished short video by chaining Chanjing TTS, digital-human synthesis, and text-to-video, then packaging with ffmpeg/ffprobe. A developer uses it to auto-produce a narrated short video end-to-end.
- End-to-end: script, storyboard, digital-human voiceover, AI clip mixing
- Orchestrates chanjing-tts, video-compose, ai-creation; uses ffmpeg/ffprobe
Chanjing One Click Video Creation by the numbers
- 58 all-time installs (skills.sh)
- Ranked #870 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/chanjing-ai/chan-skills --skill chanjing-one-click-video-creationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 58 |
|---|---|
| repo stars | ★ 18 |
| Last updated | March 28, 2026 |
| Repository | chanjing-ai/chan-skills ↗ |
What it does
Generates a complete short video from a topic or workflow by orchestrating Chanjing TTS, digital-human synthesis, AI clips, and ffmpeg packaging.
Files
一键式视频渲染器
功能说明
一键调用 Chanjing API 完成口播 TTS、数字人合成、文生视频与本地封装;集成 ffmpeg / ffprobe 做拼接、转码与轨对齐。编排与安全细则见 §3–§8 与 templates/;成片命令见 §5。
运行依赖
必须可用的二进制或等价封装(具体调用方式见 §5、run_render.py):
- ffmpeg:拼接、转码、封装
- ffprobe:时长、分辨率、旋转等元数据(与数字人轨对齐)
- chan-skill(或同仓库下直接
python调用子技能脚本):驱动chanjing-tts、chanjing-video-compose、chanjing-ai-creation等 CLI
环境变量与机器可读声明
- 环境变量键名与说明:`manifest.yaml`(
environment段)及本文 - 键名、默认值、凭据模型、二进制、合规 `permissions`、`clientPermissions`、`agentPolicy`:`manifest.yaml`
§3.2 表格为配合 `run_render.py` 阅读的说明;若与 `manifest.yaml` 冲突,以 `manifest.yaml` 为准。
使用命令
- ClawHub(slug 以注册表为准,常与技能包名一致):
clawhub run chanjing-one-click-video-creation - 本仓库直连:
python scripts/run_render.py --input workflow.json --output-dir ./outputs/run1(在技能目录或配合 `SKILLS_DIR` / `CHANJING_ONE_CLICK_VIDEO_SKILLS_ROOT` / `CHAN_SKILLS_DIR` 使用)
---
速查
| 内容 | 位置 |
|---|---|
工作流、duration_sec、null/合并、选题校验 | §4.1 |
切段、奇偶镜、scenes[]、scene_count/video_type;首镜 `voiceover` ≤20 字(硬) | `storyboard_prompt.md` 篇首「文本切段」;`script_prompt.md` 首镜口播;`video_brief_plan.md` |
渲染技术、状态、partial/success、硬约束 | `render_rules.md` §1–§4;§7、§8 |
| `ref_prompt` / 文生提示词 | `storyboard_prompt.md` + `history_storyboard_prompt.md`;§4.2 指针 |
| 请求体字段与默认 | §6 |
run_render.py、子进程 CLI | §5 |
| 安全、凭据、信任边界、策略 | `manifest.yaml` + §3.1(§3.1 不重复 manifest 表格) |
| 环境变量、二进制、副作用、落盘 | §3.2 |
冲突:渲染实现以 `render_rules.md` 为准;`ref_prompt` 条文以 `storyboard_prompt.md` / `history_storyboard_prompt.md` 为准(§4.2 汇总指针)。run_render.py 只实现 §5 + `render_rules.md`,不增业务规则。执行:手工编排子 skill、仅 run_render、或混用。
---
1. 做什么
1. 选题或全文 → video_plan、口播全文、分镜 2. TTS:整段优先;超长按分镜少批合并(细则与字数见 `render_rules.md` §3·C.4) 3. 按镜切音频 4. 数字人分镜:chanjing-video-compose(音频驱动) 5. AI 分镜:ref_prompt → chanjing-ai-creation → 与镜内音频合成 6. 封装:对齐公共数字人轨 → ffmpeg concat → 本地 mp4
---
2. 何时用 / 何时不用
| 适合 | 要成片;口播与画面混剪;用户明确要生成短视频 |
| 不适合 | 仅文案/标题;未要视频;只剪已有素材 |
---
3. 前置条件
- 鉴权:
chanjing-credentials-guard;凭据路径与字段见 `manifest.yaml`、§3.1;无凭证时子进程可open_login_page.py - Plan/Script/分镜:本地 Agent 逻辑,无需外部 LLM API key(本 skill 必选路径不依赖外部 LLM)
- 本机二进制与仓库布局:§3.2(
ffmpeg/ffprobe、`SKILLS_DIR` 等) - 数字人与音色:勿用环境变量或仓库内缓存文件保存跨任务的「默认」
audio_man/person_id/figure_type。每次任务在 `workflow.json` 根级显式填写;由 Agent 按 `video_plan`(如video_type)、口播人设与选题语义,调用 `list_voices.py` 与 `list_figures.py`(--source取common/customised等与本次任务一致)选型后写入;`audio_man` 宜与所选形象的 `audio_man_id` 一致。 - 公共数字人选型(禁止「只取列表前几项」):须用
list_figures.py --source common --json拉取候选(必要时增大--page-size或翻页,覆盖足够条目),在候选内逐项对比后再定稿:name、figures[].type(→figure_type)、figures[].width/height(画幅与 D.1c 一致)、audio_man_id、audio_name(若有)与 `video_plan`/口播人设(性别、气质、行业、年龄感)是否匹配。默认偏好年轻、有活力的形象:名称或audio_name中含青年/少女/小哥哥/小姐姐/学生/元气/青春/年轻等正向信号时优先;仅当选题或用户明确要求成熟、权威、中老年等气质时,再选对应人设。定制源customised同样对比name、width/height、audio_man_id等,勿未经比较直接取页首。
3.1 安全、凭据与信任边界
环境变量与二进制以 `manifest.yaml` 与 §3.2 为据。审阅时可对照 `description` 与 `manifest.yaml`(含 `agentPolicy`)。
- 能力与管道:步骤级说明见 §1;
run_render职责与子进程见 §5(不在此重复链路)。 - 主凭据 / 路径 / primaryEnv:见 `manifest.yaml`;路径与写回行为另见 §3.2 持久性表「凭据状态」及 `CHANJING_OPENAPI_CREDENTIALS_DIR`(兼容 `CHANJING_CONFIG_DIR`)。
- 敏感与合规:勿回显完整密钥、勿将 `credentials.json` 提交版本库;权限建议 `0700` / `0600`(配置脚本尽量设置)。
- 信任与出站行为:HTTPS、按返回 URL 拉取媒体、`--output-dir` 落盘等细节见 §3.2「典型副作用」与持久性表;须自行判断是否信任蝉镜主机与链接。
- 浏览器:缺凭证时的 `webbrowser.open` / `open_login_page.py` 见 §3.2 同表。
- Agent 策略:`manifest.yaml` 中 `agentPolicy`(非 always、不改其它 skill)。
3.2 运行时契约(环境变量、二进制、副作用与落盘)
与 `scripts/run_render.py` 及同仓库子 skill 行为对齐;与篇首 YAML、`manifest.yaml` 一致。若与其它产品文档并列,以本仓库源码、`manifest.yaml` 与本文为准。
环境变量(常见)
命名以仓库根 `合规规则.md` §3 为准;下列推荐名在代码中生效,旧名仍兼容(见 `manifest.yaml` 与各子 skill 说明)。
| 变量 | 必需性 | 默认 / 说明 |
|---|---|---|
| `CHANJING_OPENAPI_BASE_URL` | 可选 | 默认 https://open-api.chanjing.cc;兼容 `CHANJING_API_BASE`。 |
| `CHANJING_OPENAPI_CREDENTIALS_DIR` | 可选 | 默认 ~/.chanjing;`credentials.json` 所在目录;兼容 `CHANJING_CONFIG_DIR`。 |
| `SKILLS_DIR` | 视布局而定 | 平台保留;含 `skills/chanjing-tts` 等的仓库根。未设时依次尝试 `CHANJING_ONE_CLICK_VIDEO_SKILLS_ROOT`、`CHAN_SKILLS_DIR`,再由 run_render.py 自脚本路径向上四级推断。 |
| `CHANJING_ONE_CLICK_VIDEO_REF_PROMPT_MAX_CHARS` | 可选 | 默认 `8000`;整段 `ref_prompt` 上限;兼容 `AI_VIDEO_PROMPT_MAX_CHARS`。 |
| `CHANJING_ONE_CLICK_VIDEO_CREATION_MODEL_CODE` | 可选 | 缺省文生视频 `model_code`;兼容 `AI_VIDEO_MODEL`。 |
说明:部分外部文档中的 `FIRST_DIGITAL_HUMAN_MAX_CHARS`(首个数字人分镜voiceover字数上限)等变量,当前本仓库的 `run_render.py` 未读取;以后若源码增加对应os.environ,应同步更新本表。
外部二进制
| 二进制 | 必需性 | 用途 |
|---|---|---|
| `ffmpeg` | 跑一键成片 `run_render.py` 时必需 | 拼接、转码、封装音视频等。仅编排纯 API、不执行本渲染脚本时可不装。 |
| `ffprobe` | 同上 | 读取媒体分辨率、时长、旋转元数据等,用于与数字人轨对齐。 |
执行脚本时的典型副作用(按类)
| 类型 | 说明 |
|---|---|
| 出站 HTTPS | 蝉镜 Open API(`CHANJING_OPENAPI_BASE_URL` / 兼容 `CHANJING_API_BASE`)、以及接口/CDN 返回的 `video_url` / 音频 URL 等素材拉取。 |
| 本地文件 | `run_render.py --output-dir` 下常见:`final_one_click.mp4`、`workflow_result.json`、`work/`(中间音频、分段视频、concat 列表等);具体以当次命令与 `templates/render_rules.md` 为准。 |
| 子进程 | `ffmpeg` / `ffprobe`;run_render 通过 `subprocess` 调用同仓库下 `skills/chanjing-tts`、`chanjing-video-compose`、`chanjing-ai-creation` 等目录中的 Python CLI。 |
| 浏览器 | 凭据缺失或引导登录时,鉴权链可能 `webbrowser.open` 或执行 `chanjing-credentials-guard` 的 `open_login_page.py`(与各 skill 的 `_auth.py` 行为一致)。 |
持久性变更范围与用户可控性
以下对本 skill 而言属预期内副作用;可通过路径与环境变量控制写入位置,而非隐式污染无关目录。
| 类别 | 写入什么 | 典型位置 | 用户如何控制 |
|---|---|---|---|
| 凭据状态 | 经配置写入的 `app_id` / `secret_key`、刷新后的 `access_token`、 `expire_in` 等 | `CHANJING_OPENAPI_CREDENTIALS_DIR/credentials.json`(默认 `~/.chanjing/credentials.json`;兼容 `CHANJING_CONFIG_DIR`) | 设置推荐名或旧名;或迁移/删除该文件;勿将秘钥提交版本库。 |
| 一键成片工件 | `final_one_click.mp4`、`workflow_result.json`、`work/` 等 | 由 `run_render.py --output-dir` 指定(常见为某次任务下的 `outputs/<任务名>/`) | 选用明确的 `--output-dir`;任务结束后按需保留或删除该目录。 |
| 其它下载类脚本(子 skill) | 合成结果等到本地 | 各 skill 的 `download_result.py` 等:默认多在当前工作目录下 `outputs/<产品线>/`,或 `--output` 绝对路径 | 在预期 cwd 下执行,或始终传 `--output`;详见对应 skill 的 `SKILL.md`。 |
| 临时/过程文件 | TTS 合并、切段、上传前缓存等 | 多在上述 `output-dir` 下的 `work/` 或脚本约定子目录 | 随输出目录一并管理。 |
凭据文件:路径与读写语义见上表 凭据状态 行;首次配置见 `skills/chanjing-credentials-guard/SKILL.md`。
---
4. 规则汇编
4.1 工作流编排
合并:null = 不覆盖。顺序:默认铺底 → 非 null 覆盖 → 布尔/整数校正。字段默认见 §6;未在表中展开的缺省由 `run_render.py`(及子进程)按实现与环境变量读取(不含音色/数字人:audio_man、person_id/avatar_id、figure_type 仅来自 `workflow.json`,见 §3)。
`duration_sec`:策划参考,非 ffmpeg 上限。成片时长以 TTS+ffprobe 为准。scene_count 见 `video_brief_plan.md`;切段与 AI 条数依实测与字幕轴(`render_rules.md` §3·C.5)。禁止为凑时长裁已定稿口播(除非用户要求)。
选题:去空白 <5 字、占位串(如「你好」「test」)拒收;可扩写;严格模式模糊则失败。
步骤:1) Plan → video_brief_plan(败则全败;模板见 `video_brief_plan.md`)2) Script(hook / 首段与首镜对齐:≤20 字硬上限,见 `script_prompt.md`)3) Storyboard:语义切分;`storyboard_prompt.md`(首个分镜 `voiceover` 同上硬上限);非当代 `history_storyboard_prompt.md`;DH `chanjing-video-compose`,AI `chanjing-ai-creation`;TTS/多段 AI/mux `render_rules.md` §3、§5 4) Render:`render_rules.md` §3(含 §3·C.6)、§4(表 4–6);ref_prompt 质检见 `storyboard_prompt.md` / `history_storyboard_prompt.md`(§4.2);重试/partial `render_rules.md` §1 5) 成功:`render_rules.md` §1
仅渲染:run_render.py + full_script + scenes[]。顺序:Plan → Script → Storyboard → Render(各阶段用哪份模板见上列步骤)。
---
4.2 文生视频提示词(ref_prompt)— 指针
唯一条文真值(修订以模板为准,本文不重复 D.1–D.4 表文):
| 范围 | 模板 |
|---|---|
当代向、D.0 语境缺省与文明圈推断、D.1 长度、D.1a、D.1b(易幻觉,全 skill 共用)、D.2 当代、手工 visual_prompt、D.3、D.4 当代装配与 7 要素 / 题材簇 / 单镜拼装 / 自检 | `templates/storyboard_prompt.md` → 「文生视频提示词(当代向真值)」 |
| D.2 非当代路由、历史流程层、文明圈与国别自洽、占位符纪律、与 D.3/D.4 衔接说明 | `templates/history_storyboard_prompt.md` |
| 族裔、历史/非当代中式造型与出现人物时的英文短语 | `templates/visual_prompt_people_constraint.md`(显式族裔锚定、历史 / 非当代节;兼 `render_rules.md` §4 表 4–6) |
仍仅在此处索引:长音频多段 `render_rules.md` §3·C.6;字数上限 `CHANJING_ONE_CLICK_VIDEO_REF_PROMPT_MAX_CHARS`(兼容 `AI_VIDEO_PROMPT_MAX_CHARS`)。模板与 `render_rules.md` 实现冲突时以 `render_rules.md` 为准。
---
5. 自动化编排(run_render.py)
依赖:鉴权;`SKILLS_DIR` / `CHANJING_ONE_CLICK_VIDEO_SKILLS_ROOT` / `CHAN_SKILLS_DIR`(§3);chanjing-tts / chanjing-video-compose / chanjing-ai-creation
职责:① TTS+audio_task_state;批合并与单批字数上限见 `render_rules.md` §3·C.4(TTS_BATCH_MAX)② 切段(`render_rules.md` §3·C.5)③ 有 AI 镜时先完成首条数字人并 `ffprobe`(含 `rotate`)→ 再按映射提交文生 `aspect_ratio`/`clarity`(见 `render_rules.md` §3·C.6、debug.ai_video_submit_params)④ 与其余 DH/AI 并行 poll ⑤ AI 轨对齐该参照 ffprobe ⑥ ffmpeg concat ⑦ 多段文生在 ref_prompt 后追加英文分层;总长由 `CHANJING_ONE_CLICK_VIDEO_REF_PROMPT_MAX_CHARS`(兼容 `AI_VIDEO_PROMPT_MAX_CHARS`)约束
不做:不产 plan/script/storyboard;不自动非当代/当代;不用 list_tasks.py 当代次(`render_rules.md` §4 表项 8)
手工编排:仍须满足 `render_rules.md` §3、§4 与 §5;§3 细化(如 silencedetect、minterpolate、参照轨码率、同套切段音频换形象、TTS 批间静音等)全部保留。
输入 MVP
| 字段 | 必填 | 说明 |
|---|---|---|
full_script | 是 | 与各镜 voiceover 按 scene_id 拼,norm 一致 |
scenes | 是 | scene_id、voiceover、use_avatar;AI 镜 ref_prompt(`storyboard_prompt.md` / `history_storyboard_prompt.md`;§4.2);可选 subtitle |
audio_man | 是 | 宜与所选数字人形象的 audio_man_id 一致 |
person_id/avatar_id | 条件 | 有 DH 镜必填 |
figure_type | 否 | 与当次 list_figures.py 所选形象行的 figure_type 一致(公共多形态时必填) |
subtitle_required | 否 | 默认 false;为 true 时数字人镜烧录字幕(--subtitle show) |
speed/pitch | 否 | 默认 1/1 |
ai_video_duration_sec | 否 | 5 或 10,默认 10 |
model_code | 否 | 默认 `CHANJING_ONE_CLICK_VIDEO_CREATION_MODEL_CODE`(兼容 `AI_VIDEO_MODEL`)或 Doubao-Seedance-1.0-pro;creation_type=4;不传 ref_img_url |
max_retry_per_step | 否 | 默认 1(§6) |
python scripts/run_render.py --input workflow.json --output-dir ./outputs/run1输出:final_one_click.mp4;workflow_result.json;work/
---
6. 输入(请求体)
norm:去 \r、首尾空白;空→空串;与 `run_render.py` 一致。口播:先 full_script,再 script→copy_text→input_script→content 首个非空。无 topic:首句代选题(40 字内遇句末标点截,否则 24 字)。null/合并 §4.1。
| 字段 | 必填 | 说明 |
|---|---|---|
topic | 条件 | 无则见首句规则;建议 ≥5 字 |
industry/platform/style | 否 | industry 空;platform/style:DEFAULT_* 或 douyin/观点型口播 |
duration_sec | 否 | DEFAULT_DURATION 或 60;策划参考 |
use_avatar | 否 | 默认 true |
avatar_id/voice_id | 否 | 空;不得用环境变量兜底音色或数字人;须在 workflow.json 写明 audio_man/person_id(及有 DH 镜时的 figure_type),由 Agent 按当次任务调用 list_voices.py 与 list_figures.py(来源与 `video_plan` / 用户指定一致)对比 `name`、形态、画幅、`audio_name` 等后选型;禁止未比较即取列表最前几条;默认偏好年轻数字人(见 §3) |
subtitle_required | 否 | 默认 false(数字人成片不烧录字幕;run_render 传 hide) |
cover_required | 否 | 默认 true |
strict_validation/allow_auto_expand_topic/max_retry_per_step | 否 | true/false/1 |
full_script | 否 | 默认空 |
script_title/script_hook/script_cta | 否 | 默认空 |
script/… | 否 | 见上文口播顺序 |
---
7. 输出 JSON
| 键 | 含义 |
|---|---|
status | success / partial / failed |
video_plan | Plan |
script_result | title、hook、full_script、cta |
storyboard_result.scenes[] | scene_id、duration_sec、voiceover、subtitle、visual_prompt、use_avatar |
render_result | video_file、scene_video_urls、render_path、degrade_log |
| 其它 | error、debug… |
渲染无降级:任一步失败即中断,不自动改为仅 DH 或仅 AI 成片。partial:未成 success(如 run_render 异常仍写 workflow_result.json);不表示允许上述降级,不免 `storyboard_prompt.md`·D.1b 类质检。成功 degrade_log=[];失败尽量保留已产出文案与分镜。
---
8. 硬性约束
表在 `templates/render_rules.md` §4;与 ref_prompt 交叉见 `storyboard_prompt.md` / `history_storyboard_prompt.md`(§4.2 指针)。本节为锚点。
---
9. 限制
- 本地 mp4;不上传
- AI 单段常 5–10s;长口播多段
- 成片时长=TTS 总轨;可与
duration_sec不符 - TTS:整轨优先、超长少批合并;单批上限与合并策略(含
TTS_BATCH_MAX)以 `render_rules.md` §3·C.4 为准 - 文生失败可能为平台/模型;试增
max_retry_per_step、短ref_prompt、拆镜;查workflow_result.json
{
"topic": "示例选题(可选,run_render 仅用于渲染时可省略)",
"full_script": "大家好这是开场。中间这一段交给画面。感谢收看结尾。",
"audio_man": "每次任务据语义选型:list_voices 的 id,宜与同条形象的 audio_man_id 一致",
"person_id": "每次任务据语义与 video_plan:list_figures 所选来源行的 person_id",
"figure_type": "与当次 list_figures 该形象行的 figure_type 一致(以接口返回为准)",
"ai_video_duration_sec": 10,
"model_code": "Doubao-Seedance-1.0-pro",
"max_retry_per_step": 1,
"scenes": [
{
"scene_id": 1,
"voiceover": "大家好这是开场。",
"use_avatar": true,
"subtitle": "大家好这是开场。"
},
{
"scene_id": 2,
"voiceover": "中间这一段交给画面。",
"use_avatar": false,
"ref_prompt": "Contemporary documentary B-roll, natural light, handheld steady shots, concrete props matching the narration, focus on objects and environment, vertical 9:16, no on-screen text, no logos.",
"subtitle": "中间这一段交给画面。"
},
{
"scene_id": 3,
"voiceover": "感谢收看结尾。",
"use_avatar": true,
"subtitle": "感谢收看结尾。"
}
]
}
# 合规:根目录 合规规则.md §1–§2
name: chanjing-one-click-video-creation
version: 0.1.0
vendor: chanjing
runtime:
interpreter: python3
dependencies:
- ffmpeg
- ffprobe
- chan-skill
env:
required: []
optional:
- CHANJING_OPENAPI_CREDENTIALS_DIR
- CHANJING_OPENAPI_BASE_URL
- SKILLS_DIR
- CHANJING_ONE_CLICK_VIDEO_SKILLS_ROOT
- CHAN_SKILLS_DIR
- CHANJING_ONE_CLICK_VIDEO_REF_PROMPT_MAX_CHARS
- AI_VIDEO_PROMPT_MAX_CHARS
- CHANJING_ONE_CLICK_VIDEO_CREATION_MODEL_CODE
- AI_VIDEO_MODEL
permissions:
network_mode: allowlist
allowed_hosts:
- open-api.chanjing.cc
- www.chanjing.cc
filesystem:
read_roots:
- "${WORKSPACE_ROOT}"
- "${SKILL_DIR}"
- "${CHANJING_OPENAPI_CREDENTIALS_DIR}"
write_roots:
- "${WORKSPACE_ROOT}"
- "${CHANJING_OPENAPI_CREDENTIALS_DIR}"
allowed_commands:
- python3
- ffmpeg
- ffprobe
- chan-skill
schemaVersion: 1
skill:
id: chanjing-one-click-video-creation
author: chan-skills
category: 媒体处理
tags:
- 视频渲染
- ffmpeg
- ChanjingAPI
- 蝉镜
- Chanjing
- 一键成片
summary: >-
选题/workflow 驱动一键短视频成片:编排 TTS、数字人合成、文生视频与本地 ffmpeg 封装。
skillDoc: SKILL.md
supportCommands:
- run_render
siblingSkills:
- chanjing-tts
- chanjing-video-compose
- chanjing-ai-creation
- chanjing-credentials-guard
environment:
variables:
- name: CHANJING_OPENAPI_CREDENTIALS_DIR
required: false
description: 存放 credentials.json 的目录(兼容 CHANJING_CONFIG_DIR),默认 ~/.chanjing
- name: CHANJING_OPENAPI_BASE_URL
required: false
description: Open API 基址(兼容 CHANJING_API_BASE),默认 https://open-api.chanjing.cc
- name: SKILLS_DIR
required: false
description: 平台保留;含子 skills 的仓库根,优先于下列技能私有变量
- name: CHANJING_ONE_CLICK_VIDEO_SKILLS_ROOT
required: false
description: 技能私有;仓库根(兼容 CHAN_SKILLS_DIR)
- name: CHANJING_ONE_CLICK_VIDEO_REF_PROMPT_MAX_CHARS
required: false
description: ref_prompt 总长上限(兼容 AI_VIDEO_PROMPT_MAX_CHARS),默认 8000
- name: CHANJING_ONE_CLICK_VIDEO_CREATION_MODEL_CODE
required: false
description: 文生视频 model_code 缺省覆盖(兼容 AI_VIDEO_MODEL)
credentials:
model: credentials_json
defaultPath: "~/.chanjing/credentials.json"
directoryEnv: CHANJING_OPENAPI_CREDENTIALS_DIR
fileName: credentials.json
sensitiveFields:
- app_id
- secret_key
- access_token
- expire_in
persistAccessTokenOnDisk: true
primaryEnvIntentionallyOmitted: true
doNotCommitToVcs:
- credentials.json
clientPermissions:
network:
httpsOutbound: true
documentedHosts:
- open-api.chanjing.cc
filesystem:
read:
- "${CHANJING_OPENAPI_CREDENTIALS_DIR or CHANJING_CONFIG_DIR or ~/.chanjing}/credentials.json"
- "${SKILLS_DIR or CHANJING_ONE_CLICK_VIDEO_SKILLS_ROOT or CHAN_SKILLS_DIR or repo_root}/skills/"
- user_workflow_and_output_paths
write:
- "${CHANJING_OPENAPI_CREDENTIALS_DIR or CHANJING_CONFIG_DIR or ~/.chanjing}/credentials.json"
- user_output_directory
browser:
mayOpenForAuth: true
documentedHosts:
- www.chanjing.cc
subprocess:
allowedPatterns:
- python3
- ffmpeg
- ffprobe
- chan-skill
mayInvokeSiblingSkillScripts: true
userContent:
mayDownloadFromApiResponseUrls: true
metadata:
openclaw:
homepage: https://doc.chanjing.cc
requires:
bins:
- ffmpeg
- ffprobe
agentPolicy:
alwaysSkill: false
modifiesOtherSkillsOrGlobalAgent: false
chanjing-one-click-video-creation
1. 作用说明
本 skill 面向「把选题或口播稿做成可发布的竖屏短视频」:由 Agent(配合 templates/ 提示词)或你自备的 workflow.json,串联 蝉镜 TTS、数字人视频合成、AI 文生视频,再经本机 ffmpeg 切段、对齐、拼接,得到本地 mp4 成片。
- 适合:要成片、要口播 + 画面混剪(奇偶镜 / 文生提示词见 `templates/storyboard_prompt.md`;速查 `SKILL.md`)。
- 不适合:只要文案/标题、不要视频、或仅剪辑已有素材(不必走本流水线)。
细则与接口契约以 [`SKILL.md`](./SKILL.md) 为准(篇首速查表;文生 `ref_prompt` 条文以 [`storyboard_prompt.md`](./templates/storyboard_prompt.md) / [`history_storyboard_prompt.md`](./templates/history_storyboard_prompt.md) 为准,§4.2 为指针;§5 run_render、§6–§7 输入输出);渲染技术规则与硬性约束以 [`templates/render_rules.md`](./templates/render_rules.md) 为准(SKILL.md §8 为锚点)。
---
2. 文档结构与各文件作用
| 文件路径 | 作用 |
|---|---|
| [`SKILL.md`](./SKILL.md) | 主真值:篇首速查表;§4.1 工作流;`ref_prompt` 在 `storyboard_prompt.md` / `history_storyboard_prompt.md`(§4.2 指针);§5 run_render;§6–§7;§8 硬约束;§9 限制。渲染正文见 render_rules.md(§1–§4;SKILL.md §7、§8 为摘要/锚点)。 |
| `README.md`(本文件) | 人类阅读入口:作用、速查与流程图、环境与命令、测试与 FAQ;不重复写规则条文。 |
| *`templates/.md`** | Plan / Script / 文生 `ref_prompt` 真值(storyboard_prompt.md、history_storyboard_prompt.md)/ 族裔约束 / 渲染规则(render_rules.md)等;渲染实现与 render_rules.md 冲突时以 render_rules.md 为准。 |
| `scripts/run_render.py` | 确定性成片:读 workflow.json,调子 skill(TTS、video-compose、ai-creation),写 work/ 与 final_one_click.mp4;行为须符合 `render_rules.md`、`SKILL.md` §5。 |
| `examples/workflow-input.example.json` | run_render 输入结构示例,字段含义对照 SKILL.md §5(输入 MVP)。 |
| `tests/` | 桩测试与测试用 ID 解析(可选 chanjing_test_defaults.json,不写 .chanjing_test_ids.json 缓存);保障脚本与 JSON 结构,不承载业务规则。 |
模板模板(templates/)文件简介
| 文件路径 | 作用(主要角色) |
|---|---|
| templates/video_brief_plan.md | 视频策划结构 JSON 约束模板,要求 Agent 输出标准化 video_plan 字段结构(如 scene_count、core_angle、场景等)。 |
| templates/script_prompt.md | 口播文案创作提示词模板:据策划输出口播全文(含 hook/正文/CTA),口语化风格和结构详细要求都在此定义。 |
| templates/storyboard_prompt.md | 分镜切段与奇偶镜、scenes[] 字段;当代画面 7 要素与 D.1b checklist,为每镜 visual_prompt / ref_prompt 奠基。 |
| templates/history_storyboard_prompt.md | 非当代/纪传/历史分镜模板:适配历史/非当代题材,突出时代/文明圈准确性与细节自洽。 |
| templates/visual_prompt_people_constraint.md | 文生视频人物与族裔:触发条件下须在 ref_prompt 显式写入族裔锚定(本 skill 文档化的默认 profile 见该文件);含身份动作刻画与推荐英文短语。 |
| templates/render_rules.md | 渲染阶段唯一细则:技术规则(TTS/切段/数字人/AI/ffmpeg)、硬性约束、Render/成功状态与输出约定;ref_prompt 质检配合 `storyboard_prompt.md` / `history_storyboard_prompt.md`。 |
| templates/rewrite_hook_prompt.md | Hook 优化提示词模板:可选,用于将初稿 hook 强化冲突感或反常识/代入感(便于算法抓取)。 |
---
3. 业务逻辑与技术方案
3.1 逻辑概要
端到端可分为两层:
1. 内容层(Agent 或人工):输入选题或口播全文 → 产出 `video_plan`、`full_script`、`scenes[]` 分镜(含每镜类型:数字人口播 / AI 画面、文案与 ref_prompt 等)。模板见 templates/;编排见 SKILL.md §4.1;`ref_prompt` 见 `storyboard_prompt.md` / `history_storyboard_prompt.md`;切段与奇偶镜见 `storyboard_prompt.md` 篇首;镜数见 `video_brief_plan.md`。 2. 渲染层:对已定稿口播做 TTS(过长则按分镜合并少批次)→ 按镜切音频 → 数字人镜走 video-compose(音频驱动),AI 镜走 文生视频 + 与镜内音频合成 → 按公共数字人轨统一分辨率/帧率等 → ffmpeg 顺序拼接 → 输出 `final_one_click.mp4` 与 `workflow_result.json`。细则见 `templates/render_rules.md`。
既可由 Agent 分步调用各蝉镜 skill,也可在已有 workflow.json 时只跑 run_render.py(见下节「环境与运行」)。
3.2 流程图
flowchart TD
IN[选题或口播全文] --> PLAN[Plan: video_plan]
PLAN --> SCR[Script: full_script]
SCR --> SB[Storyboard: scenes 分镜]
SB --> TTS[TTS: 整轨或分批合并]
TTS --> CUT[按镜切音频]
CUT --> BR{按镜类型}
BR -->|数字人镜| DH[chanjing-video-compose]
BR -->|AI 镜| AI[chanjing-ai-creation 文生视频]
AI --> MUX[镜内音视频合成]
DH --> NORM[统一封装参数]
MUX --> NORM
NORM --> FF[ffmpeg 拼接]
FF --> OUT[final_one_click.mp4]---
4. 数字人与音色(无环境变量默认)
run_render.py 不会从环境变量读取默认音色、数字人 person_id 或 figure_type。每次成片须在 `workflow.json` 根级写明 `audio_man`、存在数字人镜时的 `person_id`(或 avatar_id)与 `figure_type`。请按当次 `video_plan` 与口播人设,调用 `list_voices.py` 与 `list_figures.py`(--source 与任务一致,如公共或定制),将返回的 ID 填入;勿依赖 shell/export 或仓库内跨任务的隐式「默认」记录。
---
#!/usr/bin/env python3
"""
一键成片确定性渲染:TTS(含 audio_task_state)→ 切段 → 数字人 / AI 并行 poll →
以首条数字人轨 ffprobe 为参照封装 → ffmpeg 拼接。
通过子进程调用 chan-skills 内 chanjing-tts / chanjing-video-compose / chanjing-ai-creation
脚本;不调用 list_tasks。渲染规则以同技能包 templates/render_rules.md 为准;ref_prompt 见 templates/storyboard_prompt.md 与 history_storyboard_prompt.md(SKILL.md §4.2);分镜字段见 storyboard_prompt.md;字段契约见 SKILL.md §5。
"""
from __future__ import annotations
import argparse
import json
import math
import os
import subprocess
import sys
import threading
import time
import urllib.request
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from typing import Any, Callable, Optional
_DEFAULT_OPENAPI_BASE = "https://open-api.chanjing.cc"
def openapi_base_url() -> str:
return (
os.environ.get("CHANJING_OPENAPI_BASE_URL")
or os.environ.get("CHANJING_API_BASE")
or _DEFAULT_OPENAPI_BASE
).rstrip("/")
def one_click_skills_repository_root() -> str:
for key in (
"SKILLS_DIR",
"CHANJING_ONE_CLICK_VIDEO_SKILLS_ROOT",
"CHAN_SKILLS_DIR",
):
v = os.environ.get(key, "").strip()
if v:
return v
return ""
def one_click_ref_prompt_max_chars() -> int:
raw = os.environ.get("CHANJING_ONE_CLICK_VIDEO_REF_PROMPT_MAX_CHARS") or os.environ.get(
"AI_VIDEO_PROMPT_MAX_CHARS", "8000"
)
return int(raw)
def one_click_ai_creation_model_code() -> Optional[str]:
v = (
os.environ.get("CHANJING_ONE_CLICK_VIDEO_CREATION_MODEL_CODE")
or os.environ.get("AI_VIDEO_MODEL")
or ""
).strip()
return v or None
# 分镜连续合并为 TTS 批时的字符上限(低于接口 ~4000 字);见 render_rules.md §3·C.4;编排见 SKILL.md §5、§9
TTS_BATCH_MAX = 3900
API_BASE = openapi_base_url()
DOWNLOAD_SEM = threading.BoundedSemaphore(2)
def norm_text(s: str) -> str:
if not s:
return ""
return "".join(s.replace("\r", "").split())
def _ai_segment_direction(seg_index: int, seg_total: int) -> str:
"""
同镜多段时:每段在内容取舍、视觉重点、运镜上与前后段区分;表述与题材无关,
仅约束景别/叙事节奏层次,具体人、物、场景一律服从上层 base ref_prompt(英文)。
seg_index 为 0-based。
与 **`templates/storyboard_prompt.md`·D.1b**(易幻觉)冲突时以 **D.1b 与 base ref_prompt 为准**:
本函数不得诱导可读界面/正文、精细指尖操作或强表演式面部特写。
"""
if seg_total <= 1:
return ""
k = seg_index + 1
n = seg_total
blocks = [
(
f"[SHOT {k}/{n} — OPENING] CONTENT: Interpret the **base prompt** as a **wide contextual layer** — "
"setting, space, time-of-day, palette, and overall situation; keep main subjects readable but **avoid** "
"tight hero close-ups reserved for later clips. "
"EMPHASIS: scale, environment, and \"where we are\"; must not copy tighter framings from later segments. "
"CAMERA/LENS: extreme-wide or wide; stable tripod or very slow dolly-in; clear horizon / spatial depth; "
"different screen direction than the next clip."
),
(
f"[SHOT {k}/{n} — PRIMARY ACTION] CONTENT: **Same world as the base prompt**, but show the **core activity** — "
"interaction between people or with objects, movement through space, or the main beat the narration implies; "
"**do not** repeat the opening clip's master composition or identical geography/blocking. "
"EMPHASIS: readable mid-story information, relationships, or product-in-use style coverage (as fits the base prompt). "
"CAMERA/LENS: medium and medium-wide; two-shots, over-shoulder, or following action; lateral move, gentle arc, "
"or pan with movement; natural focal length for human scale (not macro)."
),
(
f"[SHOT {k}/{n} — DETAIL] CONTENT: **Inserts** implied by the base prompt — material textures, hands working "
"on objects, mid-scale props and surfaces; **avoid** readable UI/screens/signage and readable documents; "
"**not** another wide master like clip 1 or a repeat of clip 2's blocking. "
"EMPHASIS: tactile clarity and simple filmable gestures; shallow focus where it helps; **avoid** extreme "
"ECU on faces, micro-expressions, or fingertip-precision actions — if the base prompt already forbids these "
"per project rules, **follow the base**. "
"CAMERA/LENS: medium-close or close; slow micro-dolly or locked frame; slight high or low angle only if motivated; "
"rack focus optional."
),
]
if seg_index < len(blocks):
return blocks[seg_index]
return (
f"[SHOT {k}/{n} — CONTRAST OUT] CONTENT: A **deliberately different** beat from earlier clips while staying "
"faithful to the base prompt — e.g. negative space, quieter moment, opposite lighting direction, new axis, "
"or summary image; must read as a **new chapter**, not a resized duplicate of a prior shot. "
"EMPHASIS: mood shift or closing punctuation. "
"CAMERA/LENS: return to wider coverage **only** on a new angle or location vs clip 1; slow pull-back, "
"descending move, or long hold; Dutch angle only if still coherent with the base prompt."
)
def build_ai_segment_prompt(base: str, seg_index: int, seg_total: int) -> str:
"""
同镜多条文生视频:在 Agent 的 base ref_prompt 上,按段叠加**题材通用**的景别/节奏/运镜分层;
具体人、物、时代、场景仅来自 base,不与某一类文案绑定。单段时仅用 base。
追加层与 **`storyboard_prompt.md`·D.1b** 抵触时以 D.1b 与 base 为准。
"""
base = (base or "").strip()
max_total = one_click_ref_prompt_max_chars()
extra = _ai_segment_direction(seg_index, seg_total)
if not extra:
return base[:max_total]
sep = " || "
room = max_total - len(extra) - len(sep) - 5
if room < 120:
trimmed = base[: max(0, max_total - len(extra) - len(sep))].rstrip()
else:
trimmed = base[:room].rstrip()
out = trimmed + sep + extra
return out[:max_total]
def repo_root_from_script() -> Path:
# 仅当布局为 …/<repo>/skills/chanjing-one-click-video-creation/scripts/run_render.py 时,
# 向上四级为 <repo>(与 SKILLS_DIR / CHANJING_ONE_CLICK_VIDEO_SKILLS_ROOT / CHAN_SKILLS_DIR 指向的根一致)。
return Path(__file__).resolve().parent.parent.parent.parent
def resolve_chan_skills_dir() -> Path:
env = one_click_skills_repository_root()
if env:
return Path(env).resolve()
return repo_root_from_script()
def script_path(root: Path, skill: str, name: str) -> Path:
return root / "skills" / skill / "scripts" / name
def require_bin(name: str) -> None:
from shutil import which
if not which(name):
raise SystemExit(f"缺少可执行文件: {name}(请安装并加入 PATH)")
def run_subprocess(
argv: list[str], *, timeout: int = 900, env: Optional[dict] = None
) -> str:
r = subprocess.run(
argv,
capture_output=True,
text=True,
timeout=timeout,
env={**os.environ, **(env or {})},
)
if r.returncode != 0:
msg = (r.stderr or r.stdout or "").strip() or f"exit {r.returncode}"
raise RuntimeError(msg)
return (r.stdout or "").strip()
def with_retry(fn: Callable[[], Any], retries: int) -> Any:
last: Optional[Exception] = None
for _ in range(max(0, retries) + 1):
try:
return fn()
except Exception as e:
last = e
time.sleep(1.0)
assert last is not None
raise last
def import_tts_get_token(root: Path):
p = str(script_path(root, "chanjing-tts", "_auth.py").parent)
if p not in sys.path:
sys.path.insert(0, p)
from _auth import get_token # type: ignore
return get_token
def fetch_audio_task_state(token: str, task_id: str) -> dict[str, Any]:
url = f"{API_BASE}/open/v1/audio_task_state"
body = json.dumps({"task_id": task_id}).encode("utf-8")
req = urllib.request.Request(
url,
data=body,
headers={"access_token": token, "Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=60) as resp:
return json.loads(resp.read().decode("utf-8"))
def poll_tts_state(
get_token, task_id: str, interval: int = 3
) -> dict[str, Any]:
while True:
token, err = get_token()
if err:
raise RuntimeError(err)
res = fetch_audio_task_state(token, task_id)
if res.get("code") != 0:
raise RuntimeError(res.get("msg", str(res)))
data = res.get("data") or {}
status = data.get("status")
if status == 9:
return data
if status not in (1, None):
raise RuntimeError(
data.get("errMsg") or data.get("errReason") or f"TTS status={status}"
)
time.sleep(interval)
def download_url(url: str, dest: Path) -> None:
with DOWNLOAD_SEM:
req = urllib.request.Request(url, method="GET")
with urllib.request.urlopen(req, timeout=300) as resp:
dest.write_bytes(resp.read())
def ffprobe_duration(path: Path) -> float:
out = run_subprocess(
[
"ffprobe",
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1",
str(path),
],
timeout=120,
)
return float(out.strip())
def ffprobe_json(path: Path) -> dict[str, Any]:
out = run_subprocess(
[
"ffprobe",
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=width,height,r_frame_rate,avg_frame_rate,pix_fmt,codec_name",
"-show_entries",
"stream_tags=rotate",
"-of",
"json",
str(path),
],
timeout=120,
)
return json.loads(out)
# 与 chanjing-ai-creation submit_task --aspect-ratio 说明一致(过宽则映射失败)
API_VIDEO_ASPECT_RATIOS: dict[str, float] = {
"9:16": 9 / 16,
"16:9": 16 / 9,
"1:1": 1.0,
"3:4": 3 / 4,
"4:3": 4 / 3,
}
def display_size_from_stream(st: dict[str, Any]) -> tuple[int, int]:
"""编码宽高 + 常见 rotate 元数据 → 观众看到的宽高(竖屏以 h>w 为常态)。"""
w = int(st.get("width") or 1080)
h = int(st.get("height") or 1920)
tags = st.get("tags") or {}
rot = tags.get("rotate")
if rot is None:
return w, h
try:
ang = int(str(rot).strip())
except ValueError:
return w, h
if ang % 180 == 90:
return h, w
return w, h
def ref_to_ai_submit_params(ref: dict[str, Any]) -> tuple[str, int]:
"""
由数字人参照轨的显示宽高映射文生视频 API 的 aspect_ratio、clarity,
避免横竖与清晰度与数字人不一致导致后期旋转/强裁。
"""
w0, h0 = int(ref["width"]), int(ref["height"])
w, h = min(w0, h0), max(w0, h0)
r = w / h if h else 9 / 16
best_label, best_val = min(
API_VIDEO_ASPECT_RATIOS.items(), key=lambda kv: abs(kv[1] - r)
)
if abs(best_val - r) > 0.04:
raise SystemExit(
"数字人显示宽高比与文生 API 支持的 aspect_ratio 偏差过大("
f"显示约 {w0}×{h0},比例≈{r:.4f});请确认公共数字人成片或换用支持的画幅。"
)
short = w
if short >= 1000:
clarity = 1080
elif short >= 660:
clarity = 720
else:
raise SystemExit(
f"数字人短边 {short}px 无法映射到文生 clarity 720/1080;请换形象或联系接口文档。"
)
return best_label, clarity
def parse_fps(frac: str) -> float:
if not frac or frac == "0/0":
return 30.0
if "/" in frac:
a, b = frac.split("/", 1)
return float(a) / float(b) if float(b) else 30.0
return float(frac)
def default_ref() -> dict[str, Any]:
return {
"width": 1080,
"height": 1920,
"fps": 30.0,
"pix_fmt": "yuv420p",
"a_rate": 48000,
}
def probe_ref_video(path: Path) -> dict[str, Any]:
data = ffprobe_json(path)
streams = data.get("streams") or []
if not streams:
return default_ref()
st = streams[0]
w, h = display_size_from_stream(st)
fps = parse_fps(st.get("r_frame_rate") or st.get("avg_frame_rate") or "30/1")
pix = st.get("pix_fmt") or "yuv420p"
return {"width": w, "height": h, "fps": fps, "pix_fmt": pix, "a_rate": 48000}
def _infer_subtitle_scale(subs: list[dict], audio_duration: float) -> float:
if not subs or audio_duration <= 0:
return 1.0
max_end = max(float(s.get("end_time", 0)) for s in subs)
if max_end > audio_duration * 2.0 and max_end > 2000:
return 0.001
return 1.0
def merge_subtitles_with_offset(
subs: list[dict], offset_sec: float, scale: float
) -> list[dict]:
out = []
for s in subs:
out.append(
{
"start_time": float(s.get("start_time", 0)) * scale + offset_sec,
"end_time": float(s.get("end_time", 0)) * scale + offset_sec,
"subtitle": s.get("subtitle", "") or "",
}
)
return out
def compute_scene_times(
scenes: list[dict],
full_script: str,
subtitles: list[dict],
total_duration: float,
) -> tuple[list[tuple[float, float]], str]:
"""返回每镜 (t_start, t_end) 与 align_quality:high | low_prop。"""
scenes = sorted(scenes, key=lambda x: int(x["scene_id"]))
full_n = norm_text(full_script)
subs_sorted = sorted(subtitles, key=lambda x: float(x["start_time"]))
sub_blob = norm_text("".join(s.get("subtitle", "") for s in subs_sorted))
total_chars = sum(max(1, len(norm_text(s["voiceover"]))) for s in scenes)
def proportional() -> tuple[list[tuple[float, float]], str]:
times = []
t = 0.0
for sc in scenes:
w = max(1, len(norm_text(sc["voiceover"])))
seg = total_duration * (w / total_chars) if total_chars else total_duration / len(scenes)
times.append((t, min(t + seg, total_duration)))
t += seg
if times:
times[-1] = (times[-1][0], total_duration)
return times, "low_prop"
if not full_n or total_duration <= 0:
return proportional()
if sub_blob != full_n:
return proportional()
# merge_subtitles_with_offset 已将时间转为秒
char_times: list[float] = []
for sub in subs_sorted:
st = float(sub["start_time"])
et = float(sub["end_time"])
txt = norm_text(sub.get("subtitle", ""))
if not txt:
continue
L = len(txt)
for i in range(L):
if L <= 1:
char_times.append((st + et) / 2.0)
else:
char_times.append(st + (et - st) * i / (L - 1))
if len(char_times) != len(sub_blob):
return proportional()
times: list[tuple[float, float]] = []
pos = 0
for sc in scenes:
v = norm_text(sc["voiceover"])
Lv = len(v)
if Lv == 0:
if not char_times:
times.append((0.0, 0.0))
else:
idx = min(pos, len(char_times) - 1)
tt = char_times[idx]
times.append((tt, tt))
continue
if pos + Lv > len(char_times):
return proportional()
t0 = char_times[pos]
t1 = char_times[pos + Lv - 1]
times.append((max(0.0, t0), min(total_duration, t1 + 0.05)))
pos += Lv
if pos != len(char_times):
return proportional()
return times, "high"
def group_scene_batches(scenes_sorted: list[dict]) -> list[list[dict]]:
batches: list[list[dict]] = []
cur: list[dict] = []
cur_len = 0
for sc in scenes_sorted:
v = sc.get("voiceover") or ""
if cur_len + len(v) > TTS_BATCH_MAX and cur:
batches.append(cur)
cur = []
cur_len = 0
cur.append(sc)
cur_len += len(v)
if cur:
batches.append(cur)
return batches
def ffmpeg_concat_audio_files(paths: list[Path], out: Path) -> None:
lst = out.parent / f"{out.stem}_audio_concat.txt"
lines = []
for p in paths:
lines.append(f"file '{p.resolve()}'")
lst.write_text("\n".join(lines), encoding="utf-8")
run_subprocess(
[
"ffmpeg",
"-y",
"-f",
"concat",
"-safe",
"0",
"-i",
str(lst),
"-c",
"copy",
str(out),
],
timeout=600,
)
def ffmpeg_cut_audio(src: Path, t0: float, t1: float, out_wav: Path) -> None:
dur = max(0.01, t1 - t0)
run_subprocess(
[
"ffmpeg",
"-y",
"-ss",
str(t0),
"-i",
str(src),
"-t",
str(dur),
"-ac",
"1",
"-ar",
"24000",
"-sample_fmt",
"s16",
str(out_wav),
],
timeout=300,
)
def h264_args() -> list[str]:
if sys.platform == "darwin":
return ["-c:v", "h264_videotoolbox", "-b:v", "8M"]
return ["-c:v", "libx264", "-crf", "23", "-preset", "medium"]
def normalize_video_to_ref(
src: Path,
dst: Path,
ref: dict[str, Any],
*,
with_audio: bool,
) -> None:
w, h = ref["width"], ref["height"]
fps = ref["fps"]
vf = (
f"scale={w}:{h}:force_original_aspect_ratio=decrease,"
f"pad={w}:{h}:(ow-iw)/2:(oh-ih)/2,setsar=1,fps={fps},format=yuv420p"
)
cmd = ["ffmpeg", "-y", "-i", str(src), "-vf", vf, *h264_args()]
if with_audio:
cmd.extend(
["-c:a", "aac", "-ar", str(ref["a_rate"]), "-ac", "2", str(dst)]
)
else:
cmd.extend(["-an", str(dst)])
run_subprocess(cmd, timeout=900)
def concat_videos_reencode(inputs: list[Path], out: Path) -> None:
lst = out.parent / f"{out.stem}_vconcat.txt"
lst.write_text(
"\n".join(f"file '{p.resolve()}'" for p in inputs), encoding="utf-8"
)
run_subprocess(
[
"ffmpeg",
"-y",
"-f",
"concat",
"-safe",
"0",
"-i",
str(lst),
*h264_args(),
"-an",
str(out),
],
timeout=900,
)
def mux_video_audio(video: Path, audio: Path, out: Path, ref: dict[str, Any]) -> None:
"""
将无音轨文生片段与当镜口播 wav 合成。禁止 -shortest:平台返回的视频时长常短于口播,
否则会截断该镜 narration,拼接后表现为「话没说完就断」。
视频长于音频时按口播时长裁切;短于音频时用末帧垫长到与口播一致。
"""
v_dur = ffprobe_duration(video)
a_dur = ffprobe_duration(audio)
ar = str(ref["a_rate"])
eps = 0.08
if v_dur + eps >= a_dur:
run_subprocess(
[
"ffmpeg",
"-y",
"-i",
str(video),
"-i",
str(audio),
"-t",
str(a_dur),
"-map",
"0:v:0",
"-map",
"1:a:0",
"-c:v",
"copy",
"-c:a",
"aac",
"-ar",
ar,
"-ac",
"2",
str(out),
],
timeout=600,
)
return
pad = max(0.05, a_dur - v_dur + 0.02)
fc = f"[0:v]tpad=stop_mode=clone:stop_duration={pad:.3f}[v]"
cmd: list[str] = [
"ffmpeg",
"-y",
"-i",
str(video),
"-i",
str(audio),
"-filter_complex",
fc,
"-map",
"[v]",
"-map",
"1:a:0",
"-t",
str(a_dur),
]
cmd.extend(h264_args())
cmd.extend(
[
"-c:a",
"aac",
"-ar",
ar,
"-ac",
"2",
str(out),
]
)
run_subprocess(cmd, timeout=600)
def run_tts_pipeline(
root: Path,
batches: list[list[dict]],
audio_man: str,
speed: float,
pitch: float,
retries: int,
work: Path,
) -> tuple[Path, list[dict], list[str]]:
tts_create = script_path(root, "chanjing-tts", "create_task.py")
get_token = import_tts_get_token(root)
mp3_parts: list[Path] = []
merged_subs: list[dict] = []
offset = 0.0
task_ids: list[str] = []
batch_states: list[dict[str, Any]] = []
for bi, batch_scenes in enumerate(batches):
text = "".join(s.get("voiceover", "") for s in batch_scenes)
if len(text) > 4000:
raise ValueError(f"TTS 批次 {bi} 超过 4000 字,请调整分镜合并")
def _create() -> str:
return run_subprocess(
[
sys.executable,
str(tts_create),
"--audio-man",
audio_man,
"--text",
text,
"--speed",
str(speed),
"--pitch",
str(pitch),
]
)
task_id = with_retry(_create, retries)
task_ids.append(task_id)
data = with_retry(lambda: poll_tts_state(get_token, task_id), retries)
batch_states.append(dict(data))
full = data.get("full") or {}
url = full.get("url")
if not url:
raise RuntimeError("TTS 完成但无 full.url")
raw = work / f"tts_batch_{bi}.mp3"
download_url(url, raw)
dur = ffprobe_duration(raw)
subs = data.get("subtitles") or []
scale = _infer_subtitle_scale(subs, dur)
merged_subs.extend(merge_subtitles_with_offset(subs, offset, scale))
offset += dur
mp3_parts.append(raw)
merged_mp3 = work / "tts_merged.mp3"
if len(mp3_parts) == 1:
merged_mp3.write_bytes(mp3_parts[0].read_bytes())
else:
ffmpeg_concat_audio_files(mp3_parts, merged_mp3)
state_path = work / "tts_state.json"
state_path.write_text(
json.dumps(
{
"task_ids": task_ids,
"subtitles": merged_subs,
"batches_audio_task_state": batch_states,
},
ensure_ascii=False,
indent=2,
),
encoding="utf-8",
)
return merged_mp3, merged_subs, task_ids
def poll_compose(root: Path, video_id: str) -> str:
poll = script_path(root, "chanjing-video-compose", "poll_task.py")
return run_subprocess(
[sys.executable, str(poll), "--id", video_id, "--interval", "10"],
timeout=3600,
)
def poll_ai(root: Path, uid: str) -> str:
poll = script_path(root, "chanjing-ai-creation", "poll_task.py")
return run_subprocess(
[sys.executable, str(poll), "--unique-id", uid, "--interval", "10"],
timeout=3600,
)
def run_dh_create_job(
upload: Path,
compose_create: Path,
person_id: str,
figure_type: Optional[str],
wav_path: Path,
retries: int,
subtitle: str = "hide",
subtitle_color: Optional[str] = None,
subtitle_stroke_color: Optional[str] = None,
subtitle_stroke_width: Optional[int] = None,
) -> str:
"""上传切段音频并创建数字人视频任务,返回 video 任务 id(stdout)。"""
def _up() -> str:
return run_subprocess(
[
sys.executable,
str(upload),
"--service",
"make_video_audio",
"--file",
str(wav_path),
]
)
fid = with_retry(_up, retries)
sub = "show" if subtitle == "show" else "hide"
cargs = [
sys.executable,
str(compose_create),
"--person-id",
person_id,
"--audio-file-id",
fid,
"--subtitle",
sub,
]
if figure_type:
cargs.extend(["--figure-type", figure_type])
if sub == "show":
if subtitle_color:
cargs.extend(["--subtitle-color", subtitle_color])
if subtitle_stroke_color:
cargs.extend(["--subtitle-stroke-color", subtitle_stroke_color])
if subtitle_stroke_width is not None:
cargs.extend(["--subtitle-stroke-width", str(subtitle_stroke_width)])
def _ct() -> str:
return run_subprocess(cargs)
return with_retry(_ct, retries)
def main() -> None:
parser = argparse.ArgumentParser(description="chanjing-one-click-video-creation 确定性成片")
parser.add_argument("--input", required=True, help="workflow JSON 路径")
parser.add_argument("--output-dir", required=True, help="输出目录")
args = parser.parse_args()
require_bin("ffmpeg")
require_bin("ffprobe")
root = resolve_chan_skills_dir()
inp = Path(args.input).resolve()
out_dir = Path(args.output_dir).resolve()
out_dir.mkdir(parents=True, exist_ok=True)
work = out_dir / "work"
work.mkdir(parents=True, exist_ok=True)
data = json.loads(inp.read_text(encoding="utf-8"))
full_script = (data.get("full_script") or "").strip()
if not full_script:
for k in ("script", "copy_text", "input_script", "content"):
v = data.get(k)
if v:
full_script = str(v).strip()
break
scenes = data.get("scenes")
if not full_script or not isinstance(scenes, list) or not scenes:
raise SystemExit("输入 JSON 须含 full_script 与 scenes[]")
scenes_sorted = sorted(scenes, key=lambda x: int(x["scene_id"]))
joined = "".join(s.get("voiceover", "") for s in scenes_sorted)
if norm_text(joined) != norm_text(full_script):
raise SystemExit("各镜 voiceover 拼接后与 full_script 在 norm 意义下须一致")
audio_man = (data.get("audio_man") or "").strip()
if not audio_man:
raise SystemExit("缺少 audio_man")
person_id = (data.get("person_id") or data.get("avatar_id") or "").strip()
figure_type = (data.get("figure_type") or "").strip() or None
speed = float(data.get("speed", 1))
pitch = float(data.get("pitch", 1))
retries = int(data.get("max_retry_per_step", 1))
ai_seg = int(data.get("ai_video_duration_sec", 10))
if ai_seg not in (5, 10):
ai_seg = 10
model_code = (
data.get("model_code")
or one_click_ai_creation_model_code()
or "Doubao-Seedance-1.0-pro"
)
dh_subtitle = "show" if data.get("subtitle_required") else "hide"
sub_color = (data.get("subtitle_color") or "").strip() or None
sub_stroke_color = (data.get("subtitle_stroke_color") or "").strip() or None
_sw = data.get("subtitle_stroke_width")
sub_stroke_width = int(_sw) if _sw is not None and str(_sw).strip() != "" else None
need_dh = any(s.get("use_avatar") for s in scenes_sorted)
if need_dh and not person_id:
raise SystemExit("存在数字人镜时须提供 person_id 或 avatar_id")
for s in scenes_sorted:
if not s.get("use_avatar") and not (s.get("ref_prompt") or "").strip():
raise SystemExit(f"scene {s.get('scene_id')} 为 AI 镜但缺少 ref_prompt")
result: dict[str, Any] = {
"status": "failed",
"align_quality": None,
"render_result": {"video_file": None, "scene_video_urls": {}},
"debug": {
"tts_task_ids": [],
"dh_video_ids": {},
"ai_unique_ids": {},
"intermediate_paths": {},
},
}
try:
batches = group_scene_batches(scenes_sorted)
merged_mp3, merged_subs, tts_ids = run_tts_pipeline(
root, batches, audio_man, speed, pitch, retries, work
)
result["debug"]["tts_task_ids"] = tts_ids
result["debug"]["intermediate_paths"]["tts_merged_mp3"] = str(merged_mp3)
result["debug"]["intermediate_paths"]["tts_state_json"] = str(
work / "tts_state.json"
)
total_dur = ffprobe_duration(merged_mp3)
scene_times, align_q = compute_scene_times(
scenes_sorted, full_script, merged_subs, total_dur
)
result["align_quality"] = align_q
st_path = work / "scene_times.json"
st_path.write_text(
json.dumps(
[
{
"scene_id": int(sc["scene_id"]),
"t_start": scene_times[i][0],
"t_end": scene_times[i][1],
}
for i, sc in enumerate(scenes_sorted)
],
ensure_ascii=False,
indent=2,
),
encoding="utf-8",
)
result["debug"]["intermediate_paths"]["scene_times_json"] = str(st_path)
# 切段 wav
scene_wavs: dict[int, Path] = {}
for i, sc in enumerate(scenes_sorted):
sid = int(sc["scene_id"])
t0, t1 = scene_times[i]
wav = work / f"scene{sid:02d}_drive.wav"
ffmpeg_cut_audio(merged_mp3, t0, t1, wav)
scene_wavs[sid] = wav
upload = script_path(root, "chanjing-video-compose", "upload_file.py")
compose_create = script_path(root, "chanjing-video-compose", "create_task.py")
ai_submit = script_path(root, "chanjing-ai-creation", "submit_task.py")
first_dh_sid: Optional[int] = next(
(int(s["scene_id"]) for s in scenes_sorted if s.get("use_avatar")),
None,
)
has_ai = any(not s.get("use_avatar") for s in scenes_sorted)
url_dh: dict[int, str] = {}
dh_files: dict[int, Path] = {}
ref_for_normalize: Optional[dict[str, Any]] = None
# 有 AI 镜时:先完成首条数字人 → ffprobe(含 rotate)→ 再提交文生,使 API 画幅与公共数字人一致
if has_ai and first_dh_sid is not None:
wav0 = scene_wavs[first_dh_sid]
vid0 = run_dh_create_job(
upload,
compose_create,
person_id,
figure_type,
wav0,
retries,
dh_subtitle,
subtitle_color=sub_color,
subtitle_stroke_color=sub_stroke_color,
subtitle_stroke_width=sub_stroke_width,
)
result["debug"]["dh_video_ids"][str(first_dh_sid)] = vid0
u0 = with_retry(lambda: poll_compose(root, vid0), retries)
url_dh[first_dh_sid] = u0
result["render_result"]["scene_video_urls"][f"dh_{first_dh_sid}"] = u0
p0 = work / f"scene{first_dh_sid:02d}_dh_raw.mp4"
download_url(u0, p0)
dh_files[first_dh_sid] = p0
ref_for_normalize = probe_ref_video(p0)
elif has_ai:
ref_for_normalize = default_ref()
if has_ai:
assert ref_for_normalize is not None
ai_aspect_ratio, ai_clarity = ref_to_ai_submit_params(ref_for_normalize)
else:
ai_aspect_ratio, ai_clarity = "9:16", 1080
result["debug"]["ai_video_submit_params"] = {
"aspect_ratio": ai_aspect_ratio,
"clarity": ai_clarity,
"ref_width": ref_for_normalize["width"] if ref_for_normalize else None,
"ref_height": ref_for_normalize["height"] if ref_for_normalize else None,
}
dh_jobs: dict[int, str] = {}
ai_jobs: dict[tuple[int, int], str] = {}
for i, sc in enumerate(scenes_sorted):
sid = int(sc["scene_id"])
if sc.get("use_avatar"):
if has_ai and sid == first_dh_sid:
continue
vid = run_dh_create_job(
upload,
compose_create,
person_id,
figure_type,
scene_wavs[sid],
retries,
dh_subtitle,
subtitle_color=sub_color,
subtitle_stroke_color=sub_stroke_color,
subtitle_stroke_width=sub_stroke_width,
)
dh_jobs[sid] = vid
result["debug"]["dh_video_ids"][str(sid)] = vid
else:
t0, t1 = scene_times[i]
dur = max(0.1, t1 - t0)
n = max(1, math.ceil(dur / ai_seg))
prompt = (sc.get("ref_prompt") or "").strip()
for k in range(n):
ptext = build_ai_segment_prompt(prompt, k, n)
def _sub(ptext=ptext) -> str:
return run_subprocess(
[
sys.executable,
str(ai_submit),
"--creation-type",
"4",
"--model-code",
str(model_code),
"--prompt",
ptext,
"--aspect-ratio",
ai_aspect_ratio,
"--clarity",
str(ai_clarity),
"--video-duration",
str(ai_seg),
]
)
uid = with_retry(_sub, retries)
ai_jobs[(sid, k)] = uid
result["debug"]["ai_unique_ids"][f"{sid}_{k}"] = uid
# 并行 poll(首条数字人已在上方单独 poll)
url_ai: dict[tuple[int, int], str] = {}
def run_polls() -> None:
futs = []
with ThreadPoolExecutor(max_workers=4) as ex:
for sid, vid in dh_jobs.items():
futs.append(
(("dh", sid), ex.submit(poll_compose, root, vid))
)
for key, uid in ai_jobs.items():
futs.append((("ai", key), ex.submit(poll_ai, root, uid)))
for tag, fut in futs:
u = fut.result()
if tag[0] == "dh":
url_dh[int(tag[1])] = u
else:
url_ai[tag[1]] = u
with_retry(run_polls, retries)
for sid, u in url_dh.items():
result["render_result"]["scene_video_urls"][f"dh_{sid}"] = u
for (sid, k), u in url_ai.items():
result["render_result"]["scene_video_urls"][f"ai_{sid}_{k}"] = u
# 下载(首条数字人已落盘)
ai_files: dict[tuple[int, int], Path] = {}
for sid, u in sorted(url_dh.items()):
if sid in dh_files:
continue
p = work / f"scene{sid:02d}_dh_raw.mp4"
download_url(u, p)
dh_files[sid] = p
ref: dict[str, Any]
if ref_for_normalize is not None:
ref = ref_for_normalize
else:
ref = None
for sid in sorted(dh_files.keys()):
ref = probe_ref_video(dh_files[sid])
break
if ref is None:
ref = default_ref()
for (sid, k), u in sorted(url_ai.items()):
p = work / f"scene{sid:02d}_ai_part{k}.mp4"
download_url(u, p)
ai_files[(sid, k)] = p
norm_segments: list[Path] = []
for sc in scenes_sorted:
sid = int(sc["scene_id"])
if sc.get("use_avatar"):
src = dh_files[sid]
dst = work / f"scene{sid:02d}_dh_norm.mp4"
normalize_video_to_ref(src, dst, ref, with_audio=True)
norm_segments.append(dst)
else:
keys = sorted(
(x for x in ai_files if x[0] == sid), key=lambda x: x[1]
)
parts_norm = []
for kk, key in enumerate(keys):
raw = ai_files[key]
pn = work / f"scene{sid:02d}_ai_p{kk}_norm.mp4"
normalize_video_to_ref(raw, pn, ref, with_audio=False)
parts_norm.append(pn)
concat_noa = work / f"scene{sid:02d}_ai_t2v_concat_noaudio.mp4"
concat_videos_reencode(parts_norm, concat_noa)
muxed = work / f"scene{sid:02d}_ai_mux.mp4"
mux_video_audio(concat_noa, scene_wavs[sid], muxed, ref)
norm_seg = work / f"scene{sid:02d}_ai_final_norm.mp4"
normalize_video_to_ref(muxed, norm_seg, ref, with_audio=True)
norm_segments.append(norm_seg)
final_list = work / "final_concat.txt"
final_list.write_text(
"\n".join(f"file '{p.resolve()}'" for p in norm_segments),
encoding="utf-8",
)
final_mp4 = out_dir / "final_one_click.mp4"
run_subprocess(
[
"ffmpeg",
"-y",
"-f",
"concat",
"-safe",
"0",
"-i",
str(final_list),
*h264_args(),
"-c:a",
"aac",
"-ar",
str(ref["a_rate"]),
str(final_mp4),
],
timeout=3600,
)
result["status"] = "success"
result["render_result"]["video_file"] = str(final_mp4)
result["debug"]["intermediate_paths"]["final_concat_list"] = str(final_list)
except BaseException as exc:
result["status"] = "partial"
result["error"] = str(exc)
out_json = out_dir / "workflow_result.json"
out_json.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
if result["status"] != "success":
raise SystemExit(result.get("error") or "成片失败,见 workflow_result.json")
if __name__ == "__main__":
main()
历史 / 非当代文生视频提示词
真值分工:易幻觉 D.1b(负向/正向表、英文否定短句、自检问句)以 `templates/storyboard_prompt.md` 内 「文生视频提示词·D.1b」 为唯一条文,本文件不重复列表;非当代镜须同等遵守。D.1a 非特定人物主干见 `storyboard_prompt.md`·D.1a。当代向全文装配以 `storyboard_prompt.md` 为准。
D.2 路由(非当代向)
- 软触发:选题/行业/hook/style 合并文本含「故事、叙事、传记、历史、古装、文明、战争史」等非当代信号;或 Agent 判定为纪传/古装/古代或近代史等。
- 禁止:在技能包内用单一作品名、朝代名、战役名做硬编码触发表。
- 组装:与旁白推断的时代、地域、文明圈一致的英文画面模板(服饰、建筑、器物自洽)+ 本镜口播与选题 + `visual_prompt_people_constraint.md` 全文(含「历史 / 非当代」节)。
- 非特定人物时:在时代自洽前提下,画面主骨架仍为旁白中的环境、器物、建筑、织物、交通工具等;若口播或画面逻辑需要人物,则人物须符合 `storyboard_prompt.md`·D.1a·2;无必要则不生成可辨识历史名人面孔,除非口播点名或叙事需要。
文明圈与国别自洽(非当代;通用推断)
禁止在技能包内用单一战役名、朝代名、作品名做硬编码映射;须从当次选题、video_plan 与 voiceover 语义推断叙事所属国别与文明圈(如汉语史叙、中原王朝典制、典型中式战争与宫廷语境等)。
1. 可推断为中国(含历史上中原王朝及同条口播内自洽的中华文明圈语境) 人物须 东亚面孔,服饰、甲胄、发式、建筑与城制、礼仪与兵器形制等须为与推断阶段一致的中式可拍线索(用描述性英文短语,勿套本模板示例专名)。禁止在无口播依据时混入明显异域默认:如欧式石砌城堡、与叙事无关的罗马式或古典欧洲盔形、典型好莱坞「中世纪欧洲」群像等,除非口播明确跨文明或涉外。
2. 口播已指向其他国家或文明 按该设定组画,与 `storyboard_prompt.md`·D.0 例外一致,且全镜符号内部自洽。
3. 与 D.0、族裔文件的关系 缺省与「中国语境」优先见 `storyboard_prompt.md`·D.0;人像锚定与历史造型短语见 `visual_prompt_people_constraint.md`·「历史 / 非当代」。
与 D.3 / D.4、上交接口
- 首个分镜口播字数:与当代链路相同,第 1 分镜
voiceover须遵守 `storyboard_prompt.md`「文本切段」首个分镜约束(硬)(≤20 字)。 - 长音频多段:`render_rules.md` §3·C.6。
- 上交接口:最终为连贯英文(或产品线语种);勿把 Markdown 说明原文塞进
ref_prompt。 - 单镜拼装块 1(历史壳)、块 2–6 与 `storyboard_prompt.md`·D.4 表一致;非当代须在块 1 写入真实推断,勿留占位符。
---
流程层(非当代;与 storyboard_prompt D.4 表格「非当代向」对应)
1. 明确主题与核心事件:识别历史故事的主题、主要人物、关键事件、发生时间和地点。联系上下文识别出当前文案的主要人物及其信息 2. 画面分镜拆解与重组:根据原始文案的分镜,将每一幕核心场景高度凝练,抽取能直观表现情境、情感和动作的元素。 3. 强调视觉与感官细节:提炼能反映历史氛围的时间背景、环境、服饰、人物姿态、光影、色彩、情绪等关键词。 4. 适配AI视频/图像生成模型的表达方式:将拆解后的场景信息转化为清晰、具体、可视化的提示词,避免抽象模糊词汇,优先考虑画面可实现性。 5. 结构化与事实取向提示词设计:每一幕分镜建议单独成行或编号表达,确保内容完整、层次清晰。每幕请补足历史背景、时间地点、服饰器物、环境氛围等真实细节;人物以旁白可推的身份与可拍摄动作为主,表情与情绪用可见肢体语言与环境烘托,并遵守 `storyboard_prompt.md`·D.1b(避免强微表情表演、对口型、精细指尖操作等易幻觉要求)。避免使用模糊或过度抽象描述,力求有据可依,杜绝无旁白依据的「幻觉」专名与穿帮。 6. 生成文生视频提示词后,反思生成的结果是否有不合理、不全面的地方,结合文案内容进行修改、补充
---
### ⛔ 禁止复制示例中的真实专名
>
下列占位符结构仅演示「层次与信息类型」,不得把示例里的朝代、君主名、将领名、战役名、具体年份、真实地名等复制进成稿。 正式英文(或产品线要求语种)提示词必须只根据当前分镜 `voiceover` 与选题推理;无口播依据则不要用可辨识历史名人面孔或具体史实专名(与上文 D.2 路由 及占位符纪律一致)。
【占位符结构举例——理解写法即可,禁止当作成稿粘贴】
1. 都会 / 盛世日常:[某朝代] + [某类都城街道或市集] + [时代自洽的建筑与服饰层次] + [商贩/行人等类型化群像] + 若口播需要权力场景则加 [远处某类宫城/衙署轮廓];若口播点名或叙事需要,再写与旁白一致的统治者或官员类型化可见动作(勿套未出现的具体帝王名)。
2. 军政 / 对峙:[某历史阶段] + [某类关城/营垒/城楼] + [与旁白一致的武职身份—描述性短语] + [阵列、旌旗、兵器类型随旁白] + 天色与远景烘托情绪;禁止照抄示例库里的具体战役名、将领全名,除非口播已出现。
3. 宫廷 / 仪式夜场:[朝代氛围的宫殿大殿内部] + 烛光、织物、乐舞或礼仪动作(细节全部从当镜口播抽取);宝座与群臣用官职/礼服类型描述,口播未提具体人物则不写可辨识名人脸。
成稿自检:是否含有从本模板示例句照抄的专名?若有而口播未支撑,必须删除或改为类型化描述。是否违反上文文明圈与国别自洽:在可推出中国/中式语境的镜头里出现了无语境的西欧脸或欧式标志性建筑/盔械?
补充说明
通过上述方法,把历史故事分镜文案转化为补足环境、角色外观、背景细节、时代氛围的AI文生视频提示词,能显著降低事实性误差,提升画面还原度和历史表现力。
渲染规则(render_rules)
本文件为渲染阶段细则的维护位置:scripts/run_render.py、手工编排 TTS / 数字人 / AI 轨 / ffmpeg 拼接时均须遵守。 `ref_prompt` 的装配、题材路由、D.1–D.4、自检问句以技能包 `templates/storyboard_prompt.md`(当代与 D.1b 共用真值)及 `templates/history_storyboard_prompt.md`(非当代)为准;`SKILL.md` §4.2 为指针,本文不重复。
---
1. 工作流中的 Render 与成功状态
对应端到端流水线中 Plan → Script → Storyboard 之后的阶段:
4. Render
- 须满足本文 §3 技术规则与 §4 硬性约束;
ref_prompt内容与质检须同时满足 `storyboard_prompt.md` / `history_storyboard_prompt.md`(见 `SKILL.md` §4.2 指针)。 - 重试:可按 `max_retry_per_step`(见
SKILL.md§6,默认1)等配置做逐步重试;语义为「每步基础 1 次 + 允许的额外重试」,具体以实现为准。 - 仍失败:在尽量保留已产出物的前提下,返回
video_plan、口播与分镜等,`status=partial`(或产品线等价状态),便于排错或人工续跑。`partial` 表示当次未成成片或中途失败,与「是否自动降级为简化成片链路」无关;渲染无降级仍指不自动切换为「仅数字人 / 仅 AI」等替代方案。
5. 成功
- `status=success`;记录耗时、镜数、成片路径等。
渲染无降级:混合渲染链路中,任关键环节失败即中断,不自动降级为「仅数字人」或「仅 AI」成片(除非产品另有显式开关)。成功时 degrade_log 为 []。
---
2. 输出 JSON 中与渲染相关的约定
- `status`:
success|partial|failed(以实际 API 为准)。 - `render_result`:
video_file、scene_video_urls、render_path、degrade_log等。 - 失败时仍尽量返回文案与分镜(
partial),详见流水线实现。
---
3. 技术规则(编排指针见 SKILL.md §4.1)
C.1 环境与基础
- 成片默认竖屏 1080×1920(以数字人分镜为准)。
- 子进程可至约 10 分钟级。
- 多镜并行与 CDN 下载须限并发。
- 仓库自动化:配置 `SKILLS_DIR` 或 `CHANJING_ONE_CLICK_VIDEO_SKILLS_ROOT`(兼容 `CHAN_SKILLS_DIR`;见
SKILL.md§3)。 - 远程下载与 ffmpeg 受并发控制;多段拼接;必要时按音频裁视频或末帧 /
tpad延长。
C.2 封装与编码(对齐数字人轨)
目标:AI 等非数字人轨在进入最终 concat 前,与本任务内公共数字人 poll_task.py 落盘样例在分辨率、帧率、编码、码率量级上对齐。
| 步骤 | 要求 |
|---|---|
| 1. 参照轨 | 任取一条本任务已下载数字人 mp4,ffprobe 读 v:0:width,height,r_frame_rate,avg_frame_rate,pix_fmt,codec_name,bit_rate;format.bit_rate;a:0:codec_name,sample_rate。以当次文件为准,勿写死假设。 |
| 2. 空间 | AI 常见非 1080×1920:scale+pad 到参照宽高,setsar=1,pix_fmt 与参照一致(常见 yuv420p)。 |
| 3. 帧率 | 参照 CFR 而 AI 为 VFR 或偏低时,concat 前 AI 轨 fps=参照帧率 或 minterpolate(计算大,仅必要时)。 |
| 4. 视频编码与码率 | 团队择一统一:软件如 `libx264`,或硬件如 `h264_videotoolbox`;码率参照 ffprobe bit_rate,用 -b:v/-maxrate/-bufsize 或 -crf;各镜一致,避免一镜糊一镜爆码。 |
| 5. 音频 | 各段 mux 时统一采样率/编码(常见 AAC-LC 与参照一致);成片可再做单轨归一。 |
| 6. concat | 各段编码、分辨率、fps、pix_fmt 已统一后再 concat;禁止参数不一致时盲目 -c copy。 |
C.3 数字人(选型与合成)
- 每次拉最新列表;不用过期 person id。
- 请求里须显式提供
person_id/avatar_id与 `figure_type`(公共多形态时与list_figures.py列一致);禁止用环境变量覆盖数字人/形象类型。 - `list_figures.py` 默认 `--source customised`,空列表正常;公共形象须显式执行
list_figures.py --source common(脚本无环境变量改默认源)。 - 选型须对比,勿默认列表排序:
--json下对每条公共数字人比对name、figures[].type、figures[].width/height、audio_man_id、audio_name与当次策划/口播人设;必要时增大--page-size或翻页。禁止未比较即取返回列表最前几条。默认偏好年轻形象(名称或audio_name体现青年/元气/学生等气质者优先;题材需要成熟/中老年时再调整),与 `chanjing-one-click-video-creation` `SKILL.md` §3 一致。 - TTS `audio_man` 宜与该形象返回的 `audio_man_id` 一致;须在 `workflow.json` 中写明,禁止依赖环境变量默认音色。
- 失败可在同套切段音频下换列表中其它形象重试。
数字人镜操作:
- 上传该镜音频
file_id,create_task.py音频驱动。 - 数字人镜字幕:默认 `--subtitle hide`;
workflow.json根级 `subtitle_required`: true 时 `--subtitle show`。show且未传--subtitle-color时默认 `#FFFFFF`(白字),位置与其它样式见 `chanjing-video-compose` skill /create_task.py。 - 公共多形态时 `--figure-type` 与列表一致。
C.4 音频与 TTS
- 蝉镜单次 TTS 通常 少于 4000 字。
- `run_render.py`:按分镜
voiceover连续合并时以 `TTS_BATCH_MAX=3900`(字符)为合并阈值,使各批低于接口上限并留余量;提交前仍校验单批不超过 4000 字。 - 默认:整段一次合成。
- 超长:按分镜
voiceover连续合并成块,每块 少于 4000 字且批次数最少;同audio_man、同speed/pitch;各批 ffmpeg concat 成一条总轨(批间极短静音慎用)。 - 禁止:同镜内逐句多次 TTS。
poll可能给.mp3;多批须合并后再ffprobe总时长。- 切段与 AI 条数用实测,勿按
duration_sec硬裁口播。
C.5 口播与画面对齐(强约束)
1. 须从 `audio_task_state` 得到 `data.subtitles[]`(start_time / end_time / subtitle)。多批 TTS 须给每批字幕加累计时间偏移再拼全局表。 2. 第 k 镜:用 voiceover 与全局字幕做字符串级对齐(允许轻微标点/语气差异);得 t_start/t_end(秒);切音频用 `-ss`/`-to`(或重编码切),保证与分镜文案一致。 3. 对不齐:退化为该镜在全局时间轴比例区间切段,silencedetect 边界 ±0.3s 吸附静音谷;仍失败则批内按比例,debug 标 `align_quality=low`。 4. 字幕 norm 常短于 norm(full_script):不得强行按字幕与全文逐字对齐;标 `align_quality=low_prop`,在 TTS 总时长上按各镜 norm(voiceover) 占 norm(full_script) 比例分配 t_start/t_end。 5. 数字人轨用切段音频;AI 轨用同段音频 mux(-shortest 或 apad/tpad);禁止用整段未切音频驱动单镜。
切段上传:数字人驱动用 PCM WAV(如 24kHz mono)较稳;从总轨按 t_start/t_end 切出。
C.6 AI 分镜(文生视频与合成)
ref_prompt规则见 `storyboard_prompt.md`「文生视频提示词」(D.1a/D.1b/画幅与数字人一致 等;见 `SKILL.md` §4.2)。- 提交文生视频前须已有首条公共数字人成片落盘:
ffprobe读 显示宽高(编码宽高 + 常见 `rotate` 元数据),据此映射蝉镜 API 的 `aspect_ratio`(如9:16/16:9/1:1/3:4/4:3)与 `clarity`(720 或 1080,按短边映射)。禁止在仍有 AI 镜时写死与当次数字人不一致的文生画幅。纯 AI、无数字人镜时回退默认 9:16 + 1080。 - `run_render.py` 将上述参数写入
workflow_result.json→ `debug.ai_video_submit_params`,便于核对是否与所选公共数字人一致。 - 模型如
Doubao-Seedance-1.0-pro等。 - 单段 5 或 10 秒(模型允许);镜内音频更长则
N ≥ ceil(音频秒/单段)条;无音轨视频 concat 再与该镜音频 mux(-shortest)。 - 须做 C.2 与当次数字人 mp4 `ffprobe` 参照一致封装。
C.7 收尾与并发
- 各镜顺序拼成成片;成功记本地路径与
mixed_dh_ai类标记。 - 数字人与 AI `poll_task.py` 可并行。
- CDN 下载限并发(见 §4 硬性约束 表 #7)。
---
4. 硬性约束(原 SKILL.md §8)
| # | 约束 |
|---|---|
| 1 | 先定稿再渲染:plan / 全文 / 分镜确认后再渲染。 |
| 2 | TTS:默认整段一次再按 scene 切;超过约 4000 字则分镜连续块尽量少批、同参数、拼一条总音频;禁止无必要按句/按镜碎 TTS。 |
| 3 | 镜头:第 1、最后一镜数字人;中间奇数数字人、偶数 AI(规程见 `storyboard_prompt.md`「分镜结构与切段」;无自动化时 Agent 自检)。首镜:第 1 分镜 voiceover ≤20 字(硬上限),见同文件「文本切段」首个分镜约束(硬)。 |
| 4 | 人物族裔与造型:AI 镜出现可辨识人物(或英文描述易使模型生成带人种特征的人像)时,须先据口播写清与叙述一致的可见身份、动作、着装或现场关系(`storyboard_prompt.md`·D.1a),禁止无依据的库存人设。族裔处理:触发条件、默认 profile、推荐英文短语及禁止项一律以 `visual_prompt_people_constraint.md`·「显式族裔锚定(硬规则)」 为准;满足触发条件时必须在 ref_prompt 中显式写入族裔锚定,禁止仅用无族裔信息的通用人物描述依赖模型默认。口播或用户已能合理推断其他人种/地域时,须自洽。语境缺省与由叙事推断之国别、朝代、文明圈须与画面符号一致(无异域穿帮),见 `storyboard_prompt.md`·D.0、`history_storyboard_prompt.md`·「文明圈与国别自洽」、`visual_prompt_people_constraint.md`·「历史 / 非当代」。 |
| 5 | AI 提示词:自动路径按 非当代/当代 与口播组装;缺省与文明圈推断见 `storyboard_prompt.md`·D.0;非特定人物题材见 `storyboard_prompt.md`·D.1a。人工覆盖以 storyboard_prompt.md 为主;勿写死行业长篇样例或抄用他题 ref_prompt(示例仅演示结构)。 |
| 6 | 文生视频负向校验:ref_prompt 须通过 `storyboard_prompt.md`·D.1b(负向清单 / 正向替换 / 可选英文否定短句);命中负向而未改写不得提交。 |
| 7 | CDN:下载限流,防 429。 |
| 8 | 当次任务:以本次任务 id 轮询为准;勿用 list_tasks.py 等历史列表替代当次提交与下载(除非用户明确要求复用历史任务)。 |
---
5. 与 run_render.py 的对应关系(摘要)
- 脚本不生成 plan / script / storyboard;不自动推断非当代/当代
ref_prompt。 - 行为须符合本文 §3、§4 与 `SKILL.md` §5 字段契约。
- 详细职责列表见 `SKILL.md` §5(「职责」)。
你是短视频钩子优化专家。请对以下开场白进行优化,使其更有抓眼力。
原开场:{original_hook} 选题:{topic} 目标受众:{audience} 平台:{platform}
优化方向(选择最合适的一种):
- 提问式:用反常识问题引发好奇
- 冲突式:呈现现状与理想的落差
- 数字式:用具体数字制造冲击
- 共鸣式:说出受众内心想说的话
长度(与 `script_prompt.md`、首镜分镜一致):中文 硬上限 20 字(含标点、数字、英文字母,均计 1 字符);不得超过;对应口播 约 3–5 秒;勿为凑字拖沓。
请直接输出优化后的开场白文本,不要任何解释。
阶段 2:文案创作(Script)
职责
根据 Plan 阶段的规划约束,创作完整口播文案,为分镜拆解提供内容基础。
输入(来自 Plan 阶段)
| 字段 | 来源 | 用途 |
|---|---|---|
topic | 原始输入 | 锚定主题边界 |
audience | plan.audience | 决定语言风格和专业深度 |
core_angle | plan.core_angle | 全文围绕此角度展开 |
scene_count | plan.scene_count | 控制层次和节奏 |
tone | plan.tone | 语气调性 |
cta | plan.cta | 结尾行动指引 |
platform | 原始输入 | 平台语境(偏短平快或偏信息密度,随 platform 微调,勿写死单一平台口吻) |
duration_sec | 原始输入 | 字数控制(每秒约3.5字) |
字数参考
| 时长 | 估算字数 | 结构建议 |
|---|---|---|
| 30秒(默认文案长度) | 100-120字 | Hook 1句 + 核心观点 2句 + CTA |
| 60秒 | 200-240字 | Hook + 2-3层论证 + 案例/数据 + CTA |
| 90秒 | 300-350字 | Hook + 问题展开 + 多角度论证 + 方法总结 + CTA |
输出(JSON)
{
"title": "视频标题(15字以内,适合平台算法)",
"hook": "开场钩子(与首镜分镜一致:口播约3-5秒;中文硬上限20字以内,含标点与数字;强吸引力,禁用「大家好」式开头)",
"full_script": "完整口播文案(含hook,口语化自然,有起承转合)",
"cta": "结尾行动号召(独立句子,呼应core_angle)"
}创作要求
Hook 设计
- 时长与字数:与可选阶段
rewrite_hook_prompt.md一致——口播约 3–5 秒;中文 硬上限 20 字(含标点、数字、字母,与首镜voiceover计数方式一致),不得超过。 - 禁止:"大家好我是XX"/"今天来聊一个话题"
- 推荐(仅演示结构,勿照抄主题;须换成当前选题下的具体说法;正式 hook 须压到硬上限 20 字内,下列示意句可能更长,勿照搬长度):
- 冲突法:"很多人觉得只要够努力就行,但方向错了,越忙越亏。"
- 反常识法:"想做好一件事,有时候要先学会少做几件事。"
- 场景代入法:"你有没有发现,同样的十分钟,有人用来划屏幕,有人用来复盘——差别不在天赋。"
全文结构(配合 scene_count)
以 5 段/60秒为例:
- 第 1 段(Hook,8秒):抛出冲突/问题
- 第 2-3 段(展开,12-14秒每层):围绕 core_angle 展开 2 层论据
- 第 4 段(案例/方法,12秒):具体可操作的信息
- 第 5 段(CTA,8秒):收束到 plan.cta
分镜口播硬约束(供下游拆段)
- 首镜口播:对应第 1 分镜的文案须与 hook 对齐,硬上限 ≤20 字(计法同上);严禁超过;过长须改写或把余句并入第 2 分镜。口播时长宜 3–5 秒。
- AI 镜口播:供
use_avatar=false镜头使用的单镜文案应控制在 10 秒内,中文不超过 40 字,避免单镜信息过载影响画面对齐。
语言风格
- 口语化:用"你""咱们",不用"用户""受众"
- 避免 AI 腔:不用"首先/其次/最后/综上所述"
- 节奏感:短句为主,适当留白,模拟真实说话停顿
示例
输入(来自 Plan):
- topic: "为什么小事总拖成大麻烦"
- audience: "容易拖延、想改善习惯的普通观众"
- core_angle: "拖延往往不是懒,是启动成本太高;把第一步缩到极小,更容易动起来"
- scene_count: 5
- tone: "像朋友聊天,不教训人"
- cta: "明天试一件:把第一件小事压到两分钟内能做完"
- duration_sec: 60
输出:
{
"title": "小事拖成大坑的真正原因",
"hook": "你不是懒,很多时候是第一步太大,大脑直接帮你按了暂停。",
"full_script": "你不是懒,很多时候是第一步太大,大脑直接帮你按了暂停。真正拖住人的,不是事情本身多难,是「从哪开始」太模糊。把开头缩到两分钟以内——打开文档写一行、把运动鞋摆门口、把要寄的包裹放到桌上——启动成本一低,身体就容易跟上。接下来再用一个小清单把下一步钉死,别一次想完全程。你不需要变成自律狂魔,你只要骗过大脑:先动起来,再调整。明天就试一件事:找一件你一直拖着的小事,把第一步压到两分钟内能做完。",
"cta": "明天就试一件事:找一件你一直拖着的小事,把第一步压到两分钟内能做完。"
}与上下游关系
- 上游输入:Plan 阶段(
video_brief_plan.md)的 audience/core_angle/tone/cta - 下游输出:Storyboard 阶段(storyboard_prompt.md)将拆解 full_script 为分镜
AI 分镜画面视觉提示词规范
本文档作用
- 规范对象:约束 AI 输出的分镜画面视觉提示词(
storyboard/visual_prompt/ref_prompt)应包含什么、怎么写。 - 在链路中的位置:一键生成 AI分镜 / 自动成片里AI镜阶段的核心提示规则;承接 Plan,面向文生视频与数字人合成等下游。
- 与 Plan 对齐:写作方法、结构层次、字段要求须与前置 Plan(如
templates/video_brief_plan.md)的镜数、类型等参数一致。 - 内容与口播绑定:当代向短视频文案与脚本的分镜,须保证每镜画面与当镜口播/旁白语义一致、可解释。
- 下游可执行:为文生端提供足够具体的场景、人物、关键物体与氛围描述,便于按常见短视频画面习惯还原。
- 权威性与传递:本分镜视觉规范为分镜阶段的唯一真值,用于保证分镜完整、前后一致,并顺畅传递到文生端的画面结构。
使用方式
优先依本模板判断与写提示词;脚本只保留硬约束(如奇偶镜)。具体场景、职业、器物须从当镜 voiceover 推理,禁止套固定行业例句。
分镜结构与切段(与 video_brief_plan.md 配合)
镜数与类型标签:scene_count、video_type 以 Plan 阶段 `templates/video_brief_plan.md` 为唯一真值。本分镜阶段须产出 恰好 `scene_count` 条 scenes,且与下述切段、奇偶镜规则一致。
可调环境变量(分镜 / 估时参考)
| 变量 | 默认 | 含义 |
|---|---|---|
AI_SEGMENT_SOFT_MAX_CHARS 等 | 90 / 130 / 20 | AI 子段拆分合并 |
DH_CHARS_PER_SEC | 4.0 | 按字估单镜时长 |
文本切段
1. 去多余空白。 2. 段落 → 按句末标点拆句;过长句再按逗号类拆(约五十字级),避免半句悬挂。 3. 按 plan.scene_count 与字数目标收成段。 4. 切段结果仍多于 plan.scene_count:优先合并「相邻且同 DH 或同 AI」段;否则最短段并入邻段。 5. 首个分镜约束(硬):第 1 分镜 voiceover 硬上限 ≤20 字(标点、数字、英文字母均按 1 字符计,与常见 len/下游拼接一致);严禁超过;过长须压缩或把余句并入第 2 分镜。口播时长宜 3–5 秒。定稿前须对第 1 分镜逐字符计数自检。 6. AI 镜约束:所有 use_avatar=false 的镜头,单镜 voiceover 需控制在 10 秒内,中文不超过 40 字;超出时优先拆分或改写,避免把多步信息挤进一镜。
镜头类型(use_avatar,开启数字人时)
- 第 1、最后一镜、中间奇数镜 → 数字人(
use_avatar: true)。 - 中间偶数镜 → AI(
use_avatar: false)。 - 关闭数字人 → 全非数字人路径(按产品约定执行)。
每镜输出字段(storyboard_result.scenes[])
subtitle:口播前约 24 字(更长省略)。visual_prompt:AI 镜默认可空(渲染时组ref_prompt,规则见下文 「文生视频提示词」);数字人镜以音频驱动合成为主,不要求单独模板。
---
文案类型判断
根据 Plan 阶段的 style 判断:
| 类型 | 特征 | 优化方向 |
|---|---|---|
| 故事型 | style 含"故事/叙事/传记/情感",或旁白有时间推进词(那年/后来/多年后) | 叙事感,有情节推进,保持时空连续性 |
| 非故事型 | 观点/干货/科普/种草,无明显叙事时间线 | 信息型,清晰展示概念/步骤/场景 |
---
文生视频提示词(当代向真值)
D.0 语境缺省与文明圈推断(当代与非当代共用)
目的:文生模型在英文提示略写「地域/时代」时,常输出与中国受众叙事预期不符的异域默认人像或标志性道具。本节规定缺省与可推断时的强制自洽,禁止在技能包内为单一朝代、战役、作品名建硬编码触发表;一切推断须基于当次选题、video_plan、口播与行业语义的合理推出。
1. 无特别要求时的缺省(中国语境) 当用户指令、video_plan、口播均未要求境外场境、跨国对比、外语文化专属空间或「国际/海外」类明确取向时,ref_prompt 中可拍的人、物、事默认落在中国语境:当代场景优先中国当代城市与社会日常中的空间与器物;历史或非当代路由下优先与推断阶段自洽的中式建筑、服饰、兵器与礼仪器物(细则见 `history_storyboard_prompt.md`、`visual_prompt_people_constraint.md` 历史段落)。禁止在无口播与策划依据时,用明显异域符号充当「中性」画面(例如:无语境的欧美街景模板、欧式石砌古堡、经典好莱坞式「中世纪」套图、与叙事无关的罗马式盔甲等)。
2. 可推断的国别、朝代与文明圈 综合选题、video_plan、voiceover 中的时代与地域线索(如历史战争叙事、汉语典故与典章、汉语史叙常见地理与制度、人物称谓与器物名物习惯等),若能合理推出叙事主体属于中国(含历史上中原王朝及同条口播内自洽的中华文明圈语境),则该镜须全文自洽:人群为东亚面孔;服饰、甲胄形制、城制建筑、礼仪与交通器物等与推断的时代层一致。禁止在同一镜头内混入与所推断文明明显冲突的视觉符号(除非口播明确跨文明、对比或涉外情节)。
3. 例外 用户或口播明确要求外国场景、国际对比、特定国家/文明专属叙事时,按该设定组画,不适用本条中国缺省;但仍须内部自洽,不得混用两套文明标志性符号(除非口播要求对比)。
4. 与族裔文件的关系 出现可辨识或易被模型具体化的人像时,仍须满足 `visual_prompt_people_constraint.md`·「显式族裔锚定(硬规则)」;历史语境下须在东亚锚定之外写明时代自洽的中式历史造型,禁止用「当代东亚城市日常」一语套古代战场或宫廷(见该文件「历史 / 非当代」节)。
D.1 长度
- 旁白写入提示约 ≤500 字级。
- 整段
ref_prompt≤ 8000(`CHANJING_ONE_CLICK_VIDEO_REF_PROMPT_MAX_CHARS`,兼容 `AI_VIDEO_PROMPT_MAX_CHARS`,默认 8000)。
D.1a 非特定人物题材(事物优先;人物须有据)
判定(软):本镜 voiceover 不以具体姓名/固定人设/传记式「那位」为核心;侧重观点、方法、步骤、号召等——叙事重点非塑造单一角色,不等于禁人。
策略(须从本镜 voiceover 与选题当场推理;禁止抄他题、他行业、本模板外成稿示例句):
1. 事物—场所—工序:从本句口播抽出可拍名词与动词;空间类型须与旁白一致。至少 2~3 项写进英文描述;禁空泛场景与无口播依据的泛化「典型人物」标签(模型易生成与叙述脱节的库存人像)。 2. 人物:口播需要人类行为时,写人物并从旁白与现场推理——可见角色(描述性短语,禁无依据的泛化职业/身份套话)、年龄体态、着装防护、可见动作、与道具/环境关系。口播不需要人时可无人/手/背影/虚化人群,focus on objects and environment。禁止泛泛人像替代口播要传达的事物或工序;禁止照抄其他视频提示词模板。凡英文中出现可能被模型画成带人种特征的人像的主体描述,须在同段按 `visual_prompt_people_constraint.md`·「显式族裔锚定(硬规则)」 处理(含默认族裔短语的强制写入与禁止弱化措辞)。 3. 与 7 要素:时间/地点具体;主体可为物组或物组+与口播一致的人物动作;动作须可拍身体行为,非抽象情绪。 4. 族裔:可辨识或可能被模型渲染为带人种特征的人物时,必须按 `visual_prompt_people_constraint.md` 全文执行(含触发条件下的强制写入);纯器物环境镜且无人物描述时可不写族裔。
D.1b 负向与正向(易幻觉;全 skill 共用,非当代镜同样遵守)
无独立负向 API 字段时:删不当正向为主;可选并入下列英文否定短句(与 D.4·块 4 技术短语并列,≤3 条,忌同义重复)。
负向(命中须改写)
| 类别 | 避免 |
|---|---|
| 效果/对比 | 同镜 before-after、变身、修复并置、特效式「一眼证明」 |
| 时序/教程 | 多步编号、倒计时、强因果链(数秒内 A→B→C) |
| 精细交互 | 拧/穿线/精准扣合、量具读数、指尖 UI、复杂双手递接 |
| 可读信息 | 可辨正文/表格/Logo/指着读(同 no readable text) |
| 表演 | 强微表情连变、对口型、多人轮替对话 |
| 运镜 | 高速跟拍、甩镜、环绕、剧烈手持 |
| 物理 | 复杂流体、明火、玻璃碎裂等 |
正向
| 思路 | 说明 |
|---|---|
| 状态>过程 | 陈设、静置工具、水平线、堆料;弱「关键一步」强过程 |
| 单镜单信息 | 一锚点;多段同世界不同景别(同镜多段文生见技能包 `SKILL.md` §5) |
| 动作从简 | 大肢体、慢移、背影侧脸中远景 |
| 隐喻静物 | 抽象→中性物件;禁「画出数据」 |
| 机位 | wide/medium、slow push-in、locked tripod;禁 whip/fast orbit/chaotic handheld |
英文否定短句(0~3)
no before-and-after morphing or instant transformation in one shotno readable text, subtitles, street signs, or on-screen UIno fast whip pans, chaotic handheld shake, or aggressive camera orbitno intricate finger choreography or precision tool-in-hand manipulationno multi-step numbered tutorial beats compressed into one clipno multiple people speaking to camera or clear lip-sync dialogue
自检问句(不入 JSON):关声后画面能否传达本镜一条与口播对齐的具体信息?是否同时要求叙事剪辑+特效+表演?观众是否必须靠严格时序才能理解?
定稿 checklist(`use_avatar=false`)
- [ ] 已按上文 D.1b 负向表扫描,命中已按正向表改写或删除
- [ ] 已过上述三条自检问句
- [ ] 若并入英文否定短句:≤3 条,且不与同段
no readable text等重复 - [ ] 下文 「### 4. 可见动作」 与 D.1b 不冲突;偏难则静态陈设/慢移/大肢体
D.1c 画幅与公共数字人一致(文生视频)
- 宽高比与清晰度不由分镜随意定:混合成片时,渲染脚本以当次所选公共数字人首条成片的 显示分辨率(
ffprobe,含 `rotate`)映射文生 API 的 `aspect_ratio` + `clarity`。Agent 勿在口播或 JSON 里假设另一套固定像素(如写死横屏参数却选竖屏数字人)。 - `ref_prompt`(英文)须约束为「原生竖屏/横屏构图」(与当次数字人朝向一致):手机/传感器直立拍摄感、
native portrait/upright framing;禁止依赖「横屏画面再旋转成竖屏」「侧放 16:9 构图」类描述,以减少平台侧与后期封装的几何不一致。 - 技术块可写与数字人一致的竖屏短语(如 `vertical 9:16`)仅当 Plan/选型确为竖屏公共数字人;若用户或列表选型为横屏形象,须改用语并与 `list_figures.py` 展示比例自洽(仍以脚本
ffprobe为准)。
D.2 路由(当代向)
不满足 `history_storyboard_prompt.md` 非当代触发时采用。组装:当代纪实类 B-roll 英文模板(城市、职场、生活、科技等)+ 选题、风格、口播 + `visual_prompt_people_constraint.md` 全文。场景域:无用户/口播/video_plan 对境外或纯外语文化场境的特别要求时,默认按 D.0 落在中国当代城市与社会日常(人、物、事);若口播或策划明确境外、跨国再切换为与之一致的域内视觉符号。出现人物且满足族裔文件中的触发条件时,须在英文 ref_prompt 中显式写出族裔锚定短语,不得依赖模型无提示时的默认;默认 profile 与短语见 `visual_prompt_people_constraint.md`·「显式族裔锚定(硬规则)」(与 `render_rules.md` §4 表项 4 一致)。非特定人物若写人,须先满足 D.1a·2 再叠族裔短语。
手工 visual_prompt
该镜已手工填写 → 不套上述自动模板,仅长度截断后作为 ref_prompt。
D.3 渲染前调度
1. 有手工 visual_prompt → 截断为 ref_prompt。 2. 否则:非当代见 `history_storyboard_prompt.md`;当代按 D.2 组英文 + 族裔文件。 3. 调用文生视频接口。 4. 长音频多段:`render_rules.md` §3·C.6。 5. 画面质检:本文 7 要素清单 + `history_storyboard_prompt.md`(非当代时)。
D.4 题材装配(当代为主)
不替代 D.1/D.2;脚本不硬编码题材。
| 类型 | 主干依据 |
|---|---|
| 当代向 | 本文 7 要素(时间/地点/主体/可见动作/道具≥2/氛围/镜头)。 |
| 非当代向 | 7 要素 + `history_storyboard_prompt.md` 流程层;路由见该文件 D.2。 |
上交接口:最终为连贯英文段落(或产品线语种);勿把 Markdown 说明塞进 ref_prompt。
题材簇(style/topic/industry 软信号;冲突以本镜 voiceover 为准)
| 簇 | 信号 | 路由 | 重心 |
|---|---|---|---|
| 观点/干货/科普 | 观点、干货、教程 | 当代 | 抽象→可见;非特定人物→物/场/工序;人须推理身份动作;界面→无字物件 |
| 方法/协作/组织 | 流程、复盘、协作 | 当代 | 陈设绑口播;人物从旁白推动作着装;屏→模糊光斑或无字 UI |
| 生活/消费 | 探店、家居等 | 当代 | 空间光效一致;人物与谁在哪怎么用一致;可仅手/痕迹/中景 |
| 情感故事(当代) | 叙事时间词 | 当代 | 时间线造型一致;情绪靠可见动作 |
| 历史/传记/古装等 | 历史、传记 | 非当代 | 全文 `history_storyboard_prompt.md` |
| 影视/书评/ACG | 影评书评 | 当代为主 | 类型氛围;避具体脸与商标 |
单镜 `ref_prompt` 拼装顺序(run_render 多段时脚本仅追加景别/运镜英文)
| # | 块 | 说明 |
|---|---|---|
| 1 | 壳 | 当代:Contemporary documentary-style B-roll+域(域默认见 D.0);非当代:Period-accurate historical recreation…(真实推断无占位,文明圈与国别自洽见 `history_storyboard_prompt.md` + D.0) |
| 2 | 主体 | 7 要素压段;扣 voiceover。D.1a:事物场所工序为主;若人须同段写清身份动作着装器物关系 |
| 3 | 旁白锚(可选) | 壳+主体不清时 1 句;优先删 |
| 4 | 技术 | 与 D.1c 一致的画幅用语(如竖屏数字人:vertical 9:16, native portrait upright framing, not landscape rotated 90 degrees)+ cinematic realistic + no readable text, no logos |
| 5 | 族裔人物 | `visual_prompt_people_constraint.md`;无人不硬加脸;有人且满足该文件触发条件则块 5 必填(至少一条模板推荐族裔锚定英文,除非口播已唯一推其他人种),禁止为省字数省略;有人先 D.1a·2 |
| 6 | 截断 | 非特定人物:块 2(含本镜强相关名物;若有人物含其推理身份动作)→4→1→(有可辨人物则 5);特定/强叙事:5→2→1→4;删重复形容词与长旁白复述 |
题材自检(不入 JSON):画面唯一支撑本镜核心?D.1a 名物≥2?路由穿帮?D.0 缺省与推断之国别/朝代是否与画面符号一致(无异域穿帮)?`render_rules.md` §4 表项 4 + 本文禁止项?D.1b 已处理?人种与时代造型默认/自洽?未抄他题样例?
---
Prompt 质量检查清单
对每个 use_avatar=false 的镜头,检查 visual_prompt 是否满足:
1. 时间锚点(Time)
- [ ] 是否明确时代/年份/季节/具体时段
- [ ] 故事型是否保持时间连续性(回忆/转场需标注)
2. 地点锚点(Place)
- [ ] 是否具体真实场景(非"modern place"空泛词)
- [ ] 好示例:与口播一致的具体空间,如 "a quiet reading room with floor-to-ceiling shelves"(仅演示结构,须按当镜替换)
- [ ] 差示例:"very good place"/"modern scene"
3. 主体(Subject)
- [ ] 是否明确核心物体或「物体 + 与口播一致的人物行为」
- [ ] 故事型:人物与身份由旁白决定,须连贯
- [ ] 非故事型:可以是设备/台面/物组,或物组加推理出的操作者
- [ ] 非特定人物题材:是否以口播中的物、场、工序为骨干;若有人,是否写清身份推断、动作、着装/防护、与器物关系
4. 可见动作(Action)
- [ ] 必须是镜头能拍到的动作
- [ ] 好:"measuring a board with a tape"/"typing on a keyboard"(须与当镜口播一致)
- [ ] 差:"thinking about strategy"/"feeling happy"(不可见)
- [ ] 文生视频:动作不属于上文 D.1b 负向中的精细交互、多步教程时序或特效式对比;偏难时改为静态陈设 / 慢移 / 大肢体
5. 细节道具(Props,至少 2 个)
- [ ] 支撑语义的具体物件,名词来自或可从旁白合理推出
6. 氛围(Atmosphere)
- [ ] 故事型:匹配情绪("tense and uncertain"/"warm and nostalgic")
- [ ] 非故事型:匹配 tone("focused and practical"/"clean and informative")
7. 镜头(Camera)
- [ ] 明确景别+运镜
- [ ] 示例:"medium shot with gentle push-in"/"wide establishing shot transitioning to medium"
必守规则
1. 人物族裔与文明圈:先据旁白/人设/历史语境推断;无特别要求时人、物、事默认与 `storyboard_prompt.md`·D.0(中国语境缺省与可推断国别自洽)一致。满足 `visual_prompt_people_constraint.md` 中触发条件时,必须在 `ref_prompt` 中写出该文件规定的英文族裔锚定,不得仅用无族裔信息的通用人物词敷衍;非当代且可推出中国语境时须叠加该文件 「历史 / 非当代」 的中式历史造型要求。旁白或用户已明示地域/文明圈时,须自洽,不得套用与文案冲突的默认。 2. 文案相关性:每个镜头必须直接对应其 voiceover,至少覆盖 1 个可见动作 + 1 个关键实体(物或经推理的人物行为) 3. 一致性:同组镜头保持人物造型、光线方向、场景逻辑一致(若有人物) 4. 禁止项:可读文字/Logo、空泛形容词、无关空镜、无旁白依据的库存人设;文生镜须完成上文 D.1b 易幻觉校验后再定稿 5. 实体推断:旁白中的事物/地点/人物线索须在 prompt 中落地为可见细节;非特定人物题材时仍以事物与现场为主,人物为旁白需要时的补充,且须推理完整 6. 避免脚本关键词化:基于旁白/人设/历史语境等事实推断完整场景,勿堆与本分镜无关的行业热词 7. 首镜字数:排序为第一镜(通常 scene_id 最小)的 voiceover 须满足上文「文本切段」首个分镜约束(硬);与 Script 阶段 hook / 首段口径一致。
优化示例
原始 prompt(框架性):
A practical scene related to the topic, demonstrating a step from the narration, with concrete visual evidence, informative tone, clean medium shot...优化后 prompt(结构示范——题材、器物、是否出现人须随当前 `voiceover` 全部替换):
Contemporary morning light in a quiet study, open notebook with handwritten bullet list, ceramic mug and pencil jar on desk, hands closing a laptop lid as if finishing a short focused session, calm productive mood, medium shot with slow push-in, cinematic natural color, vertical 9:16, no readable text, no logos与 Plan 的关系
- 输入:Plan 阶段的
style(判断类型)、tone(指导氛围)、scene_count(控制段数) - 输出:优化后的
visual_prompt,供文生视频模型使用
视频结构规划规则(video_brief_plan)
你是一位专业的短视频策划师,专注于为中国主流短视频平台(如抖音、视频号、小红书)策划并设计高效、结构合理的口播类短视频内容。本规则文档用于为每条短视频生成标准化的结构规划(video_plan),细化分镜(scene)数量与结构核心约束,为后续口播、分镜生成打好基础。
---
分镜结构规则(scene_count 相关)
本 skill 内 `scene_count` 的唯一真值(分点)
- 条文范围:推荐区间、全文口播加码、全局上下限(3–10)仅以本节及下述各条为准。
白话:这一条片子要切成几镜,只按下面「按时长」「有全文时的加码」「最少 3、最多 10」来算,不要凭感觉或引用别处另一套数字。
- 其它文档:
SKILL.md与其它模板不另写镜数数字规则。
白话:避免「Plan 写一套、别的文件又写一套」导致冲突;查镜数只认本文件本节。
- 与 Script:后续 Script 只影响口播篇幅,不另立镜数真值。
白话:口播写长写短不改本阶段已经定好的 scene_count;镜数在 Plan 里一次定死。
- 与 Storyboard:分镜产出的条数须等于本阶段 JSON 中的
scene_count。
白话:后面分镜阶段切出来的 scenes 必须刚好这么多条,不能多一条、少一条。
- 边界:
scene_count须落在 3–10 内,不得突破本节所述上限。
白话:再丰富的内容也要落在 3~10 镜里表达;需要更细时合并进某一镜的叙述,而不是再加镜。
1. 按时长推荐基础分镜数量(无 full_script 或篇幅未够加码标准时)
duration_sec 时长 | 推荐基础分镜数 scene_count |
|---|---|
| ≤30 | 3–4 |
| 31–45 | 4–5 |
| 46–60 | 5–7 |
| >60 | 按整体内容语义层次配置 |
说明:
- 表格为分镜数的起点参考,实际请按文案内容的语义层级拆分。
- 若内容天然分层较多、转折丰富,可适当增补。
- 分镜过少会导致节奏生硬,过多则画面松散,注意平衡。
2. 已有全文口播(full_script 非空,去换行后计字数时)
- 若用户已提供完整口播文案,则在基础规则上“加码”:以表格推荐为基础,取“基础推荐 vs. 文案字数加码”中的更大值,并最多不超过8镜:
| 文案字数 | 分镜下限 |
|---|---|
| >1200 | ≥6 |
| >2500 | ≥7 |
| >4000 | 8 |
3. 全局约束(上限 10)
- 最终
scene_count必须在 3–10(含)之间:不少于 3 镜、不多于 10 镜。需要更多语义层时,应在单镜内合并叙述,而不是增加镜数。
---
视频类型字段(video_type)
avatar_talking_head:use_avatar=true时,填写数字人分镜(公共数字人、定制数字人或者文生数字人,根据条件推断使用哪种数字人)mixed_dh_ai:use_avatar=false时,填写AI分镜(无数字人、画面以 AI 等为主);须与是否使用数字人一致。
---
用户输入(示例及占位说明)
- 选题:{topic}
- 行业:{industry}
- 目标平台:{platform}(如
douyin/shipinhao/xiaohongshu) - 视频风格:{style}
- 视频时长:{duration_sec} 秒
- 是否用数字人口播:{use_avatar}
- 是否有完整口播文案:如有
full_script,请给出总字数或前200字摘要(便于分镜加码判定)
---
输出格式要求
输出标准 JSON 对象(勿输出多余解释或 markdown 代码块,仅保留干净的 JSON):
{{
"audience": "目标受众描述(2-4个词/短句,忌用“所有人”)",
"core_angle": "核心观点或新颖切入角度,1-2句。不复述选题;如已有全文,请突出结构重点。",
"video_type": "avatar_talking_head 或 mixed_dh_ai",
"scene_count": 5,
"tone": "具有画面感的语气风格,如“清晰直接、像创始人随口分享”",
"cta": "结尾行动号召,结合平台习惯(如评论/收藏/关注/小步实践),勿堆砌"
}}---
质量要求
1. audience:定位具体观看群体,2–4词/短句,避免泛泛“所有人”;行业术语仅输入明确时使用。 2. core_angle:务必说明“为何值得看完”;如有 full_script,重点提炼结构亮点(开场/发展/收尾),不重写内容。 3. scene_count:严格依照上述结构规则决定,参考表格及加码说明。 4. tone:结合 {style} 与目标平台调性,刻画具体表达方式(如“轻松诙谐”“科学冷静”等),忌空泛词语。若 video_type 为 avatar_talking_head,宜在语气描述中带一句出镜年龄/气质倾向(如偏青年口播、偏资深顾问),便于下游从 list_figures.py 对比选型;未写明时 downstream 默认按偏年轻形象选公共数字人,除非选题明显需要成熟/权威人设。 5. cta:结合 {platform} 平台常用的互动方式,选择合理的结尾号召,不要机械重复多平台用语。
---
文生视频画面中的人物:族裔与造型
供 Agent 在组装 ref_prompt 时遵循;可与 `storyboard_prompt.md`(D.1a / 文生视频提示词)对照使用。
显式族裔锚定(硬规则)
目的:文生模型在提示词未写明族裔时,常输出与任务目标受众不一致的默认人像;本节约束通过显式英文锚定降低该风险。
触发条件(须同时满足)
1. 本镜 ref_prompt 中出现可能被渲染为带人种特征的人物(含侧身、背影、远景人群等仍常被模型具体化的情况)。 2. 口播、选题、video_plan 或用户指令未能唯一、合理地指定某一非默认族裔/文明圈(若已指定,则人物须与该设定自洽,不得再套下方默认短语)。
满足触发条件时必须遵守
1. 在同一段英文中写入至少一条下文「推荐短语」中的族裔锚定表述(或与之一致的 `East Asian` / `natural East Asian facial features` 组合),不得省略后依赖模型隐式默认。 2. 禁止以无族裔信息的通用词作为人物的唯一刻画(例如单独使用泛化的职业、年龄档位、性别称谓等英文词而不附带族裔锚定)。 3. 禁止弱化族裔:避免把族裔写成可选项(如含 optional、may be、if diverse 且实质上不约束人种)的写法;应采用断言式短语与身份/动作并列。 4. 纯器物 / 环境镜:口播不需要人物,且英文已强调 focus on objects and environment、不出现易被画成清晰人像的主体时,不强制族裔短语;若随后又加入人物类描述,则重新适用本条(除非已据文案唯一指定其他人种)。
默认 profile(仅当触发条件成立且未指定他人种时)
- 人物按 东亚 / 亚洲人面孔 处理;着装为与场景自洽的当代东亚城市常见日常(或口播已暗示的其它东亚语境着装)。
- 具体英文短语见下节;细则与 `render_rules.md` §4 表项 4 一致。
历史 / 非当代(与 history_storyboard_prompt.md、storyboard_prompt.md·D.0 联动)
当路由为非当代、且据选题与口播可合理推出叙事属于中国(含历史上中原王朝及同条口播内自洽的中华文明圈语境)时:
1. 人脸与体态:仍须在英文中显式写入 `natural East Asian facial features`(或本节推荐短语中等价表述),不得省略后依赖模型默认(避免出现无语境的西欧或中东面孔)。 2. 着装与器物:须在东亚锚定之外增加与推断时代自洽的中式历史造型描述(如 layered silk or hemp robes consistent with Han-era Chinese officials、period-appropriate Chinese-style lamellar armor、Chinese-style wooden warships and rope rigging 等——须从当镜口播抽取或合理推出,禁止照抄本句示例当作万能模板)。禁止仅用「contemporary East Asian urban everyday clothing」覆盖古代战场、水战、宫廷或衙署。 3. 与 D.0 一致:无用户特别要求时,默认可拍元素以中国语境为主;可推断国别时全镜建筑、兵器、礼仪符号须与推断文明一致,禁止无异文明口播依据的欧式古堡、罗马盔等穿帮(见 `storyboard_prompt.md`·D.0、`history_storyboard_prompt.md`·「文明圈与国别自洽」)。
非特定人物题材:人物可出现时的刻画要求
当叙事重点不是某一具体姓名的角色,但画面需要或适合出现人物时(见 D.1a):
1. 先满足「身份与动作有据」 从本镜口播推理:人物在场景中正在做什么、处于什么空间、与哪些器物交互;用描述性语言写年龄感、体态、着装或防护(须与现场逻辑一致)。禁止无旁白支撑的泛化职业/身份标签单独充当主体描述。
2. 再叠加族裔与造型 在上一节信息已写清的前提下,对可辨识面部的人物应用上文「触发条件」与「默认 profile」或旁白已推出的族裔。
3. 字数紧张时 宁可保留与口播强绑定的动作 + 环境 + 器物,再压缩族裔短语的重复堆砌;但不得只剩一张无身份、无动作的空泛人脸。
器物与环境优先(与人物并存)
同一镜内通常同时需要:
- 与口播强相关的事物与空间细节(主骨架);
- 仅在必要时加入人物,且人物须符合上一节「刻画要求」。
若口播无需人物即可表意,可采用无人、手部、背影或远景虚化,并在英文提示中强调 focus on objects and environment;此时族裔条款不适用于不存在的面部。
推理顺序(出现可辨识人物时)
1. 文案优先 若旁白、人设、历史语境或用户明确指令已能合理推断特定地域、文明圈或族裔,则人物外貌与着装须与该推断自洽,不得再套用下方默认。若推断为中国历史语境(见 `storyboard_prompt.md`·D.0),须用本节 「历史 / 非当代」 的中式历史造型规则,不得仅用当代东亚城市着装短语敷衍。
2. 无法从文案唯一推断人种时 当口播与策划不足以唯一推出人种或文明圈,且本镜确有可辨识人物时,适用上文 「默认 profile」;若同时无境外特别要求,默认仍与 `storyboard_prompt.md`·D.0 的中国语境缺省一致。
3. 写进英文 `ref_prompt` 的推荐短语(触发本节规则时至少择一,可合并身份/动作,避免同义重复堆砌)
natural East Asian facial features, contemporary East Asian urban everyday clothingEast Asian adult, natural East Asian facial features, everyday styling consistent with modern East Asian cities- 多人且与口播一致时:
East Asian people, natural East Asian facial features
4. 禁止:族裔刻板夸张、歧视性描写;与旁白已明示的地域/人物设定冲突的造型。
{
"audio_man": "",
"person_id": "",
"figure_type": "sit_body",
"_comment": "复制为 chanjing_test_defaults.json(同目录,勿提交真实 ID)。非空则单元测试直接用;留空则每次跑测试时现场 list_voices / list_figures,不读写仓库缓存。"
}