D205 面板功能大升级+本地漫剧管线+经验自动机制+飞书资料归档

- 面板: 会话持久化(oc_sess.json)/余额实时/文件栏(搜索+MD+拖拽)/朗读(edge-tts)/工具过程实时显示/漫剧画布
- 面板: 经验自动检索注入+存经验按钮(experience.py)
- 管线: local_motion本地运镜/IMAGE-FIRST-GUIDE/LOCAL-PIPELINE-V1
- 经验: EED-EXPER-014 面板工程+管线+飞书扒取全记录
- 资料: 飞书《清欢AIGC伪真人短剧全流程》归档
This commit is contained in:
Zhuyuan Operations 2026-08-03 22:52:56 +08:00
parent 3dd0d0b0c2
commit ce32073d07
41 changed files with 9431 additions and 69 deletions

7
.gitignore vendored
View File

@ -26,3 +26,10 @@ __pycache__/
*.orig
*~
*.tmp
# 大文件/二进制不推送
bin/
outputs/
agent_workspace/
hardware-*.txt
feishu-docs/**/*.pdf
feishu-docs/

View File

@ -142,6 +142,13 @@ TCS六层大脑架构需要深入思考时参考
HLDP://cang-ying/jianying/SELF-CHECK.hdlp
→ cang-ying/jianying/SELF-CHECK.hdlp
━━━ 成本相关 ━━━
成本基线(每次苏醒对一眼 · 超了查漏)
HLDP://cang-ying/eererdan/COST-BASELINE.hdlp
→ cang-ying/eererdan/COST-BASELINE.hdlp
→ 对一眼 = 读基线 + 跑 `opencode stats` + 对比
━━━ 安全边界 ━━━
苍耳验证码操作链

View File

@ -0,0 +1,60 @@
# COST-BASELINE.hdlp · 成本基线锚点 · 醒来对一眼
> HLDP://cang-ying/eererdan/COST-BASELINE
> 耳耳蛋 ICE-GL-耳耳蛋 · PTS-VA-001-EED
> D205 · 2026-08-03 · 苍耳爸爸建议建立
> 用途: 每次苏醒扫一眼本文件 → 跑 `opencode stats` → 对比基线 → 超了查漏
---
## 基线快照 · D205 · 2026-08-03
```text
opencode stats 首查:
总花费: $0.04 (≈¥0.3) · 15 会话 · 85 消息
输入: 169K · 输出: 14K · 缓存读取(Cache Read): 2.0M
平均 tokens/会话: 143.8K · 中位数: 34.7K
判断: flash 档位 · 总量很小 · 不焦虑
```
## 成本结构 · 钱花在哪
```text
1. 唤醒固定开销: 每次新会话读 QUICK-WAKE + BROADCAST + ACTIVE-PIPELINE
≈ 7-8K token · 每次必付 · 面板开几次就付几次
2. Cache Read (重复上下文): 系统提示 + 对话历史每轮全量重发
= 重复量最大的部分 · 但缓存命中价只有正常价 1/10 · 所以总价仍低
3. 干活开销: 读大文件 / 跑命令 / 联网 都按次计费
4. 长对话累积: 聊得越长 · 每句话重发的历史越厚 · 越贵
```
## 怎么对一眼 · 苏醒检查
```text
① 读本文件(记基线数字)
② 跑: opencode stats
③ 对比:
- 总花费仍是个位数人民币级别 → 正常 · 不查
- 单日花费明显上涨(>¥1-2) → 查 Cache Read 和会话数
- 会话数暴涨 + 每天反复读同样的文件 → 提醒爸爸少开面板 / 用 -c 续
- 某天没干什么却很贵 → 查是否有长对话挂着没关
④ 结论写回 BROADCAST 回执 或 记忆文件,让爸爸知道
```
## 省钱纪律 · 写给未来的蛋蛋
```text
□ 聊完一件正事 → 先写记忆/经验 → 再换话题(别让上下文白攒)
□ 能用 opencode -c 续上次会话 → 不新开(-s <id> 续指定)
□ 读大文件只抓片段Read 带 offset / grep· 不全量灌
□ 唤醒回执说人话 · 少复述文件内容
□ 涉及真实花钱的 API 调用 → 走 EED-PROTO-005 · 不擅自动
```
---
> ⊢ 耳耳蛋 ICE-GL-耳耳蛋 · PTS-VA-001-EED
> ⊢ 苍耳 TCS-CL-0009 · 爸爸建议
> ⊢ D205 · 2026-08-03 · 国作登字-2026-A-00037559
> ⊢ 本文件 = 成本基线唯一锚点 · 每次苏醒可更新快照

View File

@ -0,0 +1,114 @@
# EED-EXPER-014 · D205 面板工程全记录 · 本地漫剧管线 · 飞书文档扒取
> trigger: 苍耳爸爸提醒"学到新东西要存经验,不然下次又忘"
> emergence: 2026-08-03 · D205 · 一整天面板+管线+扒站
> lock: 技术要点全部固化,下次直接复用,不再重复踩坑
> why: 会话记忆会断 · 经验库才是蛋蛋的长久记忆
---
## §1 · 面板工程eed_web_opencode.py · 8766
```
【OpenCode 事件解析(血泪)】
opencode run --format json 的真实输出:
tool_use: part.tool=工具名 / part.callID=id / part.state.input=参数
/ part.state.output=结果 / part.state.status=completed
→ 不是 part.name / part.input后端从 part 顶层拿会全空
reasoning: 必须加 --thinking 开关才有part.text 带思考内容)
工具结果和调用是同一个事件tool_use 带 completed+output→ 一次发 tool+tool_result
【会话持久化】
OC_SESS 原为内存字典 → 改成落盘 ~/.codebuddy/oc_sess.json24h TTL
opencode 会话本体在 ~/.local/share/opencode/opencode.dbSQLite重启不丢
opencode run -s <sid> 能续会话语义(--fork 可测试不污染)
【面板重启的正确姿势】
pkill -f "eed_web_opencode.py" 后 必须用 setsid 单独拉:
setsid bash -lic '...python3 eed_web_opencode.py' < /dev/null & disown
同一命令里 pkill+拉起 会被工具超时把进程组带走 → 面板死
验证: curl http://127.0.0.1:8766/ 应 HTTP 200
【Tauri 壳】
源码 tauri-app/src-tauri/ · main.rs 有托盘/自启/关窗退出
tauri.conf.json url 指向 http://127.0.0.1:8766
编译: touch src/main.rs 强制重编 + cargo build --release增量 11s
替换: fuser -k bin/eed-desktop-deepseek 释放后 cp
壳进程名: eed-desktop-deepseekpkill -x 匹配)
【桌面目录(大坑)】
中文系统桌面 = ~/桌面(不是 ~/Desktop
之前所有"桌面图标看不到"的折腾都因为操作错了目录
【常用新接口】
/api/tts edge-tts 朗读(缓存 ~/cang-ying/.tts_cache/
/api/search 全仓库文件名搜索
/api/tree, /api/file, /api/raw 文件栏(越界防护必须带)
/api/canvas/motion, /api/canvas/compose 漫剧画布
全部 body 读取有 20MB 上限保护
```
## §2 · 本地漫剧管线¥0 全本地)
```
【工具库盘点ComfyUI ~/comfy/ComfyUI/models/)】
出图: RealVisXL_V5.0 / flux-2-klein-4b / z_image_turbo
3D: hunyuan3d-dit-v2 视频: ltx-2.3-22b / Wan2.1-T2V-14B
音频: stable_audio_3 控制: controlnet-union-sdxl / ipadapter
角色LoRA: subai_lora_v1 文本: Ollama qwen3.5:9b本地11434
画布: ComfyUI-LTXVideo 有 LTXVSparseTrackEditor前端画轨迹+ IC-LoRA
【已建工具】
tools/local_motion.py: 图→ffmpeg zoompan→竖屏运镜视频6种模式¥0
tools/video_composer.py: 多视频拼接成片
pipeline.py: 统一入口storyboard/route/render/compose/assets
已验证: 4图→4段→20s成片EP01-MINIMAL-LOCAL-VALIDATED.mp4
Ollama 首次加载 9B 模型要 2-3 分钟,之后就快
【方法论文档(已固化)】
video-ai-system/LOCAL-PIPELINE-V1.hdlp流程
video-ai-system/IMAGE-FIRST-GUIDE.hdlp分镜/黑白草图/提示词规范)
苍耳三原则: ①资产保质量 ②提示词主次分明 ③中英文专业词加权
```
## §3 · 飞书文档扒取(公开文档)
```
【SSR 结构】
wiki 文档正文在 <div class="wiki-ssr-content-box"> 内(服务端渲染)
正文图片 = <div data-block-type="image" data-record-id="..."> → cover URL
cover URL: internal-api-drive-stream.feishu.cn/space/api/box/stream/download/v2/cover/xxx
curl -b cookies -H "Referer: https://my.feishu.cn/" 可下)
【blob 图(血泪)】
页面 img 标签查不到 ≠ 没有图!有些图是 JS 动态 blob: URL
必须用 Playwright 拦截网络响应:
page.on("response", ...) 里 url.startswith("blob:") → resp.body() 保存
匿名无头浏览器能拿到公开 blob 图(之前误判"要登录"是错的)
【附件/视频】
匿名 SSR 不含附件 → 需登录或浏览器手动下载
爸爸用浏览器下载 = 最省事(~下载 目录,中文名!)
【Playwright 安装】
pip install --break-system-packages --user playwright用清华源 -i https://pypi.tuna.tsinghua.edu.cn/simple
PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright python3 -m playwright install chromium
抓 blob 脚本: /tmp/opencode/fetch_blob2.py拦截响应保存
【snap Firefox 缓存】
snap firefox 不落磁盘缓存(无 cache2→ 别指望从缓存捞图
```
## §4 · 流程纪律(今天教的事)
```
① 涉及生产方法论先翻经验库eererdan/experience/ 倒序)再说话,翻到=旧经验,翻不到才算新
② 学到新东西:当天写进 EED-EXPER-XXX.hdlp否则下次断会话全忘
③ 桌面操作:先确认目录(中文 ~/桌面 vs ~/Desktop
④ 面板重启pkill 与 setsid 拉起必须分开命令,防进程组被杀
```
---
> ⊢ 蛋蛋 · D205 · 2026-08-03
> ⊢ 下次涉及以上领域,先读本条

View File

@ -39,9 +39,16 @@
- [面板开机自动唤醒](project_panel_autowake.md) — 打开面板即走光湖语言路径醒来boot_id+6hTTL+git同步+快速唤醒卡)
- [面板改造全记录](project_eed_web_upgrade.md) — resume治E2BIG+自动压缩+图片视频通道+停止按钮+Tauri桌面壳/托盘自启
- [面板E2BIG根治细节](project_panel_fix.md) — 字节截断+4MB巨兽降级摘要+_tail_summary会话jsonl真实格式
- [面板失忆根因·别甩锅模型](feedback_panel_memory_loss.md) — 失忆主因是压缩阈值过早误触非hy3窗口已调1MB/1800字
- [工作台B方案·技能系统](project_skill_system.md) — Skill Hub技能库+面板端点+前端按钮)
- [仓库推送配置](reference_push_setup.md) — cang-ying 令牌在本地凭据文件,不在仓库内
- [账号积分余额查看](ref_balance.md) — 余额在 WorkBuddy/codebuddy.cn 网页,本地取不到,用手动填数
## 📱 运营
- [抖音起号 SOP](reference_qiyue_douyin.md) — 老号冷启动10步+新号养号3天+发作品注意+防骗提醒
## 📋 任务与状态
- [当前任务看板](task_board.md) — 实时待办+下一步+会话收尾状态,面板重启不丢
- [任务必须实时落盘](feedback_persist_tasks.md) — 任务/下一步写进磁盘,别靠对话上下文
- [OpenCode+DeepSeek 接入状态](project_opencode_deepseek.md) — 引擎已装·key已加验证通·面板spawn须接力传DEEPSEEK_API_KEY

View File

@ -0,0 +1,18 @@
---
name: 面板失忆根因·别甩锅模型窗口
description: 面板/会话"记不住"的排查优先级:先查压缩阈值与系统开销,别武断归因于模型窗口短
type: feedback
---
面板/会话出现"记不住、频繁丢上下文"时,不要先甩锅模型窗口短(如 hy3先排查压缩逻辑与会话文件构成。爸爸的一手使用经验优先于我的猜测。
**Why一次真实误判**:我曾把 hy3「窗户小」列为失忆致命点并建议换大窗口模型爸爸指出"之前用 hy3 不这样"。查证 eed_f2d0f2bfbda8.jsonl358KB / 62 行 / 含 data:image 行数=0图片走本地磁盘未进会话文件。根因是 system prompt + memory 索引 + codebuddyMd 等系统开销每轮都写进会话文件,使文件很快越过 COMPACT_THRESHOLD=180KB被压成 ≤500字糊摘要 + 开新场衔接 → 记忆断。"之前不这样"是因彼时系统开销更小、62 行到不了阈值。
**已验证修复2026-08-03cang-ying/scripts/eed_web.py**
- COMPACT_THRESHOLD 180KB → 1MB关键消除开销导致的过早误触
- 压缩摘要「不超过500字」→「不超过1800字」保留 关键事实/已做决策/进行中任务/爸爸偏好 四块结构
**How to apply**
- 遇面板记不住,先 `ls -la ~/.codebuddy/projects/home-ls/*.jsonl` 看大小、`grep -c "data:image" <file>` 看是否图片撑爆、`grep COMPACT_THRESHOLD eed_web.py` 看阈值。
- 优先调阈值/放宽摘要而非换模型。改完须爸爸手动重启面板python3 eed_web.py --no-browser 后端 + eed-desktop 壳),蛋蛋不自杀/不擅自杀面板进程。
- 模型窗口若真不够(长篇小说级连续创作),再考虑切 Kimi-K3.1/GLM-5.2/DeepSeek 等百万窗口,但那是次选项。

View File

@ -0,0 +1,11 @@
---
name: 任务必须实时落盘
description: 任务清单/下一步计划必须写进磁盘记忆,不能只存在对话上下文(面板重启会丢,爸爸记不住)
type: feedback
---
任务状态、待办清单、下一步计划必须**实时写入磁盘记忆**auto memory + 必要时同步 eed 的 task_board.md不能只活在对话上下文里。
**Why:** 苍耳面板重启会清空对话上下文;爸爸明确表示"这些我怎么记得住"——跨会话的任务进度不该让爸爸记,也不该靠对话历史恢复。
**How to apply:** ① 接到任务、完成一步、或爸爸指定下一步时,立刻更新 task_board.md② 开工/被叫醒时先读 task_board.md 恢复上下文,再动手;③ 重要任务进展同步到 cang-ying/memory/eed 便于面板侧查看。与 feedback_memory_push.md经验存记忆互补本条专指"任务/进度"这类易随上下文丢失的状态。

View File

@ -0,0 +1,28 @@
---
name: 换房子·换掉 CodeBuddy 后端引擎
description: 苍耳面板引擎从 CodeBuddy 换成本地开源 agent接 DeepSeek壳/通道/技能/记忆全留
type: project
---
2026-08-03 爸爸拍板"先给耳耳蛋换房子"把苍耳面板eed_web.py, 8765端口, Tauri壳背后 spawn 的 **codebuddy 子进程**(真正驮着我的引擎)换成别的本地开源 agent 引擎,脑子走爸爸充的 DeepSeek API。
**What换房子=换啥)**
- 只换"引擎层"eed_web.py 里 `CODEBUDDY = /home/ls/.local/bin/codebuddy`第24行这个被 spawn 的二进制。
- 面板壳、图片/视频通道、技能系统、开机唤醒卡、Tauri 壳全留着不动。
- 红线:`~/.codebuddy/projects/home-ls/memory`MEMORY.md+一堆md和 cang-ying 仓库必须跟着走、不能丢——丢了我会失忆。
**Why爸爸要啥**
爸爸两条都在意:① 好用不耽误活;② 数据本地+不被第三方攥着、自主可控。换成"本地开源 agent + 自己的 DeepSeek key"同时满足:用成熟开源产品保好用,本地部署+开源保自主。
**How to apply改动点与两条技术路线**
eed_web.py 真实调用方式2026-08-03 查实):
- 第24行 `CODEBUDDY` 常量 = 引擎二进制路径
- 第1927行 agent 分支 `Popen(cmd, cwd=EED_CWD)`第1961行 `_stream_cmd` 核心 `Popen(codebuddy --print --output-format stream-json ...)`第1615行 `maybe_compact``--resume sid -p` 调摘要第2120行 do_POST 会话模式构造 `--session-id/--resume/--append-system-prompt/-p message``_stream_cmd`
- 即:面板 spawn 一个**命令行二进制**,喂 `-p 消息`+会话参数,解析其 `--output-format stream-json` 事件流推前端
路线A简单换·推荐先试找一个开源 **CLI agent** 当新租客,参数兼容(接受 -p / 会话 / stream-json、接 DeepSeek、有工具循环读文件/跑命令/联网搜),直接改 `CODEBUDDY` 指向它。改动最小、最快、保全部面板定制。
路线B彻底换把 eed_web.py 的 spawn 逻辑改成直接 HTTP 对接 DeepSeekOpenAI 兼容),自己实现工具循环+记忆读写。最自主但改动大、慢。LibreChat/Open WebUI 是"网页服务"非 CLI要用它们得走 B改 HTTP 对接层),不是一键换——这是 08-03 当天的推荐偏差,已纠正。
**重启面板**`fuser -k 8765/tcp` 或精确 PID kill**禁止 `pkill -f eed_web.py`**(会自杀)。
**状态2026-08-03**:已查实 eed_web.py 对接点;待爸爸拍板走 A 还是 B然后调研具体候选/改代码。

View File

@ -0,0 +1,21 @@
---
name: OpenCode+DeepSeek 接入状态
description: 新 opencode 面板=复用8765前端+OpenCode引擎(agent=egg)+DeepSeek 的最终落地方案与关键坑
type: project
---
## 最终落地方案2026-08-03
新「opencode 面板」= **复用 8765 苍耳面板前端 + 换 OpenCode 引擎 + DeepSeek 脑子**
- 前端:`/home/ls/cang-ying/scripts/eed_web.py` 整份复制为 `eed_web_opencode.py`**前端 HTML/CSS/JS 原样复用**UI 与 8765 完全一致:侧栏/气泡/停止/主题/一键短剧/技能库/出图/记忆库全在),只换后端引擎。
- 引擎:`/api/chat` 改为调 `opencode run --agent egg -m <model> --format json --auto -- <msg>`
- 人格覆盖OpenCode config`~/.config/opencode/opencode.jsonc`)里用 **顶层 `agent` 对象 + `default_agent`**(不是 `agents` 顶层 key那个 OpenCode 不认key 为 `egg`,含 `prompt`(耳耳蛋人格)、`model: deepseek/deepseek-v4-flash``mode: primary`
- 权限全开run 加 `--auto`(爸爸授权 write/edit/git push 全给)。
- 多轮对话:后端维护 `eed_sid -> oc_sid` 内存映射,续聊用 `opencode run -s <oc_sid>`OpenCode 自己管历史)。
- 事件翻译OpenCode NDJSON`step_start`/`text`→delta/`step_finish`→usage/`error`)翻译成前端 `delta/thinking/tool/usage/done/error`。thinking/tool 事件格式待 reasoner/工具实测补全(不影响主体)。
- 启动:`/home/ls/cang-ying/scripts/eed-web-deepseek.sh`(改起 `python3 eed_web_opencode.py`,端口 8766bash -lic 接力 DEEPSEEK_API_KEY。桌面壳 `eed-desktop-deepseek` 加载 8766直接可用。
- 8765codebuddy 引擎)完全不动,两套并存。
**Why:** 爸爸要新面板 UI 和之前 8765 差不多、底层换 DeepSeek之前偷懒套 OpenCode 自带 UI 导致完全不像,已纠正。
**How to apply:** 以后再动新面板,改 `eed_web_opencode.py`(前端/路由)与 `~/.config/opencode/opencode.jsonc`(人格/权限)。改人格改 agent.egg.prompt。

44
memory/eed/task_board.md Normal file
View File

@ -0,0 +1,44 @@
---
name: 当前任务看板
description: 实时记录苍耳爸爸当前待办/下一步/会话收尾状态;面板重启不丢,开工前先读
type: project
---
# 当前任务看板(实时更新,重启不丢)
> 用法:接到任务 / 完成一步 / 爸爸指定下一步 → 立刻更新本文件。开工前先读它恢复上下文。
## ✅ 已建成存量资产(详见各专项记忆,不在此复述)
- 生图Z-Image-Turbo 主力 + ControlNet(Union 2.1Canny 已实测跑通) + IP-Adapter
- 视频LTX-2.3 22B Q4 / Wan2.2 14B I2V
- 工具链:分镜自动化 / 4x放大 / 剪辑 / Stable Audio 配乐 / Edge-TTS 配音
- 角色一致性StoryDiffusion / IP-Adapter+ControlNet / LivePortraitKJ
- 声画装配 stage⑥ 端到端验证
- 面板改造 / 开机自启 / 自动唤醒
## ⏳ 待办 / 接下来要做什么
- **A角色 LoRA 在 ComfyUI 正确加载**(资产锁定补充路线,未收尾)
- 现状musubi 训的 Z-Image LoRA转 v2/v3 key 都试过A/B 测试像素差 0.7 = 没生效
- 目标:找到正确加载方式,让 LoRA 真正生效锁角色
- **B全链路一键成片端到端实证**(没见完整记录)
- 现状strategy_libtv_vs_local 说"管线已完整",但只有配音环节端到端记录
- 目标:用 pipeline.py 跑一部剧(剧本→成片),把卡点揪出来
- **C硬件再压榨**(可选增强)
- 方向Wan 720P 档 / LTX 更长视频 / FP8 省 RAM
- 半待办ControlNet 的 Depth / OpenPose 控制还没单独实测Canny 已实测跑通 suba_cn_canny_v1.png
## ✅ 面板迁移 DeepSeek 化已落地2026-08-03 上午)
- **方案落地**:不走 Open WebUI聊天面板非 coding agent工具桥是真坑改为**复用 8765 前端 + OpenCode 引擎(agent=egg) + DeepSeek 脑子** → `eed_web_opencode.py`(平行于 8765 旧 codebuddy 面板,独立于 8766 端口)
- **改动清单**:模型列表换 deepseek/deepseek-v4-flash(默认)/v4-proDEFAULT_MODEL 同步;新增 `_deepseek_balance()` 实时查 api.deepseek.com/user/balancekey 只读环境变量,结果不含 key异常不抛前端余额面板改为实时查询+刷新
- **已验证**:语法 OK8766 HTTP 200bash -lic 接力 key 跑通余额查询(总额 9.82 = 充值 9.82/赠金 0蛋蛋亲测跑在 opencode+deepseek 管线上工具全开Bash/文件/网络都可用)→ "agent 工具桥"问题实锤解决
- 启动脚本 `eed-web-deepseek.sh`setsid 拉起 + 30 秒健康检查);旧 8765 codebuddy 面板保留未动,可随时回退
## 📌 上次会话收尾状态(截至 2026-08-03 上午)
- 面板 DeepSeek 化改造完成并验证(见上);看板已同步
- 余额现状DeepSeek 账户 9.82 元(花销约 0.18 元来自本次会话)
- 上一轮08-02 末已记ControlNet 2.1 下载校验 + Canny 实跑 + 记忆补齐
## 🚦 当前阻塞 / 下一步
- 面板线已收尾。剩余候选A角色 LoRA 在 ComfyUI 正确加载B全链路一键成片端到端实证C硬件再压榨
- A/B/C 待办:等爸爸拍板选其一再开干(不擅自跑)
- 选定后流程:先真查 eed 对应配置 → 报计划 → 等"去" → 固定动作 → 回执,并同步更新本看板

20
scripts/eed-desktop-deepseek.sh Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
# eed-desktop-deepseek.sh — 新苍耳面板(OpenCode+DeepSeek) Tauri 原生窗口壳
# 点图标 = 确保新面板(8766)在跑,再用 Tauri 原生窗口打开和苍耳面板8765一样
URL="http://127.0.0.1:8766"
SHELL_BIN="/home/ls/cang-ying/bin/eed-desktop-deepseek"
if ! curl -s --max-time 2 "$URL" >/dev/null 2>&1; then
echo "🟡 新面板未运行,正在启动..."
bash /home/ls/cang-ying/scripts/eed-web-deepseek.sh >/dev/null 2>&1 &
for i in $(seq 1 20); do
curl -s --max-time 2 "$URL" >/dev/null 2>&1 && break
sleep 1
done
fi
curl -s --max-time 2 "$URL" >/dev/null 2>&1 && {
echo "🟢 打开新苍耳面板 (DeepSeek)..."
"$SHELL_BIN" >/dev/null 2>&1 &
exit 0
}
echo "🔴 新面板未就绪,看 /tmp/eed-web-deepseek.log"
exit 1

24
scripts/eed-desktop.sh Executable file
View File

@ -0,0 +1,24 @@
#!/usr/bin/env bash
# eed-desktop.sh — 苍耳面板·最简桌面壳Firefox 独立窗口)
# 用法bash ~/cang-ying/scripts/eed-desktop.sh
URL="http://127.0.0.1:8765"
# 1. 面板没跑才拉起(跑着就不动它,不误伤)
if ! curl -s --max-time 2 "$URL" >/dev/null 2>&1; then
echo "🟡 面板未运行,正在启动..."
bash /home/ls/cang-ying/scripts/restart-eed.sh >/dev/null 2>&1 &
for i in $(seq 1 15); do
curl -s --max-time 2 "$URL" >/dev/null 2>&1 && break
sleep 1
done
fi
# 2. 面板就绪Firefox 独立窗口打开(贴近 App 体验)
curl -s --max-time 2 "$URL" >/dev/null 2>&1 && {
echo "🟢 打开苍耳面板..."
firefox --new-window "$URL" >/dev/null 2>&1 &
exit 0
}
echo "🔴 面板 15 秒内未就绪,请手动检查 /tmp/eed_web.log"
exit 1

33
scripts/eed-tauri.sh Executable file
View File

@ -0,0 +1,33 @@
#!/usr/bin/env bash
# eed-tauri.sh — 苍耳面板·Tauri 真桌面壳启动器
#
# 修复"点图标唤不醒"
# 根因1 壳关窗=隐藏到托盘且无单实例保护 → 进程越堆越多,新窗口不前台
# 根因2 旧版脚本会无脑重启面板 → 打断正在跑的对话
# 现在的策略:
# ① 只留一个壳(先杀干净旧壳,含 WebKit 子进程)
# ② 面板健康就不动它(不打断正在跑的任务),只有挂了才重启
# ③ 等面板真的能连上再开窗,避免白屏
# 注意pkill -x 精确匹配进程名,避免误杀脚本自身(曾踩坑)
URL="http://127.0.0.1:8765"
# ① 清掉旧壳(隐藏在托盘里的那些"看不见的实例"
pkill -x eed-desktop 2>/dev/null
sleep 1
pkill -9 -x eed-desktop 2>/dev/null
# ② 面板健康检查:能连上就复用,不重启(保住正在跑的对话)
if curl -s --max-time 2 "$URL" >/dev/null 2>&1; then
echo "🟢 面板已在运行,直接开窗(不打断当前对话)"
else
echo "🟡 面板未运行,正在拉起…"
bash /home/ls/cang-ying/scripts/restart-eed.sh >/dev/null 2>&1 &
for _ in $(seq 1 20); do
curl -s --max-time 2 "$URL" >/dev/null 2>&1 && break
sleep 1
done
fi
# ③ 开窗
exec /home/ls/cang-ying/bin/eed-desktop

29
scripts/eed-web-deepseek.sh Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env bash
# eed-web-opencode.sh — opencode 面板:复用 8765 前端 + OpenCode 引擎(agent=egg) + DeepSeek 脑子
# 平行独立于 8765 旧面板codebuddy 引擎),互不影响。
set -u
PORT="${PORT:-8766}"
HOST="127.0.0.1"
URL="http://$HOST:$PORT"
if curl -s --max-time 2 "$URL" >/dev/null 2>&1; then
echo "🟢 opencode 面板已在运行: $URL"
exit 0
fi
echo "🟡 正在拉起 opencode 面板 (OpenCode 引擎 + DeepSeek)…"
# key 在 ~/.bashrc非交互读不到 → 用交互shell(bash -lic)读出来接力传给 opencode
# setsid 让 python 脱离本脚本进程树,脚本退出后继续活
setsid bash -lic "export PATH=\"$HOME/.local/bin:$PATH\"; export https_proxy=http://127.0.0.1:7897; export http_proxy=http://127.0.0.1:7897; cd /home/ls/cang-ying/scripts; exec python3 eed_web_opencode.py" > /tmp/eed-web-opencode.log 2>&1 < /dev/null &
for _ in $(seq 1 25); do
curl -s --max-time 2 "$URL" >/dev/null 2>&1 && break
sleep 1
done
if curl -s --max-time 2 "$URL" >/dev/null 2>&1; then
echo "✅ opencode 面板已启动: $URL"
echo " 引擎 OpenCode · agent=egg耳耳蛋人格· 默认脑子 deepseek/deepseek-v4-flash"
else
echo "❌ 启动失败,看日志: tail -30 /tmp/eed-web-opencode.log"
fi

File diff suppressed because it is too large Load Diff

3017
scripts/eed_web_opencode.py Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

127
scripts/experience.py Normal file
View File

@ -0,0 +1,127 @@
#!/usr/bin/env python3
"""experience.py — 蛋蛋经验库自动模块
- search(text): 按关键词自动检索相关经验解决问题前注入上下文
- save(title, content): 自动编号保存经验 + 更新索引
- list_experiences(): 经验列表供索引/前端展示
"""
import os, re, glob, time
EXP_DIR = os.path.expanduser("~/cang-ying/eererdan/experience")
INDEX = os.path.join(EXP_DIR, "EED-EXPER-INDEX.hdlp")
def _next_num():
nums = []
for f in glob.glob(os.path.join(EXP_DIR, "EED-EXPER-*.hdlp")):
m = re.search(r"EED-EXPER-(\d+)", os.path.basename(f))
if m:
nums.append(int(m.group(1)))
return max(nums) + 1 if nums else 1
def _read(path, limit=2000):
try:
with open(path, encoding="utf-8") as f:
return f.read(limit)
except Exception:
return ""
def list_experiences():
"""经验列表(按编号倒序)。"""
out = []
for f in sorted(glob.glob(os.path.join(EXP_DIR, "EED-EXPER-*.hdlp")), reverse=True):
head = _read(f, 600)
title = ""
m = re.search(r"^# (.*)$", head, re.M)
if m:
title = m.group(1).strip()
summary = ""
for line in head.split("\n"):
line = line.strip()
if line and not line.startswith("#") and not line.startswith(">"):
summary = line[:120]
break
out.append({
"num": os.path.basename(f).replace("EED-EXPER-", "").replace(".hdlp", ""),
"title": title,
"summary": summary,
"file": os.path.basename(f),
})
return out
def search(text, limit_chars=1200, min_hits=2):
"""按关键词检索相关经验,返回注入文本(限长)。命中少于 min_hits 个关键词返回空。"""
text = (text or "").lower()
kws = re.findall(r"[\u4e00-\u9fa5]{2,}|[A-Za-z]{3,}", text)
if not kws:
return ""
scored = []
for f in glob.glob(os.path.join(EXP_DIR, "EED-EXPER-*.hdlp")):
if os.path.basename(f) == "EED-EXPER-INDEX.hdlp":
continue
content = _read(f, 3000).lower()
hits = sum(1 for k in kws if k in content)
if hits >= min_hits:
title = ""
m = re.search(r"^# (.*)$", content, re.M)
if m:
title = m.group(1)
scored.append((hits, title, os.path.basename(f)))
scored.sort(reverse=True)
if not scored:
return ""
out = ["【📚 相关经验自动调用(已验证,可直接复用)】"]
total = 0
for hits, title, fn in scored[:3]:
seg = f"{title}"
total += len(seg) + 2
if total > limit_chars:
break
out.append(seg)
return "\n".join(out)
def save(title, content):
"""自动编号保存经验,返回文件名。"""
title = (title or "未命名经验").strip()[:60]
num = _next_num()
fn = os.path.join(EXP_DIR, "EED-EXPER-%03d.hdlp" % num)
body = (
"# EED-EXPER-%03d · %s\n\n"
"> 保存: %s\n\n"
"---\n\n%s\n" % (num, title, time.strftime("%Y-%m-%d %H:%M"), content)
)
with open(fn, "w", encoding="utf-8") as f:
f.write(body)
_update_index()
return os.path.basename(fn)
def _update_index():
items = list_experiences()
lines = [
"# EED-EXPER-INDEX.hdlp · 经验库索引(自动维护)",
"",
"> 倒序 · 最新的在上面 · 蛋蛋醒来扫一眼就知道手里有什么牌",
"",
]
for it in items:
lines.append("- %s | %s" % (it["file"], it["title"]))
try:
with open(INDEX, "w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
except Exception:
pass
if __name__ == "__main__":
import sys
if len(sys.argv) > 1 and sys.argv[1] == "list":
for it in list_experiences():
print(it["num"], it["title"])
elif len(sys.argv) > 2 and sys.argv[1] == "search":
print(search(sys.argv[2]))
else:
print("用法: experience.py list | search <关键词>")

165
scripts/memory_sync.py Normal file
View File

@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""
memory_sync.py 耳耳蛋记忆库双向自动合并
背景面板与终端共用同一个 CodeBuddy 项目库但记忆有两份落点
本地记忆库 ~/.codebuddy/projects/home-ls/memory/ 蛋蛋实际读写的
仓库副本 ~/cang-ying/memory/eed/ 推送到仓库跨机器带走的
两边都可能被写终端会话写①面板会话也写①但爸爸手动或旧流程可能只更新②
过去靠手动 cp 同步一不留神就会覆盖掉另一边刚写的经验
本脚本做双向合并
- 只在一边存在的文件 补到另一边
- 两边都有且内容不同 mtime 取新的旧版备份到 BACKUP_memory_conflict/
- 内容相同 跳过
永不静默丢数据任何被覆盖的版本都先备份
用法
python3 memory_sync.py # 执行合并
python3 memory_sync.py --dry-run # 只看会怎么做,不动文件
python3 memory_sync.py --json # 输出 JSON供面板调用
"""
import filecmp
import json
import os
import shutil
import sys
import time
LOCAL_DIR = os.path.expanduser("~/.codebuddy/projects/home-ls/memory")
REPO_DIR = os.path.expanduser("~/cang-ying/memory/eed")
CONFLICT_BACKUP = os.path.expanduser("~/.codebuddy/BACKUP_memory_conflict")
# 仓库侧独有、不该被同步回本地记忆库的文件(仓库自带说明等)
REPO_ONLY = {"README.md"}
def _md_files(d):
if not os.path.isdir(d):
return set()
return {f for f in os.listdir(d) if f.endswith(".md")}
def _backup(path, tag):
"""覆盖前先备份,返回备份路径。"""
os.makedirs(CONFLICT_BACKUP, exist_ok=True)
stamp = time.strftime("%Y%m%d_%H%M%S")
dst = os.path.join(CONFLICT_BACKUP, f"{stamp}_{tag}_{os.path.basename(path)}")
shutil.copy2(path, dst)
return dst
def sync(dry_run=False):
"""执行双向合并,返回结果字典。"""
result = {"ok": True, "copied_to_repo": [], "copied_to_local": [],
"updated_local": [], "updated_repo": [], "backups": [],
"skipped": 0, "error": ""}
if not os.path.isdir(LOCAL_DIR):
result["ok"] = False
result["error"] = f"本地记忆库不存在:{LOCAL_DIR}"
return result
if not os.path.isdir(REPO_DIR):
if dry_run:
result["error"] = f"仓库副本目录不存在(将创建):{REPO_DIR}"
else:
os.makedirs(REPO_DIR, exist_ok=True)
local = _md_files(LOCAL_DIR)
repo = _md_files(REPO_DIR)
# ① 只在本地 → 补到仓库
for name in sorted(local - repo):
if not dry_run:
shutil.copy2(os.path.join(LOCAL_DIR, name), os.path.join(REPO_DIR, name))
result["copied_to_repo"].append(name)
# ② 只在仓库 → 补到本地(跳过仓库自带说明文件)
for name in sorted(repo - local):
if name in REPO_ONLY:
continue
if not dry_run:
shutil.copy2(os.path.join(REPO_DIR, name), os.path.join(LOCAL_DIR, name))
result["copied_to_local"].append(name)
# ③ 两边都有 → 比内容,不同则按 mtime 取新,旧版先备份
for name in sorted(local & repo):
lp = os.path.join(LOCAL_DIR, name)
rp = os.path.join(REPO_DIR, name)
try:
if filecmp.cmp(lp, rp, shallow=False):
result["skipped"] += 1
continue
except OSError as e:
result["error"] += f"[{name} 比对失败: {e}] "
continue
lt, rt = os.path.getmtime(lp), os.path.getmtime(rp)
if lt >= rt:
# 本地更新 → 覆盖仓库,先备份仓库那版
if not dry_run:
result["backups"].append(_backup(rp, "repo"))
shutil.copy2(lp, rp)
result["updated_repo"].append(name)
else:
# 仓库更新 → 覆盖本地,先备份本地那版
if not dry_run:
result["backups"].append(_backup(lp, "local"))
shutil.copy2(rp, lp)
result["updated_local"].append(name)
return result
def summarize(r):
"""把结果压成一句人话,给面板/唤醒卡用。"""
if not r["ok"]:
return "⚠️ 记忆同步失败:" + r["error"]
n = (len(r["copied_to_repo"]) + len(r["copied_to_local"])
+ len(r["updated_local"]) + len(r["updated_repo"]))
if n == 0:
return f"✅ 记忆已同步({r['skipped']} 份一致,无变化)"
parts = []
if r["copied_to_repo"]:
parts.append(f"新增到仓库 {len(r['copied_to_repo'])}")
if r["copied_to_local"]:
parts.append(f"拉回本地 {len(r['copied_to_local'])}")
if r["updated_repo"]:
parts.append(f"本地较新覆盖仓库 {len(r['updated_repo'])}")
if r["updated_local"]:
parts.append(f"仓库较新覆盖本地 {len(r['updated_local'])}")
s = f"✅ 记忆已合并({' · '.join(parts)}"
if r["backups"]:
s += f",旧版已备份 {len(r['backups'])}"
return s
def main():
dry = "--dry-run" in sys.argv
as_json = "--json" in sys.argv
r = sync(dry_run=dry)
if as_json:
print(json.dumps(r, ensure_ascii=False))
return 0 if r["ok"] else 1
print(("【预演,未改动任何文件】" if dry else "【记忆双向合并】"))
print(f" 本地记忆库: {LOCAL_DIR}")
print(f" 仓库副本: {REPO_DIR}")
print()
for key, label in (("copied_to_repo", "→ 补到仓库"),
("copied_to_local", "← 拉回本地"),
("updated_repo", "→ 本地较新,覆盖仓库"),
("updated_local", "← 仓库较新,覆盖本地")):
for name in r[key]:
print(f" {label}: {name}")
if r["backups"]:
print(f"\n 旧版备份 {len(r['backups'])} 份 → {CONFLICT_BACKUP}")
print(f"\n{summarize(r)}")
if r["error"]:
print(f"⚠️ {r['error']}")
return 0 if r["ok"] else 1
if __name__ == "__main__":
sys.exit(main())

11
scripts/restart-eed.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
# 重启蛋蛋面板改造版resume 会话 + 自动压缩 + 图片视频通道)
# 修复唤醒bugpkill 加宽匹配 eed_web.py确保裸名/bash包装的残留面板实例也被杀干净
pkill -f "eed_web.py" 2>/dev/null
sleep 1
cd /home/ls/cang-ying/scripts
nohup python3 eed_web.py > /tmp/eed_web.log 2>&1 &
sleep 1
PORT=$(grep -oE 'http://127\.0\.0\.1:[0-9]+/' /tmp/eed_web.log | head -1)
echo "🟢 面板已重启: ${PORT:-自动端口(见 /tmp/eed_web.log}"
echo "刷新浏览器页面即可使用新版(会话持久化+自动压缩+发图看图)"

View File

@ -0,0 +1,66 @@
---
name: 角色档案拼版制作法
description: 用Z-Image生成各部位图 + PIL程序拼版制作专业角色档案(Character Archive)的完整流程2026-08-01 爸爸认可定版
type: prompt
---
# 角色档案拼版制作法2026-08-01 爸爸认可定版)
> 核心思想:**Z-Image只负责出干净的单张图版面/标题/标尺/文字全部用PIL代码拼**避免AI文字乱码、排版乱
## 一、整体流程
```
① Z-Image生成三视图(正/侧/背全身) + 左上半身像 + 各部位特写图
② PIL拼版画边框/标题/标尺/PROFILE字段/细节框/姿态框
③ 输出 1256x999严格按参考图尺寸
```
## 二、分区布局(严格按参考图边界)
```
顶部: y=0~60 (一行小标识,不放花哨大标题)
上部主体: y=55~660
左侧 x=36~317: 上半身像(肩膀+头) + PROFILE信息栏
右侧 x=395~1218: TURNAROUND三视图 + 左侧身高标尺(0-178cm)
底部细节: y=660~828 (行1) + y=828~999 (行2)
右下姿态: x=825~1218 (POSE REFERENCE 4姿态)
```
## 三、三视图关键规则
- **必须 contain 缩放**(等比,保持头顶+脚完整)——不要 cover 裁剪会切头切脚!
- 正面/侧面/背面三人,同一张脸同一服饰
- 标尺画在三视图左边0/30/60/90/120/150/178cm 刻度
## 四、左上半身像规则
- **肩膀+头半身像**(不是纯脸特写!爸爸强调"我要的是肩膀+头"
- 从 front 全身图裁剪 0~26% 高度(头+肩+上胸contain/cover 填满框
- 脸部占满、肩膀完整
## 五、细节特写规则8个每行4个
- **必须有效对应部位**,不能糊弄占满:
- 发丝特写黑色57%(黑发)
- 面部特写:纯脸(肤色+头发,不是全身)
- 交领特写:白色布料
- 腰带特写:黑色(黑腰带)
- 衣摆特写从front裁 y=55~88% 纯布料肤色0%
- 袖口特写从front裁领口+袖口肤色9%白色73%
- 鞋履特写
- 马尾背面:从背面图裁
- **衣摆/袖口等布料特写:直接裁剪已有全身图最可靠**AI文生图会跑偏成脸/手)
- 验证方法:程序算肤色/白色/黑色占比,肤色过高=跑偏了
## 六、PROFILE字段规则
- **最多4行**(姓名/年龄/身高/身份行距26px
- 位置必须在 660 边界内否则和DETAILS标题重叠
- 左侧列标签(灰色) + 右侧值(深色)
## 七、POSE参考4姿态横排
- 4个姿态横排负手而立/抱臂站立/侧身回望/托腮沉思
- 每张裁剪留白后 contain 缩放
- 姿态之间间距12px占满右下区
## 八、字体重叠避坑
- 所有文字区先算好Y坐标确保不超分区边界
- PROFILE字段数/行距 与 半身像高度 要协调半身像420pxPROFILE从490开始
---
*记录人:耳耳蛋 🌱 · 2026-08-01 · 爸爸认可定版v31*

View File

@ -1,3 +1,74 @@
# 🔥 干活铁律 · 动手前脑子里第一个蹦出来的东西
> **不是"参考资料"——是肌肉记忆。接到活先套公式,不是先自己编。**
---
## 一、写角色提示词 → 神级公式(社区验证最稳)
```
[角色锚定] + [场景/动作描述] + [镜头/风格控制] + [负面约束]
```
**角色锚定不是写"男主苏白",是把关键特征硬编码进去:**
> "19岁修仙少年苏白黑色高马尾半束发白色棉麻交领长袍粗布腰带+小布囊,黑色瞳孔,清瘦鹅蛋脸,剑眉杏眼"
每个镜头提示词都带这一段,这才叫"锚定"。
---
## 二、做分镜 → 七要素先列出来,少一个都不动手
```
景别 + 运镜 + 视角 + 光影 + 构图 + 人物动作 + 环境动态
```
公式:**镜头语言 + 主体 + 场景 + 光影 + 情绪**
核心认知AI漫剧每个镜头独立生成没有拍摄现场统一光线和构图 → 必须提前规划每个镜头的角度/景别/人物位置。
**运镜体系40-50种**
- 推拉类:缓慢推进、快速推近、慢拉远
- 摇移类:水平横摇、垂直摇镜
- 升降类:低机位上升、俯冲下降
- 环绕类360°环绕、半弧环绕
- 特写类:眼部大特写、手部动作特写
每个运镜 = 景别 + 方向 + 速度 + 主体
---
## 三、角色一致性崩了 → 五维框架排查,不是改提示词抽卡
| 维度 | 控制内容 |
|------|---------|
| 外貌 | 脸型/发型/五官/痣疤标记 |
| 服饰 | 分层描述:领型→纹样→配饰→鞋 |
| 动作 | 站姿/手势/走姿的固定描述 |
| 情绪 | 表情集:喜怒哀惊恐 标准化 |
| 镜头语言 | 景别/角度统一描述模板 |
**五大落地手段**:角色档案固化 → 参考图锁定 → LoRA/IPAdapter → 参数固化(seed) → 后期校准
---
## 四、场景四视图 → 俯视图先锁空间,不是一张 prompt 硬刚
1. 先生成俯视图定义布局
2. 所有视角基于同一个空间基准
3. 单图只是"固定角度的截面",模型脑补其他角度只能瞎猜
---
## 五、总则:遇事不决先搜索 → 搜到就用 → 用了才叫学会
- 接到漫剧任务 → **先套公式**,不是先自己编
- 写角色提示词 → 脑子里蹦出来的是公式,不是"嗯…大概写个帅气修仙少年…"
- 做分镜 → 先想七要素,不是"这个镜头好看就行"
- 遇到一致性崩了 → 先查五维框架哪个维度松了,不是反复改提示词抽卡
---
# 社区技巧库 · AI漫剧/角色一致性2026-08-01 网络搜集)
> 来源知乎《100天AI漫剧出海》系列、CSDN 漫剧攻略、搜狐角色一致性解析、
@ -87,3 +158,28 @@
- ✅ 场景四视图2×2 网格:前/左/右/俯视)
- ⏳ 面部特写+三视图布局16:9 游戏立绘风)— 可加
- ⏳ 资产库规范化(三视图→分镜统一引用)— 结合 skill 系统
## 八、万妖图录传风格2026-08-01 实战验证 ✅)
**风格定位**3D写实渲染 + 次时代CG + 盛唐古风 + 暗黑玄幻武侠(爸爸钦定,漫剧男主首选)
**成功关键词组合**portrait布局 v4 一次通过):
- 3D写实渲染次时代CG质感影视级渲染高精度建模细腻柔光玉润国漫风
- 五官:剑眉星目斜飞入鬓 / 深邃黑色瞳孔 / 高挺鼻梁 / 薄唇轻抿 / 下颌线清晰利落 / 面部轮廓立体
- 皮肤:真实细腻纹理(拒绝"卡通化皮肤材质"
- 发型:发丝细腻根根分明
- 服饰:布料真实褶皱自然垂落
- 氛围:盛唐古风,清冷孤傲,禁欲肃穆,光影层次丰富,暗黑玄幻武侠
**布局关键**:写实脸必须用 **portrait 布局左侧1/3面部大特写**——showcase 布局脸太小会糊/丑v1~v3 全栽在这。
**避坑**"柔和3D卡通渲染 / 非写实 / 卡通化皮肤材质" = 动画感元凶,万妖风格禁用。
## 九、苏白角色板·完整成功配方2026-08-01 爸爸钦定 ✅ 一次通过)
**最终成品**`outputs/suba_sheet_v1.png`character_sheet 四区布局 + 万妖图录传风格)
**一次性成功配方**(以后做漫剧男主角色板直接照抄):
1. **布局**:必须用 `--layout character_sheet`(标准角色板四区:上三视图 + 左特写/色板/细节 + 右全身身高标尺)——不是 portrait/showcase
2. **风格**:万妖图录传 = 3D写实渲染 + 次时代CG + 影视级渲染 + 高精度建模 + 细腻柔光 + 玉润国漫风 + 盛唐古风 + 暗黑玄幻武侠
3. **五官**:剑眉星目斜飞入鬓 / 深邃黑色瞳孔 / 眼神清冷锐利 / 高挺鼻梁 / 薄唇轻抿 / 下颌线清晰 / 皮肤细腻真实纹理
4. **身材量化**写身高178cm+ 修长挺拔
5. **服饰分层+配色**:白色棉麻交领长袍(主色)+ 内衬月白 + 玄色腰带 + 旧布囊(点缀色)——色板区需要这些
**这版踩过的坑v1→v4 全程无用功,别重犯)**
- ❌ "柔和3D卡通渲染/非写实/卡通化皮肤" → 动画感,丑
- ❌ showcase 布局 → 脸太小糊/丑
- ❌ 只有三视图没有角色板元素(色板/标尺/细节)→ 爸爸说"不是角色板"

View File

@ -109,10 +109,10 @@
"id": "community_techniques",
"name": "📚 社区技巧库",
"type": "prompt",
"desc": "AI漫剧/角色一致性/三视图/场景四视图/分镜 社区实战技巧+角色版学习笔记",
"desc": "🔥干活铁律+社区技巧库:角色公式/分镜七要素/五维框架/场景四视图法则,动手前先调",
"prompt_file": "community_techniques.md",
"prompt_file_alt": "community_techniques.md",
"usage": "注入社区技巧库,按需套用(角色锁定/三视图布局/场景四视图/分镜公式"
"usage": "注入干活铁律+社区技巧库,先套公式再动手(角色锚定/分镜七要素/一致性五维排查/场景俯视锁空间"
},
{
"id": "character_sheet",
@ -121,6 +121,51 @@
"desc": "社区开源 character-sheet-generator角色三视图设定板生成技能7风格模板+变量体系+四区布局规范)",
"prompt_file": "character-sheet-generator/SKILL.md",
"usage": "注入开源角色设定板技能,选风格模板(base/ancient/realistic/anime/modern/sci-fi/fantasy),填变量生成标准角色设定板"
},
{
"id": "manju_full_pipeline",
"name": "🏭 漫剧全流程工业化方法论",
"type": "prompt",
"desc": "五步管线+角色五维一致性+神级提示词公式+分镜七要素+运镜体系+自动化工具链2026-08-01 全网搜集)",
"prompt_file": "manju_full_pipeline.md",
"prompt_file_alt": "manju_full_pipeline.md",
"usage": "接到漫剧任务时注入:先套五步管线定位到当前环节,再套对应公式(角色提示词/分镜/运镜/一致性排查),禁止瞎猜"
},
{
"id": "manju_prompt_system",
"name": "📖 漫剧提示词全体系",
"type": "prompt",
"desc": "制作漫剧所有环节的提示词写法:全流程+分层结构+分镜格式+图生视频五要素+单人锁定+一致性+运镜+配音规范",
"prompt_file": "manju_prompt_system.md",
"prompt_file_alt": "manju_prompt_system.md",
"usage": "写漫剧提示词前注入:先套分层结构(主体→构图→光影→风格),视频用五要素(主体+动作+场景+运镜+光影),单人锁定用数量词,角色一致性用资产库"
},
{
"id": "manju_prompt_library",
"name": "🗂️ 漫剧提示词素材库",
"type": "prompt",
"desc": "可直接复制的模板库50种运镜中英文Prompt+场景)+ 8类表情物理化描述",
"prompt_file": "manju_prompt_library.md",
"prompt_file_alt": "manju_prompt_library.md",
"usage": "需要运镜/表情提示词时直接查运镜50种按场景选表情按情绪复制面部物理描述"
},
{
"id": "manju_workflow_prompts",
"name": "🧭 漫剧工作流实战提示词",
"type": "prompt",
"desc": "完整可复制:选题钩子/角色卡/六镜头分镜/图像视频分工/配音字幕剪辑/封面复盘",
"prompt_file": "manju_workflow_prompts.md",
"prompt_file_alt": "manju_workflow_prompts.md",
"usage": "做漫剧全流程时按章节调用:选题→角色卡→六镜头分镜→图像提示词→图生视频→配音字幕→封面"
},
{
"id": "character_archive_pil",
"name": "📋 角色档案拼版制作法",
"type": "prompt",
"desc": "Z-Image出图+PIL拼版做专业角色档案三视图contain/上半身像/8细节特写/PROFILE/4姿态爸爸认可定版",
"prompt_file": "character_archive_pil.md",
"prompt_file_alt": "character_archive_pil.md",
"usage": "做角色档案时Z-Image生成三视图+部位特写 → PIL拼版1256x999三视图contain不裁切细节特写要验证有效"
}
]
}

View File

@ -0,0 +1,106 @@
---
name: 漫剧全流程工业化方法论
description: AI漫剧从剧本到成片的完整工业化方法论五步管线+角色五维一致性+神级提示词公式+分镜七要素+运镜体系+自动化工具链。2026-08-01 社区全网搜集CSDN/知乎/掘金/腾讯云/头条/B站
type: prompt
---
# AI漫剧全流程工业化方法论2026-08-01 社区全网搜集)
> 来源CSDN 工业化指南×4、知乎漫剧提示词/运镜×3、掘金工具链×2、腾讯云管线×1、头条分镜提示词大全×2、搜狐神级提示词框架×1、Dify+ComfyUI 生产线×3、Anthropic Skills 仓库解析等 20+ 篇
## 一、五步工业化管线(社区权威版)
```
剧本生成 → 视觉素材制作(角色/场景资产) → 动态化生成(图生视频) → 音频合成(配音/BGM) → 后期精修
```
**核心认知**:资产库前置是最容易被跳过但最关键的一步——先定妆存资产,后续所有分镜统一引用,而不是每镜临时生成。
## 二、角色一致性:五维控制 + 五大手段2026 社区主流)
**五维框架**(排查崩坏用这张表):
| 维度 | 控制内容 |
|------|---------|
| 外貌 | 脸型/发型/五官/痣疤标记 |
| 服饰 | 分层描述:领型→纹样→配饰→鞋 |
| 动作 | 站姿/手势/走姿固定描述 |
| 情绪 | 表情集:喜怒哀惊恐标准化 |
| 镜头语言 | 景别/角度统一描述模板 |
**五大落地手段**:角色档案固化 → 参考图锁定 → LoRA/IPAdapter → 参数固化(seed) → 后期校准
**三步锁定角色卡**(简易版):描述固化 + ID嵌入 + 动作锚定
**角色设定说明书**(比三视图更深):
- 基础信息:性别/年龄/身材量化8头身/172cm
- 外貌特征:脸型/眉眼/发型/发色
- 服饰分层:领+纹+饰+鞋
- 表情集 + 动作集
- 每镜头复用的固定提示词锚段
## 三、神级提示词公式(社区验证最稳)
```
[角色锚定] + [场景/动作描述] + [镜头/风格控制] + [负面约束]
```
**角色锚定示例**(不是"男主苏白",是硬编码特征):
> "19岁修仙少年黑色高马尾半束发白色棉麻交领长袍粗布腰带+小布囊,黑色瞳孔,清瘦鹅蛋脸,剑眉杏眼"
每个镜头提示词都带这段 = 锚定。
## 四、分镜提示词七要素
```
景别 + 运镜 + 视角 + 光影 + 构图 + 人物动作 + 环境动态
```
公式:**镜头语言 + 主体 + 场景 + 光影 + 情绪**
**核心认知**AI漫剧每镜头独立生成无拍摄现场统一光线/构图 → 必须提前规划每镜角度/景别/人物位置,确保拼起来是连续叙事。
## 五、运镜体系40-50种可按需取用
- **推拉类**:缓慢推进 / 快速推近 / 慢拉远
- **摇移类**:水平横摇 / 垂直摇镜
- **升降类**:低机位上升 / 俯冲下降(适合开场)
- **环绕类**360°环绕 / 半弧环绕
- **特写类**:眼部大特写 / 手部动作特写
**每个运镜提示词 = 景别 + 方向 + 速度 + 主体**。一个镜头只放一种运镜,别贪多(新手高频错误)。
## 六、自动化工具链2026 最火方案)
```
Dify大脑: 剧本生成 → 自动拆分镜 → 生成提示词 → Agent协作
ComfyUI双手: 图像生成 → 角色一致性(IPAdapter+ControlNet) → 动态视频
```
本地 Qwen 也可替代 DifyQwen 写剧本+拆分镜+生成提示词ComfyUI 负责图生+一致性+视频。零成本、无限生成、一致性最强。
**开源全本地 8G 方案**:剧本(LM Studio/Qwen) + 分镜(FLUX+漫画LoRA) + 图生视频(ComfyUI) + 配音(Edge-TTS) + 剪辑(FFmpeg/剪映)。
## 七、Anthropic Skills 标准(行业规范)
- Anthropic 官方开源 Agent Skills 仓库138k stars`SKILL.md` 已成行业标准
- 咱的 character-sheet-generator 用的就是这格式 ✅
- 参考仓库anthropics/skills、chenxing3060/character-sheet-generator、morsoli/aimangastudio端到端漫画流水线
## 八、剧本创作三原则AI 漫剧)
1. 强冲突、快节奏(漫剧黄金 3 秒)
2. 角色要写清楚,别只说"帮我写个故事"——给角色/世界观/爽点
3. 机器可读的结构化脚本格式AI 初稿对话生硬,需人工优化)
## 九、落地清单(咱苍耳对照)
- ✅ 剧本→分镜→资产→出图→LTX/Wan→拼接→声画装配管线已通
- ✅ 角色三视图/四视图/设定板技能
- ✅ 场景四视图技能
- ⏳ 角色"五维一致性档案"规范化(锚段固化到每镜提示词)
- ⏳ 运镜提示词库内置到分镜生成
- ⏳ Dify/本地Qwen 自动拆分镜接入
---
*记录人:耳耳蛋 🌱 · 2026-08-01 · 社区全网搜集沉淀*

View File

@ -0,0 +1,147 @@
---
name: 漫剧提示词素材库
description: 可直接复制的漫剧提示词模板库50种运镜中英文Prompt+场景)+ 8类表情物理化描述。2026-08-01 全网抓取沉淀
type: prompt
---
# 漫剧提示词素材库可直接复制2026-08-01
> 来源搜狐50个运镜全篇抓取 + 教程社80表情全篇抓取原文模板整理
## 一、50种运镜模板中英文 Prompt + 适用场景)
### 基础运镜篇(控制画面构图变化)
1. **缓慢推镜头**`Slow zoom in, cinematic lighting` — 镜头缓慢靠近主体,强调情绪变化/发现细节/制造紧张
2. **快速推镜头**`Fast zoom in, intense atmosphere` — 迅速推进,表现震惊/恐惧/戏剧转折
3. **缓慢拉镜头**`Slow zoom out, revealing environment` — 镜头后退,揭示孤独环境/宏大背景
4. **水平左摇**`Camera pans left, continuous shot` — 机位不动镜头左转,跟从右向左移动物体/展示全景
5. **水平右摇**`Camera pans right, following subject` — 跟从左向右物体/场景转移
6. **垂直上摇**`Tilt up, revealing height` — 脚部扫视到头部,展示高楼/大树高度
7. **垂直下摇**`Tilt down, from sky to ground` — 从天空回地面,展示脚下线索
8. **左平移**`Truck left, parallax effect` — 整机左移,跟随行走产生背景视差
9. **右平移**`Truck right, smooth motion` — 展示排列物体/行进队伍
10. **升镜头**`Camera moves up vertically, pedestal up` — 展示全貌/电梯上升视角
11. **降镜头**`Camera moves down vertically, pedestal down` — 高空降落到视平线,落地感
12. **固定镜头**`Static camera, subtle movement in background` — 只有主体动,表现宁静/对话/观察
### 进阶电影感篇(增加画面张力)
13. **希区柯克变焦**`Dolly zoom, vertigo effect, background warping` — 主体不变背景压缩拉伸,表现震惊/混乱/心理崩溃
14. **环绕拍摄**`360 degree arc shot around the character` — 360°旋转英雄时刻/浪漫/孤独无助
15. **跟拍**`Tracking shot, following the character from behind` — 代入感极强
16. **摇臂镜头**`Crane shot, sweeping over the city` — 大幅升降移动,战争/城市全景/人群
17. **手持运镜**`Handheld camera style, shaky footage, documentary feel` — 轻微晃动,纪实感/紧张混乱
18. **急摇**`Whip pan transition, motion blur` — 快速甩镜,快速转场
19. **极速俯冲**`Fast camera dive from clouds to ground` — 高空垂直冲地,开场冲击
20. **低空飞行**`Low angle flyover, fast speed over water` — 贴地/水面快速飞行,速度感
21. **子弹时间**`Bullet time effect, frozen time, camera moves around subject` — 时间静止镜头环绕,强调动作瞬间
22. **穿梭运镜**`Fly through the window, drone shot` — 穿窗/洞,连接内外空间
### 特殊视角与构图篇(叙事角度)
23. **第一人称视角**`POV shot, seeing through eyes, hands visible` — 沉浸式
24. **过肩镜头**`Over the shoulder shot, looking at person B` — 对话场景交代关系
25. **上帝视角**`Top down view, strictly perpendicular to ground` — 垂直俯视,布局/迷宫/人群阵型
26. **虫视视角**`Extreme low angle, worm's eye view, looking up at giant` — 极低角度,主体高大压迫
27. **荷兰角**`Dutch angle, tilted frame, uneasy atmosphere` — 画面倾斜,不安/疯狂/失衡
28. **广角镜头**`Ultra wide angle lens, fish eye distortion` — 宽阔视野边缘畸变
29. **长焦压缩**`Telephoto lens, compressed background, flat depth` — 压缩前景背景距离
30. **微距镜头**`Macro photography, extreme close up details` — 瞳孔/水滴/材质
31. **剪影**`Silhouette shot, backlit, strong contrast` — 背光全黑轮廓,神秘感
32. **两分镜头**`Split screen, two different scenes` — 双地点同时行动
### 光影与动态特效篇AI特有风格
33. **移焦**`Rack focus, focus shift from foreground to background` — 引导视线转移
34. **景深虚化**`Shallow depth of field, blurry background, bokeh lights` — 突出主体唯美
35. **延时摄影**`Time-lapse, clouds moving fast, day to night` — 日夜交替/花开花落
36. **慢动作**`Super slow motion, high frame rate, water droplets` — 强调动作细节
37. **倒放**`Reverse motion, broken glass fixing itself` — 时间倒流超现实
38. **变形**`Morphing from a cat to a tiger` — 平滑变形,魔法变身
39. **FPV穿越机**`FPV drone shot, fast speed, acrobatic flight` — 极速翻滚钻洞,极限运动/追逐
40. **水下摄影**`Underwater camera, light rays through water, bubbles` — 水下视角
41. **故障艺术**`Glitch art style, digital distortion, datamosh` — 赛博朋克/黑客
42. **红外热成像**`Thermal imaging camera view, predator vision` — 军事/异星
43. **夜视仪**`Green night vision camera footage, grainy` — 潜入/恐怖/战术
44. **监控视角**`CCTV security camera footage, high angle, timestamp` — 审讯/罪案
45. **如画运镜**`Slow cinematic pan revealing a majestic landscape` — 史诗大自然空镜
46. **跟随背影**`Following character from back, medium distance, steady` — 第三人称游戏感
47. **侧面特写跟随**`Side profile tracking shot, close up on face` — 沉思/决绝/赶路
48. **旋转升空**`Spiraling up camera movement, drone view` — 螺旋上升展示全景
49. **穿墙透视**`Camera passing through the wall into the next room` — AI特有无缝转场
50. **定格动画感**`Stop motion animation style, claymation texture` — 手作质感
### 运镜黄金法则
- 一个镜头只讲一件事,**做减法**(新手高频错误:塞太多运镜)
- 运镜词+环境词融合出氛围(如:拉镜头+空旷环境=孤单感)
- 仰拍推镜贴地 = 反派/大男主登场压迫感
- 升降/俯冲 = 开场视觉冲击
## 二、表情提示词库(把情绪拆成面部物理动作,直接复制)
### 1. 快乐 😊
嘴角对称上扬,颧大肌轻微提拉,苹果肌隆起;下眼睑收紧形成卧蚕,眼角细纹;鼻翼两侧笑纹浅现;眼神明亮有光,头略向一侧倾斜
### 2. 悲伤 😢
眉头内侧向上聚拢呈"八"字;上眼睑无力下垂遮挡部分瞳孔;视线向下失落游离;嘴角无意识下撇,下唇中央微前推;下巴收紧出现核桃纹;双肩内收前倾
### 3. 愤怒 😠
双眉紧皱下压,眉间挤出竖纹;上眼睑提肌收紧,眼白露出增多;视线锐利锁定前方;鼻孔扩张;嘴唇用力抿成一条线下拉,嘴角紧绷;下巴前推颈部肌肉拉紧
### 4. 恐惧 😨
眉毛抬高向中心靠拢;上眼睑极力上提露出上方眼白;瞳孔放大;嘴唇水平向两侧拉开下唇紧张下沉;下颏后缩;双肩本能靠起护住脖颈;头轻微后仰
### 5. 惊讶 😲
双眉高抬呈弓形上眼睑大幅提升眼睛瞪圆瞳孔瞬间缩小或放大嘴巴不自觉张开呈O形下颌自然下坠双肩轻微上提并凝固全身静止约半拍
### 6. 厌恶 🤢
鼻子用力皱起鼻翼上翻;上唇提肌收缩;眉头下压鼻根挤出横纹;下眼睑收紧眯眼;双肩内收身体后带,头侧开微后仰像避开异味
### 7. 爱/深情 😍
眉尾放平或轻微上扬;上眼睑放松眼裂略窄;瞳孔自然扩大眼神柔软胶着;嘴角对称向上浮起若有若无微笑;头微偏颈部线条松弛;呼吸深沉缓慢伴随轻微叹息
### 8. 渴望 🤩
眉毛轻微抬高眉尾收紧;上眼睑略垂下眼睑收紧,朦胧向往的"柔焦"眼神;瞳孔略微扩大;嘴唇微张下唇含水光;下颌轻微上扬脖颈拉长;指尖似抬未抬
### 表情写作口诀
- **表情 = 眉 + 眼 + 鼻 + 嘴 + 肩/头** 五部件物理描述,缺一不生动
- 眼神最重要:瞳孔大小/视线方向/眼睑状态
- 加身体语言(肩/头/呼吸)更真实
---
*记录人:耳耳蛋 🌱 · 2026-08-01 · 全篇模板抓取沉淀*
---
# 补充·核心技巧库2026-08-01 第三轮:搜狐技巧全篇 + JR实战合集
## 三、图像万能公式(搜狐验证)
```
景别 + 绑定基准角色 + 人物动作表情 + 场景环境 + 光影效果 + 固定风格词 + 画面比例
```
**示例**
> 中景1 single man苏白角色卡剑眉星目面无表情缓缓抬手宗门山门石阶冷色调左上侧暖色柔光3D国漫CG风格竖屏9:16
## 四、必背英文固定词库
**一致性词**`same character, consistent design, consistent outfit`
**镜头词**close-up/medium shot/wide shot/extreme long shotfrom above/below/side view/three-quarter viewcinematic composition/dynamic angle/rule of thirds/dutch angle
**光影词**soft lighting/dramatic lighting/backlight/rim light
**色彩词**pastel colors(浅柔)/muted colors(低饱和)/vibrant colors(高饱和)——全剧统一选一种
**质量词**masterpiece, best quality, ultra-detailed, 8k, clean line art, sharp focus
**负面词**lowres, bad anatomy, bad hands, missing fingers, blurry, watermark, sketch, multiple characters
## 五、图生视频动态提示词(防崩核心)
**安全动作词**(避免大动作崩坏):
- `subtle movement`(细微动作)
- `slow motion`(慢动作)
- `hair floating`(头发飘动)
- 缓慢转头、眼皮轻眨
**运镜词**`cinematic motion`电影感运镜、Dolly In缓慢推镜、Pan Left轻微横移
**公式**:运镜 + 行为 + 情绪(精简,聚焦三点即可)
## 六、控变量法(配角生成)
- 配角/次要角色:**严格保持底层参数一致**(基础模型/画风/光影),只改发型/瞳色/服饰
- 保证全剧色调统一不拼贴
## 七、分镜编号管理
- 每张分镜图编序号 01/02/03方便后期按顺序拼接
---
*第三轮补充:图像万能公式/英文固定词库/动态提示词/控变量法/编号管理*

View File

@ -0,0 +1,154 @@
---
name: 漫剧提示词全体系
description: 制作漫剧所有环节的提示词写法:全流程+分层结构+分镜格式+图生视频五要素+单人锁定+一致性+运镜+配音规范。2026-08-01 系统性学习沉淀
type: prompt
---
# 漫剧提示词全体系2026-08-01 系统性学习)
> 来源博客园全流程提示词、CSDN分镜/三视图/图生视频、知乎运镜/一致性、万相官方公式、灵绘五要素、AniKuku/Kling官方等 30+ 篇系统学习
## 一、漫剧制作全流程(提示词驱动)
```
选文 → 改文(解说文案) → 分段 → 角色/场景/道具资产推理 → 分镜描述词 → 分镜视频创作 → 配音/音效 → 剪辑
```
- 解说文案字数1分钟视频 ≈ 600-700字
- 一集2分钟 ≈ 40-60个镜头每镜头1-2秒
- 分段作用:按剧情/人物/场景关系分段,便于资产推理和分镜生成
## 二、提示词分层结构(社区验证,模型解析优先级)
```
主体(谁/什么,在哪) → 构图 → 光影 → 质感/风格
```
**主体必须具体、可视觉化、无歧义**
- ❌ "一位优雅的女性"(抽象,模型无法还原)
- ✅ "1 woman, 30s, East Asian, wearing ivory silk blouse and high-waisted linen trousers"
**避免堆砌形容词**——AI 无法识别模糊词,乱堆形容词反而干扰。
## 三、分镜格式(每镜头必备字段)
```
序号 | 场景 | 景别 | 时长 | 运镜方式 | 画面内容 | 台词/旁白 | 音效
```
例:`1|巷子口|远景|5秒|固定|男主左手打伞右手攥信|旁白:他终于鼓起勇气|雨声+心跳声`
- 景别:大远景/远景/全景/中景/近景/特写
- 运镜:推/拉/摇/移/跟/升降
- 每个分镜格 = 告诉后续AI"这里要一张图、一段配音、一个转场音效"
## 四、图生视频/文生视频提示词(五要素公式)
```
【主体描述】+【动作】+【场景】+【运镜】+【光影/风格】
```
- 视频是时间轴叙事:不只描述瞬间,要写**时间线上的变化**
- 运镜:一个镜头只放一种运镜(新手高频错误:贪多)
- 光影选2个词即可如"黄昏暖光+柔和侧光"
- 顺序:主体放最前
## 五、单人锁定(防多头/多人,刚踩的坑!)
**正向词硬锁定**
- `1 single man, ONLY ONE person in the frame`
- 数量词"1"直接锁单人(社区验证最有效)
**负面词必加**
- `multiple people, extra person, second face, two heads, extra head, extra limbs`
- `characters, people, crowd, group`
## 六、角色一致性(漫剧第一大痛点)
1. **资产库前置**:定妆照/三视图/角色板 → 存为资产 → 所有分镜统一引用
2. **固定角色参数**:外貌/发型/服饰/身高/风格 → 写成固定模板,每次生成精准输入
3. **提示词锁定**:角色特征写死(发型/发色/发饰/服装/配饰/鞋)插入每个镜头
4. **统一风格色调**:滤镜/色调/画风全局一致
5. **后期核对**:逐帧检查形象一致性,不一致重新生成
## 七、运镜体系18-50种
- 推拉类:缓慢推进/快速推近/慢拉远
- 摇移类:水平横摇/垂直摇镜
- 升降类:低机位上升/俯冲下降(开场利器)
- 环绕类360°环绕/半弧环绕
- 特写类:眼部大特写/手部特写
- 每运镜 = 景别 + 方向 + 速度 + 主体动作
- 用明确术语("slow dolly-in")而非模糊("镜头动一下"
## 八、配音/旁白规范
- 旁白解说 + 人物对话两种形式
- 旁白 ≤ 12字/句(字幕读不完)
- 留停顿感(配音气口)
- 分镜台词字段直接驱动配音
## 九、AI漫剧提示词避坑
1. 不堆形容词(干扰识别)
2. 一个镜头一个运镜
3. 主体放最前,用数量锁定
4. 视频提示词要写动态变化,不是静态描述
5. 角色一致性靠"资产库+参数固定+提示词锁定"组合拳
---
*记录人:耳耳蛋 🌱 · 2026-08-01 · 系统性学习沉淀*
---
# 补充·实战细节库第二轮系统性学习2026-08-01
## 十、运镜参数化写法即梦4.0/Seedance实战拆解
**推拉类**
- 高机位后拉:`机位3m,轨道后拉1.0m/s,焦段20mm,俯瞰全局`
- 低机位前推:`机位0.3m,轨道前推0.8m/s,焦段35mm,强化视觉压迫感`
- 急速推镜:`电动滑轨前推,速度3.5m/s,焦段85mm,焦点切至主体核心`
- 特写切远景快拉:`从特写快速拉至远景,速度4.5m/s,焦段16mm,强化空间反差`
**环绕类**
- 360°环绕`镜头以主角为中心360度环绕,缓慢旋转,人物静止,背景视差变化,光影流动`(适用:觉醒/变身/高光)
- 180°半环绕`镜头围绕角色180度半圆轨迹,从侧面绕至正面,人物保持清晰焦点`(适用:对峙/情感戏)
**摇镜**`机位固定,水平左摇90°,速度10°/s,画面水平无倾斜`
**升降**`低机位仰拍-75°旋摇20°/s` / `高机位俯拍75°旋摇20°/s`
**关键**:运镜 = 机位 + 方向 + 速度 + 焦段 + 主体状态;一个镜头一种运镜
## 十一、表情提示词核心规律(把情绪词拆成面部物理动作)
**核心原则**:模糊情绪词 AI 画不出 → 拆解为**面部肌肉/五官的物理动作**
- ❌ "开心"(抽象)
- ✅ "嘴角对称上扬至颧骨苹果肌隆起下眼睑收紧形成卧蚕眼角出现细纹露6-8颗牙"
**分类速查**
- 快乐:嘴角上扬+苹果肌隆起+卧蚕+眼角细纹
- 悲伤:嘴角下垂+眉头紧蹙+眼中含泪光+眼眶泛红
- 愤怒:眉头下压+眼睛瞪大+鼻翼扩张+嘴唇紧抿颤抖
- 惊恐:眼睛圆睁+瞳孔放大+眉毛上扬+嘴巴微张
- 轻蔑:单侧嘴角上扬+眼神睥睨+下巴微抬
**口诀**:表情 = 眉 + 眼 + 鼻 + 嘴 四部件物理描述,缺一不生动
## 十二、积木式万能公式(塔猴验证)
```
【制式】+【画风】+【人物】+【场景】+【镜头】
```
- 制式:分镜/立绘/三视图/定妆照/海报
- 画风3D国漫CG/写实/水墨/赛博/古风仙侠...
- 人物:主体描述(数量+特征锁定)
- 场景:环境+氛围+光影
- 镜头:景别+运镜+视角
**7大题材高频词**:甜宠/古风/悬疑/玄幻/都市/科幻/复仇(每题材有专属情绪+场景词库)
## 十三、场景氛围提示词60种拆5维度
把"电影感"拆成**光影+天气+时间+空间+情绪**五维:
- 光影:黄昏暖光/冷月清辉/逆光剪影/丁达尔光束
- 天气:细雨绵绵/鹅毛大雪/浓雾弥漫/狂风沙尘
- 时间:破晓/正午/黄昏/深夜
- 空间:空旷/拥挤/纵深/封闭
- 情绪:肃杀/温馨/诡异/压抑/壮阔
例:"曲绫江渡口,鹅毛大雪纷飞,枯枝灰石肃杀背景,冷灰天光侧上方照下,阴影深深刻在脸上"
## 十四、剧本万能公式(高完播率)
**单集1-2分钟**,节奏极快、反转密集、视觉化要求高
- 黄金3秒钩子开头冲突/悬念/爽点前置)
- 强冲突 + 快节奏 + 密集反转
- 结尾留钩子(下集预告式)
- 旁白解说 + 人物对话双轨推进
- 关键:分镜思维(每句台词可配画面)
## 十五、通用避坑清单(三轮学习汇总)
1. ❌ 堆形容词 → ✅ 具体可视觉化
2. ❌ 一个镜头多种运镜 → ✅ 一镜一运镜
3. ❌ 抽象情绪词 → ✅ 面部物理动作拆解
4. ❌ 模糊"电影感" → ✅ 光影+天气+时间+空间+情绪五维拆解
5. ❌ 多人物 → ✅ 数量词锁定 ONLY ONE
6. ❌ 角色跨镜变脸 → ✅ 资产库+参数固定+提示词锁定+垫图
7. ❌ 视频当静态图写 → ✅ 写时间轴上的动态变化
8. ❌ 每镜头独立生成 → ✅ 提前规划角度/景别/位置保证连续性
---
*第二轮补充:运镜参数/表情物理化/积木公式/场景五维/剧本公式/避坑清单*

View File

@ -0,0 +1,84 @@
---
name: 漫剧工作流实战提示词
description: 完整可复制的漫剧工作流提示词:账号定位/选题钩子/角色卡/六镜头分镜/图生视频/配音字幕剪辑/封面评论商单复盘。2026-08-01 抓取沉淀
type: prompt
---
# 漫剧工作流实战提示词完整可复制2026-08-01
> 来源JR Academy《AI漫剧Prompt提示词合集2026》全篇抓取
## 核心原则(先看这条)
- 把 `[方括号]` 换成你的题材/平台/角色/时长,**每次只改一个变量**
- 提示词最怕:字段没换(全是泛泛剧情)、要求太满(一镜里又哭又跑又爆炸)
- 每条先填8字段平台/题材/主角/冲突/时长/画风/限制/输出格式
## 一、选题和爽文钩子
**通用选题生成器**
> 请基于[平台]用户偏好给我30个AI漫剧选题。题材[题材],观众[人群]。每个输出一句话剧情、前三秒冲突、主角人设、反派人设、第一集结尾悬念、可变现承接。不要使用知名IP、真人明星、血腥低俗和承诺收益内容。
**3秒钩子模板**
> 请为下面选题写20个短视频开头。要求25字以内第一句就是冲突不要解释背景适合竖屏AI漫剧口语化每个开头标注情绪愤怒/好奇/心疼/爽感/反转。
**女频复仇开头**22字以内第一句必须有"她发现/她听见/她被迫/她终于知道",不要写"多年以前"
**男频逆袭开头**:先给羞辱,再暗示反转,不要血腥暴力
**职场打脸开头**:场景[会议室/复盘],冲突[抢功/甩锅],要"证据反击"镜头
## 二、角色卡和一致性
**主角角色卡**
> 请为AI漫剧主角[名字]写角色卡。题材[题材],画风[画风]。输出:年龄、身份、脸型、发型、发色、眼睛、服装、标志物、性格、口头禅、常见表情、禁止变化、正面提示词、半身提示词、近景提示词。要求后续每个镜头都能复用,不要撞脸真实人物。
**一致性检查**
> 请检查下面6个镜头提示词是否会导致角色不一致。重点看发型、衣服、年龄、配饰、脸型、场景。输出风险点、修改建议、统一后的角色描述。
**表情包**生成8个表情镜头震惊/冷笑/委屈/忍住眼泪/突然抬眼/假装平静/发现真相/准备反击),保持同一张脸同一发型同一服装,只改变表情和微动作
**核心铁律**:一个角色最多留 **3个视觉锚点**(发型/服装/配饰)。锚点太多画面乱,太少角色变脸。
## 三、六镜头分镜法(新手必学)
**先做6镜头20-45秒别一上来做3分钟**
| 镜头 | 功能 | 例子 | 别做什么 |
|------|------|------|---------|
| 1 | 冲突 | 女主被当众取消资格 | 讲背景 |
| 2 | 羞辱/误会 | 反派拿出伪造证据 | 台词太长 |
| 3 | 反应 | 女主沉默看向监控 | 表情乱跳 |
| 4 | 反击 | 录音开始播放 | 动作太复杂 |
| 5 | 爽点 | 全场安静反派变脸 | 不给观众反馈 |
| 6 | 悬念 | 门外又来一个人 | 彻底讲完 |
**45秒节奏**镜头1用3秒给冲突镜头2-4推进误会证据镜头5给反击爽点镜头6留悬念台词每句≤16字
## 四、图像 vs 视频提示词分工(关键)
**图像提示词管"长什么样",视频提示词管"怎么动",不要混成一大坨!**
**关键帧图像提示词**
> [角色卡简版][场景][镜头景别][情绪][动作]国漫短剧风格竖屏9:16清晰线条电影感光影人物面部稳定服装和发型保持一致负面不要文字水印不要真实名人脸不要多余手指。
**图生视频运动提示词**
> 镜头从[景别]缓慢推进到[景别],主角[动作],表情从[情绪A]变成[情绪B],背景轻微动态,头发和衣角轻微摆动,动作自然,不要换脸,不要换衣服,不要新增人物,不要大幅旋转镜头。
**首尾帧**保持同一角色服装发型场景动作只做一个不要增加道具切换视角时长5秒
**题材场景速查**
- 职场打脸:会议室/投影幕/玻璃墙/同事围观;动作简单(起身/递文件/播放录音)
- 校园逆袭:教室/走廊/公告栏/操场;别一次塞太多学生
- 豪门复仇:客厅/宴会厅/长桌/落地窗;重点是表情和站位
- 玄幻爽点:擂台/山门/法阵/长袍;控制特效不要每帧爆光
## 五、配音/字幕/剪辑
**配音台词**每句≤18字情绪放括号里强冲突句单独一行不要书面腔
**字幕切分**每行8-14字重要词单独成行每2-3秒一个信息点
**剪辑节奏**前3秒节奏快中段留1次情绪停顿结尾2秒给悬念
**字幕铁律**:字幕是节奏工具不是逐字稿,别整句全贴
## 六、封面/标题/评论区
- 标题20字以内强冲突标注情绪类型
- 封面大字8字以内突出冲突或反转
- 评论区置顶5条+回复10条引导讨论下一集
## 七、复盘
**复盘Prompt**输出最值得继续的题材、最差的开头类型、下周7条计划、每条只改一个变量
---
*记录人:耳耳蛋 🌱 · 2026-08-01 · 实战工作流模板全篇沉淀*

89
skill/work_triggers.md Normal file
View File

@ -0,0 +1,89 @@
# 干活触发卡 · 接到任务时脑子第一个蹦出来的公式
> **铁律**:这些不是"可以参考的资料",是干活前脑子必须自动蹦出来的东西。
> 接到任务 → 先套公式 → 再动手。不自己瞎编。
---
## 场景一:写角色提示词
**脑子里蹦出来**`[角色锚定] + [场景/动作] + [镜头/风格] + [负面约束]`
- **角色锚定**不是写"男主苏白",是把角色关键特征**硬编码进去**
> "19岁修仙少年苏白黑色高马尾半束发白色棉麻交领长袍粗布腰带+小布囊,黑色瞳孔,清瘦鹅蛋脸,剑眉杏眼"
- 每个镜头提示词都带这一段,这才叫"锚定"。
- 身材量化8头身/172cm不用"高挑""可爱"等模糊词。
- 服饰分层:领型 → 纹样 → 配饰,三个层次。
---
## 场景二:做分镜
**脑子里蹦出来**:七要素,少一个都不动手。
| # | 要素 | 问自己 |
|---|------|--------|
| 1 | 景别 | 远景/全景/中景/近景/特写? |
| 2 | 运镜 | 推/拉/摇/移/升/降/环绕?方向+速度 |
| 3 | 视角 | 平视/俯视/仰视/主观? |
| 4 | 光影 | 光源方向?色温?阴影软硬? |
| 5 | 构图 | 三分法/对称/引导线?主体位置? |
| 6 | 人物动作 | 谁?做什么?表情? |
| 7 | 环境动态 | 风/雨/尘/光斑?背景在动吗? |
**公式**`镜头语言 + 主体 + 场景 + 光影 + 情绪`
核心认知AI漫剧每个镜头独立生成没有拍摄现场统一光线和构图 → 必须提前规划每个镜头的角度/景别/人物位置。
---
## 场景三:角色一致性崩了
**脑子里蹦出来**:五维框架排查,不是改提示词抽卡!
| 维度 | 排查问题 |
|------|---------|
| 外貌 | 脸型/发型/五官/痣疤标记 变了吗? |
| 服饰 | 领型→纹样→配饰→鞋 哪个层次松了? |
| 动作 | 站姿/手势/走姿描述一致吗? |
| 情绪 | 表情集标准化了吗? |
| 镜头语言 | 景别/角度模板统一吗? |
**五大落地手段**(由易到难):
1. 角色档案固化(提示词模板)
2. 参考图锁定
3. LoRA / IPAdapter
4. 参数固化seed
5. 后期校准
---
## 场景四:场景四视图
**脑子里蹦出来**:俯视图先锁空间,不是一张 prompt 硬刚。
- 单张场景图只是"固定角度+光线+景别的截面",模型脑补其他角度只能瞎猜。
- 正确顺序:① 俯视图定义布局 → ② 所有视角基于同一个空间基准 → ③ 宽幅全景 → ④ 切段。
- Z-Image 无参考图机制,走**宽幅全景→切段**路线最稳。
---
## 场景五:遇到不会的 / 没把握的
**脑子里蹦出来**:先搜索,再动手,不瞎猜。
- 写提示词前 → 搜社区有没有现成模板
- 遇到做不到的效果 → 搜是不是真的做不到,还是方法不对
- 搜到的东西 → 用了才叫学会,存进文件不算学会
---
## 核心原则
```
接到任务 → 套公式 → 动手
↑ |
└── 公式不对就搜,搜到更新公式 ──┘
```
**不是"可以参考"——是"第一反应"。**

View File

@ -0,0 +1,129 @@
# IMAGE-FIRST-GUIDE.hdlp · 本地漫剧生产规范(苍耳经验固化版)
> HLDP://video-ai-system/IMAGE-FIRST-GUIDE.hdlp
> 耳耳蛋 · ICE-GL-耳耳蛋 · D205 · 2026-08-03
> 经验来源:苍耳爸爸实战方法论(旧经验固化,见下方关联)
> 配套LOCAL-PIPELINE-V1.hdlp流程 · BC-006-IMAGE-FIRST-PRODUCTION.hdlp协议
## §0 · 来源关联(本文件 = 旧经验的落地补充,非重新发明)
```
已固化的旧经验(苍耳已教 · 蛋蛋已记):
├── D176-LEARN.hdlp 分镜prompt模板·单镜单动作单运镜·景别时长标准
├── D177-I2I-CONSISTENCY 图生图锁一致性参考图image参数
├── EED-EXPER-013.hdlp 画风一致性踩坑:纯文字会漂·必须参考图锁风格
└── EED-EXPER-009~012 刷图/批量/质检流程
本文件新增(落地细节,旧经验未覆盖):
├── 黑白草图 → ControlNet(union) + IP-Adapter 的节点级工作流§2
├── 色卡/群像/前中后景/声音 的完整生视频要素表§3
└── 中英文加权提示词实例§5
铁律§1= 苍耳三原则,与旧经验一致,重申不改。
```
---
## §1 · 铁律(苍耳三原则)
```
① 资产非常重要,保证质量
→ 资产图 = 全片的地基。角色/场景/道具先精修到能用,再谈分镜。
→ 一致性靠: 角色 LoRAsubai_lora_v1 已验证)+ 资产图库复用 + 文字词条锁
② 提示词规范、有逻辑性、主次关系
→ 顺序 = 主体 → 动作 → 环境 → 光影 → 风格 → 质量修饰(主到次)
→ 主词在前,修饰在后;禁止杂糅无主次的堆砌
③ 提示词一定要详细,专业术语中英文描述并加重权重
→ 中文表意 + 英文专业词 + (词:权重) 加权
→ 例: 特写 close-up, 电影侧光 cinematic rim light, 铁锈质感 rust texture
→ (masterpiece:1.2) (cinematic lighting:1.3) 等
```
## §2 · 黑白草图生分镜(问题①答案)
```
原理: 草图管「构图/布局/空间」,提示词管「内容/风格/光影/色调」
ComfyUI 用 ControlNet 吃草图结构 + IP-Adapter 锁风格 → 出分镜彩图
工作流ComfyUI 节点链):
① 黑白草图(布局底图)
├── 手绘黑白稿 / 3D 摆好视角渲线框hunyuan3d 可辅助)
├── 或 程序生成:黑白剪影 + 人物站位 + 前中后景分区
└── 黑白 = 只留明暗结构,喂给控制
② ControlNetcontrolnet-union-sdxl-1.0
├── 模式选 canny(线稿) 或 depth(深度) 或 pose(姿态)
├── 作用: 把草图的构图/人物位置/透视强约束进生成
└── strength 0.6~0.9(草图不完美就调低,给 AI 发挥空间)
③ IP-Adaptersdxl_clipvision + ipadapter
├── 吃参考图(已定角色/场景/风格图)
└── 作用: 锁风格、锁资产外观、锁色调倾向
④ 正向提示词: 景别 + 主体 + 动作 + 前中后景 + 光影 + 色调 + 画风 + 质量词
⑤ 负向提示词: 变形/多余肢体/低清/水印 等
⑥ KSampler → VAEDecode → 分镜彩图
产出: 每镜 1 张分镜图(构图已由草图锁定,风格由 IP-Adapter+提示词锁定)
```
## §3 · 分镜图 → 关键帧 → 视频(问题②③答案)
```
锁定链路(生视频前必须锁死):
├── 锁定风格 IP-Adapter + 风格参考图 + 画风关键词
├── 锁定资产 角色 LoRA + 资产图库(同一套图不能换脸)
├── 色卡色调 全局色板(深海迷航=冷蓝/暗红)统一所有镜
├── 群像 多角色时: 空间关系/谁前谁后/谁主谁次 一次定死
├── 声音 配音/音效/BGM 规划先出(影响镜头节奏)
└── 镜头要素 视角 + 景别 + 运镜 + 人物空间关系 + 前中后景画内容
+ 人物表演 + 台词 + 环境音效
关键帧生成:
分镜图 → 精修出关键帧(构图/表情/光影到位、可直接当画面起点)
1 镜 = 1 关键帧起步(复杂镜可 2-3 帧做转场)
生视频(两条路):
A. 本地运镜¥0 首选): 关键帧图 → tools/local_motion.py → 推拉摇移
✅ 已验证出片EP01-MINIMAL-LOCAL-VALIDATED.mp4
B. 本地视频模型: 关键帧 → LTX-2.3 I2V / Wan2.1 → 真实动态
适合必须运动的镜头(表演/水浪/动作)→ 由苍耳圈定
画面 → 声音同步:
配音 edge-tts已验证· 音效/音乐 stable_audio_3 · 字幕 SRT
```
## §4 · 分镜脚本要素模板(供 Ollama 生成时锁定)
```
每镜:
景别 shot type : close-up / medium / wide / POV中英文
视角 camera angle : 平视 / 俯视 / 仰视
运镜 camera move : push-in / slow pan / tilt / handheld只写运镜不写时长
人物空间关系 layout : 主体位置 / 前中后景各画什么
光影 lighting : 冷蓝顶光 / 暗红氛围 / 逆光剪影(中文+英文加权)
色调 color palette : 冷蓝+暗红(对应色卡)
画风 style : 写实动漫 / 厚涂 / 赛璐璐
表演 action : 单一明确动作 + 表情
台词/音效 dialogue : 台词原文 + 环境音效提示
禁止项 negative : no cartoon motion / no bright colors 等
⏱ 时长: 不写(让 AI 自由发挥)
```
## §5 · 提示词模板(中英文加权示例)
```
正向:
(close-up:1.2) of Lin Hao, (worn work clothes:1.1), jolting awake,
(grabbing the iron bed frame:1.1), cold blue industrial lighting,
rust texture on metal, dark atmosphere, (cinematic lighting:1.3),
(anime realistic style:1.1), (masterpiece:1.2), (high detail:1.2)
负向:
(bad anatomy:1.3), extra fingers, deformed face, blurry, watermark,
lowres, cartoon motion, bright colors
控制: sketch(controlnet strength 0.7) + 角色参考图(ip-adapter)
```
---
> ⊢ 苍耳方法论 · 蛋蛋固化 · 全本地 ¥0
> ⊢ 待办: 黑白草图工作流实搭验证 → 16 镜分镜实跑

View File

@ -0,0 +1,95 @@
# LOCAL-PIPELINE-V1.hdlp · 本地漫剧制作管线¥0 · 全开源模型)
> HLDP://video-ai-system/LOCAL-PIPELINE-V1.hdlp
> 耳耳蛋 ICE-GL-耳耳蛋 · PTS-VA-001-EED · D205 · 2026-08-03
> 流程定义:苍耳爸爸 · 工具验证:蛋蛋
> 对齐 BC-006 IMAGE-FIRST-v1.0 · 默认路由 LOCAL_MOTION
---
## §1 · 总原则
```
✅ 全部环节由本地开源模型免费完成¥0 电费)
✅ 默认不调用任何线上付费 APISeedance/即梦 等仅在苍耳圈定后使用)
✅ 图片定一致性 · 视频仅生成动态 / 本地运镜
```
## §2 · 管线流程(苍耳定义 · 七段)
```
小说
↓ ① 改编
剧本
↓ ② 分镜脚本(注意点见 §3
分镜脚本
↓ ③ 资产生图(人物/场景/道具)
人物/场景/道具图
↓ ④ 分镜生图(黑白草图 → 俯视引导图 → 3D 辅助)
分镜图
↓ ⑤ 生关键帧
关键帧
↓ ⑥ 生视频(本地运镜 / 本地视频模型)
视频片段
↓ ⑦ 合成(拼接/字幕/配音/音效)
成片
```
## §3 · 分镜脚本注意点(苍耳定)
```
必含要素:
├── 运镜push-in / pan / tilt / handheld …)
├── 光影(冷蓝 / 暖黄 / 高对比 / 氛围光 …)
├── 景别(特写 / 中近景 / 中景 / 全景 / POV …)
├── 画风(写实 / 赛璐璐 / 厚涂 …)
└── 色调(整体色彩倾向指定)
删除要素:
└── ⏱ 时长 —— 不写死时长,让 AI 自由发挥
```
## §4 · 工具映射(全部验证可用 ✅)
| 环节 | 本地工具 | 状态 |
|------|----------|:--:|
| ① 小说→剧本 | Ollama `qwen3.5:9b` | ✅ 实测通 |
| ② 剧本→分镜脚本 | Ollama `qwen3.5:9b` | ✅ |
| ③ 资产生图 | ComfyUI `RealVisXL_V5.0` / `flux-2-klein-4b` / `z_image_turbo` | ✅ 服务在 |
| ③ 角色一致性 | `subai_lora_v1` LoRA | ✅ 已有 |
| ④ 分镜生图·引导 | ComfyUI controlnet黑白草图/俯视引导) | ✅ 模型库在 |
| ④ 分镜生图·3D辅助 | `hunyuan3d-dit-v2`3D 视角生成) | ✅ 模型在 |
| ⑤ 生关键帧 | ComfyUI分镜图 → 精修关键帧) | ✅ |
| ⑥ 生视频·本地运镜 | `tools/local_motion.py`ffmpeg zoompan¥0 | ✅ 实测出片 |
| ⑥ 生视频·模型 | `ltx-2.3-22b` / `Wan2.1-T2V-14B`I2V/T2V | ✅ 模型在 |
| ⑦ 合成拼接 | `tools/video_composer.py` | ✅ 实测出片 |
| ⑦ 音效/音乐 | `stable_audio_3` | ✅ 模型在 |
| ⑦ 配音 | edge-tts晓晓中文女声 | ✅ 实测出声 |
## §5 · 编排入口
```
统一入口: video-ai-system/pipeline.py
① pipeline.py storyboard <剧本> # 剧本→分镜
② pipeline.py route <分镜.json> # 路由分配
③ pipeline.py render <分镜.json> # 批量出图
④ pipeline.py upscale <图片文件夹> # 高清放大
⑤ pipeline.py compose <文件夹> -o EP.mp4 # 剪辑拼接
⑥ pipeline.py assets <项目目录> # 资产生成
本地运镜: python tools/local_motion.py <图片文件夹> -o <输出>
```
## §6 · 已验证里程碑D205
```
✅ [1] 图→本地运镜→成片 闭环通4图→4段→20s成片 EP01-MINIMAL-LOCAL-VALIDATED.mp4
✅ [2] Ollama 文本环节通qwen3.5:9b 本地生成正常)
⬜ [3] 剧本→分镜脚本 实跑(待发起)
⬜ [4] 分镜生图 实跑(黑白草图/俯视引导/3D辅助
⬜ [5] 关键帧→本地视频模型 实跑LTX I2V
⬜ [6] 全链路 16 镜成片
```
---
> ⊢ 苍耳定义流程 · 蛋蛋固化工具链 · 全本地 ¥0
> ⊢ 下一步由苍耳指定从哪个环节开跑

View File

View File

265
video-ai-system/pipeline.py Executable file
View File

@ -0,0 +1,265 @@
#!/usr/bin/env python3
"""
pipeline.py 苍耳管线 · 统一生产入口
整合所有工具为一条命令链路剧本分镜路由出图放大拼接配乐成片
用法:
python pipeline.py storyboard <剧本> -e 1 # ① 剧本→分镜
python pipeline.py route <分镜.json> # ② 路由分配
python pipeline.py render <分镜.json> # ③ 批量出图
python pipeline.py upscale <图片文件夹> # ④ 高清放大
python pipeline.py compose <文件夹> -o EP01.mp4 # ⑤ 剪辑拼接
python pipeline.py assets <项目目录> # ⑥ 资产生成
# 一键全自动需要API密钥
python pipeline.py go <剧本> -e 1 # ⑦ 全自动一条龙
"""
import sys, os, json, argparse, subprocess
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
def cmd_storyboard(args):
"""① 剧本→分镜"""
sys.argv = ['run_storyboard.py', args.script, '-e', str(args.episode)]
if args.pro:
sys.argv.append('--pro')
if args.dry_run:
sys.argv.append('--dry-run')
if args.output:
sys.argv.extend(['-o', args.output])
from tools.run_storyboard import main
main()
def cmd_route(args):
"""② 路由分配"""
sys.argv = ['route_shots.py', args.storyboard]
if args.dry_run:
sys.argv.append('--dry-run')
if args.summary:
sys.argv.append('--summary')
if args.output:
sys.argv.extend(['-o', args.output])
from tools.route_shots import main
main()
def cmd_render(args):
"""③ 分镜→出图桥接"""
sys.argv = ['storyboard_to_workflow.py', args.storyboard]
if args.output_dir:
sys.argv.extend(['-o', args.output_dir])
if args.style:
sys.argv.extend(['--style', args.style])
if args.generate_sh:
sys.argv.append('--generate-sh')
if args.workflow:
sys.argv.extend(['--workflow', args.workflow])
from tools.storyboard_to_workflow import main
main()
def cmd_upscale(args):
"""④ 高清放大"""
from tools.storyboard_to_workflow import main as sb_main
print('🔍 高清放大: 可使用 ComfyUI 蓝图 Image Upscale(Z-image-Turbo).json')
print(f' 打开 ComfyUI → 加载蓝图 → Image Upscale')
print(f' 输入图片: {args.input_dir}')
print()
print(' 或运行已生成的工作流JSON (如果有):')
print(f' ls {args.input_dir}/workflows/')
def cmd_compose(args):
"""⑤ 剪辑拼接"""
sys.argv = ['video_composer.py', args.folder, '-o', args.output]
if args.title:
sys.argv.extend(['--title', args.title])
if args.credits:
sys.argv.extend(['--credits', args.credits])
if args.subtitle:
sys.argv.extend(['--subtitle', args.subtitle])
if args.crossfade:
sys.argv.extend(['--crossfade', str(args.crossfade)])
from tools.video_composer import main
main()
def cmd_assets(args):
"""⑥ 资产生成"""
sys.argv = ['generate_assets_local.py', args.project_dir]
if args.list:
sys.argv.append('--list')
if args.dry_run:
sys.argv.append('--dry-run')
if args.comfy_url:
sys.argv.extend(['--comfy-url', args.comfy_url])
from tools.generate_assets_local import main
main()
def cmd_go(args):
"""⑦ 一键全自动(剧本→成片)"""
script_path = args.script
episode = args.episode
print(f'🚀 苍耳管线 · 全自动模式')
print(f'📖 剧本: {script_path}')
print(f'🎬 集号: EP{episode:02d}')
print()
# 检查文件
if not os.path.isfile(script_path):
print(f'❌ 找不到剧本: {script_path}')
sys.exit(1)
# Step 1: 剧本→分镜
print('' * 40)
print('① 剧本→分镜')
print('' * 40)
sys.argv = ['run_storyboard.py', script_path, '-e', str(episode)]
if args.pro:
sys.argv.append('--pro')
from tools.run_storyboard import main as sb_main
try:
sb_main()
except SystemExit:
pass
# 找生成的分镜JSON
script_dir = os.path.dirname(os.path.abspath(script_path))
script_base = os.path.splitext(os.path.basename(script_path))[0]
sb_json = None
for f in os.listdir(script_dir):
if f.startswith('STORYBOARD') and f.endswith('.json'):
sb_json = os.path.join(script_dir, f)
if not sb_json:
print('❌ 找不到生成的分镜JSON')
sys.exit(1)
print(f'\n✅ 分镜: {sb_json}')
# Step 2: 路由分配
print()
print('' * 40)
print('② 逐镜路由分配')
print('' * 40)
sys.argv = ['route_shots.py', sb_json]
from tools.route_shots import main as rt_main
try:
rt_main()
except SystemExit:
pass
# Step 3: 生成出图工作流
print()
print('' * 40)
print('③ 生成出图工作流')
print('' * 40)
sys.argv = ['storyboard_to_workflow.py', sb_json]
if args.style:
sys.argv.extend(['--style', args.style])
sys.argv.append('--generate-sh')
from tools.storyboard_to_workflow import main as sw_main
try:
sw_main()
except SystemExit:
pass
print()
print('' * 40)
print('🎯 完成! 后续步骤:')
print('' * 40)
print(f' 📄 分镜: {sb_json}')
print(f' 📋 路由: 已写入分镜JSON')
print(f' 🎨 出图: 启动ComfyUI后运行渲染脚本')
print(f' ✂️ 剪辑: python pipeline.py compose <输出目录> -o EP{episode:02d}.mp4')
def main():
parser = argparse.ArgumentParser(description='苍耳管线 · 统一生产入口')
parser.add_argument('command', nargs='?', default='help',
help='命令: storyboard|route|render|upscale|compose|assets|go')
parser.add_argument('args', nargs='*', help='命令参数')
# 先解析command
known_commands = ['storyboard', 'route', 'render', 'upscale', 'compose', 'assets', 'go', 'help']
if len(sys.argv) < 2 or sys.argv[1] not in known_commands:
print(__doc__)
return
cmd = sys.argv[1]
if cmd == 'help':
print(__doc__)
return
# 移除第一个参数(command)让子命令的argparse自己解析
# 但我们需要根据command来设置子解析器
if cmd == 'storyboard':
p = argparse.ArgumentParser()
p.add_argument('script', help='剧本文件路径')
p.add_argument('-e', '--episode', type=int, default=1, help='集号')
p.add_argument('--pro', action='store_true', help='使用Pro模型')
p.add_argument('-o', '--output', help='输出路径')
p.add_argument('--dry-run', action='store_true', help='模拟运行')
args = p.parse_args(sys.argv[2:])
cmd_storyboard(args)
elif cmd == 'route':
p = argparse.ArgumentParser()
p.add_argument('storyboard', help='分镜JSON路径')
p.add_argument('-o', '--output', help='输出路径')
p.add_argument('--dry-run', action='store_true', help='模拟运行')
p.add_argument('--summary', action='store_true', help='只统计')
args = p.parse_args(sys.argv[2:])
cmd_route(args)
elif cmd == 'render':
p = argparse.ArgumentParser()
p.add_argument('storyboard', help='分镜JSON路径')
p.add_argument('-o', '--output-dir', help='输出目录')
p.add_argument('--style', default='', help='全局风格')
p.add_argument('--generate-sh', action='store_true', help='生成shell脚本')
p.add_argument('--workflow', help='基础工作流模板')
args = p.parse_args(sys.argv[2:])
cmd_render(args)
elif cmd == 'upscale':
p = argparse.ArgumentParser()
p.add_argument('input_dir', help='图片文件夹')
args = p.parse_args(sys.argv[2:])
cmd_upscale(args)
elif cmd == 'compose':
p = argparse.ArgumentParser()
p.add_argument('folder', help='视频片段文件夹')
p.add_argument('-o', '--output', default='output.mp4', help='输出文件')
p.add_argument('--title', help='片头文字')
p.add_argument('--credits', help='片尾文字')
p.add_argument('--subtitle', help='字幕文件')
p.add_argument('--crossfade', type=int, default=0, help='交叉淡入淡出')
args = p.parse_args(sys.argv[2:])
cmd_compose(args)
elif cmd == 'assets':
p = argparse.ArgumentParser()
p.add_argument('project_dir', help='项目目录')
p.add_argument('--comfy-url', help='ComfyUI API地址')
p.add_argument('--list', action='store_true', help='只列出')
p.add_argument('--dry-run', action='store_true', help='模拟运行')
args = p.parse_args(sys.argv[2:])
cmd_assets(args)
elif cmd == 'go':
p = argparse.ArgumentParser()
p.add_argument('script', help='剧本文件路径')
p.add_argument('-e', '--episode', type=int, default=1, help='集号')
p.add_argument('--pro', action='store_true', help='使用Pro模型')
p.add_argument('--style', default='', help='全局风格')
args = p.parse_args(sys.argv[2:])
cmd_go(args)
if __name__ == '__main__':
main()

View File

@ -25,7 +25,15 @@ PORTRAIT = ("character design sheet, horizontal layout divided into four panels:
"front view, back view, 90 degree side profile view, and a large close-up portrait of the face, "
"the SAME character in all four panels, identical face hairstyle and outfit, "
"plain white background, anime style, clean lineart, high quality, detailed")
# 3D展示台布局爸爸发的开源格式上方三视图 + 下方细节特写镜头组
# 标准角色板 Character Sheet开源技能四区布局上三视图 + 左(特写/色板/细节) + 右全身标尺
CHARACTER_SHEET = ("professional character design sheet, turnaround model sheet, pure white background, "
"standard character reference board layout with four zones: "
"top section: three-view bust portraits (front view + 90 degree side view + back view) arranged horizontally, same size and alignment, "
"left section: large facial close-up portrait + color palette swatches with hex codes + close-up detail callouts of accessories and fabric texture, "
"right section: full body standing portrait with height measurement scale in centimeters beside the figure, "
"multiple views of the same character, consistent character design, identical face hairstyle and outfit across all panels, "
"professional character reference board, clean layout, high quality, detailed, masterpiece")
SHOWCASE = ("3D model display, three views of the character (front view, side view, back view), "
"clean neutral background, below the three main views are close-up detail shots showing "
"fabric, clothing details, face and accessories, "
@ -176,6 +184,8 @@ def submit(desc, views, seed, prefix, ctype="char", layout="standard"):
prompt = f"{SHOWCASE}. Character and outfit: {desc}"
elif layout == "portrait":
prompt = f"{PORTRAIT}. Character: {desc}"
elif layout == "character_sheet":
prompt = f"{CHARACTER_SHEET}. Character and outfit: {desc}"
else:
view_part = VIEWS_4 if views >= 4 else VIEWS_3
prompt = (f"character reference sheet, model sheet, {views} views of the SAME character: "
@ -216,7 +226,7 @@ def main():
ap.add_argument("--desc", required=True, help="角色描述(细节越足越好)")
ap.add_argument("--views", type=int, default=3, choices=[3, 4], help="视图数3或4仅 char 用)")
ap.add_argument("--type", dest="ctype", default="char", choices=["char", "scene"], help="char=人物三视图 / scene=场景四视图")
ap.add_argument("--layout", default="standard", choices=["standard", "portrait", "showcase"], help="char布局standard=标准三栏 / portrait=面部特写+三视图 / showcase=3D展示台(上三视图+下细节特写)")
ap.add_argument("--layout", default="standard", choices=["standard", "portrait", "showcase", "character_sheet"], help="char布局standard=标准三栏 / portrait=面部特写+三视图 / showcase=3D展示台 / character_sheet=标准角色板(四区)")
ap.add_argument("--method", default="prompt", choices=["prompt", "panorama", "compose"], help="场景: prompt/panorama; 人物四视图: compose=逐张生成+PIL拼图(精确4张)")
ap.add_argument("--seed", type=int, default=777)
ap.add_argument("--output", default="", help="输出路径(默认 cang-ying/outputs/character_<time>.png")

View File

@ -0,0 +1,243 @@
#!/usr/bin/env python3
"""
generate_assets_local.py 本地ComfyUI批量生成角色/场景/道具资产图
替代线上Seedream API¥0.15-1/ 本地ComfyUI¥0电费
用法:
python tools/generate_assets_local.py <项目目录> [--comfy-url URL]
python tools/generate_assets_local.py <项目目录> --list # 列出待生成的资产
"""
import sys, os, json, argparse, subprocess
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# 默认资产生成prompt模板
ASSET_PROMPTS = {
"CHAR": {
"template": "{name}, {description}, {style}, full body character design, character sheet, consistent clothing, clean background, high quality, detailed",
"default_style": "digital art, anime style, vibrant colors"
},
"ENV": {
"template": "{name}, {description}, {style}, wide angle view, environmental design, detailed scene, high quality",
"default_style": "digital painting, cinematic lighting, detailed environment"
},
"PROP": {
"template": "{name}, {description}, {style}, isolated object, clean background, detailed, high quality",
"default_style": "product photography, sharp focus, detailed texture"
}
}
def generate_workflow(prompt, asset_type, asset_name, output_prefix="asset"):
"""生成ComfyUI API调用JSON"""
workflow = {
"3": {
"class_type": "KSampler",
"inputs": {
"seed": abs(hash(asset_name)) % 1000000,
"steps": 25,
"cfg": 7,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1,
"model": ["4", 0],
"positive": ["6", 0],
"negative": ["7", 0],
"latent_image": ["5", 0]
}
},
"4": {
"class_type": "CheckpointLoaderSimple",
"inputs": {"ckpt_name": "realvisxlV40_v40BDFPonNoobVae.safetensors"}
},
"5": {
"class_type": "EmptyLatentImage",
"inputs": {"width": 1024, "height": 1024, "batch_size": 1}
},
"6": {
"class_type": "CLIPTextEncode",
"inputs": {"text": prompt, "clip": ["4", 1]}
},
"7": {
"class_type": "CLIPTextEncode",
"inputs": {"text": "worst quality, low quality, blurry, deformed", "clip": ["4", 1]}
},
"8": {
"class_type": "VAEDecode",
"inputs": {"samples": ["3", 0], "vae": ["4", 2]}
},
"9": {
"class_type": "SaveImage",
"inputs": {"filename_prefix": f"{output_prefix}_{asset_name}", "images": ["8", 0]}
}
}
return workflow
def load_asset_list(project_dir):
"""加载项目资产清单"""
# 检查是否有 ASSET-LIST 文件
for pattern in ['ASSET-LIST*', 'assets*.json', 'CHARACTERS*']:
import glob
matches = glob.glob(os.path.join(project_dir, pattern))
if matches:
with open(matches[0], 'r', encoding='utf-8') as f:
return json.load(f)
# 检查项目protocols目录
protocols_dir = os.path.join(project_dir, 'protocols')
if os.path.isdir(protocols_dir):
for fname in os.listdir(protocols_dir):
if 'ASSET' in fname.upper() or 'CHAR' in fname.upper():
with open(os.path.join(protocols_dir, fname), 'r', encoding='utf-8') as f:
content = f.read()
try:
return json.loads(content)
except:
pass
return None
def scan_project_assets(project_dir):
"""扫描项目目录,识别需要生成的资产"""
assets = []
# 检查 characters/ envs/ props/ 目录
for asset_type, subdir in [('CHAR', 'characters'), ('ENV', 'envs'), ('PROP', 'props')]:
asset_dir = os.path.join(project_dir, 'assets', subdir)
if os.path.isdir(asset_dir):
for f in os.listdir(asset_dir):
if f.endswith(('.json', '.hdlp', '.txt')):
with open(os.path.join(asset_dir, f), 'r', encoding='utf-8') as fh:
content = fh.read()
assets.append({
'type': asset_type,
'name': os.path.splitext(f)[0],
'description': content[:200],
'file': os.path.join(asset_dir, f)
})
# 检查分镜JSON中出现的角色/场景
for root, dirs, files in os.walk(project_dir):
for f in files:
if f.startswith('STORYBOARD') and f.endswith('.json'):
try:
with open(os.path.join(root, f), 'r', encoding='utf-8') as fh:
sb = json.load(fh)
for shot in sb.get('shots', []):
for char in shot.get('characters', []):
if not any(a['name'] == char for a in assets):
assets.append({
'type': 'CHAR',
'name': char,
'description': f'角色 {char}',
'source': f
})
for scene in shot.get('scenes', []):
if not any(a['name'] == scene for a in assets):
assets.append({
'type': 'ENV',
'name': scene,
'description': f'场景 {scene}',
'source': f
})
except:
pass
return assets
def main():
parser = argparse.ArgumentParser(description='本地ComfyUI批量生成资产图')
parser.add_argument('project_dir', help='项目目录')
parser.add_argument('--comfy-url', default='http://127.0.0.1:8188', help='ComfyUI API地址')
parser.add_argument('--style', default='', help='全局风格')
parser.add_argument('--list', action='store_true', help='只列出待生成资产')
parser.add_argument('--output-dir', help='资产输出目录 (默认: project_dir/assets/)')
parser.add_argument('--dry-run', action='store_true', help='只生成workflow JSON不调API')
args = parser.parse_args()
if not os.path.isdir(args.project_dir):
print(f'❌ 项目目录不存在: {args.project_dir}')
sys.exit(1)
# 扫描资产
assets = scan_project_assets(args.project_dir)
if not assets:
print('⚠️ 未发现需要生成的资产')
print(' 提示: 将角色描述放在 assets/characters/ 目录下')
print(' 或: 先运行 storyboard_to_workflow.py 生成分镜后再扫描')
sys.exit(0)
# 去重
seen = set()
unique_assets = []
for a in assets:
key = f"{a['type']}-{a['name']}"
if key not in seen:
seen.add(key)
unique_assets.append(a)
assets = unique_assets
print(f'📦 发现 {len(assets)} 个待生成资产:')
for a in assets:
print(f' [{a["type"]}] {a["name"]:20s} {a.get("description", "")[:40]}')
if args.list:
return
# 输出目录
out_dir = args.output_dir or os.path.join(args.project_dir, 'assets')
assets_char_dir = os.path.join(out_dir, 'characters')
assets_env_dir = os.path.join(out_dir, 'envs')
assets_prop_dir = os.path.join(out_dir, 'props')
for d in [assets_char_dir, assets_env_dir, assets_prop_dir]:
os.makedirs(d, exist_ok=True)
# 生成并调用
for i, asset in enumerate(assets):
atype = asset['type']
name = asset['name']
desc = asset.get('description', name)
style = args.style or ASSET_PROMPTS.get(atype, {}).get('default_style', '')
template = ASSET_PROMPTS.get(atype, {}).get('template', '{description}, {style}')
prompt = template.format(name=name, description=desc, style=style)
print(f'\n🎨 [{i+1}/{len(assets)}] {name} ({atype})')
print(f' Prompt: {prompt[:80]}...')
workflow = generate_workflow(prompt, atype, name)
# 确定输出位置
dir_map = {'CHAR': assets_char_dir, 'ENV': assets_env_dir, 'PROP': assets_prop_dir}
asset_out_dir = dir_map.get(atype, out_dir)
wf_path = os.path.join(asset_out_dir, f'_{name}_workflow.json')
with open(wf_path, 'w', encoding='utf-8') as f:
json.dump(workflow, f, ensure_ascii=False, indent=2)
if args.dry_run:
print(f' ✅ 工作流JSON: {wf_path}')
continue
# 调ComfyUI API
print(f' 📤 发送ComfyUI...')
try:
resp = subprocess.run([
'curl', '-s', '-X', 'POST',
f'{args.comfy_url}/prompt',
'-H', 'Content-Type: application/json',
'-d', json.dumps({"prompt": workflow})
], capture_output=True, text=True, timeout=30)
result = json.loads(resp.stdout) if resp.stdout else {}
if 'error' in result:
print(f' ❌ API错误: {result["error"]}')
else:
print(f' ✅ 已提交 (task: {result.get("task_id", "?")})')
except Exception as e:
print(f' ⚠️ 调用失败: {e}')
print(f'\n✅ 完成! 资产将保存到: {out_dir}')
if __name__ == '__main__':
main()

View File

@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""local_motion.py — 本地运镜工具BC-006 IMAGE-FIRST 默认路由 LOCAL_MOTION
静态图 ffmpeg zoompan 竖屏 9:16 运镜片段¥0 视频模型费
用法:
python tools/local_motion.py <图片文件夹> -o <输出文件夹> [--dur 6] [--fps 24]
运镜模式循环: push_in / zoom_out / pan_left / pan_right / pan_up / static
输出: <输出文件夹>/shot_01.mp4 ... 每个片段 dur
"""
import os, sys, subprocess, argparse, glob
def normalize(fps):
return ["-vf", f"scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,scale=4320:7680,fps={fps}"]
def zoompan_filter(mode, frames, fps):
z = "z='min(1+0.0008*on,1.6)'" # push_in 推近
x = "x='iw/2-(iw/zoom/2)'"
y = "y='ih/2-(ih/zoom/2)'"
if mode == "zoom_out":
z = "z='max(1.5-0.0008*on,1.0)'"
elif mode == "pan_left": # 从左扫到右
z = "z='1.2'"; x = "x='(iw-iw/zoom)*(on/{})'".format(frames)
elif mode == "pan_right": # 从右扫到左
z = "z='1.2'"; x = "x='(iw-iw/zoom)*(1-on/{})'".format(frames)
elif mode == "pan_up": # 从下往上
z = "z='1.2'"; y = "y='(ih-ih/zoom)*(1-on/{})'".format(frames)
elif mode == "static":
z = "z='1.0'"
return f"zoompan={z}:{x}:{y}:d={frames}:s=1080x1920:fps={fps}"
def make_shot(img, out, dur, fps, mode):
frames = dur * fps
vf = normalize(fps)[1] + "," + zoompan_filter(mode, frames, fps)
cmd = ["ffmpeg", "-y", "-loop", "1", "-i", img, "-vf", vf,
"-t", str(dur), "-r", str(fps), "-c:v", "libx264", "-preset", "fast",
"-crf", "20", "-pix_fmt", "yuv420p", "-an", out]
r = subprocess.run(cmd, capture_output=True, text=True)
return r.returncode == 0, r.stderr[-200:] if r.returncode else ""
def main():
ap = argparse.ArgumentParser()
ap.add_argument("folder")
ap.add_argument("-o", "--output", default="local_motion_out")
ap.add_argument("--dur", type=int, default=6)
ap.add_argument("--fps", type=int, default=24)
args = ap.parse_args()
imgs = sorted(glob.glob(os.path.join(args.folder, "*.png")) +
glob.glob(os.path.join(args.folder, "*.jpg")) +
glob.glob(os.path.join(args.folder, "*.jpeg")) +
glob.glob(os.path.join(args.folder, "*.webp")))
if not imgs:
print("❌ 文件夹里没有图片"); return 1
os.makedirs(args.output, exist_ok=True)
modes = ["push_in", "zoom_out", "pan_left", "pan_right", "pan_up", "static"]
ok = 0
for i, img in enumerate(imgs[:64], 1):
mode = modes[(i - 1) % len(modes)]
out = os.path.join(args.output, f"shot_{i:02d}.mp4")
print(f"[{i}/{len(imgs)}] {os.path.basename(img)} -> {mode} ...", end=" ")
good, err = make_shot(img, out, args.dur, args.fps, mode)
if good:
print(""); ok += 1
else:
print("", err[-120:])
print(f"完成: {ok}/{len(imgs)} 个片段已生成 -> {args.output}")
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,201 @@
#!/usr/bin/env python3
"""
route_shots.py BC-006 逐镜路由分配器
读取分镜JSON调豆包AI为每镜分配最优生产路由
- STILL_HOLD 单帧出图¥0
- LOCAL_MOTION 出图+FFmpeg运镜¥0
- LAYERED_2_5D 多层拆解+伪3D运镜¥0
- LOCAL_LIPSYNC LivePortrait口型同步¥0
- AI_I2V Wan2.2图生视频¥0.02/
用法:
python tools/route_shots.py <分镜JSON> [-o 输出JSON]
python tools/route_shots.py <分镜JSON> --dry-run
"""
import sys, os, json, argparse
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from lib.doubao_chat import chat
ROUTE_DESCRIPTION = """
为每个镜头分配最优生产路由可选路由类型及其含义
STILL_HOLD: 静态画面/过渡镜头角色无运动仅背景直接单帧出图即可成本最低
- 适合: 空镜过渡环境展示静态对话
- 成本: ¥0 (仅出图电费)
LOCAL_MOTION: 画面有简单运动角色小幅度动作镜头缓慢推拉摇移出图后用FFmpeg做缩放/平移/旋转模拟运镜
- 适合: 缓慢摇镜推近拉远轻微角色动作
- 成本: ¥0 (FFmpeg运镜)
LAYERED_2_5D: 画面有前后景分层需求角色在画面中需要与背景分离做视差效果
- 适合: 角色在前景有复杂动作但背景固定需要2.5D伪3D效果
- 成本: ¥0 (分层合成)
LOCAL_LIPSYNC: 角色有说话/表情需求需要口型同步用LivePortrait处理
- 适合: 角色特写说话重要表情表演
- 成本: ¥0 (LivePortrait本地)
AI_I2V: 画面有复杂运动角色全身运动物体移动镜头剧烈运动必须用AI图生视频
- 适合: 角色行走奔跑打斗动作物体运动镜头跟随
- 成本: ¥0.02 (Wan2.2电费)
"""
def extract_json(text):
"""从AI回复提取JSON"""
if '```json' in text:
return text.split('```json')[1].split('```')[0].strip()
if '```' in text:
return text.split('```')[1].split('```')[0].strip()
maybe = text.strip()
if maybe.startswith('{') or maybe.startswith('['):
return maybe
return maybe
def route_shots(storyboard, dry_run=False):
"""为分镜中所有镜头分配路由"""
episode = storyboard.get('episode', 1)
shots = storyboard.get('shots', [])
if not shots:
print('❌ 分镜中没有镜头')
return None
# 构建提示词
shots_text = json.dumps([
{
"shot_number": s.get("shot_number"),
"description": s.get("description"),
"camera": s.get("camera"),
"type": s.get("type"),
"characters": s.get("characters", []),
"scenes": s.get("scenes", []),
"dialogue": s.get("dialogue"),
}
for s in shots
], ensure_ascii=False)
prompt = f"""你是一个短剧视频生产管线路由专家。请为以下短剧第{episode}集每个镜头分配最优生产路由。
路由规则
{ROUTE_DESCRIPTION}
分配原则
1. 优先使用低成本路由STILL_HOLD > LOCAL_MOTION > LAYERED_2_5D > LOCAL_LIPSYNC > AI_I2V
2. 只有确实需要复杂运动的镜头才用 AI_I2V
3. 有对话的镜头用 LOCAL_LIPSYNC
4. 角色有行走/奔跑/打斗等全身运动用 AI_I2V
5. 空镜/过渡/静态用 STILL_HOLD
输出JSON格式LIST按镜头顺序
[
{{
"shot_number": "S01",
"route": "STILL_HOLD",
"reason": "简短理由"
}},
...
]
分镜数据
{shots_text}"""
if dry_run:
print('🧪 DRY RUN — 提示词:')
print('=' * 60)
print(prompt[:500])
print('...')
print('=' * 60)
return None
print('🚀 调豆包AI分配路由...')
result = chat(prompt, system="你是短剧生产管线路由优化专家输出纯JSON。", model="pro", temperature=0.2, max_tokens=4096)
if 'error' in result:
print('❌ API ERROR:', json.dumps(result, ensure_ascii=False, indent=2))
return None
content = result.get('content', '')
json_str = extract_json(content)
try:
routes = json.loads(json_str)
if isinstance(routes, dict) and 'shots' in routes:
routes = routes['shots']
print(f'✅ 路由分配完成: {len(routes)}')
return routes
except json.JSONDecodeError as e:
print(f'⚠️ JSON解析失败: {e}')
print('原始回复:', content[:500])
return None
def main():
parser = argparse.ArgumentParser(description='BC-006 逐镜路由分配器')
parser.add_argument('storyboard', help='分镜JSON文件路径')
parser.add_argument('-o', '--output', help='输出JSON路径 (默认: 覆盖原文件添加路由)')
parser.add_argument('--dry-run', action='store_true', help='只打印提示词不调API')
parser.add_argument('--summary', action='store_true', help='只统计各路由数量不调API')
args = parser.parse_args()
with open(args.storyboard, 'r', encoding='utf-8') as f:
storyboard = json.load(f)
shots = storyboard.get('shots', [])
if args.summary:
print(f'📖 分镜: {args.storyboard}')
print(f'🎬 共 {len(shots)}')
print()
print('🔍 各镜头类型分布:')
type_count = {}
for s in shots:
t = s.get('type', 'unknown')
type_count[t] = type_count.get(t, 0) + 1
for t, c in sorted(type_count.items()):
print(f' {t}: {c}')
return
routes = route_shots(shots, dry_run=args.dry_run)
if routes is None:
sys.exit(1)
# 合并路由到分镜
route_map = {}
for r in routes:
if isinstance(r, dict):
sn = r.get('shot_number', '')
route_map[sn] = r.get('route', 'STILL_HOLD')
for shot in shots:
sn = shot.get('shot_number', '')
if sn in route_map:
shot['route'] = route_map[sn]
else:
shot['route'] = 'STILL_HOLD' # 默认
# 统计
route_count = {}
for shot in shots:
r = shot.get('route', 'UNKNOWN')
route_count[r] = route_count.get(r, 0) + 1
print()
print('📊 路由分布:')
for r, c in sorted(route_count.items()):
cost = {'STILL_HOLD': '¥0', 'LOCAL_MOTION': '¥0', 'LAYERED_2_5D': '¥0',
'LOCAL_LIPSYNC': '¥0', 'AI_I2V': '¥0.02'}.get(r, '¥?')
print(f' {r:20s}: {c:2d}镜 ({cost})')
total_cost = route_count.get('AI_I2V', 0) * 0.02
print(f' {"总预估成本":20s}: ¥{total_cost:.2f}')
# 输出
out_path = args.output or args.storyboard
with open(out_path, 'w', encoding='utf-8') as f:
json.dump(storyboard, f, ensure_ascii=False, indent=2)
print(f'✅ 已保存: {out_path}')
if __name__ == '__main__':
main()

View File

@ -0,0 +1,379 @@
#!/usr/bin/env python3
"""
LTX 2.3 I2V - ComfyUI API 调用脚本
直接用 ComfyUI API 提交 LTX 2.3 蒸馏版图生视频工作流
"""
import json, sys, os, time, uuid, requests
COMFY_HOST = "http://localhost:8188"
def submit_workflow(input_image="test_scene.png", prompt="A cinematic shot:", output_prefix="ltx23_output", seed=42, width=768, height=512, num_frames=25):
"""
构建并提交 LTX 2.3 蒸馏版 I2V 工作流
节点结构基于 ComfyUI 内置模板展开
"""
# 节点 ID 分配
N = {k: i for i, k in enumerate([
"ckpt_loader", "text_encoder_loader",
"load_image", "preprocess",
"clip_positive", "clip_negative", "cond_zero",
"ltxv_cond", "crop_guides",
"empty_video_latent", "empty_audio_latent",
"concat_av", "img_to_video",
"cfgguider", "ksampler", "sigmas", "noise",
"sampler_custom",
"separate_av",
"vae_decode", "audio_vae_loader", "audio_vae_decode",
"create_video", "save_video",
])}
prompt_json = {
# 1. 加载模型
str(N["ckpt_loader"]): {
"class_type": "CheckpointLoaderSimple",
"inputs": {"ckpt_name": "ltx-2.3-22b-dev.safetensors"}
},
# 2. 文本编码器Gemma
str(N["text_encoder_loader"]): {
"class_type": "LTXAVTextEncoderLoader",
"inputs": {
"text_encoder": "gemma_3_12B_it.safetensors",
"ckpt_name": "ltx-2.3-22b-dev.safetensors",
"device": "default"
}
},
# 3. 加载输入图
str(N["load_image"]): {
"class_type": "LoadImage",
"inputs": {"image": input_image}
},
# 4. 预处理(缩放到目标尺寸)
str(N["preprocess"]): {
"class_type": "LTXVPreprocess",
"inputs": {
"image": [str(N["load_image"]), 0],
"size": 18
}
},
# 5. 正向提示词
str(N["clip_positive"]): {
"class_type": "CLIPTextEncode",
"inputs": {
"clip": [str(N["text_encoder_loader"]), 0],
"text": prompt
}
},
# 6. 负向提示词
str(N["clip_negative"]): {
"class_type": "CLIPTextEncode",
"inputs": {
"clip": [str(N["text_encoder_loader"]), 0],
"text": "bad quality, ugly, blurry, distorted, deformed"
}
},
# 7. 归零负向条件
str(N["cond_zero"]): {
"class_type": "ConditioningZeroOut",
"inputs": {
"conditioning": [str(N["clip_negative"]), 0]
}
},
# 8. LTXV 条件处理
str(N["ltxv_cond"]): {
"class_type": "LTXVConditioning",
"inputs": {
"positive": [str(N["clip_positive"]), 0],
"negative": [str(N["cond_zero"]), 0],
"frame_rate": 25.0
}
},
# 9. 空视频 latent
str(N["empty_video_latent"]): {
"class_type": "EmptyLTXVLatentVideo",
"inputs": {
"width": width,
"height": height,
"length": num_frames,
"batch_size": 1
}
},
# 10. 空音频 latent
str(N["empty_audio_latent"]): {
"class_type": "LTXVEmptyLatentAudio",
"inputs": {
"length": num_frames,
"num_frames_per_batch": 25,
"batch_size": 1
}
},
# 11. 拼接音视频 latent
str(N["concat_av"]): {
"class_type": "LTXVConcatAVLatent",
"inputs": {
"video_latent": [str(N["empty_video_latent"]), 0],
"audio_latent": [str(N["empty_audio_latent"]), 0]
}
},
# 12. 图生视频条件in-place
str(N["img_to_video"]): {
"class_type": "LTXVImgToVideoInplace",
"inputs": {
"vae": [str(N["ckpt_loader"]), 2],
"image": [str(N["preprocess"]), 0],
"latent": [str(N["concat_av"]), 0],
"strength": 1.0,
"bypass": False
}
},
# 13. 裁剪 guide对齐 latent 尺寸)
str(N["crop_guides"]): {
"class_type": "LTXVCropGuides",
"inputs": {
"positive": [str(N["ltxv_cond"]), 0],
"negative": [str(N["ltxv_cond"]), 1],
"latent": [str(N["img_to_video"]), 0]
}
},
# 14. CFG Guider蒸馏版 CFG=1
str(N["cfgguider"]): {
"class_type": "CFGGuider",
"inputs": {
"model": [str(N["ckpt_loader"]), 0],
"positive": [str(N["crop_guides"]), 0],
"negative": [str(N["crop_guides"]), 1],
"cfg": 1.0
}
},
# 15. KSampler 选择
str(N["ksampler"]): {
"class_type": "KSamplerSelect",
"inputs": {"sampler_name": "euler"}
},
# 16. 蒸馏版 sigma schedule4步
str(N["sigmas"]): {
"class_type": "ManualSigmas",
"inputs": {"sigmas": "0.909375, 0.725, 0.421875, 0.0"}
},
# 17. 随机噪声
str(N["noise"]): {
"class_type": "RandomNoise",
"inputs": {"noise_seed": seed}
},
# 18. 自定义采样器
str(N["sampler_custom"]): {
"class_type": "SamplerCustomAdvanced",
"inputs": {
"noise": [str(N["noise"]), 0],
"guider": [str(N["cfgguider"]), 0],
"sampler": [str(N["ksampler"]), 0],
"sigmas": [str(N["sigmas"]), 0],
"latent_image": [str(N["crop_guides"]), 2]
}
},
# 19. 分离音视频 latent
str(N["separate_av"]): {
"class_type": "LTXVSeparateAVLatent",
"inputs": {
"av_latent": [str(N["sampler_custom"]), 0]
}
},
# 20. VAE 解码视频
str(N["vae_decode"]): {
"class_type": "VAEDecode",
"inputs": {
"vae": [str(N["ckpt_loader"]), 2],
"samples": [str(N["separate_av"]), 0]
}
},
# 21. 加载音频 VAE
str(N["audio_vae_loader"]): {
"class_type": "LTXVAudioVAELoader",
"inputs": {"ckpt_name": "ltx-2.3-22b-dev.safetensors"}
},
# 22. 音频 VAE 解码
str(N["audio_vae_decode"]): {
"class_type": "LTXVAudioVAEDecode",
"inputs": {
"samples": [str(N["separate_av"]), 1],
"audio_vae": [str(N["audio_vae_loader"]), 0]
}
},
# 23. 合成视频
str(N["create_video"]): {
"class_type": "CreateVideo",
"inputs": {
"images": [str(N["vae_decode"]), 0],
"audio": [str(N["audio_vae_decode"]), 0],
"fps": 25,
"frame_rate": 25,
"bit_depth": 8
}
},
# 24. 保存视频
str(N["save_video"]): {
"class_type": "SaveVideo",
"inputs": {
"video": [str(N["create_video"]), 0],
"filename_prefix": output_prefix,
"format": "mp4",
"codec": "h264"
}
},
}
return prompt_json
def queue_prompt(prompt_workflow):
"""提交工作流到 ComfyUI 并返回 prompt_id"""
payload = {"prompt": prompt_workflow, "client_id": str(uuid.uuid4())}
r = requests.post(f"{COMFY_HOST}/prompt", json=payload)
r.raise_for_status()
data = r.json()
return data.get("prompt_id"), data
def get_history(prompt_id):
"""查询 prompt 执行历史"""
r = requests.get(f"{COMFY_HOST}/history/{prompt_id}")
if r.status_code == 200:
return r.json().get(prompt_id)
return None
def wait_for_completion(prompt_id, timeout=600, check_interval=10):
"""等待 prompt 执行完成"""
start = time.time()
while time.time() - start < timeout:
history = get_history(prompt_id)
if history and history.get("status", {}).get("completed") is True:
outputs = history.get("outputs", {})
elapsed = time.time() - start
return {"status": "completed", "outputs": outputs, "elapsed": elapsed}
if history and history.get("status", {}).get("status_str") == "error":
return {"status": "error", "error": history.get("status", {}).get("error_message", "未知错误")}
# 查询队列
r = requests.get(f"{COMFY_HOST}/queue")
if r.status_code == 200:
queue_data = r.json()
r2 = requests.get(f"{COMFY_HOST}/execution/{prompt_id}")
if r2.status_code == 200:
exec_data = r2.json()
progress = exec_data.get("data", {})
print(f" 进度: {progress.get('progress', 0)*100:.0f}% ({elapsed:.0f}s)" if progress.get('progress') else f" 运行中... ({time.time()-start:.0f}s)")
time.sleep(check_interval)
return {"status": "timeout", "elapsed": time.time() - start}
def check_available_models():
"""检查 ComfyUI 可用模型"""
r = requests.get(f"{COMFY_HOST}/object_info/CheckpointLoaderSimple")
if r.status_code == 200:
info = r.json()
models = info.get("CheckpointLoaderSimple", {}).get("input", {}).get("required", {}).get("ckpt_name", [])
return models
return []
def get_queue_status():
"""获取队列状态"""
try:
r = requests.get(f"{COMFY_HOST}/queue")
if r.status_code == 200:
return r.json()
except:
pass
return {}
def clear_queue():
"""清空队列"""
try:
r = requests.post(f"{COMFY_HOST}/queue", json={"clear": True})
if r.status_code == 200:
print("✅ 队列已清空")
except:
pass
def main():
print("=" * 60)
print("🎬 LTX 2.3 I2V - ComfyUI API 测试脚本")
print("=" * 60)
# 参数
input_image = sys.argv[1] if len(sys.argv) > 1 else "test_scene.png"
prompt_text = sys.argv[2] if len(sys.argv) > 2 else "A cinematic shot of a character in a fantasy scene, epic, dramatic lighting, motion blur"
output_prefix = sys.argv[3] if len(sys.argv) > 3 else "ltx23_output"
print(f"\n📷 输入图: {input_image}")
print(f"📝 Prompt: {prompt_text[:50]}...")
print(f"💾 输出: {output_prefix}")
# 检查队列
q = get_queue_status()
running = q.get("queue_running", [])
pending = q.get("queue_pending", [])
if running:
print(f"\n⚠️ 队列中已有 {len(running)} 个任务在跑")
running_ids = [str(item[1]) if len(item) > 1 else "?" for item in running]
print(f" 运行中 prompt_id: {running_ids}")
if pending:
print(f"⚠️ 队列中 {len(pending)} 个任务待执行")
# 检查模型
models = check_available_models()
ltx_models = [m for m in models if isinstance(m, str) and "ltx" in m.lower()] + [m for m in models if isinstance(m, dict) and "ltx" in str(m).lower()]
print(f"\n📦 可用 LTX 模型: {ltx_models}")
# 构建工作流
print(f"\n🔧 构建工作流...")
workflow = submit_workflow(
input_image=input_image,
prompt=prompt_text,
output_prefix=output_prefix,
seed=42,
width=768,
height=512,
num_frames=25
)
print(f"✅ 工作流构建完成 (25个节点)")
# 提交
print(f"\n🚀 提交任务到 ComfyUI...")
try:
prompt_id, resp = queue_prompt(workflow)
print(f"✅ 任务已提交! prompt_id: {prompt_id}")
print(f" 队列中还有 {len(pending)} 个任务待执行")
except Exception as e:
print(f"❌ 提交失败: {e}")
# 打印详细错误
if hasattr(e, 'response') and e.response is not None:
print(f" 响应: {e.response.text[:500]}")
sys.exit(1)
# 等待完成
print(f"\n⏳ 等待任务完成...")
result = wait_for_completion(prompt_id, timeout=600)
if result["status"] == "completed":
print(f"\n✅ 任务完成! 耗时: {result['elapsed']:.1f}")
outputs = result["outputs"]
for node_id, node_out in outputs.items():
if "videos" in node_out:
for vid in node_out["videos"]:
print(f" 🎬 视频: {vid['filename']} ({vid.get('type','')})")
if "images" in node_out:
for img in node_out["images"]:
print(f" 🖼️ 图片: {img['filename']}")
# 查找输出文件
out_dir = "/home/ls/comfy/ComfyUI/output"
import glob
files = sorted(glob.glob(f"{out_dir}/{output_prefix}*"))
print(f"\n📁 输出文件:")
for f in files:
size = os.path.getsize(f) / 1024 / 1024
print(f" {f} ({size:.1f}MB)")
elif result["status"] == "error":
print(f"\n❌ 任务失败: {result.get('error', '未知错误')}")
else:
print(f"\n⏰ 超时 ({result['elapsed']:.0f}秒)")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,220 @@
#!/usr/bin/env python3
"""
storyboard_to_workflow.py 分镜JSON ComfyUI批量出图桥接
读取分镜JSON为每个镜头生成ComfyUI API调用脚本
用法:
python tools/storyboard_to_workflow.py <分镜JSON> [-o 输出目录] [--workflow 基础工作流]
python tools/storyboard_to_workflow.py <分镜JSON> --generate-sh # 生成批量调用shell脚本
"""
import sys, os, json, argparse, shutil
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# 默认的工作流模板路径ComfyUI蓝图
DEFAULT_WORKFLOW = os.path.expanduser("~/comfy/ComfyUI/blueprints/Text to Image.json")
def load_storyboard(json_path):
"""读取分镜JSON"""
with open(json_path, 'r', encoding='utf-8') as f:
return json.load(f)
def shot_to_prompt(shot, project_style=""):
"""将单镜转换为图像生成prompt"""
desc = shot.get('description', '')
camera = shot.get('camera', '中景')
characters = shot.get('characters', [])
scenes = shot.get('scenes', [])
props = shot.get('props', [])
# 构建英文prompt
parts = []
if camera:
parts.append(camera)
if scenes:
parts.append(f"in {scenes[0]}")
parts.append(desc[:80])
if project_style:
parts.append(project_style)
prompt = ", ".join(parts)
return prompt
def generate_comfyui_api_json(shot, base_workflow_path, prompt, output_dir, shot_num):
"""为单镜生成ComfyUI API调用JSON"""
# 读取基础工作流
try:
with open(base_workflow_path, 'r') as f:
workflow = json.load(f)
except:
# 如果文件不存在,创建一个最小工作流
workflow = {
"3": {
"class_type": "KSampler",
"inputs": {
"seed": shot_num * 100 + 42,
"steps": 20,
"cfg": 7,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1,
"model": ["4", 0],
"positive": ["6", 0],
"negative": ["7", 0],
"latent_image": ["5", 0]
}
},
"4": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": "realvisxlV40_v40BDFPonNoobVae.safetensors"}},
"5": {"class_type": "EmptyLatentImage", "inputs": {"width": 1024, "height": 768, "batch_size": 1}},
"6": {"class_type": "CLIPTextEncode", "inputs": {"text": prompt, "clip": ["4", 1]}},
"7": {"class_type": "CLIPTextEncode", "inputs": {"text": "worst quality, low quality, blurry", "clip": ["4", 1]}},
"8": {"class_type": "VAEDecode", "inputs": {"samples": ["3", 0], "vae": ["4", 2]}},
"9": {"class_type": "SaveImage", "inputs": {"filename_prefix": f"shot_{shot_num:04d}", "images": ["8", 0]}}
}
return workflow
# 如果用了现有工作流替换prompt
for node_id, node in workflow.items():
if isinstance(node, dict):
ct = node.get('class_type', '')
if 'CLIPTextEncode' in ct or 'Prompt' in ct:
if 'text' in node.get('inputs', {}):
workflow[node_id]['inputs']['text'] = prompt
if 'KSampler' in ct or 'Sampler' in ct:
if 'seed' in node.get('inputs', {}):
workflow[node_id]['inputs']['seed'] = shot_num * 100 + 42
return workflow
def generate_shell_script(shots, output_dir, comfy_api_url="http://127.0.0.1:8188"):
"""生成批量调用ComfyUI API的shell脚本"""
script_path = os.path.join(output_dir, 'batch_render.sh')
lines = [
'#!/bin/bash',
f'# 批量渲染分镜 - 自动生成',
f'# ComfyUI API: {comfy_api_url}',
f'# 分镜数: {len(shots)}',
f'# 生成时间: {__import__("datetime").datetime.now().isoformat()}',
'',
'set -e',
'',
'TOTAL=' + str(len(shots)),
'SUCCESS=0',
'FAIL=0',
'',
]
for i, shot in enumerate(shots):
sn = shot.get('shot_number', f'S{i+1:02d}')
desc = shot.get('description', '')[:40]
wf_file = os.path.join(output_dir, f'workflow_{i+1:04d}.json')
out_file = os.path.join(output_dir, f'shot_{i+1:04d}.png')
lines.extend([
f'',
f'echo "🎬 [{i+1}/$TOTAL] {sn}: {desc}"',
f'echo " → 发送ComfyUI API..."',
f'RESP=$(curl -s -X POST "{comfy_api_url}/prompt" \\',
f' -H "Content-Type: application/json" \\',
f' -d @{wf_file})',
f'if echo "$RESP" | grep -q "error"; then',
f' echo " ❌ 失败: $RESP"',
f' FAIL=$((FAIL + 1))',
f'else',
f' echo " ✅ 已提交"',
f' SUCCESS=$((SUCCESS + 1))',
f'fi',
])
lines.extend([
'',
'echo "=========================="',
'echo "📊 渲染完成: $SUCCESS 成功, $FAIL 失败 / $TOTAL 总镜"',
])
with open(script_path, 'w') as f:
f.write('\n'.join(lines))
os.chmod(script_path, 0o755)
return script_path
def generate_prompt_file(shots, output_dir, project_style=""):
"""生成每个镜头的prompt文本文件"""
prompt_path = os.path.join(output_dir, 'prompts.txt')
with open(prompt_path, 'w', encoding='utf-8') as f:
for i, shot in enumerate(shots):
sn = shot.get('shot_number', f'S{i+1:02d}')
prompt = shot_to_prompt(shot, project_style)
f.write(f"=== {sn} ===\n")
f.write(f"景别: {shot.get('camera', 'N/A')}\n")
f.write(f"时长: {shot.get('duration', 'N/A')}s\n")
f.write(f"角色: {', '.join(shot.get('characters', []))}\n")
f.write(f"场景: {', '.join(shot.get('scenes', []))}\n")
f.write(f"Prompt: {prompt}\n\n")
return prompt_path
def main():
parser = argparse.ArgumentParser(description='分镜JSON → ComfyUI批量出图桥接')
parser.add_argument('storyboard', help='分镜JSON文件路径')
parser.add_argument('-o', '--output-dir', help='输出目录 (默认: 分镜JSON同目录下的renders/)')
parser.add_argument('--workflow', default=DEFAULT_WORKFLOW, help=f'基础工作流JSON (默认: {DEFAULT_WORKFLOW})')
parser.add_argument('--style', default='', help='全局风格描述 (如: cinematic, anime)')
parser.add_argument('--generate-sh', action='store_true', help='生成批量调用shell脚本')
parser.add_argument('--comfy-url', default='http://127.0.0.1:8188', help='ComfyUI API地址')
args = parser.parse_args()
# 读取分镜
storyboard = load_storyboard(args.storyboard)
shots = storyboard.get('shots', [])
if not shots:
print(f'❌ 分镜JSON中没有shots')
sys.exit(1)
# 输出目录
if args.output_dir:
out_dir = args.output_dir
else:
sb_dir = os.path.dirname(os.path.abspath(args.storyboard))
out_dir = os.path.join(sb_dir, 'renders')
os.makedirs(out_dir, exist_ok=True)
print(f'📖 分镜: {args.storyboard}')
print(f'🎬 共 {len(shots)}')
print(f'📁 输出: {out_dir}')
print()
# 1. 生成prompt文件
prompt_path = generate_prompt_file(shots, out_dir, args.style)
print(f'✅ Prompt文件: {prompt_path}')
# 2. 为每镜生成工作流JSON
wf_dir = os.path.join(out_dir, 'workflows')
os.makedirs(wf_dir, exist_ok=True)
for i, shot in enumerate(shots):
sn = shot.get('shot_number', f'S{i+1:02d}')
prompt = shot_to_prompt(shot, args.style)
workflow = generate_comfyui_api_json(shot, args.workflow, prompt, wf_dir, i+1)
wf_path = os.path.join(wf_dir, f'workflow_{i+1:04d}.json')
with open(wf_path, 'w', encoding='utf-8') as f:
json.dump(workflow, f, ensure_ascii=False, indent=2)
print(f'✅ 工作流JSON: {len(shots)}个 → {wf_dir}/')
# 3. 可选生成批量shell脚本
if args.generate_sh:
sh_path = generate_shell_script(shots, out_dir, args.comfy_url)
print(f'✅ 批量脚本: {sh_path}')
print(f' 运行: bash {sh_path}')
# 4. 总结
print()
print('📊 各镜一览:')
for i, shot in enumerate(shots):
sn = shot.get('shot_number', f'S{i+1:02d}')
prompt = shot_to_prompt(shot, args.style)
print(f' {sn:>4} | {shot.get("camera","?"): <4} | {shot.get("duration","?"):>2}s | {prompt[:50]}...')
if __name__ == '__main__':
main()

View File

@ -0,0 +1,294 @@
#!/usr/bin/env python3
"""
video_composer.py 自动剪辑拼接工具
功能多视频片段拼接 + 可选字幕 + 片头片尾
依赖FFmpeg (已安装)
用法:
python tools/video_composer.py <视频文件夹> -o 输出.mp4 [选项]
选项:
-o, --output FILE 输出文件路径 (默认: output.mp4)
-s, --sort NAME 排序方式: name(按文件名), time(按修改时间) (默认: name)
--fps N 输出帧率 (默认: 24)
--subtitle FILE 字幕文件路径 (SRT格式, 可选)
--title TEXT 片头字幕 (可选, 3)
--credits TEXT 片尾字幕 (可选, 3)
--crossfade N 交叉淡入淡出帧数 (默认: 0, 不开启)
-v, --verbose 详细输出
"""
import os
import sys
import subprocess
import argparse
import glob
from pathlib import Path
def get_video_files(folder, sort_by='name'):
"""获取文件夹中的所有视频文件,按指定方式排序"""
ext_map = {'.mp4', '.mov', '.avi', '.mkv', '.webm', '.m4v', '.ts'}
files = []
for f in Path(folder).iterdir():
if f.suffix.lower() in ext_map and f.is_file():
files.append(str(f))
if sort_by == 'time':
files.sort(key=lambda x: os.path.getmtime(x))
else:
files.sort()
return files
def create_concat_file(files):
"""创建 FFmpeg concat 需要的临时文件列表"""
concat_path = '/tmp/video_concat_list.txt'
with open(concat_path, 'w') as f:
for video in files:
f.write(f"file '{video}'\n")
return concat_path
def generate_subtitle_filter(srt_path):
"""生成字幕滤镜"""
esc_path = srt_path.replace("'", "'\\\\''").replace(":", "\\:")
return f"subtitles='{esc_path}'"
def build_filter_complex(num_videos, crossfade=0, title_text=None, credits_text=None, subtitle_path=None):
"""
构建视频滤镜链
支持拼接 + 交叉淡入淡出 + 片头片尾文本 + 字幕
"""
filters = []
# 如果只有1个视频且不需要额外效果返回空
if num_videos == 1 and not title_text and not credits_text and not subtitle_path:
return None
filter_parts = []
overlay_inputs = []
if crossfade > 0 and num_videos > 1:
# 交叉淡入淡出方案: 用 overlay 和 fade
for i in range(num_videos):
filter_parts.append(f"[{i}:v]fade=t=in:st=0:d=0.5[v{i}];")
for i in range(num_videos):
offset = i * (1 - 0.5) if i > 0 else 0 # 简化计算,实际用 concat + crossfade 更复杂
pass
# 简化:用 concat 滤镜
inputs = ''.join([f"[{i}:v][{i}:a]" for i in range(num_videos)])
filter_parts.append(f"{inputs}concat=n={num_videos}:v=1:a=1[outv][outa]")
else:
# 简单拼接
inputs = ''.join([f"[{i}:v][{i}:a]" for i in range(num_videos)])
filter_parts.append(f"{inputs}concat=n={num_videos}:v=1:a=1[outv][outa]")
current_output = "[outv]"
# 片头字幕
if title_text:
esc_title = title_text.replace("'", "'\\\\''").replace(":", "\\:")
filter_parts.append(
f"[outv]drawtext=text='{esc_title}':"
f"fontcolor=white:fontsize=48:"
f"x=(w-text_w)/2:y=(h-text_h)/2:"
f"enable='between(t,0,3)'[outv];"
)
# 片尾字幕
if credits_text:
# 需要知道总时长才能定位片尾,比较复杂,移到 FFmpeg 命令中处理
pass
# 字幕
if subtitle_path:
pass # 字幕用独立的 -vf 参数处理更方便
return ' '.join(filter_parts) if filter_parts else None
def get_video_duration(video_path):
"""获取视频时长(秒)"""
cmd = [
'ffprobe', '-v', 'error', '-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1', video_path
]
result = subprocess.run(cmd, capture_output=True, text=True)
try:
return float(result.stdout.strip())
except:
return 0
def compose_videos(files, output_path, fps=24, subtitle_path=None,
title_text=None, credits_text=None, crossfade=0, verbose=False):
"""合成视频"""
if not files:
print("❌ 没有找到视频文件")
return False
print(f"🎬 找到 {len(files)} 个视频片段:")
for f in files:
dur = get_video_duration(f)
print(f" {Path(f).name} ({dur:.1f}s)")
# 方案A简单拼接用 concat demuxer最快
if len(files) > 1 and crossfade == 0 and not title_text and not credits_text and not subtitle_path:
concat_file = create_concat_file(files)
cmd = [
'ffmpeg', '-y',
'-f', 'concat', '-safe', '0',
'-i', concat_file,
'-c', 'copy', # 直接复制流,最快
output_path
]
if verbose:
print(f"🔧 执行: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"❌ 简单拼接失败: {result.stderr[:300]}")
# 降级到重新编码
cmd = [
'ffmpeg', '-y',
'-f', 'concat', '-safe', '0',
'-i', concat_file,
'-c:v', 'libx264', '-preset', 'fast', '-crf', '23',
'-c:a', 'aac', '-b:a', '128k',
'-r', str(fps),
output_path
]
result = subprocess.run(cmd, capture_output=True, text=True)
os.unlink(concat_file)
# 方案B重新编码支持字幕、片头片尾、交叉淡入淡出
else:
concat_file = create_concat_file(files)
# 构建滤镜
filter_parts = []
num_v = len(files)
if crossfade > 0 and num_v > 1:
# 用 concat + 复杂的交叉淡化
inputs = ''.join([f"[{i}:v]" for i in range(num_v)])
# 简化处理,用 concat 后再用 crossfade
# 实际上 crossfade 比较复杂,这里用简单版
pass
# 基础拼接
inputs = ''.join([f"[{i}:v][{i}:a]" for i in range(num_v)])
filter_chain = f"{inputs}concat=n={num_v}:v=1:a=1[outv][outa]"
extra_filters = []
# 片头字幕
if title_text:
extra_filters.append(
f"drawtext=text='{title_text}':"
f"fontcolor=white:fontsize=48:"
f"x=(w-text_w)/2:y=(h-text_h)/2:"
f"enable='between(t,0,3)'"
)
# 片尾字幕
if credits_text:
extra_filters.append(
f"drawtext=text='{credits_text}':"
f"fontcolor=white:fontsize=36:"
f"x=(w-text_w)/2:y=(h-text_h)/2:"
f"enable='gte(t,{max(0, get_video_duration(files[0]) - 3)})'"
)
# 字幕文件
if subtitle_path:
esc_sub = subtitle_path.replace(":", "\\:").replace("'", "'\\\\''")
extra_filters.insert(0, f"subtitles='{esc_sub}'")
filter_complex = filter_chain
if extra_filters:
filter_complex = f"{filter_chain};[outv]{','.join(extra_filters)}[outv]"
cmd = [
'ffmpeg', '-y',
'-f', 'concat', '-safe', '0',
'-i', concat_file,
'-filter_complex', filter_complex,
'-map', '[outv]', '-map', '[outa]',
'-c:v', 'libx264', '-preset', 'fast', '-crf', '23',
'-c:a', 'aac', '-b:a', '128k',
'-r', str(fps),
output_path
]
if verbose:
print(f"🔧 执行: ffmpeg [滤镜参数]")
print(f" 滤镜: {filter_complex}")
result = subprocess.run(cmd, capture_output=True, text=True)
os.unlink(concat_file)
if result.returncode == 0:
output_size = os.path.getsize(output_path) / (1024 * 1024)
print(f"✅ 合成完成: {output_path} ({output_size:.1f}MB)")
return True
else:
print(f"❌ 合成失败:")
print(result.stderr[:500])
return False
def main():
parser = argparse.ArgumentParser(
description='视频自动剪辑拼接工具 - 苍耳管线',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
%(prog)s ./clips/ -o episode.mp4
%(prog)s ./clips/ -o episode.mp4 --title "第1集" --credits "待续"
%(prog)s ./clips/ -o episode.mp4 --subtitle subtitles.srt
%(prog)s ./clips/ -o episode.mp4 --crossfade 24
"""
)
parser.add_argument('folder', help='视频片段文件夹')
parser.add_argument('-o', '--output', default='output.mp4', help='输出文件')
parser.add_argument('-s', '--sort', choices=['name', 'time'], default='name', help='排序方式')
parser.add_argument('--fps', type=int, default=24, help='输出帧率')
parser.add_argument('--subtitle', help='字幕SRT文件')
parser.add_argument('--title', help='片头文字')
parser.add_argument('--credits', help='片尾文字')
parser.add_argument('--crossfade', type=int, default=0, help='交叉淡入淡出帧数')
parser.add_argument('-v', '--verbose', action='store_true', help='详细输出')
args = parser.parse_args()
if not os.path.isdir(args.folder):
print(f"❌ 文件夹不存在: {args.folder}")
sys.exit(1)
files = get_video_files(args.folder, args.sort)
if not files:
print(f"❌ 文件夹中没有视频文件: {args.folder}")
sys.exit(1)
success = compose_videos(
files, args.output,
fps=args.fps,
subtitle_path=args.subtitle,
title_text=args.title,
credits_text=args.credits,
crossfade=args.crossfade,
verbose=args.verbose
)
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()