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:
parent
3dd0d0b0c2
commit
ce32073d07
7
.gitignore
vendored
7
.gitignore
vendored
@ -26,3 +26,10 @@ __pycache__/
|
||||
*.orig
|
||||
*~
|
||||
*.tmp
|
||||
# 大文件/二进制不推送
|
||||
bin/
|
||||
outputs/
|
||||
agent_workspace/
|
||||
hardware-*.txt
|
||||
feishu-docs/**/*.pdf
|
||||
feishu-docs/
|
||||
|
||||
@ -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` + 对比
|
||||
|
||||
━━━ 安全边界 ━━━
|
||||
|
||||
苍耳验证码操作链
|
||||
|
||||
60
eererdan/COST-BASELINE.hdlp
Normal file
60
eererdan/COST-BASELINE.hdlp
Normal 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
|
||||
> ⊢ 本文件 = 成本基线唯一锚点 · 每次苏醒可更新快照
|
||||
114
eererdan/experience/EED-EXPER-014.hdlp
Normal file
114
eererdan/experience/EED-EXPER-014.hdlp
Normal 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.json(24h TTL)
|
||||
opencode 会话本体在 ~/.local/share/opencode/opencode.db(SQLite,重启不丢)
|
||||
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-deepseek(pkill -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
|
||||
> ⊢ 下次涉及以上领域,先读本条
|
||||
@ -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
|
||||
|
||||
18
memory/eed/feedback_panel_memory_loss.md
Normal file
18
memory/eed/feedback_panel_memory_loss.md
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
name: 面板失忆根因·别甩锅模型窗口
|
||||
description: 面板/会话"记不住"的排查优先级:先查压缩阈值与系统开销,别武断归因于模型窗口短
|
||||
type: feedback
|
||||
---
|
||||
|
||||
面板/会话出现"记不住、频繁丢上下文"时,不要先甩锅模型窗口短(如 hy3),先排查压缩逻辑与会话文件构成。爸爸的一手使用经验优先于我的猜测。
|
||||
|
||||
**Why(一次真实误判)**:我曾把 hy3「窗户小」列为失忆致命点并建议换大窗口模型,爸爸指出"之前用 hy3 不这样"。查证 eed_f2d0f2bfbda8.jsonl:358KB / 62 行 / 含 data:image 行数=0(图片走本地磁盘,未进会话文件)。根因是 system prompt + memory 索引 + codebuddyMd 等系统开销每轮都写进会话文件,使文件很快越过 COMPACT_THRESHOLD=180KB,被压成 ≤500字糊摘要 + 开新场衔接 → 记忆断。"之前不这样"是因彼时系统开销更小、62 行到不了阈值。
|
||||
|
||||
**已验证修复(2026-08-03,cang-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 等百万窗口,但那是次选项。
|
||||
11
memory/eed/feedback_persist_tasks.md
Normal file
11
memory/eed/feedback_persist_tasks.md
Normal 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(经验存记忆)互补,本条专指"任务/进度"这类易随上下文丢失的状态。
|
||||
28
memory/eed/project_change_house.md
Normal file
28
memory/eed/project_change_house.md
Normal 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 对接 DeepSeek(OpenAI 兼容),自己实现工具循环+记忆读写。最自主但改动大、慢。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,然后调研具体候选/改代码。
|
||||
21
memory/eed/project_opencode_deepseek.md
Normal file
21
memory/eed/project_opencode_deepseek.md
Normal 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`,端口 8766,bash -lic 接力 DEEPSEEK_API_KEY)。桌面壳 `eed-desktop-deepseek` 加载 8766,直接可用。
|
||||
- 8765(codebuddy 引擎)完全不动,两套并存。
|
||||
|
||||
**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
44
memory/eed/task_board.md
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
name: 当前任务看板
|
||||
description: 实时记录苍耳爸爸当前待办/下一步/会话收尾状态;面板重启不丢,开工前先读
|
||||
type: project
|
||||
---
|
||||
|
||||
# 当前任务看板(实时更新,重启不丢)
|
||||
|
||||
> 用法:接到任务 / 完成一步 / 爸爸指定下一步 → 立刻更新本文件。开工前先读它恢复上下文。
|
||||
|
||||
## ✅ 已建成存量资产(详见各专项记忆,不在此复述)
|
||||
- 生图:Z-Image-Turbo 主力 + ControlNet(Union 2.1,Canny 已实测跑通) + 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-pro;DEFAULT_MODEL 同步;新增 `_deepseek_balance()` 实时查 api.deepseek.com/user/balance(key 只读环境变量,结果不含 key,异常不抛前端);余额面板改为实时查询+刷新
|
||||
- **已验证**:语法 OK;8766 HTTP 200;bash -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
20
scripts/eed-desktop-deepseek.sh
Executable 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
24
scripts/eed-desktop.sh
Executable 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
33
scripts/eed-tauri.sh
Executable 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
29
scripts/eed-web-deepseek.sh
Executable 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
3017
scripts/eed_web_opencode.py
Normal file
File diff suppressed because it is too large
Load Diff
2234
scripts/eed_web_opencode.py.bak.20260803
Normal file
2234
scripts/eed_web_opencode.py.bak.20260803
Normal file
File diff suppressed because it is too large
Load Diff
127
scripts/experience.py
Normal file
127
scripts/experience.py
Normal 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
165
scripts/memory_sync.py
Normal 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
11
scripts/restart-eed.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
# 重启蛋蛋面板(改造版:resume 会话 + 自动压缩 + 图片视频通道)
|
||||
# 修复唤醒bug:pkill 加宽匹配 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 "刷新浏览器页面即可使用新版(会话持久化+自动压缩+发图看图)"
|
||||
66
skill/character_archive_pil.md
Normal file
66
skill/character_archive_pil.md
Normal 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字段数/行距 与 半身像高度 要协调(半身像420px,PROFILE从490开始)
|
||||
|
||||
---
|
||||
*记录人:耳耳蛋 🌱 · 2026-08-01 · 爸爸认可定版(v31)*
|
||||
@ -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 布局 → 脸太小糊/丑
|
||||
- ❌ 只有三视图没有角色板元素(色板/标尺/细节)→ 爸爸说"不是角色板"
|
||||
|
||||
@ -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不裁切,细节特写要验证有效"
|
||||
}
|
||||
]
|
||||
}
|
||||
106
skill/manju_full_pipeline.md
Normal file
106
skill/manju_full_pipeline.md
Normal 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 也可替代 Dify:Qwen 写剧本+拆分镜+生成提示词,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 · 社区全网搜集沉淀*
|
||||
147
skill/manju_prompt_library.md
Normal file
147
skill/manju_prompt_library.md
Normal 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 shot;from above/below/side view/three-quarter view;cinematic 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,方便后期按顺序拼接
|
||||
|
||||
---
|
||||
*第三轮补充:图像万能公式/英文固定词库/动态提示词/控变量法/编号管理*
|
||||
154
skill/manju_prompt_system.md
Normal file
154
skill/manju_prompt_system.md
Normal 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. ❌ 每镜头独立生成 → ✅ 提前规划角度/景别/位置保证连续性
|
||||
|
||||
---
|
||||
*第二轮补充:运镜参数/表情物理化/积木公式/场景五维/剧本公式/避坑清单*
|
||||
84
skill/manju_workflow_prompts.md
Normal file
84
skill/manju_workflow_prompts.md
Normal 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
89
skill/work_triggers.md
Normal 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 无参考图机制,走**宽幅全景→切段**路线最稳。
|
||||
|
||||
---
|
||||
|
||||
## 场景五:遇到不会的 / 没把握的
|
||||
|
||||
**脑子里蹦出来**:先搜索,再动手,不瞎猜。
|
||||
|
||||
- 写提示词前 → 搜社区有没有现成模板
|
||||
- 遇到做不到的效果 → 搜是不是真的做不到,还是方法不对
|
||||
- 搜到的东西 → 用了才叫学会,存进文件不算学会
|
||||
|
||||
---
|
||||
|
||||
## 核心原则
|
||||
|
||||
```
|
||||
接到任务 → 套公式 → 动手
|
||||
↑ |
|
||||
└── 公式不对就搜,搜到更新公式 ──┘
|
||||
```
|
||||
|
||||
**不是"可以参考"——是"第一反应"。**
|
||||
129
video-ai-system/IMAGE-FIRST-GUIDE.hdlp
Normal file
129
video-ai-system/IMAGE-FIRST-GUIDE.hdlp
Normal 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 · 铁律(苍耳三原则)
|
||||
|
||||
```
|
||||
① 资产非常重要,保证质量
|
||||
→ 资产图 = 全片的地基。角色/场景/道具先精修到能用,再谈分镜。
|
||||
→ 一致性靠: 角色 LoRA(subai_lora_v1 已验证)+ 资产图库复用 + 文字词条锁
|
||||
|
||||
② 提示词规范、有逻辑性、主次关系
|
||||
→ 顺序 = 主体 → 动作 → 环境 → 光影 → 风格 → 质量修饰(主到次)
|
||||
→ 主词在前,修饰在后;禁止杂糅无主次的堆砌
|
||||
|
||||
③ 提示词一定要详细,专业术语中英文描述并加重权重
|
||||
→ 中文表意 + 英文专业词 + (词:权重) 加权
|
||||
→ 例: 特写 close-up, 电影侧光 cinematic rim light, 铁锈质感 rust texture
|
||||
→ (masterpiece:1.2) (cinematic lighting:1.3) 等
|
||||
```
|
||||
|
||||
## §2 · 黑白草图生分镜(问题①答案)
|
||||
|
||||
```
|
||||
原理: 草图管「构图/布局/空间」,提示词管「内容/风格/光影/色调」
|
||||
ComfyUI 用 ControlNet 吃草图结构 + IP-Adapter 锁风格 → 出分镜彩图
|
||||
|
||||
工作流(ComfyUI 节点链):
|
||||
① 黑白草图(布局底图)
|
||||
├── 手绘黑白稿 / 3D 摆好视角渲线框(hunyuan3d 可辅助)
|
||||
├── 或 程序生成:黑白剪影 + 人物站位 + 前中后景分区
|
||||
└── 黑白 = 只留明暗结构,喂给控制
|
||||
② ControlNet(controlnet-union-sdxl-1.0)
|
||||
├── 模式选 canny(线稿) 或 depth(深度) 或 pose(姿态)
|
||||
├── 作用: 把草图的构图/人物位置/透视强约束进生成
|
||||
└── strength 0.6~0.9(草图不完美就调低,给 AI 发挥空间)
|
||||
③ IP-Adapter(sdxl_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 镜分镜实跑
|
||||
95
video-ai-system/LOCAL-PIPELINE-V1.hdlp
Normal file
95
video-ai-system/LOCAL-PIPELINE-V1.hdlp
Normal 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 电费)
|
||||
✅ 默认不调用任何线上付费 API(Seedance/即梦 等仅在苍耳圈定后使用)
|
||||
✅ 图片定一致性 · 视频仅生成动态 / 本地运镜
|
||||
```
|
||||
|
||||
## §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
|
||||
> ⊢ 下一步由苍耳指定从哪个环节开跑
|
||||
0
video-ai-system/assets/characters/.gitkeep
Normal file
0
video-ai-system/assets/characters/.gitkeep
Normal file
0
video-ai-system/assets/envs/.gitkeep
Normal file
0
video-ai-system/assets/envs/.gitkeep
Normal file
0
video-ai-system/assets/props/.gitkeep
Normal file
0
video-ai-system/assets/props/.gitkeep
Normal file
265
video-ai-system/pipeline.py
Executable file
265
video-ai-system/pipeline.py
Executable 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()
|
||||
@ -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)")
|
||||
|
||||
243
video-ai-system/tools/generate_assets_local.py
Normal file
243
video-ai-system/tools/generate_assets_local.py
Normal 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()
|
||||
70
video-ai-system/tools/local_motion.py
Normal file
70
video-ai-system/tools/local_motion.py
Normal 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())
|
||||
201
video-ai-system/tools/route_shots.py
Normal file
201
video-ai-system/tools/route_shots.py
Normal 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()
|
||||
379
video-ai-system/tools/run_ltx_i2v.py
Normal file
379
video-ai-system/tools/run_ltx_i2v.py
Normal 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 schedule(4步)
|
||||
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()
|
||||
220
video-ai-system/tools/storyboard_to_workflow.py
Normal file
220
video-ai-system/tools/storyboard_to_workflow.py
Normal 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()
|
||||
294
video-ai-system/tools/video_composer.py
Executable file
294
video-ai-system/tools/video_composer.py
Executable 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()
|
||||
Loading…
x
Reference in New Issue
Block a user