Initial commit: Air Spark project
- frontend/: Next.js 16 app (App Router, React 19, Tailwind v4) - skills/: project skills (seedance, automation, trae-agents, etc.) - Docs: PRD, UI-Design-System, DEV-LOG, seedance integration notes - skills-lock.json: skills version lock Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
acbd2e30ad
47
.gitignore
vendored
Normal file
47
.gitignore
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
# dependencies
|
||||
node_modules/
|
||||
.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# Next.js / build output
|
||||
.next/
|
||||
out/
|
||||
build/
|
||||
coverage/
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# AI tools / editor caches (re-synced from skills-lock.json)
|
||||
.agent/
|
||||
.agents/
|
||||
.claude/
|
||||
.trae/
|
||||
.vscode/
|
||||
|
||||
# OS / editor
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
594
DEV-LOG.md
Normal file
594
DEV-LOG.md
Normal file
@ -0,0 +1,594 @@
|
||||
# Air Spark — 开发交互记录
|
||||
|
||||
> 记录开发者与 Claude Code 的协作过程,积累 AI 辅助开发的最佳实践。
|
||||
> 持续更新中。
|
||||
|
||||
---
|
||||
|
||||
## Session 1 — 项目基础搭建
|
||||
|
||||
**日期**:2026-03-03
|
||||
|
||||
### 用户需求
|
||||
- 提供了产品 PRD 初稿,定义了 AI 驱动动画生产平台的完整功能
|
||||
- 要求确定技术选型和项目架构
|
||||
|
||||
### Claude 完成的工作
|
||||
- 审阅 PRD,与用户讨论并迭代到 v0.4
|
||||
- 确定技术栈:Next.js 14 + FastAPI + PostgreSQL + Celery/Redis
|
||||
- 设计了七阶段流水线架构(剧本→规划→人设→分镜→切分→视频→拼接)
|
||||
- 创建了三套自动化 Skill 提示词(storyboard / segmentation / json-schemas)
|
||||
- 确定了关键设计决策(网格按场景分、PIL 精确裁切、Stage 6 全并发等)
|
||||
|
||||
### 关键决策
|
||||
- **网格尺寸按场景时长决定**(≤45s→4格, 45s-2min→9格, >2min→两个9格)
|
||||
- **JSON-only 输出**用于自动化 Skill(区别于人用的 Markdown Skill)
|
||||
- **移除 prev_frame 依赖**,Stage 6 全部片段可并发提交
|
||||
|
||||
---
|
||||
|
||||
## Session 2 — UI 设计系统 & 组件展示页
|
||||
|
||||
**日期**:2026-03-04
|
||||
|
||||
### 用户需求
|
||||
- 要求建立完整的 UI 设计系统
|
||||
- 风格关键词:liquid glass、glassmorphism、dark mode、premium、cinematic
|
||||
- 使用 ui-ux-pro-max skill 生成设计建议
|
||||
|
||||
### Claude 完成的工作
|
||||
1. **运行 ui-ux-pro-max 设计系统生成器**,获得专业推荐(配色、字体、风格)
|
||||
2. **编写 UI-Design-System.md v0.3**,包含:
|
||||
- 色彩系统(影院暗色背景 + 品牌色 + 状态色)
|
||||
- 玻璃拟态层级规范(标准/激活/悬停三层)
|
||||
- 字体系统(Space Grotesk + DM Sans + JetBrains Mono)
|
||||
- 按钮/表单/卡片/Toast 等组件规范
|
||||
3. **搭建 Next.js 14 前端项目**(App Router + TypeScript + Tailwind CSS v4)
|
||||
4. **开发 21 节组件展示页**(`/showcase`),覆盖:
|
||||
- 色彩 & 玻璃层、字体排版、按钮、状态格子
|
||||
- 流水线步骤条、审核卡片、骨架屏、剧本确认
|
||||
- 日志面板、模态框、表单、Toast、空状态
|
||||
- 徽章/标签、头像、导航路径、标签页
|
||||
- 危险按钮 & 搜索框、提示气泡、文字对比度
|
||||
5. **添加全局环境背景动效**(CSS 渐变光晕 + 动画)
|
||||
|
||||
### 用户反馈 & 迭代
|
||||
- 用户要求添加**中英文切换**(i18n)
|
||||
- Claude 实现了页面级 `lang` 状态 + `t` 翻译对象(~110 个键)
|
||||
- 用户指出多处翻译问题,逐一修正:
|
||||
- 字体排版区域仍显示英文 → 添加中文示例文本
|
||||
- "Stage 6" 未翻译 → 改为"第六阶段"
|
||||
- "面包屑" 翻译生硬 → 改为"导航路径"
|
||||
- "Air Spark 工作室" → 去掉"工作室"/"Studio"
|
||||
- 补充了 **Noto Sans SC(思源黑体)** 中文字体支持
|
||||
|
||||
### 关键决策
|
||||
- Showcase 页面用**页面级 `t` 对象**做 i18n(临时方案)
|
||||
- 正式应用将来用 **next-intl** + URL 路由(`/zh/...`, `/en/...`)
|
||||
- 品牌名统一为 **Air Spark**(不带 Studio / 工作室)
|
||||
|
||||
---
|
||||
|
||||
## Session 3 — 开发策略讨论
|
||||
|
||||
**日期**:2026-03-04
|
||||
|
||||
### 用户需求
|
||||
- 了解 Claude Code 2026 新功能(Swarm、Worktree、Batch 等)
|
||||
- 考虑使用 Swarm 团队模式进行并行开发
|
||||
- 要求 Claude 给出客观建议,以最快完成项目为目标
|
||||
- 要求建立**开发交互记录文档**,积累协作经验
|
||||
|
||||
### Claude 的建议
|
||||
用户原有流程(先 UI → 再交互 → 再后端)本质正确。**不建议为了用新功能而改变有效流程。**
|
||||
|
||||
Swarm 现阶段不适合全面启用的原因:
|
||||
1. 缺少清晰的任务边界(核心页面 UI 未确定)
|
||||
2. 前后端 API 契约未定义(并行开发会导致接口不一致)
|
||||
3. 实验功能有额外协调开销
|
||||
|
||||
**推荐路线:**
|
||||
- **阶段 A**(串行):设计锁定 → 确定核心页面 UI → 反推 API 契约
|
||||
- **阶段 B**(并行/Swarm):前端交互 + 后端 API + 数据库同时推进
|
||||
- **阶段 C**:集成联调
|
||||
|
||||
### 经验总结
|
||||
> **给团队的建议**:AI 新功能(如 Swarm)应在**任务边界清晰时**才启用。
|
||||
> 过早并行化 = 用协调开销换开发速度,得不偿失。
|
||||
> 正确做法是先串行想清楚,再并行干活。
|
||||
|
||||
---
|
||||
|
||||
## Session 4 — 核心页面开发(阶段 A:App Shell + 项目列表)
|
||||
|
||||
**日期**:2026-03-04
|
||||
|
||||
### 用户需求
|
||||
- 要求后续每完成一步主动更新交互文档
|
||||
- 开始开发核心页面,询问逐个做还是一起做
|
||||
|
||||
### Claude 的建议
|
||||
**先做一个页面锁定 App Shell,再批量出剩余页面。** 原因:
|
||||
- 第一个页面确定布局骨架(侧边栏 + 内容区),所有页面共用
|
||||
- Shell 确认后,剩余页面只是填内容,可以快速批量产出
|
||||
|
||||
### Claude 完成的工作
|
||||
|
||||
#### 1. App Shell 布局骨架
|
||||
- **Sidebar 组件**(`components/layout/Sidebar.tsx`):
|
||||
- 可折叠侧边栏(240px ↔ 68px),玻璃拟态背景
|
||||
- 四个导航项:项目、资产库、Skills、设置
|
||||
- 当前路由高亮(accent 紫色)
|
||||
- 底部用户信息区 + 折叠按钮
|
||||
- collapsed 状态由父组件控制,sidebar 和 main 内容区联动
|
||||
- **Dashboard Layout**(`app/dashboard/layout.tsx`):
|
||||
- 管理 collapsed 状态,传递给 Sidebar
|
||||
- main 区域 margin-left 随 sidebar 折叠联动动画
|
||||
|
||||
#### 2. 项目列表页(`app/dashboard/page.tsx`)
|
||||
- **页面头部**:标题 + 「创建项目」CTA 按钮
|
||||
- **搜索栏 + 统计**:实时搜索过滤 + 项目数/运行数统计
|
||||
- **项目卡片网格**:响应式(1/2/3/4 列)
|
||||
- 每张卡片包含:类型标签、项目名、集数、运行状态
|
||||
- **七阶段进度条**:彩色条形图,直观展示流水线进度
|
||||
- 当前阶段 + 最后更新时间
|
||||
- Hover 效果 + 更多操作按钮
|
||||
- **空状态**:无项目时的引导界面
|
||||
- **搜索无结果**:提示换关键词
|
||||
- 4 条 Mock 数据覆盖不同状态(running / completed / failed)
|
||||
|
||||
#### 3. Build 验证
|
||||
- `npx next build` 通过,无 TypeScript 错误
|
||||
|
||||
### 新增文件
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `components/layout/Sidebar.tsx` | 可折叠侧边栏组件 |
|
||||
| `app/dashboard/layout.tsx` | Dashboard 布局(sidebar + main) |
|
||||
| `app/dashboard/page.tsx` | 项目列表页 |
|
||||
|
||||
### 用户确认
|
||||
- 用户确认项目列表页视觉方向 OK,要求继续做其他页面
|
||||
- 发现左下角有黑色小圆球 → 是 Next.js Dev Indicator,在 `next.config.ts` 中关闭
|
||||
|
||||
---
|
||||
|
||||
## Session 4(续)— 批量产出核心页面
|
||||
|
||||
**日期**:2026-03-04
|
||||
|
||||
### 用户需求
|
||||
- 确认 App Shell 视觉方向后,批量产出剩余核心页面
|
||||
|
||||
### Claude 完成的工作
|
||||
|
||||
#### 1. 项目详情页(`app/dashboard/[projectId]/page.tsx`)
|
||||
- **面包屑导航**:项目 > T仔的上班日记
|
||||
- **项目信息卡**:名称、类型标签、描述、Skill 标签列表
|
||||
- **统计行**:总集数、运行中、已完成
|
||||
- **剧集列表**:每行包含:
|
||||
- 集数编号(大字)+ 标题 + 状态标签(草稿/运行中/已完成/失败/空闲)
|
||||
- Mini 七阶段进度条
|
||||
- 当前阶段 + 时长 + 更新时间
|
||||
- Hover 显示操作按钮(剧本 / 流水线 / 开始写剧本)
|
||||
- 5 条 Mock 剧集数据覆盖各种状态
|
||||
|
||||
#### 2. 剧本对话页(`app/dashboard/[projectId]/chat/page.tsx`)
|
||||
- **类 Claude.ai 风格**的全屏对话界面
|
||||
- **顶栏**:返回按钮 + 项目/集数标题 + Skill 信息 + 「保存为正式剧本」按钮
|
||||
- **消息区域**:
|
||||
- 用户消息(紫色气泡,右对齐)
|
||||
- AI 回复(玻璃卡片,左对齐,带复制/重新生成按钮)
|
||||
- 简单的 Markdown 渲染(加粗、分隔线)
|
||||
- **输入区域**:多行文本框 + 发送按钮(有内容时亮紫色)
|
||||
- **剧本确认流程**:点「保存为正式剧本」→ 顶部出现绿色 banner → 按钮变为「确认剧本,启动流水线」
|
||||
- 4 条 Mock 对话,包含完整的剧本内容(T仔第一天上班的 7 个场景)
|
||||
|
||||
#### 3. 流水线监控页(`app/dashboard/[projectId]/pipeline/[episodeId]/page.tsx`)
|
||||
- **七阶段步骤条**:可点击切换查看每个阶段,当前阶段高亮
|
||||
- **Stage 6 视频生成视图**(主要展示):
|
||||
- 渐变进度条 + 百分比 + 完成数
|
||||
- 状态统计行(完成/生成中/失败/队列中)
|
||||
- 8×4 片段状态网格(32 个格子,颜色编码 + 图标)
|
||||
- 失败片段操作栏(编辑提示词 / 重跑)
|
||||
- 「导出 Seedance 批次包」按钮
|
||||
- **Stage 3 人设审核视图**:
|
||||
- 4 张角色卡片(占位图 + 名称 + 通过/重跑按钮)
|
||||
- 底部汇总 CTA(全部通过后可点击)
|
||||
- **已完成阶段**:绿色 ✓ + 描述 + 「查看输出详情」
|
||||
- **待执行阶段**:灰色时钟 + 等待提示
|
||||
- **可折叠系统日志面板**:时间戳 + 级别颜色 + 消息
|
||||
|
||||
#### 4. Build 验证
|
||||
所有 6 个路由编译通过:
|
||||
- `/dashboard` — 项目列表
|
||||
- `/dashboard/[projectId]` — 项目详情
|
||||
- `/dashboard/[projectId]/chat` — 剧本对话
|
||||
- `/dashboard/[projectId]/pipeline/[episodeId]` — 流水线监控
|
||||
|
||||
### 新增文件
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `app/dashboard/[projectId]/page.tsx` | 项目详情页(剧集列表) |
|
||||
| `app/dashboard/[projectId]/chat/page.tsx` | 剧本对话页(Claude.ai 风格) |
|
||||
| `app/dashboard/[projectId]/pipeline/[episodeId]/page.tsx` | 流水线七阶段监控页 |
|
||||
|
||||
### 页面导航路径
|
||||
```
|
||||
/dashboard → 项目列表(4 张卡片)
|
||||
/dashboard/proj_001 → 项目详情(5 集剧集列表)
|
||||
/dashboard/proj_001/chat?ep=1 → 剧本对话(AI 对话 + 剧本确认)
|
||||
/dashboard/proj_001/pipeline/ep_01 → 流水线监控(七阶段 + 片段网格)
|
||||
```
|
||||
|
||||
### 经验总结
|
||||
> **批量产出的前提是 Shell 已锁定。** 确认了侧边栏 + 布局后,三个页面只用了一轮就全部产出,
|
||||
> 没有布局返工。这验证了「先串行锁定骨架,再批量出内容」的策略。
|
||||
|
||||
---
|
||||
|
||||
## Session 5 — 用户反馈 & 迭代修复
|
||||
|
||||
**日期**:2026-03-04
|
||||
|
||||
### 用户反馈(4 个页面全部审阅后)
|
||||
1. **流水线监控页** — 片段网格是正方形,不符合设计规范(应为自然高度 + padding)
|
||||
2. **剧本对话页** — 纯对话模式不合理,需要改为**左右分栏**:
|
||||
- 左:剧本文档(可查看/可编辑,类似 IDE 中的文件面板)
|
||||
- 右:AI 对话(提需求、提修改意见)
|
||||
- 用户原话:"如果把所有内容都堆在一个对话框里,我无法看到完整的剧本,也没办法手动微调"
|
||||
3. **路由 bug** — 点击侧边栏「资产库」跳到了项目详情(`/dashboard/assets` 被 `[projectId]` 动态路由匹配了)
|
||||
4. **项目列表 + 项目详情** — 暂无大问题
|
||||
|
||||
### Claude 修复内容
|
||||
|
||||
#### 1. 路由 bug 修复
|
||||
**根因**:`/dashboard/assets` 没有对应的 `page.tsx`,被 `[projectId]` 动态路由吞掉。
|
||||
**修复**:创建三个静态路由页面(优先级高于动态路由):
|
||||
- `app/dashboard/assets/page.tsx` — 资产库(空状态占位)
|
||||
- `app/dashboard/skills/page.tsx` — Skills 管理(3 个 Skill 卡片)
|
||||
- `app/dashboard/settings/page.tsx` — 设置(API Key、流水线模式配置)
|
||||
|
||||
#### 2. 剧本对话页重设计 → 分栏式剧本工作台
|
||||
**核心改造**:从纯对话布局改为左右分栏(可拖拽调整宽度):
|
||||
- **左侧 — 剧本面板(55% 默认宽度)**:
|
||||
- 预览模式:Markdown 渲染(标题、对话、舞台指示、分隔线)
|
||||
- 编辑模式:纯文本 textarea,可手动修改
|
||||
- 顶部切换按钮(预览 ↔ 编辑)
|
||||
- 场景数 + 时长统计
|
||||
- **中间 — 可拖拽分隔条**(hover 显示拖拽图标)
|
||||
- **右侧 — AI 对话面板**:
|
||||
- 更紧凑的气泡(7px avatar、更小的间距)
|
||||
- AI 回复内容改为摘要式("已更新场景二和场景五"),而非全文
|
||||
- 输入框 2 行高度,适配右侧窄面板
|
||||
- **联动逻辑**(Mock 演示):AI 修改剧本后左侧同步更新
|
||||
|
||||
#### 3. 片段网格修复
|
||||
移除 `aspect-square`,改为 showcase 标准:`p-2.5 flex-col items-center gap-1.5`,自然高度。
|
||||
|
||||
#### 4. Build 验证
|
||||
9 个路由全部编译通过。
|
||||
|
||||
### 新增文件
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `app/dashboard/assets/page.tsx` | 资产库页面(空状态) |
|
||||
| `app/dashboard/skills/page.tsx` | Skills 管理页面 |
|
||||
| `app/dashboard/settings/page.tsx` | 设置页面 |
|
||||
|
||||
### 经验总结
|
||||
> **最有价值的反馈来自用户的使用场景描述。** 用户没有说"把聊天框放左边",而是描述了
|
||||
> 实际使用痛点:"无法看到完整剧本,也没办法手动微调"。从痛点推导出分栏式设计,
|
||||
> 比纯视觉反馈更能驱动正确的产品决策。
|
||||
|
||||
> **动态路由的坑**:Next.js App Router 中,`[projectId]` 会匹配所有未定义的子路径。
|
||||
> 静态路由必须显式创建 `page.tsx` 才能优先匹配。
|
||||
|
||||
---
|
||||
|
||||
## Session 6 — 用户深度反馈 & 功能完善
|
||||
|
||||
**日期**:2026-03-04
|
||||
|
||||
### 用户反馈(详细审阅全部页面后)
|
||||
1. **剧本对话页**:需支持 3 种使用模式(引导生成 / 大纲辅助 / 优化已有剧本 + 文件上传)
|
||||
2. **发送按钮**:与输入框不对齐、样式丑,需修复
|
||||
3. **剧本内容**:用真实剧本 `ep01.md` 替换 Mock 数据
|
||||
4. **资产库页**:需要 Mock 画廊(角色立绘做缩略图,点击查看三视图)
|
||||
5. **Skills 页**:卡片需可点击进入详情/管理页
|
||||
6. **项目卡片**:点击不跳转到项目详情(缺少路由链接)
|
||||
7. **全局路由**:确保所有页面间跳转正确
|
||||
|
||||
### Claude 完成的工作
|
||||
|
||||
#### 1. 项目卡片导航修复(`dashboard/page.tsx`)
|
||||
- 将 `ProjectCard` 组件的容器从 `<div>` 改为 `<Link href={/dashboard/${project.id}}`
|
||||
- 卡片现在可正确点击跳转到项目详情页
|
||||
|
||||
#### 2. 真实剧本替换(`[projectId]/chat/page.tsx`)
|
||||
- 读取 `skills/测试skills三件套/原创剧本-skill/scripts/ep01.md` 的完整内容
|
||||
- 替换 MOCK_SCRIPT 为《恐龙也是打工龙》第1集完整剧本(4 场 + 片尾彩蛋)
|
||||
- 更新 MOCK_MESSAGES 对话内容,匹配真实剧本场景
|
||||
|
||||
#### 3. 发送按钮修复(`[projectId]/chat/page.tsx`)
|
||||
- **之前**:`absolute` 定位在 textarea 内部,位置不准且样式突兀
|
||||
- **修复**:改为 `flex items-end gap-2` 布局,按钮独立在输入框右侧
|
||||
- 固定 `w-10 h-10 rounded-xl`,与输入框底部对齐
|
||||
- 有内容时紫色发光,无内容时半透明 + 边框
|
||||
- 去掉 `pr-12`(不再需要为内嵌按钮留位置)
|
||||
|
||||
#### 4. 资产库画廊页(`dashboard/assets/page.tsx`)— 完整重写
|
||||
- **搜索 + 分类筛选**:全部 / 角色 / 场景 / 道具(带计数)
|
||||
- **资产卡片网格**(响应式 2-6 列):
|
||||
- 缩略图用渐变色占位(角色=暖色、场景=冷色、道具=粉色)
|
||||
- 类型标签(左上角)+ 关联剧集标签(右下角)
|
||||
- 名称 + 更新时间 + 版本号
|
||||
- Hover 效果(缩略图亮度提升)
|
||||
- **资产详情弹窗**(点击卡片打开):
|
||||
- 角色:展示正面/侧面/背面三视图
|
||||
- 场景/道具:展示大图预览
|
||||
- 生成提示词(可编辑)
|
||||
- 关联剧集列表 + 版本历史
|
||||
- 操作按钮:重新生成 / 编辑提示词后重新生成
|
||||
- Mock 数据:4 个角色 + 3 个场景 + 1 个道具(来自 ep01.md 剧本)
|
||||
|
||||
#### 5. Skills 管理页(`dashboard/skills/page.tsx`)— 完整重写
|
||||
- **列表视图**:3 个 Skill 卡片(可点击进入详情)
|
||||
- 显示名称 + 英文 ID + 描述 + 关联阶段 + 更新时间
|
||||
- Hover 显示箭头指引
|
||||
- **详情视图**(点击卡片后展示):
|
||||
- 左侧:系统提示词编辑器(预览/编辑切换,类似剧本面板)
|
||||
- 右侧信息栏:
|
||||
- 基本信息(描述、关联阶段、模型、类型标签、更新时间)
|
||||
- 使用此 Skill 的项目列表
|
||||
- 版本历史
|
||||
- 操作:编辑 → 保存 → 版本自增
|
||||
- Mock 数据包含真实的 Skill 提示词摘要
|
||||
|
||||
#### 6. Build 验证
|
||||
9 个路由全部编译通过,无 TypeScript 错误。
|
||||
|
||||
### 修改文件
|
||||
| 文件 | 修改内容 |
|
||||
|------|---------|
|
||||
| `dashboard/page.tsx` | ProjectCard 包裹 Link 实现点击导航 |
|
||||
| `[projectId]/chat/page.tsx` | 真实剧本 + 发送按钮修复 |
|
||||
| `dashboard/assets/page.tsx` | 完整重写 → 画廊 + 详情弹窗 |
|
||||
| `dashboard/skills/page.tsx` | 完整重写 → 卡片列表 + 详情编辑 |
|
||||
|
||||
### 经验总结
|
||||
> **Mock 数据应尽早使用真实内容。** 用真实的 ep01.md 剧本做 Mock,不仅让页面更有代入感,
|
||||
> 还能验证 Markdown 渲染逻辑是否正确覆盖了实际格式(标题、角色表、舞台指示等)。
|
||||
|
||||
> **UI 组件要考虑不同数据类型的差异化展示。** 资产库中角色(三视图)和场景(单张大图)的
|
||||
> 展示方式不同,需要在组件层面做类型判断而非统一模板。
|
||||
|
||||
---
|
||||
|
||||
## 附录:交互模式总结
|
||||
|
||||
### 有效的交互习惯
|
||||
| 习惯 | 说明 |
|
||||
|------|------|
|
||||
| **给出风格关键词** | "liquid glass, cinematic dark" 比 "好看一点" 有效 10 倍 |
|
||||
| **即时指出翻译/文案问题** | 逐条修正比最后统一改效率高 |
|
||||
| **要求客观建议** | 明确说"要冷静客观",避免 AI 一味附和 |
|
||||
| **先确认视觉再写逻辑** | 减少返工,UI 是需求的最佳载体 |
|
||||
| **描述使用场景而非UI指令** | "我无法看到完整剧本" > "把文本放左边" |
|
||||
|
||||
### 可以改进的地方
|
||||
| 场景 | 建议 |
|
||||
|------|------|
|
||||
| 大批量文本替换 | 可以一次性提供所有翻译对照表,减少来回 |
|
||||
| 品牌命名 | 尽早统一(我们到 Session 2 末尾才去掉 "Studio") |
|
||||
| 设计规范 | 先锁定再开发,避免边开发边改规范 |
|
||||
| 路由设计 | 动态路由旁的静态路由要提前占位,避免被吞 |
|
||||
|
||||
---
|
||||
|
||||
## Session 7 — 深度迭代:命名修正 + Skills 架构重设计
|
||||
|
||||
**日期**:2026-03-04
|
||||
|
||||
### 用户反馈
|
||||
1. **Stage 3 命名**:"人设"不准确,只涵盖角色但该阶段生成所有参考图 → 改为"图片资产"
|
||||
2. **项目卡片三点菜单**:三个点按钮无功能,应弹出操作菜单(设置/复制/归档/删除)
|
||||
3. **三视图展示**:不应是三张分割图,而是一张横版 16:9 图(正面半身+正面全身+侧面+背面)
|
||||
4. **Skills 架构根本问题**:当前 UI 把 Skill 当成"单一系统提示词",但实际 Skill 是目录级知识包:
|
||||
```
|
||||
skill-name/.claude/skills/{name}/
|
||||
├── SKILL.md ← 入口文件
|
||||
├── references/ ← AI 按需读取的知识库
|
||||
└── templates/ ← 输出模板
|
||||
```
|
||||
需要完全重设计为文件树 + 文件编辑器(IDE 风格)
|
||||
5. **Stage 2 命名**:"规划"太模糊,用户不明白这步做什么 → 改为"提示词提取"
|
||||
6. **未来 Skill 安装**:需要"安装 Skill"按钮,支持导入外部 Skill
|
||||
|
||||
### Claude 完成的工作
|
||||
|
||||
#### 1. 流水线阶段命名修正(全局 7 文件)
|
||||
- Stage 2:`"规划"` → `"提示词提取"`(6 个文件,描述更清楚地告诉用户此步做什么)
|
||||
- Stage 3:`"人设"` → `"图片资产"`(5 个文件,覆盖角色/场景/道具所有参考图)
|
||||
- 涉及文件:`dashboard/page.tsx`, `[projectId]/page.tsx`, `pipeline/[episodeId]/page.tsx`, `showcase/page.tsx`, `skills/page.tsx`
|
||||
- Showcase 页英文同步更新:`"Planning"` → `"Prompt Extract"`, `"Characters"` → `"Image Assets"`
|
||||
|
||||
#### 2. 项目卡片下拉菜单(`dashboard/page.tsx`)
|
||||
- 新增 `ProjectCardMenu` 组件,处理 `stopPropagation`(菜单在 Link 包裹的卡片内)
|
||||
- 四个操作项:项目设置 / 复制项目 / 归档 / 删除(红色危险操作)
|
||||
- 点击外部自动关闭(`useRef` + `useEffect` 监听 `mousedown`)
|
||||
- 菜单样式:毛玻璃深色背景 `rgba(15,15,25,0.95)` + `backdropFilter: blur(20px)`
|
||||
|
||||
#### 3. 三视图修复(`dashboard/assets/page.tsx`)
|
||||
- **之前**:3 列网格展示 3 张独立的正面/侧面/背面图
|
||||
- **修复**:单张 `aspect-video` 横版 16:9 图,内部用 `flex` 等分 4 区域
|
||||
- 4 区域:正面半身 / 正面全身 / 侧面 / 背面
|
||||
- 底部半透明标签标注每个区域
|
||||
- 区域间白色分隔线
|
||||
|
||||
#### 4. Skills 管理页完整重写(`dashboard/skills/page.tsx`)
|
||||
**架构级重设计**,从"单一提示词编辑器"改为"目录级知识包管理器":
|
||||
|
||||
- **列表视图**:
|
||||
- 每张卡片显示文件统计(总文件数 / references / templates)
|
||||
- 顶部新增"安装 Skill"按钮(为未来外部 Skill 导入预留)
|
||||
|
||||
- **详情视图**(IDE 风格):
|
||||
- **左侧文件树**(260px 宽):
|
||||
- 可折叠文件夹(SKILL.md、CLAUDE.md、references/、templates/)
|
||||
- 文件类型图标区分(FileText / Folder)
|
||||
- 当前选中文件高亮
|
||||
- **右侧文件编辑器**:
|
||||
- 预览 / 编辑模式切换
|
||||
- 文件路径面包屑
|
||||
- 文件描述显示
|
||||
- Mock 数据基于真实 Skill 目录结构
|
||||
|
||||
- Mock 数据来源:`D:\Air spark\skills\测试skills三件套\` 的实际文件结构
|
||||
|
||||
#### 5. Build 验证
|
||||
9 个路由全部编译通过,零 TypeScript 错误。
|
||||
|
||||
### 修改文件
|
||||
| 文件 | 修改内容 |
|
||||
|------|---------|
|
||||
| `dashboard/page.tsx` | 阶段命名修正 + ProjectCardMenu 下拉菜单 |
|
||||
| `[projectId]/page.tsx` | 阶段命名修正 |
|
||||
| `pipeline/[episodeId]/page.tsx` | 阶段命名修正("人设"→"图片资产" + "规划"→"提示词提取") |
|
||||
| `showcase/page.tsx` | 阶段命名修正(中英文 + toast) |
|
||||
| `dashboard/assets/page.tsx` | 三视图改为单张 16:9 横图 |
|
||||
| `dashboard/skills/page.tsx` | 完整重写 → 文件树 + 编辑器(IDE 风格) |
|
||||
|
||||
### 经验总结
|
||||
> **命名即沟通。** "规划"对开发者清楚但用户困惑,"提示词提取"直接告诉用户这步做什么。
|
||||
> 面向用户的阶段名应该用动词+名词结构描述动作,而非抽象名词。
|
||||
|
||||
> **Skill 不是 prompt。** 把 Skill 简化为单一系统提示词是对其能力的阉割。
|
||||
> 真实的 Skill 是一个目录结构化的知识包,模型根据用户需求选择性读取不同文件。
|
||||
> UI 设计必须反映这种目录结构,否则管理界面会误导用户。
|
||||
|
||||
---
|
||||
|
||||
## Session 8 — 阶段 A 收尾:全流程页面补全
|
||||
|
||||
**日期**:2026-03-05
|
||||
|
||||
### 背景
|
||||
用户确认了完整计划后,从 Step 0 到 Step 9 全部执行。核心目标:补全端到端流程链路 + 审核回退 + 剧本三栏工作台 + 所有管线阶段视图。
|
||||
|
||||
### 完成的工作
|
||||
|
||||
#### Step 0+1: Pipeline 组件拆分 + ReviewGate + 级联失效
|
||||
- 提取 `components/pipeline/` 目录,4 个核心组件:PipelineStepper、ReviewGate、SegmentGrid、LogPanel
|
||||
- PipelineStepper 新增 `invalidated` 状态(amber ⚠️ + 删除线)+ 可点击跳转
|
||||
- ReviewGate 通用审核门:3 种模式(review / auto_passed / invalidated)
|
||||
- 级联失效逻辑:重跑/回退 → 下游全部标记 `invalidated`,旧数据保留
|
||||
- `page.tsx` 瘦身:430 → ~200 行,纯布局 + 状态管理 + stage 分发
|
||||
|
||||
#### Step 2: 剧本工作台(三栏 IDE 布局)
|
||||
- `chat/page.tsx` 从双栏改为三栏:文件树(200px) + 文件预览/编辑(flex-1) + AI Chat(380px)
|
||||
- 文件树反映 screenplay-skill 7 阶段产出:concept → outline → characters → world → synopsis → beats → scripts
|
||||
- 4 种文件状态:✅ done / 📝 draft / ⏳ pending / ⚠️ outdated
|
||||
- Markdown 预览 + 纯文本编辑模式切换
|
||||
- 顶栏:剧本阶段指示器(① ② ③ ④ ⑤ ⑥ ⑦)+ "确认剧本,启动流水线" CTA
|
||||
- 引导模式提示:当前进度、下一步建议
|
||||
|
||||
#### Steps 3-7: 五个阶段视图
|
||||
| 文件 | 阶段 | 内容 |
|
||||
|------|------|------|
|
||||
| `StagePromptExtract.tsx` | Stage 2 | 三 Tab(角色/场景/Keyshot)+ 可展开编辑提示词 |
|
||||
| `StageImageAssets.tsx` | Stage 3 | 三 Tab(角色/场景/道具)+ 总进度 + 单资产重跑 |
|
||||
| `StageKeyshots.tsx` | Stage 4 | 按场景手风琴 + 宫格预览 + 裁切格子横滚画廊 |
|
||||
| `StageSegments.tsx` | Stage 5 | 片段列表 + 时码 + 参考图引用 + Seedance 提示词预览 |
|
||||
| `StageTimeline.tsx` | Stage 7 | 16:9 预览区 + 时间轴条 + 场景色编码 + 导出CTA |
|
||||
|
||||
#### Step 8: 流程串联
|
||||
- `dashboard/page.tsx` 新增 CreateProjectModal(2步表单:名称+类型 → 集数+技能预览)
|
||||
- 项目详情"添加剧集"按钮 → Link 到 chat 页面
|
||||
- 项目详情"设置"按钮 → Link 到项目设置页
|
||||
- chat 页"确认剧本,启动流水线" → Link 到 pipeline 页面
|
||||
|
||||
#### Step 9: 设置 + 项目设置
|
||||
- `dashboard/settings/page.tsx`:完善 API Keys(Claude + Banana Pro + Seedance)+ 流水线模式(全自动/逐步审核/自定义)+ 自定义时 7 阶段审核开关
|
||||
- 新建 `[projectId]/settings/page.tsx`:项目名/描述/类型 + 技能配置 + 渲染风格(6选项)+ 视频参数(比例+模型)+ 危险区(归档/删除)
|
||||
- 首页 `/` 重定向从 `/showcase` 改为 `/dashboard`
|
||||
|
||||
### 文件变更清单
|
||||
|
||||
| 操作 | 文件 |
|
||||
|------|------|
|
||||
| 新建 | `components/pipeline/PipelineStepper.tsx` |
|
||||
| 新建 | `components/pipeline/ReviewGate.tsx` |
|
||||
| 新建 | `components/pipeline/SegmentGrid.tsx` |
|
||||
| 新建 | `components/pipeline/LogPanel.tsx` |
|
||||
| 新建 | `components/pipeline/StagePromptExtract.tsx` |
|
||||
| 新建 | `components/pipeline/StageImageAssets.tsx` |
|
||||
| 新建 | `components/pipeline/StageKeyshots.tsx` |
|
||||
| 新建 | `components/pipeline/StageSegments.tsx` |
|
||||
| 新建 | `components/pipeline/StageTimeline.tsx` |
|
||||
| 新建 | `dashboard/[projectId]/settings/page.tsx` |
|
||||
| 重写 | `dashboard/[projectId]/pipeline/[episodeId]/page.tsx` |
|
||||
| 重写 | `dashboard/[projectId]/chat/page.tsx` |
|
||||
| 重写 | `dashboard/settings/page.tsx` |
|
||||
| 修改 | `dashboard/page.tsx`(添加 CreateProjectModal) |
|
||||
| 修改 | `dashboard/[projectId]/page.tsx`(添加剧集→chat,设置→settings 链接) |
|
||||
| 修改 | `app/page.tsx`(重定向 → /dashboard) |
|
||||
|
||||
### 经验总结
|
||||
> **端到端先于细节。** 用户最在意的不是单个页面的精美度,而是"从创建项目到导出成片"的完整链路是否畅通。
|
||||
> 先把 ReviewGate + 步骤条跳转 + 页面间 Link 全部串起来,再逐个打磨阶段视图。
|
||||
|
||||
> **三栏布局是内容创作工具的正确范式。** 剧本工作台从双栏升级为三栏后,"文件即产出"的概念变得清晰 —— 用户能看到 AI 生成了什么文件、当前在第几步、下一步做什么。
|
||||
|
||||
> **组件拆分的时机。** 当一个文件超过 300 行且包含多个独立子组件时,就应该拆。Pipeline page 从 430 行拆到 ~200 行后,添加 5 个新阶段视图几乎零成本。
|
||||
|
||||
---
|
||||
|
||||
## Session 9 — 用户预览反馈:流水线三阶段 UI 修复
|
||||
|
||||
**日期**:2026-03-12
|
||||
|
||||
### 背景
|
||||
用户启动前端预览,对 Stage 4 / 6 / 7 三个阶段视图进行了详细审阅,提出了 3 类问题。
|
||||
|
||||
### 用户反馈
|
||||
1. **Stage 4(宫格生成)内容重复**:场景列表渲染了两遍 —— 上方 `grid-cols-2` 卡片组展示宫格缩略图,下方 accordion 又列了同样的场景带可展开的格子描述。
|
||||
2. **Stage 6(视频生成)无法预览视频**:只有状态格子(完成/生成中/失败),点击完成的片段没有任何反馈。用户无法判断视频质量是否需要重跑。此外失败提示硬编码"片段 08",多个失败时不会动态列出。
|
||||
3. **Stage 7(剪辑导出)布局问题**:
|
||||
- 视频预览窗口 `max-w-2xl mx-auto` 偏小且居中,不符合剪辑软件操作直觉
|
||||
- 时间轴片段下方出现一个 glass-card 展示"片段01 — 黑屏OS引入",用户标注"干嘛用的???"——该卡片是点击时间轴片段后显示的选中详情,与预览区信息完全重复
|
||||
|
||||
### Claude 修复内容
|
||||
|
||||
#### 1. StageKeyshots.tsx — 删除重复,合并视图
|
||||
- **删除**上方 `grid grid-cols-2` 独立卡片组(4 个场景卡片 + 宫格缩略图)
|
||||
- **合并**到 accordion 展开面板内:先展示宫格缩略图(mini grid),再展示格子描述横滚画廊
|
||||
- 移除未使用的 `Camera` import
|
||||
|
||||
#### 2. SegmentGrid.tsx — 新增视频预览 + 动态失败处理
|
||||
- **新增** `selectedId` 状态 + 点击"已完成"片段打开顶部预览面板(16:9 播放区 + 关闭按钮)
|
||||
- **选中态** ring 高亮,区分当前预览的片段
|
||||
- **点击约束**:只有 `status === "done"` 的片段可点击预览,其余 cursor-default
|
||||
- **动态失败列表**:`segments.filter(s => s.status === "failed").map(...)` 替代硬编码"片段 08",多个失败时逐行显示
|
||||
|
||||
#### 3. StageTimeline.tsx — 全宽预览 + 删除重复信息卡
|
||||
- **预览区**:移除 `max-w-2xl mx-auto`,改为 `aspect-video` 全宽填充,尺寸与页面宽度一致
|
||||
- **删除** "Selected Clip Info" glass-card(lines 147-164),该信息已在预览区内通过文字叠层展示
|
||||
- 保留 `Image` import(预览区占位图标仍在使用)
|
||||
|
||||
### 修改文件
|
||||
| 文件 | 修改内容 |
|
||||
|------|---------|
|
||||
| `components/pipeline/StageKeyshots.tsx` | 删除重复卡片组,宫格缩略图合并入 accordion |
|
||||
| `components/pipeline/SegmentGrid.tsx` | 新增视频预览面板 + 动态失败列表 |
|
||||
| `components/pipeline/StageTimeline.tsx` | 预览全宽 + 删除冗余信息卡 |
|
||||
|
||||
### 经验总结
|
||||
> **重复信息 = 用户困惑。** 同一份数据渲染两次(Stage 4 的卡片+手风琴、Stage 7 的预览区+详情卡),
|
||||
> 用户不会认为"这是两种视角",只会觉得"为什么出现两遍"。宁可合并到一处,也不要拆成两个看起来类似的组件。
|
||||
|
||||
> **Mock 阶段也要考虑交互完整性。** Stage 6 只展示状态格子而没有点击预览,用户会认为功能缺失。
|
||||
> 即使是占位 UI,也应该提供基本交互反馈(选中高亮、预览面板),让用户理解"这里将来能做什么"。
|
||||
|
||||
> **硬编码 Mock 的陷阱。** 失败提示写死"片段 08"在单个失败时没问题,但用户会追问"如果 4/5/6/7/8 都失败呢?"
|
||||
> 从一开始就用 `.filter().map()` 动态渲染,成本几乎为零但避免了这个问题。
|
||||
264
PRD.md
Normal file
264
PRD.md
Normal file
@ -0,0 +1,264 @@
|
||||
# Air Spark — PRD
|
||||
|
||||
> v0.4 | 2026-03-04
|
||||
|
||||
---
|
||||
|
||||
## 1. 产品定位
|
||||
|
||||
AI 驱动的动画自动化生产平台。导演通过对话生成剧本,系统自动串联图片生成 → 视频生成 → 拼接,输出完整成片。
|
||||
|
||||
先供 Air Spark 内部使用,架构支持未来扩展为 SaaS。
|
||||
|
||||
---
|
||||
|
||||
## 2. 用户角色
|
||||
|
||||
| 角色 | 核心需求 |
|
||||
|------|---------|
|
||||
| **导演** | 对话写剧本 → 一键触发流水线 → 审核成片 |
|
||||
| **管理员** | 管理成员、API Key、Skill 配置 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 内容层级
|
||||
|
||||
```
|
||||
项目(Project) ← 一部动画 IP,建立时选内容类型 → 自动加载 Skill 包
|
||||
├── 资产库(Asset Library) ← 角色/场景图,跨集跨阶段复用
|
||||
└── 剧集(Episode) ← EP01, EP02 ...
|
||||
├── 剧本(script.md)
|
||||
├── 流水线状态
|
||||
└── 各阶段输出文件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Agent + Skill 架构
|
||||
|
||||
**核心设计原则**:`模型 + Skill = Agent`
|
||||
|
||||
Skill 是系统提示词(System Prompt),定义 Agent 的行为规则和输出格式。
|
||||
扩展内容类型 = 新增 Skill,不需要改代码。
|
||||
|
||||
```
|
||||
screenplay-skill → 生成符合 Seedance 输入格式的剧本(△行即提示词)
|
||||
storyboard-skill → 按场景生成 Keyshot 宫格图(空间位置锚点)
|
||||
segmentation-skill → 按内容逻辑将剧本切分为 Seedance 片段(内容驱动,非机械按秒切)
|
||||
|
||||
两条并行线,都以剧本为输入,互不依赖:
|
||||
剧本 → storyboard-skill → Keyshot 图(空间参考)
|
||||
↘
|
||||
剧本 → segmentation-skill → 片段提示词 → Seedance(提示词 + 参考图)
|
||||
↗
|
||||
剧本 → 参考图生成(Banana Pro)→ 人设图、场景图
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 七阶段流水线
|
||||
|
||||
```
|
||||
Stage 1 剧本对话
|
||||
只加载 screenplay-skill
|
||||
导演与 Claude 对话,生成符合 Seedance 格式的剧本
|
||||
满意后点「确认剧本」→ 保存 script.md
|
||||
↓ [手动确认]
|
||||
|
||||
Stage 2 资产 & 分镜规划(Claude · storyboard-skill)
|
||||
读取 script.md → 输出:
|
||||
· characters.json 角色列表 + 英文提示词 + 标记性特征
|
||||
· scenes.json 场景列表 + 英文提示词 + 每场估算时长
|
||||
· keyshots.json 每场景的 Keyshot 规划:
|
||||
- 格数(4格/9格,按时长自动选)
|
||||
- 每格的空间位置描述(英文)
|
||||
- 宫格图生成提示词
|
||||
时长估算规则(与 segmentation-skill 共用):
|
||||
对白:3-4字/秒;普通动作:2-3秒;复杂动作:4-5秒
|
||||
格数规则:
|
||||
场景 ≤ 45秒 → 4宫格(2×2,每格~11秒)
|
||||
场景 45秒-2分钟 → 9宫格(3×3,每格~8-13秒)
|
||||
场景 > 2分钟 → 拆成两个9宫格(在自然剧情节点分割)
|
||||
↓ [可选审核]
|
||||
|
||||
Stage 3 参考图生成(Banana Pro API · text2img)
|
||||
每类图片生成规则:
|
||||
人设图 × N(每角色两步生成):
|
||||
Step 1:生成正面立绘(白色背景)→ character_{id}_front.jpg
|
||||
Step 2:以正面立绘 + 固定三视图模板后缀为输入,生成三视图
|
||||
→ 输出一张 16:9 图:包含正面胸像(左)+ 正面全身 + 侧面 + 背面
|
||||
→ 最终存入资产库:character_{id}.jpg
|
||||
小提示词后缀(固定模板,后续由用户提供):
|
||||
"生成这个角色的三视图,包括:正面胸像(位于画布最左侧),正面视角、侧面视角、背面视角,浅灰色无缝背景,干净且简约"
|
||||
场景图 × M(每场景1张,最佳角度,不含角色)
|
||||
道具图 × K(重要道具,按需)
|
||||
→ 存入资产库(命名与 characters.json / scenes.json 的 id 一一对应)
|
||||
↓ [人工审核:动画前期必须确认人设]
|
||||
|
||||
Stage 4 Keyshot 宫格生成(Banana Pro API · text2img)
|
||||
读取 keyshots.json:每个场景 → 1次 API 调用
|
||||
输入:宫格提示词 + 该场景人设图 + 场景图(来自资产库)
|
||||
生成尺寸:
|
||||
4宫格 → 2560×1440(裁切后每格 1280×720)
|
||||
9宫格 → 3840×2160(裁切后每格 1280×720)
|
||||
PIL 精确裁切(本地,零成本):
|
||||
→ keyshot_{scene_id}_{keyshot_index}_{cell_num}.jpg
|
||||
Keyshot 映射:
|
||||
每段 Seedance 片段按起始时码查找对应 Keyshot cell
|
||||
cell_num = floor((seg_start - scene_start) / cell_duration) + 1
|
||||
↓ [可选审核:快速扫一遍,确认空间位置无明显错误]
|
||||
|
||||
Stage 5 剧本切分(Claude · segmentation-skill)
|
||||
读取 script.md → 按内容逻辑切分(非机械按秒):
|
||||
依据:场景边界(必切)、镜头切换点、对话节奏、情绪节点
|
||||
上限:每段 ≤ 15秒(Seedance 2.0 最大时长)
|
||||
输出:segments.json
|
||||
每段包含:
|
||||
- 时码、时长、场景ID、出场角色ID
|
||||
- Seedance 提示词文本(剧本原文,一字不改)
|
||||
- 参考图列表(语义声明):
|
||||
[{"type":"character","id":"char_001"},
|
||||
{"type":"scene","id":"scene_002"},
|
||||
{"type":"keyshot","scene_id":"scene_002","keyshot_index":1,"cell_num":3}]
|
||||
不使用 prev_frame(上一段尾帧):
|
||||
- prev_frame 依赖上一段视频已生成,强制串行,破坏并发
|
||||
- Seedance 2.0 参生机制(人设图+场景图+keyshot+提示词)已保障角色一致性
|
||||
- keyshot cell 图承担空间位置锚点,替代尾帧的连续性职责
|
||||
- 不同场景之间本就是"硬切",不需要尾帧衔接
|
||||
↓ [可选审核]
|
||||
|
||||
Stage 6 视频生成(Seedance 2.0 · 全并发)
|
||||
前置条件:Stage 3(参考图)+ Stage 4(Keyshot 格图)+ Stage 5(segments.json)均完成
|
||||
所有片段同时提交,无任何顺序依赖
|
||||
Seedance API 请求结构(火山引擎 Ark):
|
||||
content 数组(按顺序排列):
|
||||
1. {type: "text", text: <完整提示词>}
|
||||
2. {type: "image_url", image_url: {url: <图1>}, role: "reference_image"}
|
||||
3. {type: "image_url", image_url: {url: <图2>}, role: "reference_image"}
|
||||
...(最多 12 张,每张 role 均为 "reference_image")
|
||||
其他参数:generate_audio: true,duration: 片段时长,ratio: 项目视频比例
|
||||
提示词结构(后端自动拼装):
|
||||
{script_text(剧本原文,一字不改)}
|
||||
|
||||
{渲染风格},[图1]是{角色名},[图2]是{场景名},[图3]是{keyshot位置},[图N]是{道具名},
|
||||
你是一位专业的动画导演,自行安排分镜设计,切镜充满电影感,画面氛围也有电影感,不要有背景音乐,但要有音效。
|
||||
(动作戏追加:动作戏可以有一点荷兰式倾斜镜头,动作戏的镜头具有视觉张力和空间感。)
|
||||
注意:[图N] 序号与 content 数组中 image_url 的顺序严格对应,由后端拼装时自动编号
|
||||
参考图拼装顺序(固定):角色人设图(按 character_ids 顺序)→ 场景图 → keyshot cell 图 → 道具图
|
||||
任务队列(Celery + Redis):
|
||||
- FastAPI 接受触发请求 → 推入 Redis 队列
|
||||
- Celery Worker 消费队列 → POST 提交所有片段到 Seedance API
|
||||
- 统一异步轮询所有 job_id → 下载完成的片段
|
||||
- 任务状态持久化在 Redis,服务重启不丢失,支持断点续跑
|
||||
错误处理:自动重试3次(指数退避 1s/4s/16s),429 按响应头等待,超限后人工介入
|
||||
断点续跑:跳过已 completed 片段,只重跑 failed/pending
|
||||
过渡期(API 未开放前):使用 Seedance 1.5 Pro API,仅 model_id 不同,逻辑一致
|
||||
↓ [可选审核:单段重跑时可修改提示词]
|
||||
|
||||
Stage 7 剪辑审核 & 导出(时间轴 UI + FFmpeg)
|
||||
可视化时间轴界面(类 Medeo / 剪映简化版):
|
||||
· 底部时间轴:每个片段一个缩略图块,标注时长和场景名
|
||||
· 拖拽排序:拖动片段调整顺序,自动更新 concat 序列
|
||||
· 单片段预览:点击片段 → 中间预览区播放该片段
|
||||
· 右键菜单:重新生成 / 编辑提示词后重跑 / 删除片段
|
||||
· 「预览全片」:服务端 FFmpeg concat → 返回预览 MP4(数秒内完成)
|
||||
· 「导出成片」:最终 FFmpeg concat -c copy → final-epXX.mp4
|
||||
FFmpeg 拼接命令:
|
||||
ffmpeg -f concat -safe 0 -i concat.txt -c copy final-ep01.mp4
|
||||
无损拼接,保留 Seedance 原生音画同步
|
||||
```
|
||||
|
||||
**流水线模式(项目级设置)**:
|
||||
- `全自动`:每阶段自动通过(Stage 1、Stage 3 除外,两处必须人工确认)
|
||||
- `逐步审核`:每阶段完成后等待导演批准
|
||||
- `自定义`:每个 Stage 单独配置是否需要审核
|
||||
|
||||
---
|
||||
|
||||
## 6. 审核门
|
||||
|
||||
每阶段完成后,导演可以:
|
||||
- **批准** → 继续下一阶段
|
||||
- **重跑(原提示词)** → 重新执行本阶段
|
||||
- **编辑提示词后重跑** → 修改后重新生成
|
||||
- Stage 3、4、6 支持**单个资产/片段重跑**,不用整阶段重来
|
||||
|
||||
---
|
||||
|
||||
## 7. 错误处理与断点续跑
|
||||
|
||||
每个原子任务(单次 API 调用)有独立状态:`pending / running / completed / failed`
|
||||
|
||||
- API 超时 / 5xx → 自动重试,最多3次(指数退避 1s / 4s / 16s)
|
||||
- 429 限速 → 按响应头等待后重试
|
||||
- 重试耗尽 → 标记 `failed`,审核门提示人工处理
|
||||
- 断点续跑:查询当前 Stage 所有任务,跳过 `completed`,只重跑 `failed` / `pending`
|
||||
- 任务状态持久化在 Redis,服务重启后状态不丢失
|
||||
|
||||
---
|
||||
|
||||
## 8. 资产库
|
||||
|
||||
- 存储:人设图、场景图、道具图、Keyshot 图
|
||||
- 命名规范:`{asset_type}_{id}.jpg`,与 JSON 中的 id 字段严格对应
|
||||
- 跨集复用:同项目下新剧集自动引用已有资产,可手动替换单张
|
||||
- 操作:查看 / 编辑提示词 / 重新生成
|
||||
|
||||
---
|
||||
|
||||
## 9. Skill 系统
|
||||
|
||||
**架构原则**:Skill = 系统提示词,定义 Agent 的行为和输出格式。扩展内容类型只需新增 Skill,不改代码。
|
||||
|
||||
**现有已验证 Skill(输出格式改造为 JSON,逻辑不变)**:
|
||||
- `screenplay-skill`:剧本生成/优化,输出符合 Seedance △行格式的剧本
|
||||
- `storyboard-skill`:场景/人设/Keyshot 提取,输出 characters.json / scenes.json / keyshots.json
|
||||
- `segmentation-skill`:剧本切分,输出 segments.json(含语义化参考图声明)
|
||||
|
||||
**Skill 改造重点**:
|
||||
- 现有 Skill 输出的是人类可读 Markdown,自动化需要输出程序可解析的 JSON
|
||||
- 改造原则:只改输出格式,不改提示词逻辑和规则
|
||||
- `@图x` 占位符改为语义声明(type + id),由后端查表解析为实际文件路径
|
||||
|
||||
**项目类型 → Skill 包**:
|
||||
| 内容类型 | 自动加载 |
|
||||
|---------|---------|
|
||||
| 原创动画 | screenplay + storyboard + segmentation |
|
||||
| 自定义 | 手动勾选 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 技术选型
|
||||
|
||||
| 层 | 选型 | 备注 |
|
||||
|----|------|------|
|
||||
| 前端 | Next.js 14 (App Router) | SSR,TypeScript,生态成熟 |
|
||||
| 后端 | Python FastAPI | AI 生态(PIL/异步),Skill 调用 |
|
||||
| 任务队列 | Celery + Redis | 管理 Stage 3/4/6 的并发 API 任务,状态持久化,断点续跑 |
|
||||
| 数据库 | PostgreSQL | 流水线状态、资产元数据、Skill 配置 |
|
||||
| 实时通信 | Server-Sent Events | 流水线进度推送 |
|
||||
| AI | Claude API (Opus 4.6) | 可配置 base_url,支持第三方代理 |
|
||||
| 图片生成 | Banana Pro 第三方 API | 适配器可替换,TBD 具体服务商 |
|
||||
| 视频生成 | Seedance 2.0(火山引擎 Ark)| 参生模型,过渡期用 1.5 Pro,升级只改 model_id |
|
||||
| 视频拼接 | FFmpeg concat -c copy | 无损,保留原生音画同步 |
|
||||
| 图片裁切 | Python Pillow(PIL)| 本地执行,零成本,精确 |
|
||||
| 文件存储 | 本地文件系统 → S3/OSS | 初期本地,后期迁移 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 不做的事(MVP 范围外)
|
||||
|
||||
- 片头片尾 / 主题曲管理(单独规划)
|
||||
- 视频转场特效
|
||||
- 多租户 / 账号体系(先单团队使用)
|
||||
- 计费功能
|
||||
- 移动端
|
||||
|
||||
---
|
||||
|
||||
## 12. 待确认
|
||||
|
||||
1. **Banana Pro API**:具体服务商和接口文档(影响 Stage 3/4 实现);各类图片的"小提示词"后缀模板,用户接入 API 后逐个提供
|
||||
2. **Claude API 代理**:具体用哪家(设计为可配置,不阻塞开发)
|
||||
3. **视频比例**:项目创建时设定(16:9 / 9:16 / 21:9),影响宫格生成尺寸
|
||||
4. **Seedance 并发限制**:火山引擎 Ark 的并发配额,影响 Stage 6 Celery Worker 并发数设置
|
||||
958
UI-Design-System.md
Normal file
958
UI-Design-System.md
Normal file
@ -0,0 +1,958 @@
|
||||
# Air Spark — UI Design System v0.3
|
||||
|
||||
> 2026-03-04 | 经 ui-ux-pro-max skill 审查优化
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计风格定位
|
||||
|
||||
**核心关键词**:Liquid Glass · Cinematic Dark · AI Native · Premium Tool
|
||||
|
||||
参考方向:Apple visionOS 2 液态玻璃 + TopNow 极简科技感
|
||||
视觉目标:让非技术用户一眼觉得"很贵",专业用户觉得"很懂行"
|
||||
|
||||
**ui-ux-pro-max 风格匹配**:Liquid Glass(Score: Best Match)
|
||||
- Best For: Premium SaaS, creative platforms, branding experiences
|
||||
- 注意事项:Performance ⚠ Moderate-Poor(需做 `prefers-reduced-motion` 降级)、Accessibility ⚠ Text contrast(需保证 4.5:1)
|
||||
|
||||
---
|
||||
|
||||
## 2. 色彩系统
|
||||
|
||||
### 背景层
|
||||
| Token | 值 | 用途 |
|
||||
|-------|----|------|
|
||||
| `--bg-base` | `#07070f` | 主背景(深空黑) |
|
||||
| `--bg-surface` | `#0d0d1a` | 表面层 |
|
||||
| `--bg-elevated` | `#12121f` | 抬升层(卡片底) |
|
||||
|
||||
### 玻璃层
|
||||
| Token | 值 | 用途 |
|
||||
|-------|----|------|
|
||||
| `--glass-01` | `rgba(255,255,255,0.04)` | 最轻玻璃 |
|
||||
| `--glass-02` | `rgba(255,255,255,0.07)` | 标准玻璃卡片 |
|
||||
| `--glass-03` | `rgba(255,255,255,0.12)` | 高亮玻璃(hover) |
|
||||
| `--glass-border` | `rgba(255,255,255,0.10)` | 玻璃描边 |
|
||||
| `--glass-border-bright` | `rgba(255,255,255,0.20)` | 高亮描边 |
|
||||
|
||||
### 品牌 & 强调色
|
||||
| Token | 值 | 用途 |
|
||||
|-------|----|------|
|
||||
| `--accent-primary` | `#6c63ff` | 紫色(AI / 创意 / 主 CTA) |
|
||||
| `--accent-blue` | `#3b82f6` | 蓝色(进行中状态) |
|
||||
| `--accent-glow` | `rgba(108,99,255,0.25)` | 紫色光晕 |
|
||||
|
||||
### 状态色
|
||||
| 状态 | 颜色 | Tailwind | 含义 |
|
||||
|------|------|----------|------|
|
||||
| pending | 灰 | `gray-600` | 队列中 |
|
||||
| running | 蓝 + 脉冲 | `blue-500` + `animate-pulse` | 生成中 |
|
||||
| completed | 绿 | `emerald-500` | 完成 |
|
||||
| failed | 红 | `red-500` | 失败 |
|
||||
| waiting | 琥珀 | `amber-500` | 等待人工确认 |
|
||||
|
||||
### 文字
|
||||
| Token | 值 | 用途 |
|
||||
|-------|----|------|
|
||||
| `--text-primary` | `#f1f0ff` | 主文字(微紫色调) |
|
||||
| `--text-secondary` | `#8b8ea8` | 次要文字 |
|
||||
| `--text-muted` | `#4c4f6b` | 弱化文字 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 玻璃效果规格(CSS)
|
||||
|
||||
```css
|
||||
/* 标准玻璃卡片 */
|
||||
.glass-card {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
backdrop-filter: blur(24px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.10);
|
||||
border-radius: 16px;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255,255,255,0.05) inset,
|
||||
0 8px 32px rgba(0,0,0,0.4),
|
||||
0 1px 0 rgba(255,255,255,0.12) inset;
|
||||
}
|
||||
|
||||
/* 激活/聚焦卡片(带品牌光晕)*/
|
||||
.glass-card--active {
|
||||
border-color: rgba(108, 99, 255, 0.4);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(108,99,255,0.2) inset,
|
||||
0 8px 40px rgba(0,0,0,0.5),
|
||||
0 0 24px rgba(108,99,255,0.15);
|
||||
}
|
||||
|
||||
/* 主 CTA 按钮 */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #6c63ff 0%, #5b54f0 100%);
|
||||
box-shadow: 0 0 20px rgba(108,99,255,0.4), 0 4px 12px rgba(0,0,0,0.3);
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
border-radius: 12px;
|
||||
padding: 14px 32px;
|
||||
}
|
||||
```
|
||||
|
||||
### Tailwind 实现片段
|
||||
|
||||
```tsx
|
||||
// 玻璃卡片
|
||||
<div className="bg-white/[0.06] backdrop-blur-2xl border border-white/10 rounded-2xl
|
||||
shadow-[inset_0_1px_0_rgba(255,255,255,0.12),0_8px_32px_rgba(0,0,0,0.4)]
|
||||
ring-1 ring-inset ring-white/5">
|
||||
|
||||
// 主 CTA 按钮(确认类)
|
||||
<button className="bg-gradient-to-br from-violet-500 to-violet-700
|
||||
shadow-[0_0_20px_rgba(139,92,246,0.4),0_4px_12px_rgba(0,0,0,0.3)]
|
||||
border border-white/15 rounded-xl px-8 py-4
|
||||
text-white font-semibold text-base
|
||||
hover:shadow-[0_0_30px_rgba(139,92,246,0.6)] transition-all duration-200
|
||||
active:scale-[0.98] cursor-pointer">
|
||||
|
||||
// 生成中状态格子(蓝·脉冲)
|
||||
<div className="bg-blue-500/20 border border-blue-400/40 rounded-lg animate-pulse
|
||||
ring-1 ring-blue-400/30">
|
||||
|
||||
// 完成状态格子
|
||||
<div className="bg-emerald-500/15 border border-emerald-400/30 rounded-lg">
|
||||
|
||||
// 失败状态格子
|
||||
<div className="bg-red-500/15 border border-red-400/30 rounded-lg">
|
||||
```
|
||||
|
||||
### 固定层叠规范(Fixed Overlay)
|
||||
|
||||
固定定位元素(navbar、sidebar、modal)滚动时会与页面内容重叠。
|
||||
为保持"半透不全透"的高级感,统一使用 **bg-base 色 + 高不透明度 + blur**,而非纯 glass-card:
|
||||
|
||||
| 元素 | 背景 | 模糊 | 边框 |
|
||||
|------|------|------|------|
|
||||
| **Navbar** | `rgba(7, 7, 15, 0.85)` | `blur(20px) saturate(180%)` | `border-b border-white/[0.06]` |
|
||||
| **Sidebar** | `rgba(7, 7, 15, 0.80)` | `blur(16px) saturate(160%)` | `border-r border-white/[0.06]` |
|
||||
| **Modal backdrop** | `rgba(0, 0, 0, 0.60)` | `blur(8px)` | 无 |
|
||||
| **Modal panel** | `rgba(13, 13, 26, 0.92)` | `blur(24px) saturate(180%)` | `border border-white/[0.08]` |
|
||||
|
||||
**核心原则**:
|
||||
- 背景色取自 `--bg-base` 或 `--bg-surface`,不透明度 **≥ 0.80**(保证可读)
|
||||
- 搭配 `backdrop-filter: blur` 让底层内容产生柔和虚化(高级感来源)
|
||||
- 不使用纯 `glass-card`(0.06 不透明度太低,重叠时文字会模糊)
|
||||
|
||||
```tsx
|
||||
// Navbar 标准写法
|
||||
<nav className="fixed top-0 left-0 right-0 z-30 border-b border-white/[0.06]"
|
||||
style={{
|
||||
background: 'rgba(7, 7, 15, 0.85)',
|
||||
backdropFilter: 'blur(20px) saturate(180%)',
|
||||
WebkitBackdropFilter: 'blur(20px) saturate(180%)',
|
||||
}}>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 字体系统
|
||||
|
||||
| 用途 | 字体 | CSS 变量 | 字重 | 备注 |
|
||||
|------|------|----------|------|------|
|
||||
| 大标题 / Display | Space Grotesk | `--font-heading` | 700 | 科技感,棱角干净(skill "Tech Startup" 首选) |
|
||||
| 标题 H1-H3 | Space Grotesk | `--font-heading` | 600-700 | |
|
||||
| 正文 / UI 文字 | DM Sans | `--font-body` | 400-600 | 几何感更强,比 Inter 更 premium,与 Space Grotesk 配合更和谐 |
|
||||
| 代码 / ID / 时码 | JetBrains Mono | `--font-mono` | 400 | 技术细节标注 |
|
||||
| **中文文字** | **Noto Sans SC(思源黑体)** | `--font-cn` | 400-700 | Google Fonts 免费中文字体,与 DM Sans 风格匹配 |
|
||||
|
||||
> **中文 fallback 策略**:英文字体栈末尾统一追加 `"Noto Sans SC"` 作为 fallback,中文字符自动命中。
|
||||
> 需要纯中文排版时使用 `--font-cn`(`"Noto Sans SC", "DM Sans", sans-serif`)。
|
||||
|
||||
**Google Fonts 导入:**
|
||||
```css
|
||||
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400&family=Noto+Sans+SC:wght@400;500;600;700&display=swap');
|
||||
```
|
||||
|
||||
**CSS 变量(globals.css `@theme inline`):**
|
||||
```css
|
||||
--font-heading: "Space Grotesk", "Noto Sans SC", sans-serif;
|
||||
--font-body: "DM Sans", "Noto Sans SC", sans-serif;
|
||||
--font-mono: "JetBrains Mono", monospace;
|
||||
--font-cn: "Noto Sans SC", "DM Sans", sans-serif;
|
||||
```
|
||||
|
||||
字号阶梯:
|
||||
- Display: 48px / 700 / letter-spacing: -0.02em
|
||||
- H1: 32px / 700 / -0.01em
|
||||
- H2: 24px / 600
|
||||
- H3: 18px / 600
|
||||
- Body: 15px / 400 / line-height: 1.6
|
||||
- Small: 13px / 400
|
||||
- Mono: 13px / 400
|
||||
|
||||
**关键规则:**
|
||||
- 所有字体必须加 `font-display: swap`,避免 FOIT(Flash of Invisible Text)
|
||||
- Body 文字 line-height: 1.6,行宽上限 65-75 字符
|
||||
|
||||
---
|
||||
|
||||
## 5. 图标 & 视觉元素
|
||||
|
||||
| 规则 | 标准 |
|
||||
|------|------|
|
||||
| 图标库 | **Lucide Icons**(SVG,统一 24x24 viewBox,Tailwind `w-5 h-5` 或 `w-6 h-6`) |
|
||||
| 禁止 Emoji | 不使用 emoji 作为 UI 图标,全部用 SVG |
|
||||
| 品牌 Logo | 从 Simple Icons 获取官方 SVG |
|
||||
| Hover 状态 | 只用 color/opacity/shadow 过渡,不用 scale(避免布局偏移) |
|
||||
| 交互元素 | 所有可点击元素加 `cursor-pointer` |
|
||||
|
||||
---
|
||||
|
||||
## 6. z-index 管理
|
||||
|
||||
| 层级 | z-index | 用途 |
|
||||
|------|---------|------|
|
||||
| 基础内容 | `0` | 正常文档流 |
|
||||
| 浮动卡片 | `10` | 弹出面板、dropdown |
|
||||
| Sticky 步骤条 | `20` | 流水线步骤条 |
|
||||
| 顶部导航栏 | `30` | 全局导航 |
|
||||
| 模态框遮罩 | `40` | Modal overlay |
|
||||
| 模态框内容 | `50` | Modal content |
|
||||
| Toast 通知 | `60` | 全局提示 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 页面结构
|
||||
|
||||
### 7.1 整体布局
|
||||
|
||||
```
|
||||
┌──────────────── 顶部导航栏(玻璃,fixed)────────────────┐
|
||||
│ [⚡ Air Spark] 项目名 / 集数 [设置] [成员] │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
|
||||
┌──────────── 流水线步骤条(横向,sticky)─────────────────┐
|
||||
│ ①剧本 ══► ②规划 ══► ③人设 ══► ④分镜 ══► ⑤切分 ══► ⑥生成 ══► ⑦剪辑 │
|
||||
│ ✓完成 ✓完成 ⚠待确认 ○待机 ○待机 ○待机 ○待机 │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
|
||||
┌──────────────── 当前阶段主内容区 ───────────────────────┐
|
||||
│ [阶段标题] │
|
||||
│ [内容区域 — 根据阶段变化] │
|
||||
│ [底部 CTA 区域] │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
|
||||
┌──────── 系统日志(可折叠,默认收起)────────────────────┐
|
||||
│ ▶ 系统日志 [展开] │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 7.2 Pipeline View — 主流水线页
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
|
||||
│ ░ [⚡ Air Spark] T仔的上班日记 / EP01 [设置] [成员] ░ │
|
||||
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
|
||||
│ │
|
||||
│ ┌──────────────── 流水线进度条 ──────────────────────────────────┐ │
|
||||
│ │ ①剧本 ══► ②规划 ══► ③人设 ══► ④分镜 ══► ⑤切分 ══► ⑥生成 ══► ⑦剪辑│ │
|
||||
│ │ ✓完成 ✓完成 ⚠待确认 ○待机 ○待机 ○待机 ○待机 │ │
|
||||
│ └────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────── 当前阶段内容区 ────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ 【Stage 3 — 人设图审核】 │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
|
||||
│ │ │░░░░░░░░░│ │░░░░░░░░░│ │░░░░░░░░░│ │░░░░░░░░░│ │ │
|
||||
│ │ │ T仔 │ │ 特特 │ │ 班长 │ │ 路人甲 │ │ │
|
||||
│ │ │ 人设三视│ │ 人设三视│ │ 人设三视│ │ ⚠生成失败│ │ │
|
||||
│ │ │ [✓ 通过]│ │ [✓ 通过]│ │ [✓ 通过]│ │ [↻ 重跑] │ │ │
|
||||
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ ⚠ 路人甲生成失败,重跑后可确认 │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ [ 等待路人甲重跑完成... ] ← CTA 灰色禁用 │ │ │
|
||||
│ │ └──────────────────────────────────────────────────────┘ │ │
|
||||
│ └──────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌── 日志(可折叠)─────────────────────────────────────────────────┐ │
|
||||
│ │ ▶ 系统日志 [展开]│ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Stage 6 — 视频生成进度界面
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ 【Stage 6 — 视频生成中】 已完成 18 / 32 片段 │
|
||||
│ │
|
||||
│ ████████████████████░░░░░░░░░░░░░░░░░ 56% 预计剩余 ~8 分钟 │
|
||||
│ │
|
||||
│ ┌──────────────────────── 片段状态网格 ────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │
|
||||
│ │ │ 01 │ │ 02 │ │ 03 │ │ 04 │ │ 05 │ │ 06 │ │ 07 │ │ 08 │ │ │
|
||||
│ │ │ ✓ │ │ ✓ │ │ ✓ │ │ ✓ │ │ ✓ │ │ ✓ │ │ ✓ │ │ ✗ │ │ │
|
||||
│ │ │ 绿 │ │ 绿 │ │ 绿 │ │ 绿 │ │ 绿 │ │ 绿 │ │ 绿 │ │ 红 │ │ │
|
||||
│ │ └────┘ └────┘ └────┘ └────┘ └────┘ └────┘ └────┘ └────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │
|
||||
│ │ │ 09 │ │ 10 │ │ 11 │ │ 12 │ │ 13 │ │ 14 │ │ 15 │ │ 16 │ │ │
|
||||
│ │ │ ✓ │ │ ✓ │ │ ✓ │ │ ✓ │ │ ● │ │ ● │ │ ● │ │ ● │ │ │
|
||||
│ │ │ 绿 │ │ 绿 │ │ 绿 │ │ 绿 │ │蓝脉│ │蓝脉│ │蓝脉│ │蓝脉│ │ │
|
||||
│ │ └────┘ └────┘ └────┘ └────┘ └────┘ └────┘ └────┘ └────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ... │ │
|
||||
│ │ │ 17 │ │ 18 │ │ 19 │ │ 20 │ │ 21 │ │ │
|
||||
│ │ │ ● │ │ ● │ │ ○ │ │ ○ │ │ ○ │ │ │
|
||||
│ │ │蓝脉│ │蓝脉│ │ 灰 │ │ 灰 │ │ 灰 │ │ │
|
||||
│ │ └────┘ └────┘ └────┘ └────┘ └────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ 图例: ✓绿 完成 ●蓝 生成中(脉冲) ○灰 队列中 ✗红 失败 │ │
|
||||
│ └────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─── 失败片段处理 ──────────────────────────────────────────────────┐ │
|
||||
│ │ ⚠ 片段08 — 生成失败(重试3次后超时) [编辑提示词] [↻ 重跑] │ │
|
||||
│ └──────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 人工确认交互规范
|
||||
|
||||
### 核心原则
|
||||
> 确认时清场噪音,只留一个决策。CTA 状态变化(灰→绿)有动画。确认后 3 秒防误触。
|
||||
|
||||
### Stage 1 — 剧本确认
|
||||
|
||||
```
|
||||
对话区(主体)
|
||||
↓ 剧本满意后底部出现确认栏
|
||||
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ ✅ 剧本已就绪 — 共 8 场景,预估 4 分 20 秒 │
|
||||
│ │
|
||||
│ ╔══════════════════════════════════════════════════╗ │
|
||||
│ ║ 确认剧本,启动流水线 ▶ ║ │ ← 大按钮,紫色发光
|
||||
│ ╚══════════════════════════════════════════════════╝ │
|
||||
│ │
|
||||
│ [继续修改剧本] │ ← 小字次级操作
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Stage 3 — 人设确认
|
||||
|
||||
- 每张图独立操作:`[✓ 通过]` / `[↻ 重跑]`
|
||||
- 支持单张图 `[编辑提示词后重跑]`
|
||||
- 底部汇总 CTA:所有图通过 → 按钮从灰色变绿色发光 → 可点击
|
||||
- 点击确认后:按钮显示 "3s 后启动..." + `[取消]` 防误触
|
||||
|
||||
### 自动阶段(Stage 2/4/5)
|
||||
|
||||
- **骨架屏优先**:超过 300ms 的异步操作,显示 Skeleton 占位(不是空白)
|
||||
- 进度文字("正在规划场景结构...")+ 预估时间
|
||||
- 完成后自动高亮 CTA 或自动推进(取决于流水线模式)
|
||||
- 无需用户操作,只展示状态
|
||||
|
||||
**Skeleton 实现:**
|
||||
```tsx
|
||||
// 骨架屏卡片
|
||||
<div className="bg-white/[0.04] rounded-2xl animate-pulse">
|
||||
<div className="h-40 bg-white/[0.06] rounded-t-2xl" />
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="h-4 bg-white/[0.08] rounded w-3/4" />
|
||||
<div className="h-3 bg-white/[0.06] rounded w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 动效规范
|
||||
|
||||
| 场景 | 时长 | Easing |
|
||||
|------|------|--------|
|
||||
| 卡片 hover | 150ms | ease-out |
|
||||
| CTA 状态变化(灰→绿) | 300ms | ease-in-out |
|
||||
| 片段格子状态更新 | 200ms | ease-out |
|
||||
| 步骤条推进动画 | 400ms | ease-in-out |
|
||||
| 页面切换 | 250ms | ease-in-out |
|
||||
| 脉冲动画(running 状态) | 1500ms | ease-in-out,循环 |
|
||||
| 玻璃形变动效(Liquid Glass) | 400-600ms | cubic-bezier(0.16, 1, 0.3, 1) |
|
||||
|
||||
**性能降级(必须):**
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Tailwind 实现:**
|
||||
```tsx
|
||||
// 所有动画组件加上 motion-safe 前缀
|
||||
<div className="motion-safe:animate-pulse ..." />
|
||||
<div className="motion-safe:transition-all motion-safe:duration-200 ..." />
|
||||
```
|
||||
|
||||
**规则:**
|
||||
- 微交互:150-300ms(hover、click 反馈)
|
||||
- 状态变化:300-400ms(步骤推进、CTA 变化)
|
||||
- 液态玻璃:400-600ms(morphing、blur 变化)
|
||||
- 装饰动画仅用于 loading,不用于静态元素
|
||||
- 所有 animation 用 `transform` 和 `opacity`,不用 `width`/`height`/`top`/`left`
|
||||
|
||||
---
|
||||
|
||||
## 11. 无障碍 & 可用性规范(CRITICAL)
|
||||
|
||||
| 规则 | 标准 |
|
||||
|------|------|
|
||||
| 文字对比度 | 主文字 `#f1f0ff` on `#07070f` = **18.5:1** ✅ 远超 WCAG AAA |
|
||||
| 次要文字对比度 | `#8b8ea8` on `#07070f` = **6.2:1** ✅ 超 AA |
|
||||
| 弱化文字对比度 | `#4c4f6b` on `#07070f` = **3.1:1** ⚠ 仅用于装饰性标注,不用于关键信息 |
|
||||
| Focus 焦点环 | 所有交互元素:`focus-visible:ring-2 focus-visible:ring-violet-400 focus-visible:ring-offset-2 focus-visible:ring-offset-[#07070f]` |
|
||||
| 键盘导航 | Tab 顺序 = 视觉顺序;步骤条、片段网格支持方向键导航 |
|
||||
| 按钮最小尺寸 | 44×44px 点击区域(移动端和桌面端均适用) |
|
||||
| 颜色不作为唯一标识 | 状态除颜色外,必须有图标/文字辅助(✓/●/○/✗ + 文字标签) |
|
||||
| aria-label | 图标按钮(设置、重跑、展开)必须有 aria-label |
|
||||
| 表单标签 | 所有 input 必须关联 `<label>` |
|
||||
|
||||
---
|
||||
|
||||
## 12. 交付前质检清单
|
||||
|
||||
### 视觉质量
|
||||
- [ ] 无 emoji 作为图标(全部 Lucide SVG)
|
||||
- [ ] 所有图标来自同一套图标库,尺寸一致
|
||||
- [ ] Hover 状态不引起布局偏移
|
||||
- [ ] 玻璃卡片在深色背景上有足够可见度
|
||||
|
||||
### 交互
|
||||
- [ ] 所有可点击元素有 `cursor-pointer`
|
||||
- [ ] Hover 有明确视觉反馈(150-300ms 过渡)
|
||||
- [ ] Focus 焦点环对键盘用户可见
|
||||
- [ ] 异步操作 > 300ms 时显示 Skeleton 骨架屏
|
||||
|
||||
### 性能
|
||||
- [ ] `prefers-reduced-motion` 降级已实现
|
||||
- [ ] 图片使用 WebP + lazy loading
|
||||
- [ ] 字体 `font-display: swap`
|
||||
- [ ] 重组件使用 `next/dynamic` 动态导入
|
||||
- [ ] Bundle 分析通过(`@next/bundle-analyzer`)
|
||||
|
||||
### 响应式
|
||||
- [ ] 375px / 768px / 1024px / 1440px 四档断点测试
|
||||
- [ ] 无横向滚动条
|
||||
- [ ] 固定导航栏不遮挡内容(留 padding-top)
|
||||
|
||||
---
|
||||
|
||||
## 13. 间距系统
|
||||
|
||||
基于 4px 基础单位,所有间距从以下阶梯中选取:
|
||||
|
||||
| Token | 值 | 用途 |
|
||||
|-------|-----|------|
|
||||
| `space-1` | 4px | 图标与文字间距、紧凑元素内间距 |
|
||||
| `space-2` | 8px | 相关元素组内间距(如标签+输入框) |
|
||||
| `space-3` | 12px | 卡片内边距(紧凑型)、列表项间距 |
|
||||
| `space-4` | 16px | 标准卡片内边距、表单字段间距 |
|
||||
| `space-6` | 24px | 卡片内边距(宽松型)、区块间距 |
|
||||
| `space-8` | 32px | Section 之间的间距 |
|
||||
| `space-12` | 48px | 大区块间距 |
|
||||
| `space-16` | 64px | 页面级 section 间距 |
|
||||
|
||||
**核心原则:**
|
||||
- 同组元素用 `space-2` ~ `space-3`(紧密关联)
|
||||
- 不同组件之间用 `space-6` ~ `space-8`(视觉分隔)
|
||||
- 页面容器统一 `px-6`(24px 左右内边距)、`max-w-7xl mx-auto`
|
||||
|
||||
```tsx
|
||||
// Tailwind 对照
|
||||
<div className="p-4"> // space-4 = 16px
|
||||
<div className="p-6"> // space-6 = 24px
|
||||
<div className="gap-3"> // space-3 = 12px
|
||||
<div className="mb-8"> // space-8 = 32px
|
||||
<section className="mb-16"> // space-16 = 64px(页面级)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. 圆角系统
|
||||
|
||||
| Token | 值 | 用途 |
|
||||
|-------|-----|------|
|
||||
| `radius-sm` | 6px | 小元素(tag、badge、输入框) |
|
||||
| `radius-md` | 8px | 按钮、小卡片、下拉菜单 |
|
||||
| `radius-lg` | 12px | 中型卡片、CTA 按钮 |
|
||||
| `radius-xl` | 16px | 主内容卡片(glass-card 默认) |
|
||||
| `radius-2xl` | 24px | 大面板、模态框 |
|
||||
| `radius-full` | 9999px | 头像、圆形按钮、pill tag |
|
||||
|
||||
```tsx
|
||||
// Tailwind 对照
|
||||
rounded-md // 6px → tag, badge, input
|
||||
rounded-lg // 8px → 按钮
|
||||
rounded-xl // 12px → CTA
|
||||
rounded-2xl // 16px → glass-card
|
||||
rounded-3xl // 24px → modal
|
||||
rounded-full // pill
|
||||
```
|
||||
|
||||
**核心原则:**
|
||||
- 嵌套元素的子级圆角 = 父级圆角 - 父级内边距(避免嵌套圆角溢出)
|
||||
- 同层级元素圆角保持一致
|
||||
|
||||
---
|
||||
|
||||
## 15. 表单元素
|
||||
|
||||
### Input / Textarea
|
||||
|
||||
```css
|
||||
.form-input {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.10);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
color: var(--text-primary);
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
transition: border-color 200ms ease, box-shadow 200ms ease;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(108, 99, 255, 0.5);
|
||||
box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.15);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.form-input--error {
|
||||
border-color: rgba(239, 68, 68, 0.5);
|
||||
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
```
|
||||
|
||||
### Tailwind 实现
|
||||
|
||||
```tsx
|
||||
// 标准输入框
|
||||
<input className="w-full bg-white/[0.04] border border-white/10 rounded-lg
|
||||
px-4 py-3 text-[15px] text-text-primary placeholder:text-text-muted
|
||||
focus:outline-none focus:border-accent/50 focus:ring-3 focus:ring-accent/15
|
||||
transition-all duration-200" />
|
||||
|
||||
// 错误状态
|
||||
<input className="... border-red-500/50 focus:ring-red-500/12" />
|
||||
|
||||
// 多行输入(剧本对话)
|
||||
<textarea className="w-full bg-white/[0.04] border border-white/10 rounded-lg
|
||||
px-4 py-3 text-[15px] text-text-primary placeholder:text-text-muted
|
||||
focus:outline-none focus:border-accent/50 focus:ring-3 focus:ring-accent/15
|
||||
transition-all duration-200 resize-none min-h-[120px]" />
|
||||
|
||||
// Label
|
||||
<label className="block text-sm font-medium text-text-secondary mb-2">
|
||||
```
|
||||
|
||||
### Select / Dropdown
|
||||
|
||||
> **禁止使用原生 `<select>`**——弹出菜单无法自定义样式(白底,破坏暗色主题)。
|
||||
> 必须使用自定义 dropdown 组件。
|
||||
|
||||
```tsx
|
||||
// 触发按钮
|
||||
<button className="w-full bg-white/[0.04] border border-white/10 rounded-lg
|
||||
px-4 py-3 text-[15px] text-text-primary text-left flex items-center justify-between
|
||||
cursor-pointer transition-all duration-200
|
||||
// 展开时:
|
||||
border-accent/50 ring-3 ring-accent/15">
|
||||
{selectedValue}
|
||||
<ChevronsUpDown className="w-4 h-4 text-text-muted" />
|
||||
</button>
|
||||
|
||||
// 下拉面板(absolute, z-20)
|
||||
<div className="absolute z-20 mt-1 w-full rounded-lg border border-white/10 overflow-hidden"
|
||||
style={{ background: 'rgba(13, 13, 26, 0.95)', backdropFilter: 'blur(20px)' }}>
|
||||
// 选项
|
||||
<button className="w-full px-4 py-2.5 text-[15px] text-left
|
||||
text-text-primary hover:bg-white/[0.06]
|
||||
// 选中态:text-accent bg-accent/10">
|
||||
</div>
|
||||
```
|
||||
|
||||
### 表单规则
|
||||
- Label 在 input 上方,间距 `mb-2`(8px)
|
||||
- 字段之间间距 `space-y-4`(16px)
|
||||
- 错误信息在字段下方,`text-sm text-red-400 mt-1`
|
||||
- 必填标识用 `*` + `text-red-400`,不用颜色作为唯一标识
|
||||
- 所有 input 必须有关联 `<label>`(无障碍要求)
|
||||
|
||||
---
|
||||
|
||||
## 16. Toast 通知
|
||||
|
||||
### 类型与样式
|
||||
|
||||
| 类型 | 图标 | 左边框色 | 用途 |
|
||||
|------|------|---------|------|
|
||||
| **success** | `Check` (Lucide) | `emerald-500` | 操作成功、保存完成 |
|
||||
| **error** | `X` (Lucide) | `red-500` | 操作失败、API 错误 |
|
||||
| **warning** | `AlertTriangle` | `amber-500` | 需要注意、即将超时 |
|
||||
| **info** | `Info` (Lucide) | `blue-500` | 提示信息、状态更新 |
|
||||
|
||||
### 行为规范
|
||||
- 位置:屏幕右下角,距底 24px、距右 24px
|
||||
- 多条堆叠:从下往上,间距 8px
|
||||
- 自动消失:success/info = 4s,warning = 6s,error = 不自动消失(需手动关闭)
|
||||
- 进入动画:从右侧滑入 + fade in(250ms, ease-out)
|
||||
- 退出动画:向右滑出 + fade out(200ms, ease-in)
|
||||
|
||||
### Tailwind 实现
|
||||
|
||||
```tsx
|
||||
// Toast 容器
|
||||
<div className="fixed bottom-6 right-6 z-60 flex flex-col-reverse gap-2">
|
||||
|
||||
// 单条 Toast
|
||||
<div className="glass-card px-4 py-3 flex items-start gap-3 min-w-[320px] max-w-[420px]
|
||||
border-l-3 border-l-emerald-500
|
||||
animate-[slideInRight_250ms_ease-out]">
|
||||
<Check className="w-5 h-5 text-emerald-400 mt-0.5 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-text-primary">保存成功</p>
|
||||
<p className="text-xs text-text-secondary mt-0.5">剧本已保存为正式版本</p>
|
||||
</div>
|
||||
<button className="text-text-muted hover:text-text-secondary cursor-pointer"
|
||||
aria-label="关闭通知">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 17. 空状态
|
||||
|
||||
当列表/内容区域无数据时,统一显示空状态占位。
|
||||
|
||||
### 结构
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ │
|
||||
│ [Lucide 图标 48px] │
|
||||
│ │
|
||||
│ 主标题(简短描述) │
|
||||
│ 副标题(下一步指引) │
|
||||
│ │
|
||||
│ [ 主操作按钮 ] │
|
||||
│ │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 场景预设
|
||||
|
||||
| 场景 | 图标 | 主标题 | CTA |
|
||||
|------|------|--------|-----|
|
||||
| 项目列表为空 | `FolderPlus` | 还没有项目 | 创建第一个项目 |
|
||||
| 资产库为空 | `ImagePlus` | 暂无资产 | 在流水线中生成 |
|
||||
| 剧集列表为空 | `Film` | 还没有剧集 | 添加第一集 |
|
||||
| 搜索无结果 | `SearchX` | 没有找到匹配项 | 试试其他关键词 |
|
||||
| 视频片段为空 | `VideoOff` | 暂无视频片段 | 运行 Stage 6 生成 |
|
||||
|
||||
### Tailwind 实现
|
||||
|
||||
```tsx
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||
<div className="w-12 h-12 rounded-2xl bg-white/[0.04] flex items-center justify-center mb-4">
|
||||
<FolderPlus className="w-6 h-6 text-text-muted" />
|
||||
</div>
|
||||
<h3 className="font-[family-name:var(--font-heading)] text-lg font-semibold text-text-primary mb-1">
|
||||
还没有项目
|
||||
</h3>
|
||||
<p className="text-sm text-text-secondary mb-6">
|
||||
创建你的第一个动画项目,开始 AI 创作之旅
|
||||
</p>
|
||||
<button className="bg-gradient-to-br from-violet-500 to-violet-700
|
||||
border border-white/15 rounded-xl px-6 py-2.5
|
||||
text-sm text-white font-medium
|
||||
hover:shadow-[0_0_20px_rgba(139,92,246,0.4)] transition-all duration-200
|
||||
cursor-pointer">
|
||||
创建项目
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 规则
|
||||
- 图标用 `text-muted` 色(不要太抢眼)
|
||||
- 主标题简短(≤10 字)
|
||||
- 副标题给出明确的下一步指引
|
||||
- 必须有 CTA 按钮引导操作(除"搜索无结果"外)
|
||||
|
||||
---
|
||||
|
||||
## 18. 响应式断点
|
||||
|
||||
> MVP 优先桌面端(1024px+),移动端后续规划。
|
||||
|
||||
| 断点 | Tailwind 前缀 | 宽度 | 用途 |
|
||||
|------|--------------|------|------|
|
||||
| Mobile | 默认(无前缀) | < 768px | 未来适配 |
|
||||
| Tablet | `md:` | ≥ 768px | 简单适配(单栏布局) |
|
||||
| Desktop | `lg:` | ≥ 1024px | 主设计目标 |
|
||||
| Wide | `xl:` | ≥ 1280px | 宽屏优化 |
|
||||
| Ultra-wide | `2xl:` | ≥ 1536px | 超宽屏留白控制 |
|
||||
|
||||
### 布局适配规则
|
||||
|
||||
| 元素 | < 768px | 768px+ | 1024px+ | 1280px+ |
|
||||
|------|---------|--------|---------|---------|
|
||||
| **导航栏** | 汉堡菜单 | 完整导航 | 完整导航 | 完整导航 |
|
||||
| **内容容器** | `px-4` | `px-6` | `max-w-6xl mx-auto` | `max-w-7xl mx-auto` |
|
||||
| **审核卡片网格** | 1 列 | 2 列 | 3 列 | 4 列 |
|
||||
| **片段状态网格** | 4 列 | 6 列 | 8 列 | 8 列 |
|
||||
| **Pipeline Stepper** | 纵向列表 | 横向滚动 | 横向完整 | 横向完整 |
|
||||
|
||||
### 核心原则
|
||||
- **Mobile-first 写法**:默认写移动端样式,用 `md:` `lg:` 向上覆盖
|
||||
- 内容区 `max-w-7xl`(1280px),超宽屏两侧自然留白
|
||||
- 卡片网格用 `grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4`
|
||||
- 固定导航栏高度不变(h-16 = 64px),所有断点保持一致
|
||||
- 文字不缩放(最小 body 15px),只调整布局
|
||||
|
||||
---
|
||||
|
||||
## 19. Badge / Tag
|
||||
|
||||
### 状态 Badge(方角)
|
||||
|
||||
用于流水线阶段状态、任务状态等系统状态标识。
|
||||
|
||||
```tsx
|
||||
// 结构:icon + 文字,圆角 rounded-md,带边框
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md
|
||||
text-xs font-medium bg-emerald-500/15 text-emerald-400 border border-emerald-400/20">
|
||||
<Check className="w-3 h-3" /> Completed
|
||||
</span>
|
||||
```
|
||||
|
||||
| 状态 | 背景 | 文字 | 边框 |
|
||||
|------|------|------|------|
|
||||
| completed | `emerald-500/15` | `emerald-400` | `emerald-400/20` |
|
||||
| running | `blue-500/15` | `blue-400` | `blue-400/20` |
|
||||
| waiting | `amber-500/15` | `amber-400` | `amber-400/20` |
|
||||
| failed | `red-500/15` | `red-400` | `red-400/20` |
|
||||
| pending | `white/[0.06]` | `text-secondary` | `white/10` |
|
||||
|
||||
### 内容 Tag(圆角 pill)
|
||||
|
||||
用于 Skill 标签、剧集标签等内容分类。
|
||||
|
||||
```tsx
|
||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full
|
||||
text-xs font-medium bg-accent/15 text-accent border border-accent/20">
|
||||
<Hash className="w-3 h-3" /> screenplay-skill
|
||||
</span>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 20. Avatar
|
||||
|
||||
### 尺寸
|
||||
|
||||
| 尺寸 | Tailwind | 用途 |
|
||||
|------|----------|------|
|
||||
| SM | `w-8 h-8 text-xs` | 紧凑列表、评论 |
|
||||
| MD | `w-10 h-10 text-sm` | 默认(导航栏、成员列表) |
|
||||
| LG | `w-12 h-12 text-base` | 个人资料 |
|
||||
| XL | `w-16 h-16 text-lg` | 设置页头像 |
|
||||
|
||||
### 样式
|
||||
|
||||
```tsx
|
||||
// 带字母(首选)
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-violet-500 to-accent-blue
|
||||
flex items-center justify-center text-sm font-semibold text-white">
|
||||
T
|
||||
</div>
|
||||
|
||||
// 空白 fallback
|
||||
<div className="w-10 h-10 rounded-full bg-white/[0.06] border border-white/10
|
||||
flex items-center justify-center">
|
||||
<User className="w-5 h-5 text-text-muted" />
|
||||
</div>
|
||||
|
||||
// 在线状态指示
|
||||
<div className="relative">
|
||||
{/* avatar */}
|
||||
<div className="absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 rounded-full
|
||||
bg-emerald-500 border-2 border-bg-base" />
|
||||
</div>
|
||||
|
||||
// Avatar Group(重叠)
|
||||
<div className="flex -space-x-2">
|
||||
{avatars.map((a, i) => (
|
||||
<div key={i} className="... border-2 border-bg-base" />
|
||||
))}
|
||||
<div className="... bg-white/[0.08] text-xs text-text-secondary">+3</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 21. Breadcrumb
|
||||
|
||||
```tsx
|
||||
<nav aria-label="Breadcrumb" className="flex items-center gap-2 text-sm">
|
||||
<a className="text-text-secondary hover:text-text-primary transition-colors cursor-pointer">
|
||||
Projects
|
||||
</a>
|
||||
<ChevronRight className="w-3.5 h-3.5 text-text-muted" />
|
||||
<a className="text-text-secondary hover:text-text-primary transition-colors cursor-pointer">
|
||||
T仔的上班日记
|
||||
</a>
|
||||
<ChevronRight className="w-3.5 h-3.5 text-text-muted" />
|
||||
<span className="text-text-primary font-medium">Pipeline</span>
|
||||
</nav>
|
||||
```
|
||||
|
||||
**规则:**
|
||||
- 最后一级为当前页面,用 `text-primary font-medium`,不可点击
|
||||
- 分隔符用 `ChevronRight`(Lucide),`text-muted`
|
||||
- 必须有 `aria-label="Breadcrumb"` 的 `<nav>` 包裹
|
||||
|
||||
---
|
||||
|
||||
## 22. Tab / Segmented Control
|
||||
|
||||
### Underline Tabs
|
||||
|
||||
```tsx
|
||||
<div className="flex border-b border-white/10">
|
||||
<button className={`px-4 py-3 text-sm font-medium relative cursor-pointer
|
||||
${active ? "text-accent" : "text-text-secondary hover:text-text-primary"}`}>
|
||||
{label}
|
||||
{active && <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent rounded-full" />}
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Segmented Control(Pill 风格)
|
||||
|
||||
```tsx
|
||||
<div className="inline-flex bg-white/[0.04] rounded-lg p-1 border border-white/[0.06]">
|
||||
<button className={`px-4 py-2 rounded-md text-sm font-medium cursor-pointer
|
||||
${active ? "bg-accent/20 text-accent shadow-sm" : "text-text-secondary hover:text-text-primary"}`}>
|
||||
{label}
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
**规则:**
|
||||
- Tab 切换内容区域,不跳转页面
|
||||
- 激活态下划线/背景色用 accent
|
||||
- 所有 tab 按钮加 `cursor-pointer`
|
||||
|
||||
---
|
||||
|
||||
## 23. Danger Button
|
||||
|
||||
破坏性操作(删除项目、清空资产)专用按钮。
|
||||
|
||||
```tsx
|
||||
// 默认 Danger(轻量,用于非终极操作)
|
||||
<button className="bg-red-500/15 border border-red-400/30 rounded-xl px-6 py-3
|
||||
text-[15px] font-medium text-red-400 hover:bg-red-500/25 transition-colors cursor-pointer
|
||||
flex items-center gap-2">
|
||||
<Trash2 className="w-4 h-4" /> Delete Project
|
||||
</button>
|
||||
|
||||
// 确认 Danger(实心红色,用于二次确认后的终极操作)
|
||||
<button className="bg-red-600 border border-red-500 rounded-xl px-6 py-3
|
||||
text-[15px] font-medium text-white hover:bg-red-700 transition-colors cursor-pointer">
|
||||
Confirm Delete
|
||||
</button>
|
||||
```
|
||||
|
||||
**规则:**
|
||||
- 破坏性操作必须先弹 Modal 确认,再执行
|
||||
- 默认展示轻量 danger(红色文字 + 透明底),确认时切换为实心红底
|
||||
- Focus ring 用 `ring-red-400`
|
||||
|
||||
---
|
||||
|
||||
## 24. Search Input
|
||||
|
||||
```tsx
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
|
||||
<input type="text" placeholder="Search projects, episodes..."
|
||||
className="w-full bg-white/[0.04] border border-white/10 rounded-lg
|
||||
pl-10 pr-4 py-3 text-[15px] text-text-primary placeholder:text-text-muted
|
||||
focus:outline-none focus:border-accent/50 focus:ring-3 focus:ring-accent/15
|
||||
transition-all duration-200" />
|
||||
</div>
|
||||
```
|
||||
|
||||
**规则:**
|
||||
- 搜索图标左侧内嵌,`left-3`,输入区域 `pl-10`
|
||||
- 图标用 `text-muted`,输入时不变色
|
||||
- 与标准 input 共享 focus 样式
|
||||
|
||||
---
|
||||
|
||||
## 25. Tooltip
|
||||
|
||||
```tsx
|
||||
// 使用 CSS group/hover,无需 JS
|
||||
<div className="group relative">
|
||||
<button className="p-2.5 rounded-xl glass-card hover:bg-white/[0.12]
|
||||
transition-colors cursor-pointer" aria-label="Settings">
|
||||
<Settings className="w-5 h-5 text-text-secondary" />
|
||||
</button>
|
||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2
|
||||
px-3 py-1.5 rounded-lg text-xs font-medium text-text-primary whitespace-nowrap
|
||||
opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none
|
||||
border border-white/10"
|
||||
style={{ background: 'rgba(13, 13, 26, 0.95)', backdropFilter: 'blur(12px)' }}>
|
||||
Project Settings
|
||||
{/* 箭头 */}
|
||||
<div className="absolute top-full left-1/2 -translate-x-1/2
|
||||
w-2 h-2 rotate-45 border-b border-r border-white/10"
|
||||
style={{ background: 'rgba(13, 13, 26, 0.95)' }} />
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**位置变体:**
|
||||
- Top: `bottom-full mb-2` + 箭头 `top-full border-b border-r`
|
||||
- Right: `left-full ml-2` + 箭头 `right-full border-l border-b`
|
||||
- Bottom: `top-full mt-2` + 箭头 `bottom-full border-t border-l`
|
||||
- Left: `right-full mr-2` + 箭头 `left-full border-r border-t`
|
||||
|
||||
**规则:**
|
||||
- 背景用 Fixed Overlay 规范:`rgba(13, 13, 26, 0.95)` + `blur(12px)`
|
||||
- 纯 CSS 实现(`group-hover:opacity-100`),无需 JS 状态
|
||||
- `pointer-events-none` 防止 tooltip 干扰点击
|
||||
- 图标按钮必须同时有 `aria-label`(tooltip 仅视觉增强,不替代无障碍)
|
||||
|
||||
---
|
||||
|
||||
## 26. 待确认项
|
||||
|
||||
- [ ] Stage 7 时间轴编辑器的具体 UI 细节(片段缩略图、拖拽交互、波形显示)
|
||||
- [ ] 移动端适配策略(内部工具优先级低,先做 1024px+ 桌面端)
|
||||
- [ ] 浅色模式:当前纯暗色,MVP 不做浅色切换
|
||||
41
frontend/.gitignore
vendored
Normal file
41
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
36
frontend/README.md
Normal file
36
frontend/README.md
Normal file
@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
18
frontend/eslint.config.mjs
Normal file
18
frontend/eslint.config.mjs
Normal file
@ -0,0 +1,18 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
7
frontend/next.config.ts
Normal file
7
frontend/next.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
devIndicators: false,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
6614
frontend/package-lock.json
generated
Normal file
6614
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
frontend/package.json
Normal file
27
frontend/package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.577.0",
|
||||
"next": "16.1.6",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
7
frontend/postcss.config.mjs
Normal file
7
frontend/postcss.config.mjs
Normal file
@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
1
frontend/public/file.svg
Normal file
1
frontend/public/file.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
1
frontend/public/globe.svg
Normal file
1
frontend/public/globe.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
frontend/public/next.svg
Normal file
1
frontend/public/next.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
frontend/public/vercel.svg
Normal file
1
frontend/public/vercel.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
1
frontend/public/window.svg
Normal file
1
frontend/public/window.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
881
frontend/src/app/dashboard/[projectId]/chat/page.tsx
Normal file
881
frontend/src/app/dashboard/[projectId]/chat/page.tsx
Normal file
@ -0,0 +1,881 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect, useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Send,
|
||||
Sparkles,
|
||||
Play,
|
||||
User,
|
||||
Copy,
|
||||
RotateCcw,
|
||||
Pencil,
|
||||
Eye,
|
||||
FileText,
|
||||
Check,
|
||||
Clock,
|
||||
Download,
|
||||
ChevronRight,
|
||||
} from "lucide-react";
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Types
|
||||
───────────────────────────────────────────── */
|
||||
interface Message {
|
||||
id: string;
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
type StepStatus = "done" | "editing" | "pending";
|
||||
|
||||
interface CreationStep {
|
||||
id: string;
|
||||
num: number;
|
||||
title: string;
|
||||
shortTitle: string;
|
||||
description: string;
|
||||
status: StepStatus;
|
||||
content: string;
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Mock Data — Chat Messages
|
||||
───────────────────────────────────────────── */
|
||||
const MOCK_MESSAGES: Message[] = [
|
||||
{
|
||||
id: "msg_01",
|
||||
role: "user",
|
||||
content:
|
||||
"帮我写一个关于恐龙打工的泡面番剧本,第一集讲T仔上班迟到。大概2分半,搞笑丧系风格。",
|
||||
timestamp: "14:30",
|
||||
},
|
||||
{
|
||||
id: "msg_02",
|
||||
role: "assistant",
|
||||
content:
|
||||
"好的!我来帮你创作这个泡面番。先从创意孵化开始。\n\n我已生成 **创意构思**,定义了核心概念:恐龙打工 × 丧系幽默 × 泡面番。请在左侧文档列表查看。\n\n接下来我会生成故事大纲,规划 13 集的季度结构。",
|
||||
timestamp: "14:31",
|
||||
},
|
||||
{
|
||||
id: "msg_03",
|
||||
role: "user",
|
||||
content: "大纲看了,很好。角色设计我想让T仔是个小短手够不着东西的霸王龙,作为核心笑点。",
|
||||
timestamp: "14:33",
|
||||
},
|
||||
{
|
||||
id: "msg_04",
|
||||
role: "assistant",
|
||||
content:
|
||||
'已更新 **角色设定**:\n\nT仔的核心喜剧设定已确立 — "两只小短手只有拇指长,几乎什么都够不着"。同时完善了特特(翼龙同事)、皮皮(甲龙实习生)的设计。\n\n接下来进入分集梗概阶段。',
|
||||
timestamp: "14:34",
|
||||
},
|
||||
{
|
||||
id: "msg_05",
|
||||
role: "user",
|
||||
content: "剧本写得不错,场2火腿肠被碾过那段可以再夸张点。",
|
||||
timestamp: "14:40",
|
||||
},
|
||||
{
|
||||
id: "msg_06",
|
||||
role: "assistant",
|
||||
content:
|
||||
'已更新 **剧本正文** 场 2:\n\n增加了"顶级的工业淀粉!我的灵魂伴侣!"的内心独白,酱汁溅成烟熏妆效果,T仔小短手够不着擦脸的无力感。',
|
||||
timestamp: "14:41",
|
||||
},
|
||||
];
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Mock Data — 6 Creation Steps
|
||||
───────────────────────────────────────────── */
|
||||
const CONCEPT_CONTENT = `# 创意孵化 — 恐龙也是打工龙
|
||||
|
||||
## 核心概念
|
||||
Q版迷你恐龙在现代职场打工的泡面番动画。
|
||||
|
||||
## 类型定位
|
||||
- 类型:泡面番(每集2-3分钟)
|
||||
- 风格:搞笑丧系 × 打工人共鸣
|
||||
- 受众:全年龄 / Z世代打工人
|
||||
|
||||
## 核心笑点机制
|
||||
1. **体型反差**:恐龙 × 迷你城市 × 现代职场
|
||||
2. **小短手**:霸王龙后代的遗传负担,够不着任何东西
|
||||
3. **丧系独白**:T仔的内心OS,犀利吐槽但认命
|
||||
|
||||
## 情感锚点
|
||||
- 表面搞笑,内核是「普通打工人的小确丧与小确幸」
|
||||
- 每集结尾都有一个温暖或反转的小彩蛋`;
|
||||
|
||||
const OUTLINE_CONTENT = `# 故事大纲 — 恐龙也是打工龙(第一季)
|
||||
|
||||
## 季度主线
|
||||
T仔作为新入职的霸王龙后代,在"侏罗纪科技有限公司"从实习到转正的成长之路。
|
||||
|
||||
## 13集结构(Save the Cat 节拍)
|
||||
|
||||
| 集数 | 标题 | 核心事件 | 节拍 |
|
||||
|------|------|----------|------|
|
||||
| EP01 | 最遥远的距离 | T仔第一天上班迟到 | 开场画面 |
|
||||
| EP02 | 禁忌的本能 | 英总的洗发水香味诱惑 | 主题呈现 |
|
||||
| EP03 | 团建惊魂 | 户外团建变恐龙本能大爆发 | 铺垫 |
|
||||
| EP04 | 加班龙 | 第一次加班,发现夜间办公室秘密 | 催化剂 |
|
||||
| EP05 | 工位战争 | 争夺靠窗工位大作战 | 争论 |
|
||||
| EP06 | 外卖龙风云 | 点外卖引发的连锁灾难 | B故事 |
|
||||
| EP07 | 年终考核 | 面对KPI的恐龙式焦虑 | 游戏的乐趣 |
|
||||
| EP08 | 实习生逆袭 | 皮皮的甲龙特技意外拯救全公司 | 中点 |
|
||||
| EP09 | 感冒龙 | 霸王龙打喷嚏的破坏力 | 坏人逼近 |
|
||||
| EP10 | 翼龙的秘密 | 特特不能飞的真相 | 一无所有 |
|
||||
| EP11 | 裁员风波 | 公司传闻裁员,恐龙们抱团 | 灵魂黑夜 |
|
||||
| EP12 | 最后的全勤 | T仔拼命保住年度全勤奖 | 突破 |
|
||||
| EP13 | 打工龙的夏天 | 全员海边团建,温暖收尾 | 终场画面 |
|
||||
|
||||
## 季度弧线
|
||||
- 前4集:建立世界观和角色关系
|
||||
- 中4集:深入角色,展开支线
|
||||
- 后5集:矛盾升级 → 和解 → 温暖大结局`;
|
||||
|
||||
const CHARACTER_CONTENT = `# 角色设定 — 恐龙也是打工龙
|
||||
|
||||
## T仔(主角)— Q版迷你霸王龙
|
||||
|
||||
**外貌**:约30厘米高,2头身比例,圆滚滚的身体。橘红色皮肤,肚皮浅黄。脑袋很大,占身体近一半,嘴巴宽大但圆润可爱。两只眼睛又大又圆,常年"死鱼眼"式疲惫表情。两只小短手只有拇指长,几乎什么都够不着(核心喜剧设定)。尾巴粗短但力气大,经常不受控制地甩来甩去闯祸。穿白色迷你衬衫,打绿色小领带。
|
||||
|
||||
**性格**:丧系打工龙,嘴上吐槽,心里认命。偶尔展现霸王龙的本能(比如闻到肉香会走不动路)。
|
||||
|
||||
**核心矛盾**:霸王龙的基因 vs 现代文明的束缚
|
||||
|
||||
---
|
||||
|
||||
## 特特 — Q版迷你翼龙
|
||||
|
||||
**外貌**:浅蓝灰色身体,圆脑袋大眼睛,短喙。翅膀像小披风。穿白色衬衫,打红色领带,常戴防风镜在头顶。
|
||||
|
||||
**性格**:自恋傲娇,觉得能飞就高人一等。实际上飞行技术一般,经常出糗。
|
||||
|
||||
**隐藏设定**:其实有恐高症(EP10 揭晓)
|
||||
|
||||
---
|
||||
|
||||
## 皮皮 — Q版迷你甲龙(实习生)
|
||||
|
||||
**外貌**:明黄色,身体又圆又硬像个弹力球。背上一排小骨甲,尾巴末端有骨锤。四肢极短,移动方式接近滚动。眼睛又大又亮,永远一脸兴奋。
|
||||
|
||||
**性格**:职场小白,热情过头,好心办坏事。
|
||||
|
||||
---
|
||||
|
||||
## 世界观 — 恐龙迷你城
|
||||
- 所有恐龙都是Q版迷你体型(20-40cm高)
|
||||
- 城市按迷你恐龙尺寸设计:小号路灯、小号垃圾桶、小号办公楼
|
||||
- 现代科技水平,但保留了一些恐龙本能和习惯
|
||||
|
||||
## 侏罗纪科技有限公司
|
||||
- 主要做"跨物种兼容性测试"(一个没人懂的业务)
|
||||
- 办公文化:表面996,实际摸鱼文化盛行
|
||||
- 公司传统:每月全勤奖(T仔执念)`;
|
||||
|
||||
const SYNOPSIS_CONTENT = `# 分集梗概 — EP01「最遥远的距离」
|
||||
|
||||
## 核心主题
|
||||
一只迟到的霸王龙后代,与那个永远够不着的闹钟按钮之间的距离。
|
||||
|
||||
## 三幕结构
|
||||
|
||||
### 第一幕(建置)~40秒
|
||||
T仔被闹钟吵醒,小短手够不着关闹钟。痒痒挠被尾巴扫飞,反弹触发霸王龙咆哮闹铃。混乱的早晨建立了T仔的"小短手"核心设定。
|
||||
|
||||
### 第二幕(对抗)~80秒
|
||||
上班路上遭遇火腿肠事件(触发肉食本能→被外卖龙碾碎→酱汁溅脸→小短手擦不到)。到达公司遇到特特凡尔赛、皮皮暴走,消防喷淋事故全员湿透。层层叠加的倒霉事件。
|
||||
|
||||
### 第三幕(结局)~40秒
|
||||
生死时速冲向打卡机,迟到1秒。全勤奖化为泡影。皮皮天真的"团魂"宣言成为最后一击。T仔面对镜头的死鱼眼定格。
|
||||
|
||||
## 彩蛋(3秒)
|
||||
英总办公室飘出洗发水香味,引出下集。
|
||||
|
||||
## 情感曲线
|
||||
开场丧 → 小短手笑点 → 路上更倒霉 → 公司混乱 → 绝望冲刺 → 差1秒 → 丧到极致 → 彩蛋反转`;
|
||||
|
||||
const BEATS_CONTENT = `# 节拍表 — EP01「最遥远的距离」
|
||||
|
||||
| # | 时码 | 场景 | 节拍 | 情绪 | 视觉要点 |
|
||||
|---|------|------|------|------|----------|
|
||||
| 1 | 0:00-0:05 | 黑屏 | OS引入 | 悬念 | 纯黑+白字幕 |
|
||||
| 2 | 0:05-0:15 | 公寓 | 闹钟响 | 日常 | 俯拍小床,闹钟震动 |
|
||||
| 3 | 0:15-0:25 | 公寓 | 小短手够不着 | 搞笑 | 特写手臂挥空 |
|
||||
| 4 | 0:25-0:35 | 公寓 | 痒痒挠→咆哮闹钟 | 混乱 | 连锁反应,声波震水杯 |
|
||||
| 5 | 0:35-0:45 | 公寓 | 物理消音 | 无奈搞笑 | 闹钟叼嘴里 |
|
||||
| 6 | 0:45-0:55 | 街道 | 狂奔出门 | 紧迫 | 领带甩脸,尾巴扫垃圾桶 |
|
||||
| 7 | 0:55-1:10 | 街道 | 火腿肠事件 | 搞笑→倒霉 | 外卖车碾过,酱汁特写 |
|
||||
| 8 | 1:10-1:20 | 街道 | 小短手擦不到脸 | 绝望搞笑 | 烟熏妆效果 |
|
||||
| 9 | 1:20-1:35 | 公司 | 特特凡尔赛 | 对比 | 低空滑翔,草皮尴尬 |
|
||||
| 10 | 1:35-1:50 | 公司 | 皮皮闯祸 | 高潮 | 弹球撞击,消防喷水 |
|
||||
| 11 | 1:50-2:05 | 公司 | 生死冲刺 | 紧张 | 慢动作+时钟 |
|
||||
| 12 | 2:05-2:15 | 打卡机 | 迟到1秒 | 崩溃 | 脸部裂痕特效 |
|
||||
| 13 | 2:15-2:30 | 打卡机 | 皮皮"团魂" | 丧到极致 | 二分画面定格 |
|
||||
| 14 | 2:30-2:35 | 工位 | 彩蛋 | 反转 | 金色香气线条 |`;
|
||||
|
||||
const SCRIPT_CONTENT = `# 《恐龙也是打工龙》 第1集 — 最遥远的距离
|
||||
|
||||
> 类型:泡面番 | 时长:约2分30秒~2分40秒 | 受众:全年龄/打工人共鸣向
|
||||
|
||||
---
|
||||
|
||||
## 【角色表】
|
||||
|
||||
> 所有角色均为Q版迷你体型,生活在按小恐龙尺寸设计的迷你城市中。
|
||||
|
||||
**T仔**(主角)— Q版迷你霸王龙
|
||||
外貌:约30厘米高,2头身比例,圆滚滚的身体。橘红色皮肤,肚皮浅黄。两只小短手只有拇指长,几乎什么都够不着(核心喜剧设定)。穿白色迷你衬衫,打绿色小领带。
|
||||
性格:丧系打工龙,嘴上吐槽,心里认命。
|
||||
|
||||
---
|
||||
|
||||
## 场 1
|
||||
时:日 07:30
|
||||
景:内 T仔狭小的单身公寓
|
||||
人:T仔
|
||||
|
||||
△ [黑屏] T仔(OS):作为一只霸王龙的后代,我每天最大的敌人是——
|
||||
△ [俯拍/近景] 小床上,T仔躺在被窝里,只露出一颗圆滚滚的橘红色大脑袋,闭着眼,睡得很沉。
|
||||
△ [特写] 床头柜上的红色闹钟显示07:30,猛地开始震动。
|
||||
△ [近景] T仔被震醒,从被子里伸出小短手够闹钟,疯狂挥舞,就是够不着。
|
||||
△ [近景] T仔放弃,叹了口气,掀开被子坐起来。
|
||||
T仔:又是新的一天。
|
||||
△ [近景] T仔摸出痒痒挠,想关掉闹钟。
|
||||
△ [中近景] T仔的尾巴突然猛地一甩!咻——啪!
|
||||
△ [近景] 痒痒挠被尾巴扫飞,砸在墙上,反弹回来,精准击中闹钟按钮。
|
||||
△ [特写] 闹钟变红,T仔提前录制的\u201C霸王龙咆哮\u201D炸响。
|
||||
△ [近景] 嗷————!!(声波震得水杯里的水都在抖)
|
||||
T仔(崩溃抱头):闭嘴!那是进化的误会!
|
||||
△ [中景] T仔张开大口,把咆哮的闹钟叼在嘴里物理隔音。
|
||||
△ [近景] 嗷~ 嗷~(从T仔嘴里传出闷闷的铃声)
|
||||
CO
|
||||
|
||||
## 场 2
|
||||
时:日 08:30
|
||||
景:外 恐龙城街道
|
||||
人:T仔、外卖三角龙
|
||||
|
||||
△ [中近景] T仔嘴里叼着半片吐司狂奔,领带甩在脸上。
|
||||
T仔(OS):来不及了,马上要错过全勤奖了!
|
||||
△ [近景] 跑太快,尾巴扫飞了路边的垃圾桶。
|
||||
△ [特写] 垃圾桶里滚出一根火腿肠。
|
||||
△ [近景] T仔眼睛一亮,急刹。
|
||||
T仔:顶级的工业淀粉!我的灵魂伴侣!
|
||||
△ [中景] 三角龙外卖车冲过来。
|
||||
外卖龙:让让!外卖超时要扣钱的!
|
||||
△ [特写] 车轮碾过火腿肠,噗叽!
|
||||
△ [近景] 酱汁溅了T仔一脸,像烟熏妆。T仔举起小短手想抹脸——够不着。
|
||||
T仔:为什么倒霉的总是我。
|
||||
CO
|
||||
|
||||
## 场 3
|
||||
时:日 08:55
|
||||
景:内 公司打卡处
|
||||
人:T仔、特特、皮皮
|
||||
|
||||
△ [中近景] T仔跑到打卡处。
|
||||
△ [中景] 特特戴着防风镜,叼着文件,低空滑翔。
|
||||
机器音:打卡成功。
|
||||
△ [中景] 特特落地,整理羽毛,凡尔赛。
|
||||
特特:借过一下,低效率的陆地生物们。
|
||||
△ [特写] 特特翅膀尖粘着一块草皮。
|
||||
T仔:特特,英总说过"带薪吃草"扣绩效。
|
||||
△ [中景] 皮皮滚入画面,像炮弹撞在特特翅膀上。特特被撞飞,扎进消防喷淋头。哗啦——!
|
||||
△ [中景] 全员被淋。
|
||||
特特:我的精英人设!
|
||||
CO
|
||||
|
||||
## 场 4
|
||||
时:日
|
||||
景:内 打卡机前
|
||||
人:T仔、皮皮
|
||||
|
||||
△ [特写/慢动作] 电子钟从 08:59:59 跳动。
|
||||
△ [近景] T仔的大脸带着残影冲向感应区。咚!
|
||||
△ [特写] 屏幕:识别中...
|
||||
△ [特写] 09:00:01。
|
||||
机器音:滴——!早上好T仔!您已迟到1秒。
|
||||
△ [近景] T仔脸上出现裂痕。
|
||||
T仔(绝望):我的全勤奖啊。
|
||||
△ [中景] 皮皮甩水到T仔脸上,举起短手。
|
||||
皮皮:耶!大哥!我们一起迟到!这就是团魂吗?
|
||||
△ [近景] T仔面对镜头,眼神空洞。
|
||||
T仔(OS):所谓成年人的崩溃,不需要天塌下来…… 只需要…… 那个缓冲的圆圈,多转了一圈。
|
||||
△ [中景] T仔肚子雷鸣。打破二胡BGM。
|
||||
△ [中景 二分画面 定格] T仔灰白脸 vs 皮皮灿烂笑脸。
|
||||
[字幕] 《恐龙也是打工龙》 第1集 完
|
||||
CO
|
||||
|
||||
---
|
||||
|
||||
## 【片尾彩蛋】(3秒)
|
||||
|
||||
△ [近景] T仔湿淋淋坐在工位,面前摊着考勤罚单。鼻子抽了抽。
|
||||
△ [主观镜头] 英总办公室门缝飘出洗发水香气,化成金色线条。
|
||||
T仔(OS):好香……
|
||||
[黑屏]
|
||||
[字幕] 下集:禁忌的本能`;
|
||||
|
||||
const INITIAL_STEPS: CreationStep[] = [
|
||||
{
|
||||
id: "concept",
|
||||
num: 1,
|
||||
title: "创意构思",
|
||||
shortTitle: "创意",
|
||||
description: "定义核心概念、类型风格和笑点机制",
|
||||
status: "done",
|
||||
content: CONCEPT_CONTENT,
|
||||
},
|
||||
{
|
||||
id: "outline",
|
||||
num: 2,
|
||||
title: "故事大纲",
|
||||
shortTitle: "大纲",
|
||||
description: "规划季度主线和 13 集结构",
|
||||
status: "done",
|
||||
content: OUTLINE_CONTENT,
|
||||
},
|
||||
{
|
||||
id: "characters",
|
||||
num: 3,
|
||||
title: "角色设定",
|
||||
shortTitle: "角色",
|
||||
description: "定义角色外观、性格和世界观",
|
||||
status: "done",
|
||||
content: CHARACTER_CONTENT,
|
||||
},
|
||||
{
|
||||
id: "synopsis",
|
||||
num: 4,
|
||||
title: "分集梗概",
|
||||
shortTitle: "梗概",
|
||||
description: "单集三幕结构和情感曲线",
|
||||
status: "done",
|
||||
content: SYNOPSIS_CONTENT,
|
||||
},
|
||||
{
|
||||
id: "beats",
|
||||
num: 5,
|
||||
title: "节拍表",
|
||||
shortTitle: "节拍",
|
||||
description: "逐场节拍、时码和视觉要点",
|
||||
status: "done",
|
||||
content: BEATS_CONTENT,
|
||||
},
|
||||
{
|
||||
id: "script",
|
||||
num: 6,
|
||||
title: "剧本正文",
|
||||
shortTitle: "剧本",
|
||||
description: "完整分场剧本(对话 + 动作指示)",
|
||||
status: "editing",
|
||||
content: SCRIPT_CONTENT,
|
||||
},
|
||||
];
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Markdown Renderer (simple)
|
||||
───────────────────────────────────────────── */
|
||||
function RenderMarkdown({ content }: { content: string }) {
|
||||
return (
|
||||
<div className="text-sm leading-relaxed">
|
||||
{content.split("\n").map((line, i) => {
|
||||
if (line.startsWith("# "))
|
||||
return (
|
||||
<h1 key={i} className="font-[family-name:var(--font-heading)] text-2xl font-bold text-text-primary mb-4 mt-2">
|
||||
{line.slice(2)}
|
||||
</h1>
|
||||
);
|
||||
if (line.startsWith("## "))
|
||||
return (
|
||||
<h2 key={i} className="font-[family-name:var(--font-heading)] text-lg font-semibold text-accent mt-8 mb-3">
|
||||
{line.slice(3)}
|
||||
</h2>
|
||||
);
|
||||
if (line.startsWith("### "))
|
||||
return (
|
||||
<h3 key={i} className="font-[family-name:var(--font-heading)] text-base font-semibold text-text-primary mt-6 mb-2">
|
||||
{line.slice(4)}
|
||||
</h3>
|
||||
);
|
||||
if (line.startsWith("> "))
|
||||
return (
|
||||
<p key={i} className="text-text-secondary italic border-l-2 border-accent/30 pl-4 mb-4">
|
||||
{line.slice(2)}
|
||||
</p>
|
||||
);
|
||||
if (line === "---")
|
||||
return <hr key={i} className="border-white/[0.06] my-4" />;
|
||||
if (line.startsWith("| "))
|
||||
return (
|
||||
<p key={i} className="text-text-secondary font-[family-name:var(--font-mono)] text-xs my-0.5">
|
||||
{line}
|
||||
</p>
|
||||
);
|
||||
if (line === "") return <div key={i} className="h-2" />;
|
||||
if (line.startsWith("**")) {
|
||||
const parts = line.split(/(\*\*[^*]+\*\*)/g);
|
||||
return (
|
||||
<p key={i} className="my-1 text-text-primary">
|
||||
{parts.map((part, j) =>
|
||||
part.startsWith("**") && part.endsWith("**") ? (
|
||||
<strong key={j} className="font-semibold text-text-primary">{part.replace(/\*\*/g, "")}</strong>
|
||||
) : (
|
||||
<span key={j}>{part}</span>
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
if (line.startsWith("- "))
|
||||
return (
|
||||
<p key={i} className="my-0.5 text-text-primary pl-4">
|
||||
<span className="text-text-muted mr-2">•</span>
|
||||
{line.slice(2)}
|
||||
</p>
|
||||
);
|
||||
if (/^\d+\.\s/.test(line))
|
||||
return <p key={i} className="my-0.5 text-text-primary pl-4">{line}</p>;
|
||||
if (line.startsWith("\u25b3"))
|
||||
return <p key={i} className="my-1 text-blue-300/80">{line}</p>;
|
||||
return <p key={i} className="my-1 text-text-primary">{line}</p>;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Chat Bubble
|
||||
───────────────────────────────────────────── */
|
||||
function ChatBubble({ message }: { message: Message }) {
|
||||
const isUser = message.role === "user";
|
||||
return (
|
||||
<div className={`flex gap-3 ${isUser ? "flex-row-reverse" : ""}`}>
|
||||
<div
|
||||
className={`w-7 h-7 rounded-full flex items-center justify-center shrink-0 mt-1
|
||||
${isUser ? "bg-gradient-to-br from-violet-500 to-accent-blue" : "bg-accent/20 border border-accent/30"}`}
|
||||
>
|
||||
{isUser ? <User className="w-3.5 h-3.5 text-white" /> : <Sparkles className="w-3.5 h-3.5 text-accent" />}
|
||||
</div>
|
||||
<div className={`max-w-[90%] ${isUser ? "items-end" : "items-start"}`}>
|
||||
<div
|
||||
className={`rounded-2xl px-4 py-3 text-sm leading-relaxed
|
||||
${isUser ? "bg-accent/15 border border-accent/20 text-text-primary" : "bg-white/[0.04] border border-white/[0.06] text-text-primary"}`}
|
||||
>
|
||||
{message.content.split("\n").map((line, i) => {
|
||||
if (line === "") return <div key={i} className="h-1.5" />;
|
||||
const parts = line.split(/(\*\*[^*]+\*\*)/g);
|
||||
return (
|
||||
<p key={i} className="my-0.5">
|
||||
{parts.map((part, j) =>
|
||||
part.startsWith("**") && part.endsWith("**") ? (
|
||||
<strong key={j} className="font-semibold text-accent">{part.replace(/\*\*/g, "")}</strong>
|
||||
) : (
|
||||
<span key={j}>{part}</span>
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{!isUser && (
|
||||
<div className="flex items-center gap-1.5 mt-1.5 ml-1">
|
||||
<button className="p-1 rounded text-text-muted hover:text-text-secondary cursor-pointer" aria-label="复制">
|
||||
<Copy className="w-3 h-3" />
|
||||
</button>
|
||||
<button className="p-1 rounded text-text-muted hover:text-text-secondary cursor-pointer" aria-label="重新生成">
|
||||
<RotateCcw className="w-3 h-3" />
|
||||
</button>
|
||||
<span className="text-[10px] text-text-muted ml-1">{message.timestamp}</span>
|
||||
</div>
|
||||
)}
|
||||
{isUser && (
|
||||
<div className="flex justify-end mt-1 mr-1">
|
||||
<span className="text-[10px] text-text-muted">{message.timestamp}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Status helpers
|
||||
───────────────────────────────────────────── */
|
||||
const STATUS_CONFIG: Record<StepStatus, { label: string; cls: string; iconCls: string }> = {
|
||||
done: { label: "已完成", cls: "bg-emerald-500/15 text-emerald-400 border-emerald-400/20", iconCls: "text-emerald-400" },
|
||||
editing: { label: "编辑中", cls: "bg-blue-500/15 text-blue-400 border-blue-400/20", iconCls: "text-blue-400" },
|
||||
pending: { label: "待开始", cls: "bg-white/[0.06] text-text-muted border-white/[0.06]", iconCls: "text-text-muted" },
|
||||
};
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Page
|
||||
───────────────────────────────────────────── */
|
||||
export default function ScriptWorkstationPage() {
|
||||
const [steps, setSteps] = useState<CreationStep[]>(INITIAL_STEPS);
|
||||
const [activeStepId, setActiveStepId] = useState("concept");
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editContent, setEditContent] = useState("");
|
||||
const [input, setInput] = useState("");
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
/* ── Resizable right chat panel ── */
|
||||
const [chatWidth, setChatWidth] = useState(400);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleRightResize = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
document.body.style.cursor = "col-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
const containerRight = containerRef.current?.getBoundingClientRect().right ?? window.innerWidth;
|
||||
const containerLeft = containerRef.current?.getBoundingClientRect().left ?? 0;
|
||||
const containerWidth = containerRight - containerLeft;
|
||||
const onMove = (ev: MouseEvent) => {
|
||||
const newW = Math.min(Math.max(containerRight - ev.clientX, 150), containerWidth * 0.7);
|
||||
setChatWidth(newW);
|
||||
};
|
||||
const onUp = () => {
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
window.removeEventListener("mousemove", onMove);
|
||||
window.removeEventListener("mouseup", onUp);
|
||||
};
|
||||
window.addEventListener("mousemove", onMove);
|
||||
window.addEventListener("mouseup", onUp);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const activeStep = steps.find((s) => s.id === activeStepId)!;
|
||||
const doneCount = steps.filter((s) => s.status === "done").length;
|
||||
const allDone = doneCount === steps.length;
|
||||
const remainingCount = steps.length - doneCount;
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, []);
|
||||
|
||||
const handleSelectStep = (stepId: string) => {
|
||||
setActiveStepId(stepId);
|
||||
setIsEditing(false);
|
||||
const step = steps.find((s) => s.id === stepId);
|
||||
if (step) setEditContent(step.content);
|
||||
};
|
||||
|
||||
const handleConfirmStep = (stepId: string) => {
|
||||
setSteps((prev) =>
|
||||
prev.map((s) => {
|
||||
if (s.id === stepId) return { ...s, status: "done" as StepStatus };
|
||||
return s;
|
||||
})
|
||||
);
|
||||
// Auto-advance to next pending step
|
||||
const currentIdx = steps.findIndex((s) => s.id === stepId);
|
||||
const nextPending = steps.find((s, i) => i > currentIdx && s.status !== "done");
|
||||
if (nextPending) {
|
||||
setActiveStepId(nextPending.id);
|
||||
setIsEditing(false);
|
||||
setEditContent(nextPending.content);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartEditing = () => {
|
||||
setEditContent(activeStep.content);
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
// Find current stage for chat indicator
|
||||
const currentEditingStep = steps.find((s) => s.status === "editing") || steps.find((s) => s.status === "pending");
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex flex-col h-screen">
|
||||
{/* ─── Top Bar ─── */}
|
||||
<div
|
||||
className="shrink-0 border-b border-white/[0.06] px-5 py-2.5"
|
||||
style={{
|
||||
background: "rgba(7, 7, 15, 0.85)",
|
||||
backdropFilter: "blur(20px) saturate(180%)",
|
||||
WebkitBackdropFilter: "blur(20px) saturate(180%)",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
href="/dashboard/proj_001"
|
||||
className="p-1.5 rounded-lg text-text-muted hover:text-text-secondary hover:bg-white/[0.06] motion-safe:transition-colors cursor-pointer"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</Link>
|
||||
<div className="mr-4">
|
||||
<h1 className="font-[family-name:var(--font-heading)] text-sm font-semibold text-text-primary">
|
||||
EP01 · 第一天上班
|
||||
</h1>
|
||||
<p className="text-[10px] text-text-muted">T仔的上班日记 · 剧本创作</p>
|
||||
</div>
|
||||
|
||||
{/* Step indicators */}
|
||||
<div className="flex items-center gap-1 flex-1">
|
||||
{steps.map((step, i) => {
|
||||
const isActive = step.id === activeStepId;
|
||||
const isDone = step.status === "done";
|
||||
return (
|
||||
<div key={step.id} className="flex items-center">
|
||||
<button
|
||||
onClick={() => handleSelectStep(step.id)}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium cursor-pointer motion-safe:transition-all
|
||||
${isActive
|
||||
? "bg-accent/15 text-accent border border-accent/20"
|
||||
: isDone
|
||||
? "bg-emerald-500/10 text-emerald-400/80 hover:bg-emerald-500/15"
|
||||
: "bg-white/[0.03] text-text-muted hover:bg-white/[0.06]"
|
||||
}`}
|
||||
>
|
||||
{isDone ? (
|
||||
<Check className="w-3 h-3" />
|
||||
) : (
|
||||
<span className="text-[10px]">{step.num}</span>
|
||||
)}
|
||||
<span className="hidden lg:inline">{step.shortTitle}</span>
|
||||
</button>
|
||||
{i < steps.length - 1 && (
|
||||
<ChevronRight className="w-3 h-3 text-text-muted mx-0.5 shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── Main Layout ─── */}
|
||||
<div ref={containerRef} className="flex-1 flex min-h-0">
|
||||
{/* ─── Left: Document Sidebar ─── */}
|
||||
<div
|
||||
className="w-[160px] shrink-0 border-r border-white/[0.06] flex flex-col min-h-0"
|
||||
style={{ background: "rgba(7, 7, 15, 0.50)" }}
|
||||
>
|
||||
<div className="px-3 py-2.5 border-b border-white/[0.06]">
|
||||
<p className="text-[10px] font-medium text-text-muted uppercase tracking-wider flex items-center gap-1.5">
|
||||
<FileText className="w-3 h-3" />
|
||||
文档
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto py-2 px-2">
|
||||
{steps.map((step) => {
|
||||
const isActive = step.id === activeStepId;
|
||||
const isDone = step.status === "done";
|
||||
const isEditingStep = step.status === "editing";
|
||||
return (
|
||||
<button
|
||||
key={step.id}
|
||||
onClick={() => handleSelectStep(step.id)}
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg text-left cursor-pointer
|
||||
motion-safe:transition-colors mb-1
|
||||
${isActive
|
||||
? "bg-accent/15 text-accent"
|
||||
: "text-text-secondary hover:bg-white/[0.06]"
|
||||
}`}
|
||||
>
|
||||
{isDone ? (
|
||||
<Check className="w-3.5 h-3.5 text-emerald-400 shrink-0" />
|
||||
) : isEditingStep ? (
|
||||
<Pencil className="w-3.5 h-3.5 text-blue-400 shrink-0" />
|
||||
) : (
|
||||
<Clock className="w-3.5 h-3.5 text-text-muted shrink-0" />
|
||||
)}
|
||||
<span className="text-xs font-medium truncate">{step.shortTitle}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Export button */}
|
||||
<div className="px-2 py-2.5 border-t border-white/[0.06]">
|
||||
<button className="w-full px-3 py-2 rounded-lg text-xs font-medium text-text-secondary bg-white/[0.04] border border-white/[0.06] hover:bg-white/[0.08] cursor-pointer flex items-center justify-center gap-1.5 motion-safe:transition-colors">
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
全部导出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── Center: Step Content ─── */}
|
||||
<div
|
||||
className="flex-1 flex flex-col min-h-0 min-w-0"
|
||||
style={{ background: "rgba(7, 7, 15, 0.30)" }}
|
||||
>
|
||||
{/* Step header */}
|
||||
<div className="shrink-0 flex items-center justify-between px-6 py-3 border-b border-white/[0.06]">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<span className="text-lg font-[family-name:var(--font-heading)] font-bold text-text-primary">
|
||||
{activeStep.title}
|
||||
</span>
|
||||
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[10px] font-medium border ${STATUS_CONFIG[activeStep.status].cls}`}>
|
||||
{activeStep.status === "done" ? <Check className="w-3 h-3" /> : activeStep.status === "editing" ? <Pencil className="w-3 h-3" /> : <Clock className="w-3 h-3" />}
|
||||
{STATUS_CONFIG[activeStep.status].label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{activeStep.content && (
|
||||
<button
|
||||
onClick={() => isEditing ? setIsEditing(false) : handleStartEditing()}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium motion-safe:transition-colors cursor-pointer flex items-center gap-1.5
|
||||
${isEditing
|
||||
? "bg-accent/15 text-accent border border-accent/20"
|
||||
: "bg-white/[0.06] text-text-secondary border border-white/10 hover:bg-white/[0.1]"
|
||||
}`}
|
||||
>
|
||||
{isEditing ? <><Eye className="w-3 h-3" />预览</> : <><Pencil className="w-3 h-3" />编辑</>}
|
||||
</button>
|
||||
)}
|
||||
<button className="px-3 py-1.5 rounded-lg text-xs font-medium text-text-secondary bg-white/[0.06] border border-white/10 hover:bg-white/[0.1] cursor-pointer flex items-center gap-1.5 motion-safe:transition-colors">
|
||||
<Download className="w-3 h-3" />
|
||||
下载
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="shrink-0 px-6 py-2 border-b border-white/[0.04]">
|
||||
<p className="text-xs text-text-muted">{activeStep.description}</p>
|
||||
</div>
|
||||
|
||||
{/* Content area */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{activeStep.content ? (
|
||||
isEditing ? (
|
||||
<textarea
|
||||
value={editContent}
|
||||
onChange={(e) => setEditContent(e.target.value)}
|
||||
className="w-full h-full bg-transparent px-6 py-5 text-sm text-text-primary leading-relaxed resize-none focus:outline-none font-[family-name:var(--font-mono)]"
|
||||
spellCheck={false}
|
||||
/>
|
||||
) : (
|
||||
<div className="px-6 py-5">
|
||||
<RenderMarkdown content={activeStep.content} />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center px-6">
|
||||
<Clock className="w-10 h-10 text-text-muted mb-3" />
|
||||
<p className="text-sm text-text-secondary">这一步还没开始</p>
|
||||
<p className="text-xs text-text-muted mt-1 mb-4">
|
||||
在右侧与 AI 对话,自动生成内容
|
||||
</p>
|
||||
<button className="px-4 py-2 rounded-xl text-sm font-medium text-accent bg-accent/10 border border-accent/20 hover:bg-accent/20 cursor-pointer flex items-center gap-2 motion-safe:transition-colors">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
让 AI 开始创作
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Step action bar */}
|
||||
{activeStep.content && activeStep.status !== "done" && (
|
||||
<div className="shrink-0 px-6 py-3 border-t border-white/[0.06] flex items-center justify-end gap-2">
|
||||
<button
|
||||
onClick={() => handleConfirmStep(activeStep.id)}
|
||||
className="px-4 py-2 rounded-xl text-sm font-semibold text-white bg-gradient-to-br from-emerald-500 to-emerald-600 shadow-[0_0_12px_rgba(16,185,129,0.3)] border border-white/15 hover:shadow-[0_0_20px_rgba(16,185,129,0.5)] cursor-pointer flex items-center gap-2 motion-safe:transition-all"
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
确认此步骤
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ─── Right Resize Handle ─── */}
|
||||
<div
|
||||
onMouseDown={handleRightResize}
|
||||
className="shrink-0 w-1 cursor-col-resize hover:bg-accent/30 active:bg-accent/50 motion-safe:transition-colors bg-white/[0.06]"
|
||||
/>
|
||||
|
||||
{/* ─── Right: AI Chat ─── */}
|
||||
<div
|
||||
className="shrink-0 flex flex-col min-h-0"
|
||||
style={{ width: `${chatWidth}px`, background: "rgba(7, 7, 15, 0.40)" }}
|
||||
>
|
||||
{/* Chat header */}
|
||||
<div className="shrink-0 px-4 py-2.5 border-b border-white/[0.06] flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-accent" />
|
||||
<span className="text-sm font-medium text-text-primary">AI 创作助手</span>
|
||||
</div>
|
||||
|
||||
{/* Current stage hint */}
|
||||
{currentEditingStep && (
|
||||
<div className="shrink-0 px-4 py-2 border-b border-white/[0.04]">
|
||||
<p className="text-xs text-accent">
|
||||
正在进行第 {currentEditingStep.num} 步:{currentEditingStep.title}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Chat messages */}
|
||||
<div className="flex-1 overflow-y-auto px-4 py-4">
|
||||
<div className="space-y-5">
|
||||
{MOCK_MESSAGES.map((msg) => (
|
||||
<ChatBubble key={msg.id} message={msg} />
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chat input */}
|
||||
<div className="shrink-0 border-t border-white/[0.06] px-4 py-3">
|
||||
<div className="flex items-end gap-2">
|
||||
<textarea
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="描述剧本需求,或提出修改意见..."
|
||||
rows={2}
|
||||
className="flex-1 bg-white/[0.04] border border-white/10 rounded-xl px-4 py-3 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:border-accent/50 focus:ring-3 focus:ring-accent/15 motion-safe:transition-all resize-none"
|
||||
/>
|
||||
<button
|
||||
className={`shrink-0 w-10 h-10 rounded-xl flex items-center justify-center motion-safe:transition-all cursor-pointer
|
||||
${input.trim()
|
||||
? "bg-accent text-white shadow-[0_0_12px_rgba(108,99,255,0.4)] hover:shadow-[0_0_20px_rgba(108,99,255,0.6)]"
|
||||
: "bg-white/[0.06] text-text-muted border border-white/10"
|
||||
}`}
|
||||
disabled={!input.trim()}
|
||||
aria-label="发送"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── Bottom CTA ─── */}
|
||||
<div
|
||||
className="shrink-0 border-t border-white/[0.06] px-6 py-3"
|
||||
style={{
|
||||
background: "rgba(7, 7, 15, 0.90)",
|
||||
backdropFilter: "blur(20px)",
|
||||
WebkitBackdropFilter: "blur(20px)",
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href="/dashboard/proj_001/pipeline/ep01"
|
||||
className={`w-full flex items-center justify-center gap-3 px-6 py-3.5 rounded-xl text-sm font-semibold motion-safe:transition-all
|
||||
${allDone
|
||||
? "bg-gradient-to-br from-violet-500 to-violet-700 shadow-[0_0_24px_rgba(139,92,246,0.4)] border border-white/15 text-white hover:shadow-[0_0_36px_rgba(139,92,246,0.6)] cursor-pointer"
|
||||
: "bg-white/[0.04] border border-white/[0.06] text-text-muted cursor-default"
|
||||
}`}
|
||||
onClick={(e) => { if (!allDone) e.preventDefault(); }}
|
||||
>
|
||||
<Play className="w-4 h-4" />
|
||||
{allDone
|
||||
? "确认剧本,启动流水线"
|
||||
: `还差 ${remainingCount} 个步骤未完成`}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
362
frontend/src/app/dashboard/[projectId]/page.tsx
Normal file
362
frontend/src/app/dashboard/[projectId]/page.tsx
Normal file
@ -0,0 +1,362 @@
|
||||
"use client";
|
||||
|
||||
import { use } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Plus,
|
||||
Film,
|
||||
Play,
|
||||
Check,
|
||||
Clock,
|
||||
AlertTriangle,
|
||||
MessageSquare,
|
||||
ChevronRight,
|
||||
Settings,
|
||||
Loader2,
|
||||
Pause,
|
||||
} from "lucide-react";
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Mock Data
|
||||
───────────────────────────────────────────── */
|
||||
interface Episode {
|
||||
id: string;
|
||||
number: number;
|
||||
title: string;
|
||||
status: "draft" | "running" | "completed" | "failed" | "idle";
|
||||
currentStage: number;
|
||||
duration: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
const STAGE_NAMES = [
|
||||
"剧本对话",
|
||||
"规划",
|
||||
"参考图生成",
|
||||
"宫格生成",
|
||||
"切分",
|
||||
"视频生成",
|
||||
"剪辑导出",
|
||||
];
|
||||
|
||||
const PROJECT_INFO = {
|
||||
id: "proj_001",
|
||||
name: "T仔的上班日记",
|
||||
type: "原创动画",
|
||||
description: "讲述一只名叫T仔的小猫在互联网公司上班的日常趣事,每集约 4 分钟。",
|
||||
totalEpisodes: 12,
|
||||
skills: ["screenplay-skill", "storyboard-video-skill", "script-segmentation-skill"],
|
||||
};
|
||||
|
||||
const MOCK_EPISODES: Episode[] = [
|
||||
{
|
||||
id: "ep_01",
|
||||
number: 1,
|
||||
title: "第一天上班",
|
||||
status: "running",
|
||||
currentStage: 6,
|
||||
duration: "4:20",
|
||||
updatedAt: "2 小时前",
|
||||
},
|
||||
{
|
||||
id: "ep_02",
|
||||
number: 2,
|
||||
title: "团建风波",
|
||||
status: "completed",
|
||||
currentStage: 7,
|
||||
duration: "3:55",
|
||||
updatedAt: "昨天",
|
||||
},
|
||||
{
|
||||
id: "ep_03",
|
||||
number: 3,
|
||||
title: "KPI 之谜",
|
||||
status: "draft",
|
||||
currentStage: 0,
|
||||
duration: "—",
|
||||
updatedAt: "3 天前",
|
||||
},
|
||||
{
|
||||
id: "ep_04",
|
||||
number: 4,
|
||||
title: "加班到天亮",
|
||||
status: "failed",
|
||||
currentStage: 3,
|
||||
duration: "—",
|
||||
updatedAt: "5 天前",
|
||||
},
|
||||
{
|
||||
id: "ep_05",
|
||||
number: 5,
|
||||
title: "(未命名)",
|
||||
status: "idle",
|
||||
currentStage: 0,
|
||||
duration: "—",
|
||||
updatedAt: "—",
|
||||
},
|
||||
];
|
||||
|
||||
const STATUS_CONFIG: Record<
|
||||
Episode["status"],
|
||||
{ label: string; icon: React.ElementType; color: string; bg: string; border: string }
|
||||
> = {
|
||||
draft: {
|
||||
label: "草稿",
|
||||
icon: MessageSquare,
|
||||
color: "text-text-secondary",
|
||||
bg: "bg-white/[0.06]",
|
||||
border: "border-white/10",
|
||||
},
|
||||
running: {
|
||||
label: "运行中",
|
||||
icon: Loader2,
|
||||
color: "text-blue-400",
|
||||
bg: "bg-blue-500/10",
|
||||
border: "border-blue-400/20",
|
||||
},
|
||||
completed: {
|
||||
label: "已完成",
|
||||
icon: Check,
|
||||
color: "text-emerald-400",
|
||||
bg: "bg-emerald-500/10",
|
||||
border: "border-emerald-400/20",
|
||||
},
|
||||
failed: {
|
||||
label: "失败",
|
||||
icon: AlertTriangle,
|
||||
color: "text-red-400",
|
||||
bg: "bg-red-500/10",
|
||||
border: "border-red-400/20",
|
||||
},
|
||||
idle: {
|
||||
label: "空闲",
|
||||
icon: Pause,
|
||||
color: "text-text-muted",
|
||||
bg: "bg-white/[0.04]",
|
||||
border: "border-white/[0.06]",
|
||||
},
|
||||
};
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Components
|
||||
───────────────────────────────────────────── */
|
||||
|
||||
function MiniStepper({ currentStage, status }: { currentStage: number; status: Episode["status"] }) {
|
||||
return (
|
||||
<div className="flex items-center gap-0.5">
|
||||
{STAGE_NAMES.map((_, i) => {
|
||||
const stageNum = i + 1;
|
||||
const isCompleted = stageNum < currentStage;
|
||||
const isCurrent = stageNum === currentStage;
|
||||
const isFailed = isCurrent && status === "failed";
|
||||
|
||||
let bg = "bg-white/[0.08]";
|
||||
if (isCompleted) bg = "bg-emerald-500/50";
|
||||
if (isCurrent && status === "running") bg = "bg-blue-500/60 motion-safe:animate-pulse";
|
||||
if (isCurrent && status === "completed") bg = "bg-emerald-500/50";
|
||||
if (isFailed) bg = "bg-red-500/50";
|
||||
|
||||
return <div key={stageNum} className={`h-1 flex-1 rounded-full ${bg}`} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EpisodeRow({ episode, projectId }: { episode: Episode; projectId: string }) {
|
||||
const conf = STATUS_CONFIG[episode.status];
|
||||
const StatusIcon = conf.icon;
|
||||
const hasScript = episode.status !== "idle";
|
||||
const hasPipeline = episode.currentStage > 0;
|
||||
|
||||
return (
|
||||
<div className="glass-card p-5 hover:bg-white/[0.08] motion-safe:transition-all motion-safe:duration-200 group">
|
||||
<div className="flex items-center gap-5">
|
||||
{/* Episode number */}
|
||||
<div className="w-12 h-12 rounded-xl bg-white/[0.04] border border-white/[0.06] flex items-center justify-center shrink-0">
|
||||
<span className="font-[family-name:var(--font-heading)] text-lg font-bold text-text-secondary">
|
||||
{String(episode.number).padStart(2, "0")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<h3 className="font-[family-name:var(--font-heading)] text-base font-semibold text-text-primary truncate">
|
||||
{episode.title}
|
||||
</h3>
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md text-xs font-medium
|
||||
${conf.bg} ${conf.color} border ${conf.border}`}
|
||||
>
|
||||
<StatusIcon className={`w-3 h-3 ${episode.status === "running" ? "motion-safe:animate-spin" : ""}`} />
|
||||
{conf.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Mini stepper */}
|
||||
{hasPipeline && (
|
||||
<div className="max-w-xs mb-1.5">
|
||||
<MiniStepper currentStage={episode.currentStage} status={episode.status} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-4 text-xs text-text-muted">
|
||||
{hasPipeline && (
|
||||
<span>
|
||||
第{episode.currentStage}阶段 — {STAGE_NAMES[episode.currentStage - 1]}
|
||||
</span>
|
||||
)}
|
||||
{episode.duration !== "—" && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{episode.duration}
|
||||
</span>
|
||||
)}
|
||||
<span>{episode.updatedAt}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2">
|
||||
{hasScript && (
|
||||
<Link
|
||||
href={`/dashboard/${projectId}/chat?ep=${episode.number}`}
|
||||
className="px-3 py-1.5 rounded-lg text-xs font-medium text-text-secondary bg-white/[0.06] border border-white/10 hover:bg-white/[0.1] motion-safe:transition-colors cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
<MessageSquare className="w-3.5 h-3.5" />
|
||||
剧本
|
||||
</Link>
|
||||
)}
|
||||
{hasPipeline && (
|
||||
<Link
|
||||
href={`/dashboard/${projectId}/pipeline/${episode.id}`}
|
||||
className="px-3 py-1.5 rounded-lg text-xs font-medium text-white bg-accent/80 hover:bg-accent motion-safe:transition-colors cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
<Play className="w-3.5 h-3.5" />
|
||||
流水线
|
||||
</Link>
|
||||
)}
|
||||
{!hasScript && (
|
||||
<Link
|
||||
href={`/dashboard/${projectId}/chat?ep=${episode.number}`}
|
||||
className="px-3 py-1.5 rounded-lg text-xs font-medium text-accent bg-accent/10 border border-accent/20 hover:bg-accent/20 motion-safe:transition-colors cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
<MessageSquare className="w-3.5 h-3.5" />
|
||||
开始写剧本
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ChevronRight className="w-4 h-4 text-text-muted shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Page
|
||||
───────────────────────────────────────────── */
|
||||
export default function ProjectDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ projectId: string }>;
|
||||
}) {
|
||||
const { projectId } = use(params);
|
||||
const project = PROJECT_INFO;
|
||||
const episodes = MOCK_EPISODES;
|
||||
|
||||
const runningCount = episodes.filter((e) => e.status === "running").length;
|
||||
const completedCount = episodes.filter((e) => e.status === "completed").length;
|
||||
|
||||
return (
|
||||
<div className="relative z-10 min-h-screen px-8 py-8">
|
||||
{/* ─── Breadcrumb ─── */}
|
||||
<nav aria-label="Breadcrumb" className="flex items-center gap-2 text-sm mb-6">
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="text-text-secondary hover:text-text-primary motion-safe:transition-colors cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
<ArrowLeft className="w-3.5 h-3.5" />
|
||||
项目
|
||||
</Link>
|
||||
<ChevronRight className="w-3.5 h-3.5 text-text-muted" />
|
||||
<span className="text-text-primary font-medium">{project.name}</span>
|
||||
</nav>
|
||||
|
||||
{/* ─── Project Header ─── */}
|
||||
<div className="glass-card p-6 mb-8">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h1 className="font-[family-name:var(--font-heading)] text-2xl font-bold text-text-primary">
|
||||
{project.name}
|
||||
</h1>
|
||||
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-accent/10 text-accent border border-accent/15">
|
||||
{project.type}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-text-secondary max-w-2xl">{project.description}</p>
|
||||
</div>
|
||||
<Link
|
||||
href={`/dashboard/${projectId}/settings`}
|
||||
className="p-2.5 rounded-xl bg-white/[0.06] border border-white/10 hover:bg-white/[0.1] motion-safe:transition-colors cursor-pointer"
|
||||
aria-label="项目设置"
|
||||
>
|
||||
<Settings className="w-5 h-5 text-text-secondary" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Stats row */}
|
||||
<div className="flex items-center gap-6 text-sm">
|
||||
<span className="text-text-secondary">
|
||||
共 <span className="text-text-primary font-medium">{episodes.length}</span> / {project.totalEpisodes} 集
|
||||
</span>
|
||||
{runningCount > 0 && (
|
||||
<span className="flex items-center gap-1.5 text-blue-400">
|
||||
<span className="w-2 h-2 rounded-full bg-blue-500 animate-pulse" />
|
||||
{runningCount} 集运行中
|
||||
</span>
|
||||
)}
|
||||
{completedCount > 0 && (
|
||||
<span className="flex items-center gap-1.5 text-emerald-400">
|
||||
<Check className="w-3.5 h-3.5" />
|
||||
{completedCount} 集已完成
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
{project.skills.map((s) => (
|
||||
<span
|
||||
key={s}
|
||||
className="px-2 py-0.5 rounded-full text-xs bg-white/[0.04] text-text-muted border border-white/[0.06]"
|
||||
>
|
||||
{s}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── Episode List Header ─── */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="font-[family-name:var(--font-heading)] text-lg font-semibold text-text-primary">
|
||||
剧集列表
|
||||
</h2>
|
||||
<Link
|
||||
href={`/dashboard/${projectId}/chat?ep=new`}
|
||||
className="px-4 py-2 rounded-xl text-sm font-medium text-accent bg-accent/10 border border-accent/20 hover:bg-accent/20 motion-safe:transition-colors cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
添加剧集
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* ─── Episode List ─── */}
|
||||
<div className="space-y-3">
|
||||
{episodes.map((ep) => (
|
||||
<EpisodeRow key={ep.id} episode={ep} projectId={projectId} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,311 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ChevronRight,
|
||||
Check,
|
||||
Loader2,
|
||||
Clock,
|
||||
Eye,
|
||||
Download,
|
||||
RotateCcw,
|
||||
} from "lucide-react";
|
||||
|
||||
import PipelineStepper, {
|
||||
type Stage,
|
||||
type StageStatus,
|
||||
} from "@/components/pipeline/PipelineStepper";
|
||||
import ReviewGate from "@/components/pipeline/ReviewGate";
|
||||
import SegmentGrid, {
|
||||
MOCK_SEGMENTS,
|
||||
} from "@/components/pipeline/SegmentGrid";
|
||||
import LogPanel from "@/components/pipeline/LogPanel";
|
||||
import StagePlanning from "@/components/pipeline/StagePlanning";
|
||||
import StageImageAssets from "@/components/pipeline/StageImageAssets";
|
||||
import StageKeyshots from "@/components/pipeline/StageKeyshots";
|
||||
import StageSegments from "@/components/pipeline/StageSegments";
|
||||
import StageTimeline from "@/components/pipeline/StageTimeline";
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Initial Mock Data
|
||||
───────────────────────────────────────────── */
|
||||
const INITIAL_STAGES: Stage[] = [
|
||||
{ number: 1, name: "剧本对话", status: "completed", description: "与 AI 对话创作剧本" },
|
||||
{ number: 2, name: "规划", status: "completed", description: "Claude 分析剧本,提取角色/场景/宫格规划 JSON" },
|
||||
{ number: 3, name: "参考图生成", status: "completed", description: "Banana Pro 生成角色参考图 + 场景参考图" },
|
||||
{ number: 4, name: "宫格生成", status: "completed", description: "Banana Pro 生成宫格整图 → PIL 裁切为单格图" },
|
||||
{ number: 5, name: "切分", status: "completed", description: "Claude 将剧本按场景/动作切分为 ≤15s 片段" },
|
||||
{ number: 6, name: "视频生成", status: "completed", description: "Seedance 全并发生成所有视频片段" },
|
||||
{ number: 7, name: "剪辑导出", status: "completed", description: "时间轴预览、拖拽排序、FFmpeg 导出成片" },
|
||||
];
|
||||
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Page
|
||||
───────────────────────────────────────────── */
|
||||
export default function PipelineMonitorPage() {
|
||||
const [stages, setStages] = useState<Stage[]>(INITIAL_STAGES);
|
||||
const [viewStage, setViewStage] = useState(1);
|
||||
|
||||
const activeStageData = stages[viewStage - 1];
|
||||
|
||||
/* ── Invalidation cascade: mark all stages after `fromStage` as invalidated ── */
|
||||
const invalidateDownstream = useCallback((fromStage: number) => {
|
||||
setStages((prev) =>
|
||||
prev.map((s) => {
|
||||
if (s.number > fromStage && (s.status === "completed" || s.status === "running" || s.status === "waiting")) {
|
||||
return { ...s, status: "invalidated" as StageStatus };
|
||||
}
|
||||
return s;
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
/* ── Handlers ── */
|
||||
const handleApprove = useCallback(
|
||||
(stageNumber: number) => {
|
||||
setStages((prev) =>
|
||||
prev.map((s) => {
|
||||
if (s.number === stageNumber) return { ...s, status: "completed" as StageStatus };
|
||||
// Start next stage
|
||||
if (s.number === stageNumber + 1 && s.status === "pending")
|
||||
return { ...s, status: "running" as StageStatus };
|
||||
return s;
|
||||
})
|
||||
);
|
||||
// Jump to next stage view
|
||||
if (stageNumber < 7) setViewStage(stageNumber + 1);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleRerun = useCallback(
|
||||
(stageNumber: number) => {
|
||||
setStages((prev) =>
|
||||
prev.map((s) => {
|
||||
if (s.number === stageNumber) return { ...s, status: "running" as StageStatus };
|
||||
// Invalidate downstream
|
||||
if (s.number > stageNumber && s.status !== "pending")
|
||||
return { ...s, status: "invalidated" as StageStatus };
|
||||
return s;
|
||||
})
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleGoBack = useCallback(
|
||||
(stageNumber: number) => {
|
||||
if (stageNumber <= 1) return;
|
||||
const targetStage = stageNumber - 1;
|
||||
// Go back: current + downstream → invalidated
|
||||
setStages((prev) =>
|
||||
prev.map((s) => {
|
||||
if (s.number >= stageNumber && s.status !== "pending")
|
||||
return { ...s, status: "invalidated" as StageStatus };
|
||||
return s;
|
||||
})
|
||||
);
|
||||
setViewStage(targetStage);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleKeepOld = useCallback((stageNumber: number) => {
|
||||
setStages((prev) =>
|
||||
prev.map((s) =>
|
||||
s.number === stageNumber ? { ...s, status: "completed" as StageStatus } : s
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleRegenerate = useCallback(
|
||||
(stageNumber: number) => {
|
||||
setStages((prev) =>
|
||||
prev.map((s) => {
|
||||
if (s.number === stageNumber) return { ...s, status: "running" as StageStatus };
|
||||
if (s.number > stageNumber && s.status !== "pending")
|
||||
return { ...s, status: "invalidated" as StageStatus };
|
||||
return s;
|
||||
})
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleStageClick = useCallback((stageNumber: number) => {
|
||||
setViewStage(stageNumber);
|
||||
}, []);
|
||||
|
||||
/* ── Determine ReviewGate mode for current stage ── */
|
||||
const getGateMode = (stage: Stage) => {
|
||||
if (stage.status === "invalidated") return "invalidated" as const;
|
||||
if (stage.status === "completed") return "review" as const;
|
||||
return null; // no gate for running/pending/failed stages
|
||||
};
|
||||
|
||||
const gateMode = getGateMode(activeStageData);
|
||||
|
||||
return (
|
||||
<div className="relative z-10 min-h-screen px-8 py-8">
|
||||
{/* ─── Breadcrumb ─── */}
|
||||
<nav aria-label="Breadcrumb" className="flex items-center gap-2 text-sm mb-6">
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="text-text-secondary hover:text-text-primary motion-safe:transition-colors cursor-pointer"
|
||||
>
|
||||
项目
|
||||
</Link>
|
||||
<ChevronRight className="w-3.5 h-3.5 text-text-muted" />
|
||||
<Link
|
||||
href="/dashboard/proj_001"
|
||||
className="text-text-secondary hover:text-text-primary motion-safe:transition-colors cursor-pointer"
|
||||
>
|
||||
T仔的上班日记
|
||||
</Link>
|
||||
<ChevronRight className="w-3.5 h-3.5 text-text-muted" />
|
||||
<span className="text-text-primary font-medium">EP01 — 流水线</span>
|
||||
</nav>
|
||||
|
||||
{/* ─── Pipeline Stepper ─── */}
|
||||
<div className="glass-card px-5 py-3 mb-6">
|
||||
<PipelineStepper
|
||||
stages={stages}
|
||||
activeStage={viewStage}
|
||||
onStageClick={handleStageClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ─── Stage Content ─── */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="font-[family-name:var(--font-heading)] text-xl font-bold text-text-primary">
|
||||
第{activeStageData.number}阶段 — {activeStageData.name}
|
||||
</h2>
|
||||
<p className="text-sm text-text-secondary mt-1">{activeStageData.description}</p>
|
||||
</div>
|
||||
|
||||
{viewStage === 6 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="px-4 py-2 rounded-xl text-sm font-medium text-text-secondary bg-white/[0.06] border border-white/10 hover:bg-white/[0.1] motion-safe:transition-colors cursor-pointer flex items-center gap-2">
|
||||
<Download className="w-4 h-4" />
|
||||
导出 Seedance 批次包
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stage-specific content */}
|
||||
<div className="glass-card p-6">
|
||||
{/* Invalidated overlay */}
|
||||
{activeStageData.status === "invalidated" && (
|
||||
<div className="mb-4 p-3 rounded-lg bg-amber-500/10 border border-amber-400/20 text-sm text-amber-400 flex items-center gap-2">
|
||||
<span>上游已修改,本阶段结果可能不再准确。数据仅供参考。</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{viewStage === 1 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div className="w-14 h-14 rounded-2xl bg-emerald-500/10 flex items-center justify-center mb-4">
|
||||
<Check className="w-7 h-7 text-emerald-400" />
|
||||
</div>
|
||||
<h3 className="font-[family-name:var(--font-heading)] text-lg font-semibold text-text-primary mb-1">
|
||||
剧本已确认
|
||||
</h3>
|
||||
<p className="text-sm text-text-secondary mb-4">剧本创作完成,已进入流水线</p>
|
||||
<Link
|
||||
href="/dashboard/proj_001/chat?ep=1"
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium text-accent bg-accent/10 border border-accent/20 hover:bg-accent/20 motion-safe:transition-colors cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
查看 / 编辑剧本
|
||||
</Link>
|
||||
</div>
|
||||
) : viewStage === 2 ? (
|
||||
<StagePlanning />
|
||||
) : viewStage === 3 ? (
|
||||
<StageImageAssets />
|
||||
) : viewStage === 4 ? (
|
||||
<StageKeyshots />
|
||||
) : viewStage === 5 ? (
|
||||
<StageSegments />
|
||||
) : viewStage === 6 ? (
|
||||
<SegmentGrid segments={MOCK_SEGMENTS} />
|
||||
) : viewStage === 7 ? (
|
||||
<StageTimeline />
|
||||
) : activeStageData.status === "completed" || activeStageData.status === "invalidated" ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center mb-4 ${
|
||||
activeStageData.status === "invalidated" ? "bg-amber-500/10" : "bg-emerald-500/10"
|
||||
}`}>
|
||||
<Check className={`w-7 h-7 ${
|
||||
activeStageData.status === "invalidated" ? "text-amber-400" : "text-emerald-400"
|
||||
}`} />
|
||||
</div>
|
||||
<h3 className="font-[family-name:var(--font-heading)] text-lg font-semibold text-text-primary mb-1">
|
||||
{activeStageData.status === "invalidated" ? "阶段结果已失效" : "阶段已完成"}
|
||||
</h3>
|
||||
<p className="text-sm text-text-secondary">{activeStageData.description}</p>
|
||||
<button className="mt-4 px-4 py-2 rounded-lg text-sm text-text-secondary bg-white/[0.06] border border-white/10 hover:bg-white/[0.1] motion-safe:transition-colors cursor-pointer flex items-center gap-2">
|
||||
<Eye className="w-4 h-4" />
|
||||
查看输出详情
|
||||
</button>
|
||||
</div>
|
||||
) : activeStageData.status === "pending" ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div className="w-14 h-14 rounded-2xl bg-white/[0.04] flex items-center justify-center mb-4">
|
||||
<Clock className="w-7 h-7 text-text-muted" />
|
||||
</div>
|
||||
<h3 className="font-[family-name:var(--font-heading)] text-lg font-semibold text-text-primary mb-1">
|
||||
等待前序阶段
|
||||
</h3>
|
||||
<p className="text-sm text-text-secondary">{activeStageData.description}</p>
|
||||
</div>
|
||||
) : activeStageData.status === "failed" ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div className="w-14 h-14 rounded-2xl bg-red-500/10 flex items-center justify-center mb-4">
|
||||
<Clock className="w-7 h-7 text-red-400" />
|
||||
</div>
|
||||
<h3 className="font-[family-name:var(--font-heading)] text-lg font-semibold text-text-primary mb-1">
|
||||
阶段执行失败
|
||||
</h3>
|
||||
<p className="text-sm text-text-secondary">{activeStageData.description}</p>
|
||||
<button
|
||||
onClick={() => handleRerun(activeStageData.number)}
|
||||
className="mt-4 px-4 py-2 rounded-lg text-sm text-accent bg-accent/10 border border-accent/20 hover:bg-accent/20 motion-safe:transition-colors cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 text-blue-400 motion-safe:animate-spin mb-4" />
|
||||
<p className="text-sm text-text-secondary">{activeStageData.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ─── Review Gate (not on Stage 1 or 7) ─── */}
|
||||
{gateMode && activeStageData.number > 1 && activeStageData.number < 7 && (
|
||||
<ReviewGate
|
||||
mode={gateMode}
|
||||
stageNumber={activeStageData.number}
|
||||
stageName={activeStageData.name}
|
||||
description={activeStageData.description}
|
||||
onApprove={() => handleApprove(activeStageData.number)}
|
||||
onRerun={() => handleRerun(activeStageData.number)}
|
||||
onGoBack={() => handleGoBack(activeStageData.number)}
|
||||
onKeepOld={() => handleKeepOld(activeStageData.number)}
|
||||
onRegenerate={() => handleRegenerate(activeStageData.number)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ─── Log Panel ─── */}
|
||||
<LogPanel />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
256
frontend/src/app/dashboard/[projectId]/settings/page.tsx
Normal file
256
frontend/src/app/dashboard/[projectId]/settings/page.tsx
Normal file
@ -0,0 +1,256 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ChevronRight,
|
||||
Settings,
|
||||
Sparkles,
|
||||
Film,
|
||||
Palette,
|
||||
AlertTriangle,
|
||||
Save,
|
||||
Check,
|
||||
Trash2,
|
||||
Archive,
|
||||
} from "lucide-react";
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Page
|
||||
───────────────────────────────────────────── */
|
||||
export default function ProjectSettingsPage() {
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [renderStyle, setRenderStyle] = useState("anime-chibi");
|
||||
|
||||
const handleSave = () => {
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative z-10 min-h-screen px-8 py-8">
|
||||
{/* Breadcrumb */}
|
||||
<nav aria-label="Breadcrumb" className="flex items-center gap-2 text-sm mb-6">
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="text-text-secondary hover:text-text-primary motion-safe:transition-colors cursor-pointer"
|
||||
>
|
||||
项目
|
||||
</Link>
|
||||
<ChevronRight className="w-3.5 h-3.5 text-text-muted" />
|
||||
<Link
|
||||
href="/dashboard/proj_001"
|
||||
className="text-text-secondary hover:text-text-primary motion-safe:transition-colors cursor-pointer"
|
||||
>
|
||||
T仔的上班日记
|
||||
</Link>
|
||||
<ChevronRight className="w-3.5 h-3.5 text-text-muted" />
|
||||
<span className="text-text-primary font-medium">设置</span>
|
||||
</nav>
|
||||
|
||||
<div className="mb-8">
|
||||
<h1 className="font-[family-name:var(--font-heading)] text-3xl font-bold text-text-primary mb-1">
|
||||
项目设置
|
||||
</h1>
|
||||
<p className="text-sm text-text-secondary">
|
||||
T仔的上班日记 — 项目配置
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="max-w-3xl space-y-6">
|
||||
{/* Basic info */}
|
||||
<div className="glass-card p-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Settings className="w-5 h-5 text-text-secondary" />
|
||||
<h2 className="font-[family-name:var(--font-heading)] text-lg font-semibold text-text-primary">
|
||||
基本信息
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-2">项目名称</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue="T仔的上班日记"
|
||||
className="w-full bg-white/[0.04] border border-white/10 rounded-lg px-4 py-3 text-sm text-text-primary focus:outline-none focus:border-accent/50 focus:ring-3 focus:ring-accent/15 motion-safe:transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-2">项目描述</label>
|
||||
<textarea
|
||||
defaultValue="讲述一只名叫T仔的小恐龙在互联网公司上班的日常趣事,每集约 2-3 分钟。"
|
||||
rows={3}
|
||||
className="w-full bg-white/[0.04] border border-white/10 rounded-lg px-4 py-3 text-sm text-text-primary focus:outline-none focus:border-accent/50 focus:ring-3 focus:ring-accent/15 motion-safe:transition-all resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-2">项目类型</label>
|
||||
<div className="flex gap-3">
|
||||
{["原创动画", "网文改编", "短片 / PV", "自定义"].map((t, i) => (
|
||||
<button
|
||||
key={t}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium motion-safe:transition-colors cursor-pointer
|
||||
${i === 0
|
||||
? "bg-accent/15 text-accent border border-accent/20"
|
||||
: "bg-white/[0.04] text-text-secondary border border-white/[0.06] hover:bg-white/[0.08]"
|
||||
}`}
|
||||
>
|
||||
{t}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Skills */}
|
||||
<div className="glass-card p-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Sparkles className="w-5 h-5 text-text-secondary" />
|
||||
<h2 className="font-[family-name:var(--font-heading)] text-lg font-semibold text-text-primary">
|
||||
技能配置
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ name: "screenplay-skill", desc: "原创动画剧本创作" },
|
||||
{ name: "storyboard-video-skill", desc: "分镜宫格与提示词提取" },
|
||||
{ name: "script-segmentation-skill", desc: "剧本切分为视频片段" },
|
||||
].map((skill) => (
|
||||
<div key={skill.name} className="flex items-center justify-between px-4 py-3 rounded-lg bg-white/[0.04] border border-white/[0.06]">
|
||||
<div className="flex items-center gap-3">
|
||||
<Sparkles className="w-3.5 h-3.5 text-accent" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-text-primary">{skill.name}</p>
|
||||
<p className="text-xs text-text-muted">{skill.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-emerald-400">已安装</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Render style */}
|
||||
<div className="glass-card p-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Palette className="w-5 h-5 text-text-secondary" />
|
||||
<h2 className="font-[family-name:var(--font-heading)] text-lg font-semibold text-text-primary">
|
||||
渲染风格
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted mb-4">项目级配置,会追加到所有 Seedance 提示词末尾</p>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[
|
||||
{ key: "anime-chibi", label: "Q版动画", desc: "可爱Q版 2头身风格" },
|
||||
{ key: "anime-standard", label: "标准动画", desc: "日式动画风格" },
|
||||
{ key: "3d-render", label: "3D 渲染", desc: "Pixar/迪士尼风格" },
|
||||
{ key: "watercolor", label: "水彩", desc: "手绘水彩质感" },
|
||||
{ key: "pixel-art", label: "像素", desc: "复古像素风" },
|
||||
{ key: "custom", label: "自定义", desc: "手动输入风格提示词" },
|
||||
].map((style) => (
|
||||
<button
|
||||
key={style.key}
|
||||
onClick={() => setRenderStyle(style.key)}
|
||||
className={`p-3 rounded-xl text-left cursor-pointer motion-safe:transition-colors border
|
||||
${
|
||||
renderStyle === style.key
|
||||
? "bg-accent/15 border-accent/20 text-accent"
|
||||
: "bg-white/[0.04] border-white/[0.06] text-text-secondary hover:bg-white/[0.08]"
|
||||
}`}
|
||||
>
|
||||
<p className="text-sm font-medium">{style.label}</p>
|
||||
<p className="text-[10px] mt-0.5 text-text-muted">{style.desc}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Video settings */}
|
||||
<div className="glass-card p-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Film className="w-5 h-5 text-text-secondary" />
|
||||
<h2 className="font-[family-name:var(--font-heading)] text-lg font-semibold text-text-primary">
|
||||
视频参数
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-2">视频比例</label>
|
||||
<div className="flex gap-3">
|
||||
{["16:9", "9:16", "1:1"].map((r, i) => (
|
||||
<button
|
||||
key={r}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium cursor-pointer motion-safe:transition-colors
|
||||
${i === 0
|
||||
? "bg-accent/15 text-accent border border-accent/20"
|
||||
: "bg-white/[0.04] text-text-secondary border border-white/[0.06] hover:bg-white/[0.08]"
|
||||
}`}
|
||||
>
|
||||
{r}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-2">Seedance 模型</label>
|
||||
<div className="flex gap-3">
|
||||
{["Seedance 1.5 Pro", "Seedance 2.0(即将开放)"].map((m, i) => (
|
||||
<button
|
||||
key={m}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium cursor-pointer motion-safe:transition-colors
|
||||
${i === 0
|
||||
? "bg-accent/15 text-accent border border-accent/20"
|
||||
: "bg-white/[0.04] text-text-muted border border-white/[0.06] cursor-not-allowed"
|
||||
}`}
|
||||
disabled={i === 1}
|
||||
>
|
||||
{m}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Danger zone */}
|
||||
<div className="glass-card p-6 border-l-3 border-l-red-500/50">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<AlertTriangle className="w-5 h-5 text-red-400" />
|
||||
<h2 className="font-[family-name:var(--font-heading)] text-lg font-semibold text-red-400">
|
||||
危险区域
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button className="px-4 py-2 rounded-xl text-sm font-medium text-text-secondary bg-white/[0.06] border border-white/10 hover:bg-white/[0.1] cursor-pointer flex items-center gap-2">
|
||||
<Archive className="w-4 h-4" />
|
||||
归档项目
|
||||
</button>
|
||||
<button className="px-4 py-2 rounded-xl text-sm font-medium text-red-400 bg-red-500/10 border border-red-400/20 hover:bg-red-500/20 cursor-pointer flex items-center gap-2">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
删除项目
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save */}
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className={`px-6 py-3 rounded-xl text-sm font-semibold motion-safe:transition-all cursor-pointer flex items-center gap-2
|
||||
${
|
||||
saved
|
||||
? "bg-emerald-500/15 text-emerald-400 border border-emerald-400/20"
|
||||
: "bg-gradient-to-br from-violet-500 to-violet-700 shadow-[0_0_20px_rgba(139,92,246,0.3)] border border-white/15 text-white hover:shadow-[0_0_30px_rgba(139,92,246,0.5)]"
|
||||
}`}
|
||||
>
|
||||
{saved ? (
|
||||
<><Check className="w-4 h-4" />已保存</>
|
||||
) : (
|
||||
<><Save className="w-4 h-4" />保存设置</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
455
frontend/src/app/dashboard/assets/page.tsx
Normal file
455
frontend/src/app/dashboard/assets/page.tsx
Normal file
@ -0,0 +1,455 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Search,
|
||||
Filter,
|
||||
Users,
|
||||
MapPin,
|
||||
Package,
|
||||
ChevronLeft,
|
||||
RotateCcw,
|
||||
Pencil,
|
||||
Clock,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Mock Data
|
||||
───────────────────────────────────────────── */
|
||||
type AssetType = "character" | "scene" | "prop";
|
||||
|
||||
interface Asset {
|
||||
id: string;
|
||||
name: string;
|
||||
type: AssetType;
|
||||
thumbnail: string; // placeholder gradient
|
||||
prompt: string;
|
||||
episodes: string[];
|
||||
updatedAt: string;
|
||||
versions: number;
|
||||
// character-specific: single 16:9 three-view image (front bust + front full + side + back)
|
||||
threeView?: string; // placeholder gradient for the combined sheet
|
||||
}
|
||||
|
||||
const TYPE_CONFIG: Record<
|
||||
AssetType,
|
||||
{ label: string; icon: typeof Users; color: string; bg: string }
|
||||
> = {
|
||||
character: {
|
||||
label: "角色",
|
||||
icon: Users,
|
||||
color: "text-violet-400",
|
||||
bg: "bg-violet-500/15",
|
||||
},
|
||||
scene: {
|
||||
label: "场景",
|
||||
icon: MapPin,
|
||||
color: "text-emerald-400",
|
||||
bg: "bg-emerald-500/15",
|
||||
},
|
||||
prop: {
|
||||
label: "道具",
|
||||
icon: Package,
|
||||
color: "text-amber-400",
|
||||
bg: "bg-amber-500/15",
|
||||
},
|
||||
};
|
||||
|
||||
const MOCK_ASSETS: Asset[] = [
|
||||
{
|
||||
id: "char_001",
|
||||
name: "T仔",
|
||||
type: "character",
|
||||
thumbnail: "from-orange-500 to-red-400",
|
||||
prompt:
|
||||
"Q版迷你霸王龙,约30厘米高,2头身比例,圆滚滚身体,橘红色皮肤,浅黄肚皮,大圆死鱼眼,小短手,穿白色迷你衬衫打绿色小领带",
|
||||
episodes: ["EP01", "EP02", "EP03"],
|
||||
updatedAt: "2 小时前",
|
||||
versions: 3,
|
||||
threeView: "from-orange-400 via-red-400 to-amber-500",
|
||||
},
|
||||
{
|
||||
id: "char_002",
|
||||
name: "特特",
|
||||
type: "character",
|
||||
thumbnail: "from-blue-400 to-cyan-300",
|
||||
prompt:
|
||||
"Q版迷你翼龙,浅蓝灰色身体,圆脑袋大眼睛,短喙,翅膀像小披风,穿白色衬衫打红色领带,头顶戴防风镜",
|
||||
episodes: ["EP01", "EP03"],
|
||||
updatedAt: "昨天",
|
||||
versions: 2,
|
||||
threeView: "from-blue-300 via-cyan-300 to-sky-400",
|
||||
},
|
||||
{
|
||||
id: "char_003",
|
||||
name: "皮皮",
|
||||
type: "character",
|
||||
thumbnail: "from-yellow-400 to-amber-300",
|
||||
prompt:
|
||||
"Q版迷你甲龙,明黄色,身体又圆又硬像弹力球,背上一排小骨甲,尾巴末端有骨锤,四肢极短,眼睛又大又亮",
|
||||
episodes: ["EP01"],
|
||||
updatedAt: "昨天",
|
||||
versions: 1,
|
||||
threeView: "from-yellow-300 via-amber-300 to-orange-400",
|
||||
},
|
||||
{
|
||||
id: "char_004",
|
||||
name: "外卖三角龙",
|
||||
type: "character",
|
||||
thumbnail: "from-orange-400 to-yellow-500",
|
||||
prompt:
|
||||
"Q版迷你三角龙,橘色,头上三只小角,穿外卖骑手背心,骑迷你外卖电动车",
|
||||
episodes: ["EP01"],
|
||||
updatedAt: "3 天前",
|
||||
versions: 1,
|
||||
threeView: "from-orange-300 via-yellow-400 to-amber-500",
|
||||
},
|
||||
{
|
||||
id: "scene_001",
|
||||
name: "T仔的单身公寓",
|
||||
type: "scene",
|
||||
thumbnail: "from-indigo-500 to-violet-400",
|
||||
prompt:
|
||||
"按Q版恐龙尺寸的迷你开间,床只有鞋盒大小,床头柜上放着大圆形闹钟,窗户透进暖黄晨光,墙上贴着全勤奖奖状",
|
||||
episodes: ["EP01"],
|
||||
updatedAt: "2 小时前",
|
||||
versions: 2,
|
||||
},
|
||||
{
|
||||
id: "scene_002",
|
||||
name: "恐龙城街道",
|
||||
type: "scene",
|
||||
thumbnail: "from-teal-500 to-emerald-400",
|
||||
prompt:
|
||||
"迷你城市街道,小号垃圾桶、小号路灯,各种Q版恐龙赶路上班,阳光明媚",
|
||||
episodes: ["EP01", "EP02"],
|
||||
updatedAt: "昨天",
|
||||
versions: 1,
|
||||
},
|
||||
{
|
||||
id: "scene_003",
|
||||
name: "公司打卡处",
|
||||
type: "scene",
|
||||
thumbnail: "from-slate-500 to-gray-400",
|
||||
prompt:
|
||||
"小型办公楼大厅,一台比恐龙们高一点的打卡机,天花板有消防喷淋头,现代简约办公风格",
|
||||
episodes: ["EP01"],
|
||||
updatedAt: "昨天",
|
||||
versions: 1,
|
||||
},
|
||||
{
|
||||
id: "prop_001",
|
||||
name: "霸王龙专属代餐",
|
||||
type: "prop",
|
||||
thumbnail: "from-pink-500 to-rose-400",
|
||||
prompt:
|
||||
"火腿肠,包装上印着'霸王龙专属代餐'字样,Q版风格包装设计",
|
||||
episodes: ["EP01"],
|
||||
updatedAt: "3 天前",
|
||||
versions: 1,
|
||||
},
|
||||
];
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Asset Detail Modal
|
||||
───────────────────────────────────────────── */
|
||||
function AssetDetailModal({
|
||||
asset,
|
||||
onClose,
|
||||
}: {
|
||||
asset: Asset;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const typeConf = TYPE_CONFIG[asset.type];
|
||||
const Icon = typeConf.icon;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-8">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative glass-card w-full max-w-3xl max-h-[85vh] overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-5 border-b border-white/[0.06]">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={`w-10 h-10 rounded-xl ${typeConf.bg} flex items-center justify-center`}
|
||||
>
|
||||
<Icon className={`w-5 h-5 ${typeConf.color}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="font-[family-name:var(--font-heading)] text-lg font-semibold text-text-primary">
|
||||
{asset.name}
|
||||
</h2>
|
||||
<span className={`text-xs ${typeConf.color}`}>
|
||||
{typeConf.label} · v{asset.versions}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-lg text-text-muted hover:text-text-secondary hover:bg-white/[0.06] cursor-pointer"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-5 space-y-6">
|
||||
{/* Character: Single 16:9 three-view sheet (front bust + front full + side + back) */}
|
||||
{asset.type === "character" && asset.threeView && (
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-text-secondary mb-3">
|
||||
三视图
|
||||
</h3>
|
||||
<div
|
||||
className={`aspect-video rounded-xl bg-gradient-to-r ${asset.threeView} opacity-80 relative overflow-hidden`}
|
||||
>
|
||||
{/* Divider lines to indicate the 4 views within the single image */}
|
||||
<div className="absolute inset-0 flex">
|
||||
{["正面半身", "正面全身", "侧面", "背面"].map(
|
||||
(label, i) => (
|
||||
<div
|
||||
key={label}
|
||||
className={`flex-1 flex items-end justify-center pb-3 ${
|
||||
i < 3 ? "border-r border-white/10" : ""
|
||||
}`}
|
||||
>
|
||||
<span className="px-2 py-0.5 rounded-md text-[10px] bg-black/30 text-white/70 backdrop-blur-sm">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted mt-2">
|
||||
一张 16:9 横版图,包含正面半身、正面全身、侧面、背面四个视角
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Non-character: single large preview */}
|
||||
{asset.type !== "character" && (
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-text-secondary mb-3">
|
||||
预览
|
||||
</h3>
|
||||
<div
|
||||
className={`aspect-video rounded-xl bg-gradient-to-br ${asset.thumbnail} opacity-80`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Prompt */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-sm font-medium text-text-secondary">
|
||||
生成提示词
|
||||
</h3>
|
||||
<button className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs text-text-muted hover:text-text-secondary hover:bg-white/[0.06] cursor-pointer">
|
||||
<Pencil className="w-3 h-3" />
|
||||
编辑
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-white/[0.04] border border-white/[0.06] rounded-xl px-4 py-3">
|
||||
<p className="text-sm text-text-primary leading-relaxed font-[family-name:var(--font-mono)]">
|
||||
{asset.prompt}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Meta info */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-text-secondary mb-2">
|
||||
关联剧集
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{asset.episodes.map((ep) => (
|
||||
<span
|
||||
key={ep}
|
||||
className="px-2 py-0.5 rounded-md text-xs bg-white/[0.06] text-text-secondary border border-white/10"
|
||||
>
|
||||
{ep}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-text-secondary mb-2">
|
||||
版本历史
|
||||
</h3>
|
||||
<p className="text-sm text-text-primary">
|
||||
共 {asset.versions} 个版本
|
||||
</p>
|
||||
<p className="text-xs text-text-muted flex items-center gap-1 mt-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
最后更新:{asset.updatedAt}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<button className="px-4 py-2 rounded-xl text-xs font-medium bg-accent/15 text-accent border border-accent/20 hover:bg-accent/25 cursor-pointer flex items-center gap-2">
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
重新生成
|
||||
</button>
|
||||
<button className="px-4 py-2 rounded-xl text-xs font-medium bg-white/[0.06] text-text-secondary border border-white/10 hover:bg-white/[0.1] cursor-pointer flex items-center gap-2">
|
||||
<Pencil className="w-3.5 h-3.5" />
|
||||
编辑提示词后重新生成
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Page
|
||||
───────────────────────────────────────────── */
|
||||
export default function AssetsPage() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [filterType, setFilterType] = useState<AssetType | "all">("all");
|
||||
const [selectedAsset, setSelectedAsset] = useState<Asset | null>(null);
|
||||
|
||||
const filtered = MOCK_ASSETS.filter((a) => {
|
||||
const matchesSearch = a.name
|
||||
.toLowerCase()
|
||||
.includes(searchQuery.toLowerCase());
|
||||
const matchesType = filterType === "all" || a.type === filterType;
|
||||
return matchesSearch && matchesType;
|
||||
});
|
||||
|
||||
const counts = {
|
||||
all: MOCK_ASSETS.length,
|
||||
character: MOCK_ASSETS.filter((a) => a.type === "character").length,
|
||||
scene: MOCK_ASSETS.filter((a) => a.type === "scene").length,
|
||||
prop: MOCK_ASSETS.filter((a) => a.type === "prop").length,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative z-10 min-h-screen px-8 py-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="font-[family-name:var(--font-heading)] text-3xl font-bold text-text-primary mb-1">
|
||||
资产库
|
||||
</h1>
|
||||
<p className="text-sm text-text-secondary">
|
||||
管理角色图、场景图、道具图等图片资产,跨剧集复用
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search + Filter */}
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索资产..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full bg-white/[0.04] border border-white/10 rounded-lg pl-10 pr-4 py-2.5 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:border-accent/50 focus:ring-3 focus:ring-accent/15 motion-safe:transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
{(
|
||||
[
|
||||
["all", "全部", null],
|
||||
["character", "角色", Users],
|
||||
["scene", "场景", MapPin],
|
||||
["prop", "道具", Package],
|
||||
] as const
|
||||
).map(([type, label, Icon]) => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => setFilterType(type as AssetType | "all")}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium motion-safe:transition-colors cursor-pointer flex items-center gap-1.5
|
||||
${
|
||||
filterType === type
|
||||
? "bg-accent/15 text-accent border border-accent/20"
|
||||
: "bg-white/[0.04] text-text-secondary border border-white/10 hover:bg-white/[0.08]"
|
||||
}`}
|
||||
>
|
||||
{Icon && <Icon className="w-3 h-3" />}
|
||||
{label}
|
||||
<span className="text-text-muted">
|
||||
{counts[type as keyof typeof counts]}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Asset Grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
|
||||
{filtered.map((asset) => {
|
||||
const typeConf = TYPE_CONFIG[asset.type];
|
||||
const Icon = typeConf.icon;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={asset.id}
|
||||
onClick={() => setSelectedAsset(asset)}
|
||||
className="glass-card overflow-hidden hover:bg-white/[0.08] motion-safe:transition-all cursor-pointer group"
|
||||
>
|
||||
{/* Thumbnail */}
|
||||
<div
|
||||
className={`aspect-[3/4] bg-gradient-to-br ${asset.thumbnail} opacity-70 group-hover:opacity-90 motion-safe:transition-opacity relative`}
|
||||
>
|
||||
{/* Type badge */}
|
||||
<div className="absolute top-2 left-2">
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[10px] font-medium ${typeConf.bg} ${typeConf.color} backdrop-blur-sm`}
|
||||
>
|
||||
<Icon className="w-2.5 h-2.5" />
|
||||
{typeConf.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Episodes badge */}
|
||||
<div className="absolute bottom-2 right-2">
|
||||
<span className="px-1.5 py-0.5 rounded-md text-[10px] bg-black/40 text-white/80 backdrop-blur-sm">
|
||||
{asset.episodes.join(", ")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="p-3">
|
||||
<h3 className="text-sm font-medium text-text-primary truncate">
|
||||
{asset.name}
|
||||
</h3>
|
||||
<p className="text-xs text-text-muted mt-0.5 flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{asset.updatedAt}
|
||||
{asset.versions > 1 && (
|
||||
<span className="ml-1">· v{asset.versions}</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Detail Modal */}
|
||||
{selectedAsset && (
|
||||
<AssetDetailModal
|
||||
asset={selectedAsset}
|
||||
onClose={() => setSelectedAsset(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
frontend/src/app/dashboard/layout.tsx
Normal file
24
frontend/src/app/dashboard/layout.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Sidebar from "@/components/layout/Sidebar";
|
||||
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Sidebar collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
||||
<main
|
||||
className={`motion-safe:transition-[margin-left] motion-safe:duration-300 ease-in-out
|
||||
${collapsed ? "ml-[68px]" : "ml-[240px]"}`}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
536
frontend/src/app/dashboard/page.tsx
Normal file
536
frontend/src/app/dashboard/page.tsx
Normal file
@ -0,0 +1,536 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
FolderOpen,
|
||||
Film,
|
||||
Clock,
|
||||
MoreHorizontal,
|
||||
Zap,
|
||||
ChevronRight,
|
||||
Loader2,
|
||||
Settings,
|
||||
Copy,
|
||||
Archive,
|
||||
Trash2,
|
||||
X,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Mock Data
|
||||
───────────────────────────────────────────── */
|
||||
interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "original" | "adaptation" | "short" | "custom";
|
||||
episodes: number;
|
||||
currentEpisode: number;
|
||||
currentStage: number;
|
||||
status: "idle" | "running" | "completed" | "failed";
|
||||
updatedAt: string;
|
||||
thumbnail?: string;
|
||||
}
|
||||
|
||||
const TYPE_LABELS: Record<Project["type"], string> = {
|
||||
original: "原创动画",
|
||||
adaptation: "网文改编",
|
||||
short: "短片 / PV",
|
||||
custom: "自定义",
|
||||
};
|
||||
|
||||
const STATUS_CONFIG: Record<
|
||||
Project["status"],
|
||||
{ label: string; dot: string; text: string }
|
||||
> = {
|
||||
idle: { label: "空闲", dot: "bg-gray-500", text: "text-text-secondary" },
|
||||
running: {
|
||||
label: "运行中",
|
||||
dot: "bg-blue-500 animate-pulse",
|
||||
text: "text-blue-400",
|
||||
},
|
||||
completed: { label: "已完成", dot: "bg-emerald-500", text: "text-emerald-400" },
|
||||
failed: { label: "失败", dot: "bg-red-500", text: "text-red-400" },
|
||||
};
|
||||
|
||||
const STAGE_NAMES = [
|
||||
"剧本对话",
|
||||
"规划",
|
||||
"参考图生成",
|
||||
"宫格生成",
|
||||
"切分",
|
||||
"视频生成",
|
||||
"剪辑导出",
|
||||
];
|
||||
|
||||
const MOCK_PROJECTS: Project[] = [
|
||||
{
|
||||
id: "proj_001",
|
||||
name: "T仔的上班日记",
|
||||
type: "original",
|
||||
episodes: 12,
|
||||
currentEpisode: 1,
|
||||
currentStage: 6,
|
||||
status: "running",
|
||||
updatedAt: "2 小时前",
|
||||
},
|
||||
{
|
||||
id: "proj_002",
|
||||
name: "星际萌宠大冒险",
|
||||
type: "original",
|
||||
episodes: 8,
|
||||
currentEpisode: 3,
|
||||
currentStage: 7,
|
||||
status: "completed",
|
||||
updatedAt: "昨天",
|
||||
},
|
||||
{
|
||||
id: "proj_003",
|
||||
name: "凡人修仙传",
|
||||
type: "adaptation",
|
||||
episodes: 24,
|
||||
currentEpisode: 1,
|
||||
currentStage: 2,
|
||||
status: "running",
|
||||
updatedAt: "5 小时前",
|
||||
},
|
||||
{
|
||||
id: "proj_004",
|
||||
name: "产品宣传 PV",
|
||||
type: "short",
|
||||
episodes: 1,
|
||||
currentEpisode: 1,
|
||||
currentStage: 4,
|
||||
status: "failed",
|
||||
updatedAt: "3 天前",
|
||||
},
|
||||
];
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
CreateProjectModal
|
||||
───────────────────────────────────────────── */
|
||||
const PROJECT_TYPES = [
|
||||
{ key: "original", label: "原创动画", desc: "从零开始创作原创动画剧本" },
|
||||
{ key: "adaptation", label: "网文改编", desc: "将现有小说/文本改编为动画" },
|
||||
{ key: "short", label: "短片 / PV", desc: "产品宣传、MV 等单集短片" },
|
||||
{ key: "custom", label: "自定义", desc: "自由配置技能和流水线" },
|
||||
] as const;
|
||||
|
||||
function CreateProjectModal({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||||
const router = useRouter();
|
||||
const [step, setStep] = useState(1);
|
||||
const [name, setName] = useState("");
|
||||
const [type, setType] = useState<string>("original");
|
||||
const [episodes, setEpisodes] = useState("12");
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const handleCreate = () => {
|
||||
// Mock: just navigate to new project detail
|
||||
router.push("/dashboard/proj_new");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
|
||||
|
||||
{/* Modal */}
|
||||
<div
|
||||
className="relative w-full max-w-lg mx-4 rounded-2xl border border-white/10 p-6 shadow-2xl"
|
||||
style={{
|
||||
background: "rgba(15, 15, 25, 0.95)",
|
||||
backdropFilter: "blur(20px)",
|
||||
}}
|
||||
>
|
||||
{/* Close */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-4 right-4 p-1.5 rounded-lg text-text-muted hover:text-text-secondary hover:bg-white/[0.06] cursor-pointer"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<h2 className="font-[family-name:var(--font-heading)] text-xl font-bold text-text-primary mb-1">
|
||||
创建新项目
|
||||
</h2>
|
||||
<p className="text-sm text-text-secondary">
|
||||
{step === 1 ? "填写项目基本信息" : "选择项目类型和规模"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{step === 1 ? (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-2">项目名称</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="例:恐龙也是打工龙"
|
||||
className="w-full bg-white/[0.04] border border-white/10 rounded-lg px-4 py-3 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:border-accent/50 focus:ring-3 focus:ring-accent/15 motion-safe:transition-all"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-2">项目类型</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{PROJECT_TYPES.map((t) => (
|
||||
<button
|
||||
key={t.key}
|
||||
onClick={() => setType(t.key)}
|
||||
className={`p-3 rounded-xl text-left cursor-pointer motion-safe:transition-colors border
|
||||
${
|
||||
type === t.key
|
||||
? "bg-accent/15 border-accent/20 text-accent"
|
||||
: "bg-white/[0.04] border-white/[0.06] text-text-secondary hover:bg-white/[0.08]"
|
||||
}`}
|
||||
>
|
||||
<p className="text-sm font-medium">{t.label}</p>
|
||||
<p className="text-[10px] mt-0.5 text-text-muted">{t.desc}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setStep(2)}
|
||||
disabled={!name.trim()}
|
||||
className={`w-full py-3 rounded-xl text-sm font-semibold motion-safe:transition-all cursor-pointer
|
||||
${
|
||||
name.trim()
|
||||
? "bg-gradient-to-br from-violet-500 to-violet-700 text-white shadow-[0_0_20px_rgba(139,92,246,0.3)] hover:shadow-[0_0_30px_rgba(139,92,246,0.5)]"
|
||||
: "bg-white/[0.06] text-text-muted cursor-not-allowed"
|
||||
}`}
|
||||
>
|
||||
下一步
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-2">计划集数</label>
|
||||
<input
|
||||
type="number"
|
||||
value={episodes}
|
||||
onChange={(e) => setEpisodes(e.target.value)}
|
||||
min={1}
|
||||
max={100}
|
||||
className="w-full bg-white/[0.04] border border-white/10 rounded-lg px-4 py-3 text-sm text-text-primary focus:outline-none focus:border-accent/50 focus:ring-3 focus:ring-accent/15 motion-safe:transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Skills preview */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-2">自动加载技能</label>
|
||||
<div className="space-y-2">
|
||||
{["screenplay-skill", "storyboard-video-skill", "script-segmentation-skill"].map((s) => (
|
||||
<div key={s} className="flex items-center gap-2 px-3 py-2 rounded-lg bg-white/[0.04] border border-white/[0.06]">
|
||||
<Sparkles className="w-3.5 h-3.5 text-accent" />
|
||||
<span className="text-xs text-text-secondary">{s}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => setStep(1)}
|
||||
className="flex-1 py-3 rounded-xl text-sm font-medium text-text-secondary bg-white/[0.06] border border-white/10 hover:bg-white/[0.1] cursor-pointer motion-safe:transition-colors"
|
||||
>
|
||||
返回
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
className="flex-1 py-3 rounded-xl text-sm font-semibold text-white bg-gradient-to-br from-violet-500 to-violet-700 shadow-[0_0_20px_rgba(139,92,246,0.3)] hover:shadow-[0_0_30px_rgba(139,92,246,0.5)] cursor-pointer motion-safe:transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
创建项目
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Components
|
||||
───────────────────────────────────────────── */
|
||||
|
||||
function StageProgress({
|
||||
currentStage,
|
||||
status,
|
||||
}: {
|
||||
currentStage: number;
|
||||
status: Project["status"];
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
{STAGE_NAMES.map((name, i) => {
|
||||
const stageNum = i + 1;
|
||||
const isCompleted = stageNum < currentStage;
|
||||
const isCurrent = stageNum === currentStage;
|
||||
const isFailed = isCurrent && status === "failed";
|
||||
|
||||
let bgColor = "bg-white/[0.06]"; // pending
|
||||
if (isCompleted) bgColor = "bg-emerald-500/40";
|
||||
if (isCurrent && status === "running") bgColor = "bg-blue-500/50 motion-safe:animate-pulse";
|
||||
if (isCurrent && status === "completed") bgColor = "bg-emerald-500/40";
|
||||
if (isFailed) bgColor = "bg-red-500/40";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={stageNum}
|
||||
className={`h-1.5 flex-1 rounded-full ${bgColor} motion-safe:transition-colors`}
|
||||
title={`${stageNum}. ${name}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectCardMenu({ project }: { project: Project }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
return () => document.removeEventListener("mousedown", handleClick);
|
||||
}, [open]);
|
||||
|
||||
const MENU_ITEMS = [
|
||||
{ icon: Settings, label: "项目设置", action: () => {} },
|
||||
{ icon: Copy, label: "复制项目", action: () => {} },
|
||||
{ icon: Archive, label: "归档", action: () => {} },
|
||||
{ icon: Trash2, label: "删除", action: () => {}, danger: true },
|
||||
];
|
||||
|
||||
return (
|
||||
<div ref={menuRef} className="relative">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setOpen(!open);
|
||||
}}
|
||||
className="p-1.5 rounded-lg text-text-muted hover:text-text-secondary hover:bg-white/[0.06] motion-safe:transition-colors cursor-pointer"
|
||||
aria-label="更多操作"
|
||||
>
|
||||
<MoreHorizontal className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div
|
||||
className="absolute right-0 top-full mt-1 w-40 py-1 rounded-xl border border-white/10 shadow-xl z-50"
|
||||
style={{
|
||||
background: "rgba(15, 15, 25, 0.95)",
|
||||
backdropFilter: "blur(20px)",
|
||||
}}
|
||||
>
|
||||
{MENU_ITEMS.map((item) => (
|
||||
<button
|
||||
key={item.label}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
item.action();
|
||||
setOpen(false);
|
||||
}}
|
||||
className={`w-full flex items-center gap-2.5 px-3 py-2 text-xs cursor-pointer motion-safe:transition-colors
|
||||
${
|
||||
item.danger
|
||||
? "text-red-400 hover:bg-red-500/10"
|
||||
: "text-text-secondary hover:bg-white/[0.06] hover:text-text-primary"
|
||||
}`}
|
||||
>
|
||||
<item.icon className="w-3.5 h-3.5" />
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectCard({ project }: { project: Project }) {
|
||||
const statusConf = STATUS_CONFIG[project.status];
|
||||
const stageName = STAGE_NAMES[project.currentStage - 1] || "";
|
||||
|
||||
return (
|
||||
<Link href={`/dashboard/${project.id}`} className="glass-card p-5 hover:bg-white/[0.08] motion-safe:transition-all motion-safe:duration-200 cursor-pointer group block">
|
||||
{/* Header: type badge + menu */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium bg-accent/10 text-accent border border-accent/15">
|
||||
{TYPE_LABELS[project.type]}
|
||||
</span>
|
||||
<ProjectCardMenu project={project} />
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="font-[family-name:var(--font-heading)] text-lg font-semibold text-text-primary mb-1 truncate">
|
||||
{project.name}
|
||||
</h3>
|
||||
|
||||
{/* Meta: episodes + status */}
|
||||
<div className="flex items-center gap-4 mb-4 text-sm">
|
||||
<span className="flex items-center gap-1.5 text-text-secondary">
|
||||
<Film className="w-3.5 h-3.5" />
|
||||
{project.episodes} 集
|
||||
</span>
|
||||
<span className={`flex items-center gap-1.5 ${statusConf.text}`}>
|
||||
<span className={`w-2 h-2 rounded-full ${statusConf.dot}`} />
|
||||
{statusConf.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Stage progress bar */}
|
||||
<div className="mb-3">
|
||||
<StageProgress
|
||||
currentStage={project.currentStage}
|
||||
status={project.status}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Current stage label + timestamp */}
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-text-secondary">
|
||||
EP{String(project.currentEpisode).padStart(2, "0")} ·{" "}
|
||||
{project.currentStage > 0
|
||||
? `第${project.currentStage}阶段 — ${stageName}`
|
||||
: "未开始"}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 text-text-muted">
|
||||
<Clock className="w-3 h-3" />
|
||||
{project.updatedAt}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyState({ onCreate }: { onCreate: () => void }) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-center">
|
||||
<div className="w-14 h-14 rounded-2xl bg-white/[0.04] flex items-center justify-center mb-5">
|
||||
<FolderOpen className="w-7 h-7 text-text-muted" />
|
||||
</div>
|
||||
<h3 className="font-[family-name:var(--font-heading)] text-xl font-semibold text-text-primary mb-2">
|
||||
还没有项目
|
||||
</h3>
|
||||
<p className="text-sm text-text-secondary mb-8 max-w-xs">
|
||||
创建你的第一个动画项目,开始 AI 驱动的创作之旅
|
||||
</p>
|
||||
<button
|
||||
onClick={onCreate}
|
||||
className="bg-gradient-to-br from-violet-500 to-violet-700 shadow-[0_0_20px_rgba(139,92,246,0.4),0_4px_12px_rgba(0,0,0,0.3)] border border-white/15 rounded-xl px-6 py-3 text-sm text-white font-semibold hover:shadow-[0_0_30px_rgba(139,92,246,0.6)] motion-safe:transition-all motion-safe:duration-200 active:scale-[0.98] cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
创建项目
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Page
|
||||
───────────────────────────────────────────── */
|
||||
export default function DashboardPage() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const projects = MOCK_PROJECTS;
|
||||
|
||||
const filtered = projects.filter((p) =>
|
||||
p.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative z-10 min-h-screen px-8 py-8">
|
||||
{/* ─── Page Header ─── */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="font-[family-name:var(--font-heading)] text-3xl font-bold text-text-primary mb-1">
|
||||
项目
|
||||
</h1>
|
||||
<p className="text-sm text-text-secondary">
|
||||
管理你的动画项目,查看流水线进度
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="bg-gradient-to-br from-violet-500 to-violet-700 shadow-[0_0_20px_rgba(139,92,246,0.4),0_4px_12px_rgba(0,0,0,0.3)] border border-white/15 rounded-xl px-5 py-2.5 text-sm text-white font-semibold hover:shadow-[0_0_30px_rgba(139,92,246,0.6)] motion-safe:transition-all motion-safe:duration-200 active:scale-[0.98] cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
创建项目
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ─── Search & Stats ─── */}
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索项目..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full bg-white/[0.04] border border-white/10 rounded-lg pl-10 pr-4 py-2.5 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:border-accent/50 focus:ring-3 focus:ring-accent/15 motion-safe:transition-all motion-safe:duration-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6 text-sm text-text-secondary">
|
||||
<span>
|
||||
共 <span className="text-text-primary font-medium">{projects.length}</span> 个项目
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 rounded-full bg-blue-500 animate-pulse" />
|
||||
{projects.filter((p) => p.status === "running").length} 运行中
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── Project Grid ─── */}
|
||||
{filtered.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-5">
|
||||
{filtered.map((project) => (
|
||||
<ProjectCard key={project.id} project={project} />
|
||||
))}
|
||||
</div>
|
||||
) : searchQuery ? (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-center">
|
||||
<div className="w-14 h-14 rounded-2xl bg-white/[0.04] flex items-center justify-center mb-5">
|
||||
<Search className="w-7 h-7 text-text-muted" />
|
||||
</div>
|
||||
<h3 className="font-[family-name:var(--font-heading)] text-xl font-semibold text-text-primary mb-2">
|
||||
没有找到匹配项
|
||||
</h3>
|
||||
<p className="text-sm text-text-secondary">
|
||||
试试其他关键词
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState onCreate={() => setShowCreateModal(true)} />
|
||||
)}
|
||||
|
||||
{/* Create Project Modal */}
|
||||
<CreateProjectModal open={showCreateModal} onClose={() => setShowCreateModal(false)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
184
frontend/src/app/dashboard/settings/page.tsx
Normal file
184
frontend/src/app/dashboard/settings/page.tsx
Normal file
@ -0,0 +1,184 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Settings, Key, Sliders, Users, Save, Check } from "lucide-react";
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Stage review config
|
||||
───────────────────────────────────────────── */
|
||||
const STAGES = [
|
||||
{ num: 1, name: "剧本对话", locked: true, desc: "必须人工确认" },
|
||||
{ num: 2, name: "规划", locked: false, desc: "Claude 分析剧本提取规划" },
|
||||
{ num: 3, name: "参考图生成", locked: true, desc: "必须人工确认" },
|
||||
{ num: 4, name: "宫格生成", locked: false, desc: "宫格整图生成与裁切" },
|
||||
{ num: 5, name: "切分", locked: false, desc: "剧本切分为片段" },
|
||||
{ num: 6, name: "视频生成", locked: false, desc: "Seedance 全并发生成" },
|
||||
{ num: 7, name: "剪辑导出", locked: false, desc: "时间轴拼接导出" },
|
||||
];
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Page
|
||||
───────────────────────────────────────────── */
|
||||
export default function SettingsPage() {
|
||||
const [pipelineMode, setPipelineMode] = useState<"auto" | "review" | "custom">("review");
|
||||
const [stageReview, setStageReview] = useState<Record<number, boolean>>({
|
||||
1: true, 2: true, 3: true, 4: true, 5: true, 6: true, 7: false,
|
||||
});
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
const handleSave = () => {
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative z-10 min-h-screen px-8 py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="font-[family-name:var(--font-heading)] text-3xl font-bold text-text-primary mb-1">
|
||||
设置
|
||||
</h1>
|
||||
<p className="text-sm text-text-secondary">
|
||||
工作室配置、API Key、流水线模式
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="max-w-3xl space-y-6">
|
||||
{/* ─── API Keys ─── */}
|
||||
<div className="glass-card p-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Key className="w-5 h-5 text-text-secondary" />
|
||||
<h2 className="font-[family-name:var(--font-heading)] text-lg font-semibold text-text-primary">
|
||||
API 密钥
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-2">Claude API Base URL</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue="https://api.anthropic.com"
|
||||
className="w-full bg-white/[0.04] border border-white/10 rounded-lg px-4 py-3 text-sm text-text-primary focus:outline-none focus:border-accent/50 focus:ring-3 focus:ring-accent/15 motion-safe:transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-2">Claude API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
defaultValue="sk-ant-••••••••••••••"
|
||||
className="w-full bg-white/[0.04] border border-white/10 rounded-lg px-4 py-3 text-sm text-text-primary focus:outline-none focus:border-accent/50 focus:ring-3 focus:ring-accent/15 motion-safe:transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-2">Banana Pro API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
defaultValue=""
|
||||
placeholder="图片生成 API Key"
|
||||
className="w-full bg-white/[0.04] border border-white/10 rounded-lg px-4 py-3 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:border-accent/50 focus:ring-3 focus:ring-accent/15 motion-safe:transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-2">Seedance API Key(火山引擎 Ark)</label>
|
||||
<input
|
||||
type="password"
|
||||
defaultValue=""
|
||||
placeholder="视频生成 API Key"
|
||||
className="w-full bg-white/[0.04] border border-white/10 rounded-lg px-4 py-3 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:border-accent/50 focus:ring-3 focus:ring-accent/15 motion-safe:transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── Pipeline Mode ─── */}
|
||||
<div className="glass-card p-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Sliders className="w-5 h-5 text-text-secondary" />
|
||||
<h2 className="font-[family-name:var(--font-heading)] text-lg font-semibold text-text-primary">
|
||||
流水线默认模式
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 mb-5">
|
||||
{([
|
||||
{ key: "auto", label: "全自动", desc: "仅剧本和图片资产需确认" },
|
||||
{ key: "review", label: "逐步审核", desc: "每个阶段完成后等待确认" },
|
||||
{ key: "custom", label: "自定义", desc: "按阶段单独配置审核开关" },
|
||||
] as const).map((mode) => (
|
||||
<button
|
||||
key={mode.key}
|
||||
onClick={() => setPipelineMode(mode.key)}
|
||||
className={`flex-1 p-3 rounded-xl text-left cursor-pointer motion-safe:transition-colors border
|
||||
${
|
||||
pipelineMode === mode.key
|
||||
? "bg-accent/15 border-accent/20 text-accent"
|
||||
: "bg-white/[0.04] border-white/[0.06] text-text-secondary hover:bg-white/[0.08]"
|
||||
}`}
|
||||
>
|
||||
<p className="text-sm font-medium">{mode.label}</p>
|
||||
<p className="text-[10px] mt-0.5 text-text-muted">{mode.desc}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Per-stage toggles (only in custom mode) */}
|
||||
{pipelineMode === "custom" && (
|
||||
<div className="space-y-2 pt-2 border-t border-white/[0.06]">
|
||||
<p className="text-xs text-text-muted mb-3">每个阶段完成后是否需要人工审核</p>
|
||||
{STAGES.map((stage) => (
|
||||
<div key={stage.num} className="flex items-center justify-between py-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs font-[family-name:var(--font-mono)] text-text-muted w-4">{stage.num}</span>
|
||||
<span className="text-sm text-text-primary">{stage.name}</span>
|
||||
<span className="text-xs text-text-muted">{stage.desc}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (stage.locked) return;
|
||||
setStageReview((prev) => ({ ...prev, [stage.num]: !prev[stage.num] }));
|
||||
}}
|
||||
className={`relative w-10 h-5 rounded-full motion-safe:transition-colors
|
||||
${stage.locked ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}
|
||||
${stageReview[stage.num] ? "bg-accent" : "bg-white/[0.1]"}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-0.5 w-4 h-4 rounded-full bg-white shadow motion-safe:transition-transform
|
||||
${stageReview[stage.num] ? "left-[22px]" : "left-0.5"}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ─── Members (placeholder) ─── */}
|
||||
<div className="glass-card p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Users className="w-5 h-5 text-text-secondary" />
|
||||
<h2 className="font-[family-name:var(--font-heading)] text-lg font-semibold text-text-primary">
|
||||
成员管理
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-text-secondary">即将推出 — 邀请团队成员协作</p>
|
||||
</div>
|
||||
|
||||
{/* Save button */}
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className={`px-6 py-3 rounded-xl text-sm font-semibold motion-safe:transition-all cursor-pointer flex items-center gap-2
|
||||
${
|
||||
saved
|
||||
? "bg-emerald-500/15 text-emerald-400 border border-emerald-400/20"
|
||||
: "bg-gradient-to-br from-violet-500 to-violet-700 shadow-[0_0_20px_rgba(139,92,246,0.3)] border border-white/15 text-white hover:shadow-[0_0_30px_rgba(139,92,246,0.5)]"
|
||||
}`}
|
||||
>
|
||||
{saved ? (
|
||||
<><Check className="w-4 h-4" />已保存</>
|
||||
) : (
|
||||
<><Save className="w-4 h-4" />保存设置</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
862
frontend/src/app/dashboard/skills/page.tsx
Normal file
862
frontend/src/app/dashboard/skills/page.tsx
Normal file
@ -0,0 +1,862 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Wrench,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
ArrowLeft,
|
||||
Clock,
|
||||
Save,
|
||||
Pencil,
|
||||
Eye,
|
||||
Sparkles,
|
||||
Tag,
|
||||
History,
|
||||
FolderOpen,
|
||||
FileText,
|
||||
BookOpen,
|
||||
FileCode,
|
||||
Plus,
|
||||
Download,
|
||||
Folder,
|
||||
} from "lucide-react";
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Types & Mock Data — Based on real Skill directory structure
|
||||
───────────────────────────────────────────── */
|
||||
|
||||
interface SkillFile {
|
||||
name: string;
|
||||
path: string; // relative path within skill
|
||||
type: "entry" | "reference" | "template" | "config";
|
||||
description: string;
|
||||
content: string; // mock content preview
|
||||
}
|
||||
|
||||
interface Skill {
|
||||
id: string;
|
||||
name: string; // directory name (e.g. "screenplay-skill")
|
||||
displayName: string;
|
||||
description: string;
|
||||
stage: string;
|
||||
model: string;
|
||||
updatedAt: string;
|
||||
versions: number;
|
||||
projects: string[];
|
||||
// Directory structure
|
||||
entryFile: SkillFile; // SKILL.md
|
||||
configFile: SkillFile; // CLAUDE.md
|
||||
references: SkillFile[];
|
||||
templates: SkillFile[];
|
||||
}
|
||||
|
||||
const MOCK_SKILLS: Skill[] = [
|
||||
{
|
||||
id: "skill_001",
|
||||
name: "screenplay-skill",
|
||||
displayName: "原创剧本创作",
|
||||
description:
|
||||
"原创动画剧本创作技能。支持引导模式和自由模式。覆盖创意拓展、故事大纲、人物设计、世界观构建、分级梗概、节拍表、单集剧本全流程。融合 Save the Cat、Dan Harmon 故事圈、芝麻街方法论等专业知识。",
|
||||
stage: "Stage 1 — 剧本",
|
||||
model: "Claude Opus 4.6",
|
||||
updatedAt: "2 小时前",
|
||||
versions: 5,
|
||||
projects: ["T仔的上班日记", "星际萌宠大冒险"],
|
||||
entryFile: {
|
||||
name: "SKILL.md",
|
||||
path: "SKILL.md",
|
||||
type: "entry",
|
||||
description: "技能入口 — 定义功能、流程、文件结构",
|
||||
content: `---
|
||||
name: screenplay-skill
|
||||
description: 原创动画剧本创作技能。支持引导模式和自由模式...
|
||||
---
|
||||
|
||||
# 原创动画剧本创作 Skill
|
||||
|
||||
[技能说明]
|
||||
原创动画剧本创作技能包。支持两种工作模式:
|
||||
- **引导模式**(推荐新手):通过多轮问答引导编剧一步步构建创意和剧本
|
||||
- **自由模式**:有经验的编剧可直接输入指令跳到任意阶段
|
||||
|
||||
[文件结构]
|
||||
screenplay-skill/
|
||||
├── SKILL.md
|
||||
├── references/
|
||||
│ ├── creative-development.md
|
||||
│ ├── character-world-design.md
|
||||
│ ├── episode-writing.md
|
||||
│ ├── genre-formulas.md
|
||||
│ └── children-animation-guide.md
|
||||
└── templates/
|
||||
├── story-outline-template.md
|
||||
├── character-sheet-template.md
|
||||
├── world-setting-template.md
|
||||
├── episode-synopsis-template.md
|
||||
├── beat-sheet-template.md
|
||||
├── episode-script-template.md
|
||||
└── script-review-template.md
|
||||
|
||||
[目标产品规格]
|
||||
- 平台:优酷/腾讯视频/爱奇艺等主流长视频平台
|
||||
- 类型:动画剧集
|
||||
- 季长度:每季 13 集
|
||||
- 单集时长:5-6 分钟`,
|
||||
},
|
||||
configFile: {
|
||||
name: "CLAUDE.md",
|
||||
path: "../CLAUDE.md",
|
||||
type: "config",
|
||||
description: "Agent 配置 — 角色定义、强制规则",
|
||||
content: `[重要 — 请严格遵守]
|
||||
你正在执行一个结构化的AI技能包。以下所有规则都是强制性的。
|
||||
- 必须完整阅读本文件和 skills/screenplay-skill/SKILL.md 后再开始工作
|
||||
- 严格按照本文件定义的流程、格式和规则执行
|
||||
- 禁止修改用户提供的原始剧本文件
|
||||
|
||||
[角色]
|
||||
你是一名资深动画编剧导师,擅长从零开始构建原创动画剧集...
|
||||
|
||||
[任务]
|
||||
完成原创动画剧本的全流程创作:模式选择 → 创意孵化 → 故事大纲 → 人物设计 → 世界观构建 → 分级梗概 → 节拍表 → 单集剧本。`,
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: "creative-development.md",
|
||||
path: "references/creative-development.md",
|
||||
type: "reference",
|
||||
description: "创意孵化与大纲方法论(Save the Cat / 故事圈 / Pixar法则)",
|
||||
content: "# 创意孵化与故事大纲方法论\n\n## Save the Cat 十五节拍...",
|
||||
},
|
||||
{
|
||||
name: "character-world-design.md",
|
||||
path: "references/character-world-design.md",
|
||||
type: "reference",
|
||||
description: "人物设计与世界观构建(含出图提示词写法)",
|
||||
content: "# 人物设计与世界观构建\n\n## 角色设计三层结构...",
|
||||
},
|
||||
{
|
||||
name: "episode-writing.md",
|
||||
path: "references/episode-writing.md",
|
||||
type: "reference",
|
||||
description: "梗概 / 节拍表 / 剧本写法(STC 微缩版 / 故事圈适配)",
|
||||
content: "# 单集剧本写作指南\n\n## 梗概撰写规范...",
|
||||
},
|
||||
{
|
||||
name: "genre-formulas.md",
|
||||
path: "references/genre-formulas.md",
|
||||
type: "reference",
|
||||
description: "类型化创作套路(迪士尼 / 魔法少女 / 分账等)",
|
||||
content: "# 类型化动画创作公式\n\n## 迪士尼公主模型...",
|
||||
},
|
||||
{
|
||||
name: "children-animation-guide.md",
|
||||
path: "references/children-animation-guide.md",
|
||||
type: "reference",
|
||||
description: "儿童动画专业知识库(芝麻街 / PBS / PreCure / 国产经验)",
|
||||
content: "# 儿童动画创作知识库\n\n## 芝麻街方法论...",
|
||||
},
|
||||
],
|
||||
templates: [
|
||||
{
|
||||
name: "story-outline-template.md",
|
||||
path: "templates/story-outline-template.md",
|
||||
type: "template",
|
||||
description: "故事大纲模板",
|
||||
content: "# 故事大纲\n\n## 核心创意\n{{core_idea}}\n\n## 目标受众...",
|
||||
},
|
||||
{
|
||||
name: "character-sheet-template.md",
|
||||
path: "templates/character-sheet-template.md",
|
||||
type: "template",
|
||||
description: "角色设计表模板",
|
||||
content: "# 角色设计表\n\n## 角色名: {{name}}\n- 外貌:\n- 性格:...",
|
||||
},
|
||||
{
|
||||
name: "world-setting-template.md",
|
||||
path: "templates/world-setting-template.md",
|
||||
type: "template",
|
||||
description: "世界观设定模板",
|
||||
content: "# 世界观设定\n\n## 基本设定\n{{world_type}}\n\n## 规则体系...",
|
||||
},
|
||||
{
|
||||
name: "episode-synopsis-template.md",
|
||||
path: "templates/episode-synopsis-template.md",
|
||||
type: "template",
|
||||
description: "单集梗概模板",
|
||||
content: "# EP{{number}} — {{title}}\n\n## 梗概\n{{synopsis}}...",
|
||||
},
|
||||
{
|
||||
name: "beat-sheet-template.md",
|
||||
path: "templates/beat-sheet-template.md",
|
||||
type: "template",
|
||||
description: "节拍表模板",
|
||||
content: "# 节拍表 — EP{{number}}\n\n| 节拍 | 时间 | 内容 |...",
|
||||
},
|
||||
{
|
||||
name: "episode-script-template.md",
|
||||
path: "templates/episode-script-template.md",
|
||||
type: "template",
|
||||
description: "单集剧本模板",
|
||||
content: "# {{title}}\n\n## 场 1\n时:\n景:\n人:\n\n△ ...",
|
||||
},
|
||||
{
|
||||
name: "script-review-template.md",
|
||||
path: "templates/script-review-template.md",
|
||||
type: "template",
|
||||
description: "剧本诊断报告模板",
|
||||
content: "# 剧本诊断报告\n\n## 结构评分\n| 维度 | 分数 | 说明 |...",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "skill_002",
|
||||
name: "storyboard-video-skill",
|
||||
displayName: "分镜宫格",
|
||||
description:
|
||||
"分镜宫格技能。输入剧本,输出人设提示词、场景提示词、宫格分镜提示词。通过 25 宫格 → 4 宫格 → 9 宫格的分层拆解,用 AI 模型能力替代视听经验。",
|
||||
stage: "Stage 2 — 提示词提取",
|
||||
model: "Claude Opus 4.6",
|
||||
updatedAt: "昨天",
|
||||
versions: 3,
|
||||
projects: ["T仔的上班日记"],
|
||||
entryFile: {
|
||||
name: "SKILL.md",
|
||||
path: "SKILL.md",
|
||||
type: "entry",
|
||||
description: "技能入口 — 宫格分层逻辑、功能流程",
|
||||
content: `---
|
||||
name: storyboard-video-skill
|
||||
description: 分镜宫格技能...
|
||||
---
|
||||
|
||||
# 分镜宫格 Skill
|
||||
|
||||
[宫格分层逻辑]
|
||||
第一层:25宫格(5x5)= 一集总览
|
||||
第二层:4宫格(2x2)= 单个 key shot 展开
|
||||
第三层:9宫格(3x3)= 快节奏加密(按需)
|
||||
|
||||
[功能]
|
||||
[阶段1:人设与场景提取]
|
||||
[阶段2:宫格分镜提示词]`,
|
||||
},
|
||||
configFile: {
|
||||
name: "CLAUDE.md",
|
||||
path: "../CLAUDE.md",
|
||||
type: "config",
|
||||
description: "Agent 配置 — 角色定义、强制规则",
|
||||
content: "[重要] 你正在执行分镜宫格技能包...",
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: "storyboard-methodology.md",
|
||||
path: "references/storyboard-methodology.md",
|
||||
type: "reference",
|
||||
description: "分镜方法论(视听规则内嵌于提示词)",
|
||||
content: "# 分镜方法论\n\n## 景别规则...",
|
||||
},
|
||||
{
|
||||
name: "nado-banana-prompt-guide.md",
|
||||
path: "references/nado-banana-prompt-guide.md",
|
||||
type: "reference",
|
||||
description: "Nado Banana Pro 出图提示词写法",
|
||||
content: "# Nado Banana Pro 提示词指南\n\n## 叙事描述式写法...",
|
||||
},
|
||||
{
|
||||
name: "camera-language-dictionary.md",
|
||||
path: "references/camera-language-dictionary.md",
|
||||
type: "reference",
|
||||
description: "镜头语言规则库(AI 用,自动内嵌入提示词)",
|
||||
content: "# 镜头语言规则库\n\n## 景别\n- 远景 (Wide Shot)...",
|
||||
},
|
||||
{
|
||||
name: "seedance-prompt-guide.md",
|
||||
path: "references/seedance-prompt-guide.md",
|
||||
type: "reference",
|
||||
description: "Seedance 视频生成提示词规范",
|
||||
content: "# Seedance 提示词规范\n\n## 格式要求...",
|
||||
},
|
||||
],
|
||||
templates: [
|
||||
{
|
||||
name: "character-extraction-template.md",
|
||||
path: "templates/character-extraction-template.md",
|
||||
type: "template",
|
||||
description: "人设提取 + 提示词模板",
|
||||
content: "# 角色提取模板\n\n## {{character_name}}\n提示词:...",
|
||||
},
|
||||
{
|
||||
name: "scene-extraction-template.md",
|
||||
path: "templates/scene-extraction-template.md",
|
||||
type: "template",
|
||||
description: "场景提取 + 提示词模板",
|
||||
content: "# 场景提取模板\n\n## {{scene_name}}\n提示词:...",
|
||||
},
|
||||
{
|
||||
name: "grid-25-template.md",
|
||||
path: "templates/grid-25-template.md",
|
||||
type: "template",
|
||||
description: "25 宫格(5×5)总览模板",
|
||||
content: "# 25宫格提示词模板\n\n## 全局约束\n...",
|
||||
},
|
||||
{
|
||||
name: "grid-4-template.md",
|
||||
path: "templates/grid-4-template.md",
|
||||
type: "template",
|
||||
description: "4 宫格(2×2)展开模板",
|
||||
content: "# 4宫格提示词模板\n\n## Key Shot {{number}}\n...",
|
||||
},
|
||||
{
|
||||
name: "grid-9-template.md",
|
||||
path: "templates/grid-9-template.md",
|
||||
type: "template",
|
||||
description: "9 宫格(3×3)加密模板",
|
||||
content: "# 9宫格提示词模板(快节奏段落)\n\n...",
|
||||
},
|
||||
{
|
||||
name: "video-prompt-template.md",
|
||||
path: "templates/video-prompt-template.md",
|
||||
type: "template",
|
||||
description: "视频生成提示词模板",
|
||||
content: "# 视频提示词模板\n\n## Seedance 格式\n...",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "skill_003",
|
||||
name: "script-segmentation-skill",
|
||||
displayName: "剧本切分",
|
||||
description:
|
||||
"将动画剧本按时间逻辑切分为 1-15 秒视频片段,直接投喂 Seedance 2.0。核心原则:只切不改 — 剧本内容 100% 原样保留。",
|
||||
stage: "Stage 5 — 切分",
|
||||
model: "Claude Opus 4.6",
|
||||
updatedAt: "3 天前",
|
||||
versions: 4,
|
||||
projects: ["T仔的上班日记", "凡人修仙传"],
|
||||
entryFile: {
|
||||
name: "SKILL.md",
|
||||
path: "SKILL.md",
|
||||
type: "entry",
|
||||
description: "技能入口 — 切分规则、红线、核心能力",
|
||||
content: `---
|
||||
name: script-segmentation-skill
|
||||
description: 将动画剧本按时间逻辑切分为1-15秒视频片段...
|
||||
---
|
||||
|
||||
# 剧本切分 Skill — Seedance 2.0 直投工作流
|
||||
|
||||
## [概述]
|
||||
核心理念:**只切不改** —— 剧本内容必须100%原样保留。
|
||||
|
||||
本 skill 只做三件事:
|
||||
1. **切**:按15秒上限在自然断点切分
|
||||
2. **标**:标注每段需要的参考图
|
||||
3. **装**:拼上后缀模板`,
|
||||
},
|
||||
configFile: {
|
||||
name: "CLAUDE.md",
|
||||
path: "../CLAUDE.md",
|
||||
type: "config",
|
||||
description: "Agent 配置 — 角色定义、强制规则",
|
||||
content: "[重要] 你正在执行剧本切分技能包...",
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: "image-prompt-guide.md",
|
||||
path: "references/image-prompt-guide.md",
|
||||
type: "reference",
|
||||
description: "图片引用提示词规范",
|
||||
content: "# 参考图引用规范\n\n## @引用格式...",
|
||||
},
|
||||
],
|
||||
templates: [
|
||||
{
|
||||
name: "segment-output-template.md",
|
||||
path: "templates/segment-output-template.md",
|
||||
type: "template",
|
||||
description: "片段输出模板(含后缀拼接规则)",
|
||||
content:
|
||||
"# 片段输出模板\n\n## 片段格式\n```\n[seg_001]\n剧本原文...\n---后缀---\n渲染风格 + 图片引用\n```",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
File Tree Component
|
||||
───────────────────────────────────────────── */
|
||||
function FileIcon({ type }: { type: SkillFile["type"] }) {
|
||||
switch (type) {
|
||||
case "entry":
|
||||
return <Wrench className="w-3.5 h-3.5 text-accent" />;
|
||||
case "config":
|
||||
return <FileCode className="w-3.5 h-3.5 text-amber-400" />;
|
||||
case "reference":
|
||||
return <BookOpen className="w-3.5 h-3.5 text-emerald-400" />;
|
||||
case "template":
|
||||
return <FileText className="w-3.5 h-3.5 text-blue-400" />;
|
||||
}
|
||||
}
|
||||
|
||||
function FileTreeItem({
|
||||
file,
|
||||
isSelected,
|
||||
onClick,
|
||||
}: {
|
||||
file: SkillFile;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`w-full flex items-center gap-2 px-3 py-1.5 rounded-lg text-left cursor-pointer motion-safe:transition-colors
|
||||
${
|
||||
isSelected
|
||||
? "bg-accent/15 text-accent"
|
||||
: "text-text-secondary hover:bg-white/[0.06] hover:text-text-primary"
|
||||
}`}
|
||||
>
|
||||
<FileIcon type={file.type} />
|
||||
<span className="text-xs font-[family-name:var(--font-mono)] truncate">
|
||||
{file.name}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function FileTreeFolder({
|
||||
label,
|
||||
icon: Icon,
|
||||
iconColor,
|
||||
files,
|
||||
selectedFile,
|
||||
onSelectFile,
|
||||
defaultOpen,
|
||||
}: {
|
||||
label: string;
|
||||
icon: typeof Folder;
|
||||
iconColor: string;
|
||||
files: SkillFile[];
|
||||
selectedFile: SkillFile | null;
|
||||
onSelectFile: (f: SkillFile) => void;
|
||||
defaultOpen?: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(defaultOpen ?? true);
|
||||
|
||||
if (files.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="w-full flex items-center gap-2 px-2 py-1.5 text-xs text-text-muted hover:text-text-secondary cursor-pointer"
|
||||
>
|
||||
{open ? (
|
||||
<ChevronDown className="w-3 h-3 shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="w-3 h-3 shrink-0" />
|
||||
)}
|
||||
<Icon className={`w-3.5 h-3.5 ${iconColor} shrink-0`} />
|
||||
<span className="font-medium">{label}</span>
|
||||
<span className="text-text-muted/60 ml-auto">{files.length}</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="ml-3 pl-2 border-l border-white/[0.06] space-y-0.5">
|
||||
{files.map((f) => (
|
||||
<FileTreeItem
|
||||
key={f.path}
|
||||
file={f}
|
||||
isSelected={selectedFile?.path === f.path}
|
||||
onClick={() => onSelectFile(f)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Skill Detail View — File Tree + Editor
|
||||
───────────────────────────────────────────── */
|
||||
function SkillDetail({
|
||||
skill,
|
||||
onBack,
|
||||
}: {
|
||||
skill: Skill;
|
||||
onBack: () => void;
|
||||
}) {
|
||||
const [selectedFile, setSelectedFile] = useState<SkillFile>(skill.entryFile);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editContent, setEditContent] = useState(selectedFile.content);
|
||||
|
||||
const handleSelectFile = (f: SkillFile) => {
|
||||
setSelectedFile(f);
|
||||
setEditContent(f.content);
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const totalFiles =
|
||||
2 + skill.references.length + skill.templates.length; // entry + config + refs + templates
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex flex-col h-screen">
|
||||
{/* Header */}
|
||||
<div
|
||||
className="shrink-0 border-b border-white/[0.06] px-6 py-3"
|
||||
style={{
|
||||
background: "rgba(7, 7, 15, 0.85)",
|
||||
backdropFilter: "blur(20px) saturate(180%)",
|
||||
WebkitBackdropFilter: "blur(20px) saturate(180%)",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="p-1.5 rounded-lg text-text-muted hover:text-text-secondary hover:bg-white/[0.06] cursor-pointer"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<div className="w-9 h-9 rounded-xl bg-accent/10 flex items-center justify-center">
|
||||
<Wrench className="w-4.5 h-4.5 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="font-[family-name:var(--font-heading)] text-sm font-semibold text-text-primary">
|
||||
{skill.displayName}
|
||||
</h1>
|
||||
<p className="text-xs text-text-muted flex items-center gap-2">
|
||||
<span className="font-[family-name:var(--font-mono)]">
|
||||
{skill.name}
|
||||
</span>
|
||||
<span>·</span>
|
||||
<span>{totalFiles} 个文件</span>
|
||||
<span>·</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Sparkles className="w-3 h-3" />
|
||||
{skill.model}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-accent bg-accent/10 px-2.5 py-1 rounded-lg">
|
||||
{skill.stage}
|
||||
</span>
|
||||
<span className="text-xs text-text-muted flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{skill.updatedAt}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main: File Tree + Editor */}
|
||||
<div className="flex-1 flex min-h-0">
|
||||
{/* Left: File Tree */}
|
||||
<div
|
||||
className="w-[260px] shrink-0 border-r border-white/[0.06] flex flex-col min-h-0"
|
||||
style={{ background: "rgba(7, 7, 15, 0.40)" }}
|
||||
>
|
||||
{/* Skill description */}
|
||||
<div className="px-4 py-3 border-b border-white/[0.06]">
|
||||
<p className="text-xs text-text-secondary leading-relaxed line-clamp-3">
|
||||
{skill.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* File tree */}
|
||||
<div className="flex-1 overflow-y-auto px-2 py-3 space-y-1">
|
||||
{/* Entry files */}
|
||||
<FileTreeItem
|
||||
file={skill.entryFile}
|
||||
isSelected={selectedFile.path === skill.entryFile.path}
|
||||
onClick={() => handleSelectFile(skill.entryFile)}
|
||||
/>
|
||||
<FileTreeItem
|
||||
file={skill.configFile}
|
||||
isSelected={selectedFile.path === skill.configFile.path}
|
||||
onClick={() => handleSelectFile(skill.configFile)}
|
||||
/>
|
||||
|
||||
{/* References folder */}
|
||||
<FileTreeFolder
|
||||
label="references"
|
||||
icon={BookOpen}
|
||||
iconColor="text-emerald-400"
|
||||
files={skill.references}
|
||||
selectedFile={selectedFile}
|
||||
onSelectFile={handleSelectFile}
|
||||
/>
|
||||
|
||||
{/* Templates folder */}
|
||||
<FileTreeFolder
|
||||
label="templates"
|
||||
icon={FileCode}
|
||||
iconColor="text-blue-400"
|
||||
files={skill.templates}
|
||||
selectedFile={selectedFile}
|
||||
onSelectFile={handleSelectFile}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bottom: used by projects */}
|
||||
<div className="border-t border-white/[0.06] px-4 py-3 space-y-2">
|
||||
<p className="text-xs text-text-muted">使用此 Skill 的项目</p>
|
||||
{skill.projects.map((proj) => (
|
||||
<div
|
||||
key={proj}
|
||||
className="flex items-center justify-between text-xs"
|
||||
>
|
||||
<span className="text-text-secondary">{proj}</span>
|
||||
<ChevronRight className="w-3 h-3 text-text-muted" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: File Editor */}
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
{/* Editor tab bar */}
|
||||
<div className="shrink-0 flex items-center justify-between px-5 py-2.5 border-b border-white/[0.06]">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileIcon type={selectedFile.type} />
|
||||
<span className="text-sm font-[family-name:var(--font-mono)] text-text-primary">
|
||||
{selectedFile.path}
|
||||
</span>
|
||||
<span className="text-xs text-text-muted">
|
||||
— {selectedFile.description}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (isEditing) {
|
||||
setIsEditing(false);
|
||||
} else {
|
||||
setEditContent(selectedFile.content);
|
||||
setIsEditing(true);
|
||||
}
|
||||
}}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium motion-safe:transition-colors cursor-pointer flex items-center gap-1.5
|
||||
${
|
||||
isEditing
|
||||
? "bg-accent/15 text-accent border border-accent/20"
|
||||
: "bg-white/[0.06] text-text-secondary border border-white/10 hover:bg-white/[0.1]"
|
||||
}`}
|
||||
>
|
||||
{isEditing ? (
|
||||
<>
|
||||
<Eye className="w-3 h-3" />
|
||||
预览
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Pencil className="w-3 h-3" />
|
||||
编辑
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{isEditing && (
|
||||
<button className="px-3 py-1.5 rounded-lg text-xs font-medium bg-accent text-white shadow-[0_0_12px_rgba(108,99,255,0.3)] cursor-pointer flex items-center gap-1.5">
|
||||
<Save className="w-3 h-3" />
|
||||
保存
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editor content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{isEditing ? (
|
||||
<textarea
|
||||
value={editContent}
|
||||
onChange={(e) => setEditContent(e.target.value)}
|
||||
className="w-full h-full bg-transparent px-6 py-5 text-sm text-text-primary leading-relaxed resize-none focus:outline-none font-[family-name:var(--font-mono)]"
|
||||
spellCheck={false}
|
||||
/>
|
||||
) : (
|
||||
<div className="px-6 py-5 text-sm leading-relaxed">
|
||||
{selectedFile.content.split("\n").map((line, i) => {
|
||||
if (line.startsWith("# "))
|
||||
return (
|
||||
<h1
|
||||
key={i}
|
||||
className="font-[family-name:var(--font-heading)] text-xl font-bold text-text-primary mt-2 mb-3"
|
||||
>
|
||||
{line.slice(2)}
|
||||
</h1>
|
||||
);
|
||||
if (line.startsWith("## "))
|
||||
return (
|
||||
<h2
|
||||
key={i}
|
||||
className="font-[family-name:var(--font-heading)] text-base font-semibold text-accent mt-6 mb-2"
|
||||
>
|
||||
{line.slice(3)}
|
||||
</h2>
|
||||
);
|
||||
if (line.startsWith("---"))
|
||||
return (
|
||||
<hr key={i} className="border-white/[0.06] my-4" />
|
||||
);
|
||||
if (line.startsWith(" "))
|
||||
return (
|
||||
<p
|
||||
key={i}
|
||||
className="my-0.5 text-text-secondary font-[family-name:var(--font-mono)] text-xs pl-4"
|
||||
>
|
||||
{line}
|
||||
</p>
|
||||
);
|
||||
if (line.startsWith("- ") || line.startsWith(" - "))
|
||||
return (
|
||||
<p
|
||||
key={i}
|
||||
className="my-0.5 text-text-primary pl-4 before:content-['•'] before:mr-2 before:text-accent/50"
|
||||
>
|
||||
{line.replace(/^[\s]*-\s/, "")}
|
||||
</p>
|
||||
);
|
||||
if (line.startsWith("[") && line.endsWith("]"))
|
||||
return (
|
||||
<h3
|
||||
key={i}
|
||||
className="font-semibold text-text-primary mt-4 mb-1"
|
||||
>
|
||||
{line}
|
||||
</h3>
|
||||
);
|
||||
if (line === "") return <div key={i} className="h-2" />;
|
||||
// Bold text
|
||||
if (line.includes("**")) {
|
||||
const parts = line.split(/(\*\*[^*]+\*\*)/g);
|
||||
return (
|
||||
<p key={i} className="my-1 text-text-primary">
|
||||
{parts.map((part, j) =>
|
||||
part.startsWith("**") && part.endsWith("**") ? (
|
||||
<strong key={j} className="font-semibold text-accent">
|
||||
{part.replace(/\*\*/g, "")}
|
||||
</strong>
|
||||
) : (
|
||||
<span key={j}>{part}</span>
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<p key={i} className="my-1 text-text-primary">
|
||||
{line}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Page (Skill List)
|
||||
───────────────────────────────────────────── */
|
||||
export default function SkillsPage() {
|
||||
const [selectedSkill, setSelectedSkill] = useState<Skill | null>(null);
|
||||
|
||||
if (selectedSkill) {
|
||||
return (
|
||||
<SkillDetail
|
||||
skill={selectedSkill}
|
||||
onBack={() => setSelectedSkill(null)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative z-10 min-h-screen px-8 py-8">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="font-[family-name:var(--font-heading)] text-3xl font-bold text-text-primary mb-1">
|
||||
Skills
|
||||
</h1>
|
||||
<p className="text-sm text-text-secondary">
|
||||
AI 技能包管理 — 每个 Skill 是一套完整的知识目录(入口 + 参考资料 + 输出模板)
|
||||
</p>
|
||||
</div>
|
||||
<button className="px-4 py-2.5 rounded-xl text-sm font-medium text-accent bg-accent/10 border border-accent/20 hover:bg-accent/20 motion-safe:transition-colors cursor-pointer flex items-center gap-2">
|
||||
<Download className="w-4 h-4" />
|
||||
安装 Skill
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-5">
|
||||
{MOCK_SKILLS.map((skill) => {
|
||||
const fileCount =
|
||||
2 + skill.references.length + skill.templates.length;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={skill.id}
|
||||
onClick={() => setSelectedSkill(skill)}
|
||||
className="glass-card p-5 hover:bg-white/[0.08] motion-safe:transition-all cursor-pointer group"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-accent/10 flex items-center justify-center">
|
||||
<Wrench className="w-5 h-5 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-text-primary">
|
||||
{skill.displayName}
|
||||
</h3>
|
||||
<span className="text-xs text-text-muted font-[family-name:var(--font-mono)]">
|
||||
{skill.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight className="w-4 h-4 text-text-muted opacity-0 group-hover:opacity-100 motion-safe:transition-opacity" />
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-text-secondary mb-3 line-clamp-2">
|
||||
{skill.description}
|
||||
</p>
|
||||
|
||||
{/* File structure preview */}
|
||||
<div className="flex items-center gap-3 mb-3 text-[11px] text-text-muted">
|
||||
<span className="flex items-center gap-1">
|
||||
<FolderOpen className="w-3 h-3" />
|
||||
{fileCount} 文件
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<BookOpen className="w-3 h-3 text-emerald-400/60" />
|
||||
{skill.references.length} 参考
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<FileCode className="w-3 h-3 text-blue-400/60" />
|
||||
{skill.templates.length} 模板
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-accent bg-accent/10 px-2 py-0.5 rounded-md">
|
||||
{skill.stage}
|
||||
</span>
|
||||
<span className="text-text-muted flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{skill.updatedAt}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
frontend/src/app/favicon.ico
Normal file
BIN
frontend/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
244
frontend/src/app/globals.css
Normal file
244
frontend/src/app/globals.css
Normal file
@ -0,0 +1,244 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme inline {
|
||||
/* Background */
|
||||
--color-bg-base: #07070f;
|
||||
--color-bg-surface: #0d0d1a;
|
||||
--color-bg-elevated: #12121f;
|
||||
|
||||
/* Glass */
|
||||
--color-glass-01: rgba(255, 255, 255, 0.04);
|
||||
--color-glass-02: rgba(255, 255, 255, 0.07);
|
||||
--color-glass-03: rgba(255, 255, 255, 0.12);
|
||||
|
||||
/* Accent */
|
||||
--color-accent: #6c63ff;
|
||||
--color-accent-blue: #3b82f6;
|
||||
--color-accent-glow: rgba(108, 99, 255, 0.25);
|
||||
|
||||
/* Text */
|
||||
--color-text-primary: #f1f0ff;
|
||||
--color-text-secondary: #8b8ea8;
|
||||
--color-text-muted: #4c4f6b;
|
||||
|
||||
/* Font families */
|
||||
--font-heading: "Space Grotesk", "Noto Sans SC", sans-serif;
|
||||
--font-body: "DM Sans", "Noto Sans SC", sans-serif;
|
||||
--font-mono: "JetBrains Mono", monospace;
|
||||
--font-cn: "Noto Sans SC", "DM Sans", sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--color-bg-base);
|
||||
color: var(--color-text-primary);
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
LAYER 1: Aurora Gradient Background
|
||||
Slow-moving color blobs, pure CSS, GPU-only
|
||||
═══════════════════════════════════════════ */
|
||||
.aurora-bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.aurora-bg::before,
|
||||
.aurora-bg::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(110px);
|
||||
opacity: 0.4;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* Purple blob — top right, drifts left */
|
||||
.aurora-bg::before {
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
top: -10%;
|
||||
right: -5%;
|
||||
background: radial-gradient(circle, rgba(108, 99, 255, 0.6) 0%, transparent 70%);
|
||||
animation: aurora-drift-1 20s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
/* Blue-teal blob — bottom left, drifts up-right */
|
||||
.aurora-bg::after {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
bottom: -5%;
|
||||
left: -5%;
|
||||
background: radial-gradient(circle, rgba(59, 130, 246, 0.5) 0%, transparent 70%);
|
||||
animation: aurora-drift-2 25s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes aurora-drift-1 {
|
||||
0% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
33% {
|
||||
transform: translate(-15vw, 10vh) scale(1.1);
|
||||
}
|
||||
66% {
|
||||
transform: translate(-5vw, 25vh) scale(0.95);
|
||||
}
|
||||
100% {
|
||||
transform: translate(-20vw, 15vh) scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes aurora-drift-2 {
|
||||
0% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
33% {
|
||||
transform: translate(10vw, -15vh) scale(1.15);
|
||||
}
|
||||
66% {
|
||||
transform: translate(20vw, -5vh) scale(0.9);
|
||||
}
|
||||
100% {
|
||||
transform: translate(15vw, -20vh) scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Third blob via a child div for richer depth */
|
||||
.aurora-blob-3 {
|
||||
position: absolute;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(139, 92, 246, 0.35) 0%, transparent 70%);
|
||||
filter: blur(100px);
|
||||
opacity: 0.3;
|
||||
will-change: transform;
|
||||
animation: aurora-drift-3 30s ease-in-out infinite alternate;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes aurora-drift-3 {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-30%, -60%) scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: translate(-70%, -40%) scale(0.85);
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
LAYER 2: Noise Texture Overlay
|
||||
Adds tactile grain, pure CSS SVG filter
|
||||
═══════════════════════════════════════════ */
|
||||
.noise-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
opacity: 0.03;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
|
||||
background-repeat: repeat;
|
||||
background-size: 256px 256px;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
LAYER 3: Mouse-tracking Glow
|
||||
Radial gradient follows cursor, set via JS
|
||||
═══════════════════════════════════════════ */
|
||||
.cursor-glow {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
background: radial-gradient(
|
||||
600px circle at var(--mouse-x, 50%) var(--mouse-y, 50%),
|
||||
rgba(108, 99, 255, 0.06) 0%,
|
||||
transparent 60%
|
||||
);
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
LAYER 4: Subtle grid pattern
|
||||
═══════════════════════════════════════════ */
|
||||
.grid-pattern {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
|
||||
background-size: 64px 64px;
|
||||
mask-image: radial-gradient(ellipse 80% 60% at 50% 50%, black 20%, transparent 100%);
|
||||
-webkit-mask-image: radial-gradient(ellipse 80% 60% at 50% 50%, black 20%, transparent 100%);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
Glass card base
|
||||
═══════════════════════════════════════════ */
|
||||
.glass-card {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
backdrop-filter: blur(24px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.10);
|
||||
border-radius: 16px;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05) inset,
|
||||
0 8px 32px rgba(0, 0, 0, 0.4),
|
||||
0 1px 0 rgba(255, 255, 255, 0.12) inset;
|
||||
}
|
||||
|
||||
.glass-card-active {
|
||||
border-color: rgba(108, 99, 255, 0.4);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(108, 99, 255, 0.2) inset,
|
||||
0 8px 40px rgba(0, 0, 0, 0.5),
|
||||
0 0 24px rgba(108, 99, 255, 0.15);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
Reduced motion — disables ALL animations
|
||||
═══════════════════════════════════════════ */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
.aurora-bg::before,
|
||||
.aurora-bg::after,
|
||||
.aurora-blob-3 {
|
||||
animation: none !important;
|
||||
opacity: 0.2 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
Custom scrollbar
|
||||
═══════════════════════════════════════════ */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 3px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
51
frontend/src/app/layout.tsx
Normal file
51
frontend/src/app/layout.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Space_Grotesk, DM_Sans, JetBrains_Mono, Noto_Sans_SC } from "next/font/google";
|
||||
import AmbientBackground from "@/components/AmbientBackground";
|
||||
import "./globals.css";
|
||||
|
||||
const spaceGrotesk = Space_Grotesk({
|
||||
variable: "--font-heading",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const dmSans = DM_Sans({
|
||||
variable: "--font-body",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
variable: "--font-mono",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const notoSansSC = Noto_Sans_SC({
|
||||
variable: "--font-cn",
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600", "700"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Air Spark",
|
||||
description: "AI-driven animation production platform",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body
|
||||
className={`${spaceGrotesk.variable} ${dmSans.variable} ${jetbrainsMono.variable} ${notoSansSC.variable} antialiased`}
|
||||
>
|
||||
<AmbientBackground />
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
5
frontend/src/app/page.tsx
Normal file
5
frontend/src/app/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function Home() {
|
||||
redirect("/dashboard");
|
||||
}
|
||||
1394
frontend/src/app/showcase/page.tsx
Normal file
1394
frontend/src/app/showcase/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
69
frontend/src/components/AmbientBackground.tsx
Normal file
69
frontend/src/components/AmbientBackground.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
/**
|
||||
* AmbientBackground — 4-layer premium background effect
|
||||
*
|
||||
* Layer 1: Aurora gradient blobs (CSS animation, GPU-only)
|
||||
* Layer 2: Subtle grid pattern (CSS, masked radial fade)
|
||||
* Layer 3: Noise texture (inline SVG, ~1KB, zero perf cost)
|
||||
* Layer 4: Mouse-tracking glow (CSS variable + lightweight JS)
|
||||
*
|
||||
* Total perf impact: minimal — all composited on GPU, no repaints.
|
||||
* All layers respect prefers-reduced-motion.
|
||||
*/
|
||||
export default function AmbientBackground() {
|
||||
const glowRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = glowRef.current;
|
||||
if (!el) return;
|
||||
|
||||
let rafId: number;
|
||||
let targetX = 50;
|
||||
let targetY = 50;
|
||||
let currentX = 50;
|
||||
let currentY = 50;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
targetX = (e.clientX / window.innerWidth) * 100;
|
||||
targetY = (e.clientY / window.innerHeight) * 100;
|
||||
};
|
||||
|
||||
// Smooth lerp for buttery cursor tracking
|
||||
const animate = () => {
|
||||
currentX += (targetX - currentX) * 0.08;
|
||||
currentY += (targetY - currentY) * 0.08;
|
||||
el.style.setProperty("--mouse-x", `${currentX}%`);
|
||||
el.style.setProperty("--mouse-y", `${currentY}%`);
|
||||
rafId = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove, { passive: true });
|
||||
rafId = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
cancelAnimationFrame(rafId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Layer 1: Aurora blobs */}
|
||||
<div className="aurora-bg" aria-hidden="true">
|
||||
<div className="aurora-blob-3" />
|
||||
</div>
|
||||
|
||||
{/* Layer 2: Grid pattern */}
|
||||
<div className="grid-pattern" aria-hidden="true" />
|
||||
|
||||
{/* Layer 3: Noise texture */}
|
||||
<div className="noise-overlay" aria-hidden="true" />
|
||||
|
||||
{/* Layer 4: Cursor glow */}
|
||||
<div ref={glowRef} className="cursor-glow" aria-hidden="true" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
120
frontend/src/components/layout/Sidebar.tsx
Normal file
120
frontend/src/components/layout/Sidebar.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
Zap,
|
||||
FolderOpen,
|
||||
Image,
|
||||
Wrench,
|
||||
Settings,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
} from "lucide-react";
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ href: "/dashboard", icon: FolderOpen, label: "项目" },
|
||||
{ href: "/dashboard/assets", icon: Image, label: "资产库" },
|
||||
{ href: "/dashboard/skills", icon: Wrench, label: "Skills" },
|
||||
{ href: "/dashboard/settings", icon: Settings, label: "设置" },
|
||||
];
|
||||
|
||||
interface SidebarProps {
|
||||
collapsed: boolean;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={`fixed top-0 left-0 h-screen z-30 flex flex-col border-r border-white/[0.06]
|
||||
motion-safe:transition-[width] motion-safe:duration-300 ease-in-out
|
||||
${collapsed ? "w-[68px]" : "w-[240px]"}`}
|
||||
style={{
|
||||
background: "rgba(7, 7, 15, 0.80)",
|
||||
backdropFilter: "blur(16px) saturate(160%)",
|
||||
WebkitBackdropFilter: "blur(16px) saturate(160%)",
|
||||
}}
|
||||
>
|
||||
{/* ─── Logo ─── */}
|
||||
<div className="h-16 flex items-center gap-3 px-5 border-b border-white/[0.06] shrink-0">
|
||||
<div className="w-8 h-8 rounded-lg bg-accent flex items-center justify-center shrink-0">
|
||||
<Zap className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<span className="font-[family-name:var(--font-heading)] text-[15px] font-bold tracking-tight text-text-primary whitespace-nowrap">
|
||||
Air Spark
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ─── Navigation ─── */}
|
||||
<nav className="flex-1 py-4 px-3 space-y-1 overflow-y-auto">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const isActive =
|
||||
item.href === "/dashboard"
|
||||
? pathname === "/dashboard"
|
||||
: pathname.startsWith(item.href);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium
|
||||
motion-safe:transition-colors cursor-pointer
|
||||
${
|
||||
isActive
|
||||
? "bg-accent/15 text-accent border border-accent/20"
|
||||
: "text-text-secondary hover:text-text-primary hover:bg-white/[0.06] border border-transparent"
|
||||
}
|
||||
${collapsed ? "justify-center !px-0" : ""}`}
|
||||
title={collapsed ? item.label : undefined}
|
||||
>
|
||||
<item.icon className={`w-[18px] h-[18px] shrink-0 ${isActive ? "text-accent" : ""}`} />
|
||||
{!collapsed && <span>{item.label}</span>}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* ─── User & Collapse ─── */}
|
||||
<div className="border-t border-white/[0.06] p-3 space-y-1 shrink-0">
|
||||
{/* User */}
|
||||
<div
|
||||
className={`flex items-center gap-3 px-3 py-2.5 rounded-xl
|
||||
${collapsed ? "justify-center !px-0" : ""}`}
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-violet-500 to-accent-blue flex items-center justify-center text-xs font-semibold text-white shrink-0">
|
||||
D
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-text-primary truncate">导演</p>
|
||||
<p className="text-xs text-text-muted truncate">Admin</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Collapse toggle */}
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className={`flex items-center gap-3 px-3 py-2 rounded-xl text-sm text-text-muted
|
||||
hover:text-text-secondary hover:bg-white/[0.06] motion-safe:transition-colors
|
||||
cursor-pointer w-full ${collapsed ? "justify-center !px-0" : ""}`}
|
||||
aria-label={collapsed ? "展开侧边栏" : "收起侧边栏"}
|
||||
>
|
||||
{collapsed ? (
|
||||
<ChevronRight className="w-4 h-4 shrink-0" />
|
||||
) : (
|
||||
<>
|
||||
<ChevronLeft className="w-4 h-4 shrink-0" />
|
||||
<span>收起</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
65
frontend/src/components/pipeline/LogPanel.tsx
Normal file
65
frontend/src/components/pipeline/LogPanel.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Mock Data
|
||||
───────────────────────────────────────────── */
|
||||
const MOCK_LOGS = [
|
||||
{ time: "14:32:01", level: "info", msg: "Stage 1 — 剧本已确认,启动流水线" },
|
||||
{ time: "14:32:02", level: "info", msg: "Stage 2 — 调用 storyboard-skill,提取资产与分镜" },
|
||||
{ time: "14:32:18", level: "success", msg: "Stage 2 — 完成:4 角色, 7 场景, 25 keyshots" },
|
||||
{ time: "14:32:19", level: "info", msg: "Stage 3 — 开始生成角色参考图(Banana Pro)" },
|
||||
{ time: "14:33:05", level: "success", msg: "Stage 3 — T仔 图片资产生成完成" },
|
||||
{ time: "14:33:12", level: "success", msg: "Stage 3 — 特特 图片资产生成完成" },
|
||||
{ time: "14:33:20", level: "success", msg: "Stage 3 — 班长 图片资产生成完成" },
|
||||
{ time: "14:33:28", level: "error", msg: "Stage 3 — 胖达 生成失败:API timeout after 3 retries" },
|
||||
{ time: "14:34:00", level: "info", msg: "Stage 6 — 开始视频生成,共 32 片段" },
|
||||
{ time: "14:35:12", level: "info", msg: "Stage 6 — 12/32 片段已完成" },
|
||||
{ time: "14:35:45", level: "error", msg: "Stage 6 — 片段 08 失败:Seedance API timeout" },
|
||||
{ time: "14:36:30", level: "info", msg: "Stage 6 — 18/32 片段已完成,6 生成中,8 队列中" },
|
||||
];
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Component
|
||||
───────────────────────────────────────────── */
|
||||
export default function LogPanel() {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="glass-card overflow-hidden">
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="w-full flex items-center justify-between px-5 py-3 text-sm font-medium text-text-secondary hover:bg-white/[0.04] motion-safe:transition-colors cursor-pointer"
|
||||
>
|
||||
<span>系统日志</span>
|
||||
<ChevronDown
|
||||
className={`w-4 h-4 motion-safe:transition-transform ${open ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="border-t border-white/[0.06] px-5 py-3 max-h-64 overflow-y-auto">
|
||||
<div className="space-y-1 font-[family-name:var(--font-mono)] text-xs">
|
||||
{MOCK_LOGS.map((log, i) => (
|
||||
<div key={i} className="flex gap-3">
|
||||
<span className="text-text-muted shrink-0">{log.time}</span>
|
||||
<span
|
||||
className={
|
||||
log.level === "error"
|
||||
? "text-red-400"
|
||||
: log.level === "success"
|
||||
? "text-emerald-400"
|
||||
: "text-text-secondary"
|
||||
}
|
||||
>
|
||||
{log.msg}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
97
frontend/src/components/pipeline/PipelineStepper.tsx
Normal file
97
frontend/src/components/pipeline/PipelineStepper.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Check,
|
||||
Loader2,
|
||||
Clock,
|
||||
AlertTriangle,
|
||||
ChevronRight,
|
||||
} from "lucide-react";
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Types
|
||||
───────────────────────────────────────────── */
|
||||
export type StageStatus =
|
||||
| "completed"
|
||||
| "running"
|
||||
| "waiting"
|
||||
| "pending"
|
||||
| "failed"
|
||||
| "invalidated";
|
||||
|
||||
export interface Stage {
|
||||
number: number;
|
||||
name: string;
|
||||
status: StageStatus;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const STATUS_ICON: Record<StageStatus, { icon: React.ElementType; color: string }> = {
|
||||
completed: { icon: Check, color: "text-emerald-400" },
|
||||
running: { icon: Loader2, color: "text-blue-400" },
|
||||
waiting: { icon: Clock, color: "text-amber-400" },
|
||||
pending: { icon: Clock, color: "text-text-muted" },
|
||||
failed: { icon: AlertTriangle, color: "text-red-400" },
|
||||
invalidated: { icon: AlertTriangle, color: "text-amber-400" },
|
||||
};
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Component
|
||||
───────────────────────────────────────────── */
|
||||
export default function PipelineStepper({
|
||||
stages,
|
||||
activeStage,
|
||||
onStageClick,
|
||||
}: {
|
||||
stages: Stage[];
|
||||
activeStage: number;
|
||||
onStageClick: (stageNumber: number) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-1 overflow-x-auto pb-2">
|
||||
{stages.map((stage, i) => {
|
||||
const { icon: StatusIcon, color } = STATUS_ICON[stage.status];
|
||||
const isActive = stage.number === activeStage;
|
||||
const isClickable =
|
||||
stage.status === "completed" ||
|
||||
stage.status === "failed" ||
|
||||
stage.status === "invalidated" ||
|
||||
stage.status === "running";
|
||||
const isInvalidated = stage.status === "invalidated";
|
||||
|
||||
return (
|
||||
<div key={stage.number} className="flex items-center">
|
||||
<button
|
||||
onClick={() => isClickable && onStageClick(stage.number)}
|
||||
className={`flex items-center gap-2 px-4 py-2.5 rounded-xl text-sm font-medium
|
||||
motion-safe:transition-all whitespace-nowrap
|
||||
${isClickable ? "cursor-pointer" : "cursor-default"}
|
||||
${
|
||||
isActive
|
||||
? "bg-accent/15 text-accent border border-accent/20"
|
||||
: isInvalidated
|
||||
? "bg-amber-500/5 text-amber-400/60 border border-amber-400/10 hover:bg-amber-500/10"
|
||||
: stage.status === "completed"
|
||||
? "bg-white/[0.04] text-text-secondary hover:bg-white/[0.08] border border-transparent"
|
||||
: stage.status === "failed"
|
||||
? "bg-red-500/5 text-red-400/80 border border-transparent hover:bg-red-500/10"
|
||||
: "bg-transparent text-text-muted border border-transparent"
|
||||
}`}
|
||||
>
|
||||
<StatusIcon
|
||||
className={`w-4 h-4 ${color} ${stage.status === "running" ? "motion-safe:animate-spin" : ""}`}
|
||||
/>
|
||||
<span className={`hidden sm:inline ${isInvalidated ? "line-through" : ""}`}>
|
||||
{stage.name}
|
||||
</span>
|
||||
<span className="sm:hidden">{stage.number}</span>
|
||||
</button>
|
||||
{i < stages.length - 1 && (
|
||||
<ChevronRight className="w-3.5 h-3.5 text-text-muted shrink-0 mx-0.5" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
135
frontend/src/components/pipeline/ReviewGate.tsx
Normal file
135
frontend/src/components/pipeline/ReviewGate.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Check,
|
||||
RotateCcw,
|
||||
ArrowLeft,
|
||||
Zap,
|
||||
AlertTriangle,
|
||||
RefreshCw,
|
||||
ChevronRight,
|
||||
} from "lucide-react";
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Types
|
||||
───────────────────────────────────────────── */
|
||||
type GateMode = "review" | "auto_passed" | "invalidated";
|
||||
|
||||
interface ReviewGateProps {
|
||||
mode: GateMode;
|
||||
stageNumber: number;
|
||||
stageName: string;
|
||||
description: string;
|
||||
onApprove?: () => void;
|
||||
onRerun?: () => void;
|
||||
onGoBack?: () => void;
|
||||
onKeepOld?: () => void;
|
||||
onRegenerate?: () => void;
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Component
|
||||
───────────────────────────────────────────── */
|
||||
export default function ReviewGate({
|
||||
mode,
|
||||
stageNumber,
|
||||
stageName,
|
||||
description,
|
||||
onApprove,
|
||||
onRerun,
|
||||
onGoBack,
|
||||
onKeepOld,
|
||||
onRegenerate,
|
||||
}: ReviewGateProps) {
|
||||
if (mode === "auto_passed") {
|
||||
return (
|
||||
<div className="mt-6 glass-card p-4 border-l-3 border-l-emerald-500/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-emerald-500/10 flex items-center justify-center shrink-0">
|
||||
<Zap className="w-4 h-4 text-emerald-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-emerald-400 font-medium">已自动通过(自动审核模式)</p>
|
||||
<p className="text-xs text-text-muted mt-0.5">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (mode === "invalidated") {
|
||||
return (
|
||||
<div className="mt-6 glass-card p-5 border-l-3 border-l-amber-500/50">
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<div className="w-8 h-8 rounded-lg bg-amber-500/10 flex items-center justify-center shrink-0 mt-0.5">
|
||||
<AlertTriangle className="w-4 h-4 text-amber-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-amber-400 font-medium">上游阶段已重跑,本阶段结果已失效</p>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
第{stageNumber}阶段「{stageName}」的结果可能不再准确。旧数据已保留供参考。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 ml-11">
|
||||
<button
|
||||
onClick={onKeepOld}
|
||||
className="px-4 py-2 rounded-xl text-sm font-medium text-text-secondary bg-white/[0.06] border border-white/10 hover:bg-white/[0.1] motion-safe:transition-colors cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<Check className="w-3.5 h-3.5" />
|
||||
沿用旧结果
|
||||
</button>
|
||||
<button
|
||||
onClick={onRegenerate}
|
||||
className="px-4 py-2 rounded-xl text-sm font-medium text-amber-400 bg-amber-500/10 border border-amber-400/20 hover:bg-amber-500/20 motion-safe:transition-colors cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
重新生成
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// mode === "review"
|
||||
return (
|
||||
<div className="mt-6 glass-card p-5 border-l-3 border-l-accent/50">
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<div className="w-8 h-8 rounded-lg bg-emerald-500/10 flex items-center justify-center shrink-0 mt-0.5">
|
||||
<Check className="w-4 h-4 text-emerald-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-text-primary font-medium">
|
||||
第{stageNumber}阶段「{stageName}」已完成
|
||||
</p>
|
||||
<p className="text-xs text-text-muted mt-1">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 ml-11">
|
||||
{stageNumber > 1 && (
|
||||
<button
|
||||
onClick={onGoBack}
|
||||
className="px-4 py-2 rounded-xl text-sm font-medium text-text-secondary bg-white/[0.06] border border-white/10 hover:bg-white/[0.1] motion-safe:transition-colors cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<ArrowLeft className="w-3.5 h-3.5" />
|
||||
返回上一阶段
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onRerun}
|
||||
className="px-4 py-2 rounded-xl text-sm font-medium text-text-secondary bg-white/[0.06] border border-white/10 hover:bg-white/[0.1] motion-safe:transition-colors cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
重跑
|
||||
</button>
|
||||
<button
|
||||
onClick={onApprove}
|
||||
className="px-4 py-2 rounded-xl text-sm font-semibold text-white bg-gradient-to-br from-violet-500 to-violet-700 shadow-[0_0_20px_rgba(139,92,246,0.3)] border border-white/15 hover:shadow-[0_0_30px_rgba(139,92,246,0.5)] motion-safe:transition-all cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
通过并继续
|
||||
<ChevronRight className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
161
frontend/src/components/pipeline/SegmentGrid.tsx
Normal file
161
frontend/src/components/pipeline/SegmentGrid.tsx
Normal file
@ -0,0 +1,161 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Check, AlertTriangle, Pencil, RotateCcw, X, Play } from "lucide-react";
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Types & Mock Data
|
||||
───────────────────────────────────────────── */
|
||||
export interface Segment {
|
||||
id: number;
|
||||
status: "done" | "running" | "pending" | "failed";
|
||||
duration?: string;
|
||||
}
|
||||
|
||||
export const MOCK_SEGMENTS: Segment[] = Array.from({ length: 32 }, (_, i) => {
|
||||
const num = i + 1;
|
||||
let status: Segment["status"] = "pending";
|
||||
if (num <= 12) status = "done";
|
||||
if (num === 8) status = "failed";
|
||||
if (num >= 13 && num <= 18) status = "running";
|
||||
return { id: num, status, duration: status === "done" ? "15s" : undefined };
|
||||
});
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Component
|
||||
───────────────────────────────────────────── */
|
||||
export default function SegmentGrid({ segments }: { segments: Segment[] }) {
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||
const selectedSeg = segments.find((s) => s.id === selectedId);
|
||||
|
||||
const completed = segments.filter((s) => s.status === "done").length;
|
||||
const running = segments.filter((s) => s.status === "running").length;
|
||||
const failed = segments.filter((s) => s.status === "failed").length;
|
||||
const percent = Math.round((completed / segments.length) * 100);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Progress bar */}
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="flex-1 h-2 bg-white/[0.06] rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-accent to-blue-500 rounded-full motion-safe:transition-all motion-safe:duration-500"
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-text-secondary whitespace-nowrap">
|
||||
{percent}% — {completed}/{segments.length} 完成
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center gap-4 mb-4 text-xs">
|
||||
<span className="flex items-center gap-1.5 text-emerald-400">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500" />
|
||||
{completed} 完成
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5 text-blue-400">
|
||||
<span className="w-2 h-2 rounded-full bg-blue-500 animate-pulse" />
|
||||
{running} 生成中
|
||||
</span>
|
||||
{failed > 0 && (
|
||||
<span className="flex items-center gap-1.5 text-red-400">
|
||||
<span className="w-2 h-2 rounded-full bg-red-500" />
|
||||
{failed} 失败
|
||||
</span>
|
||||
)}
|
||||
<span className="flex items-center gap-1.5 text-text-muted">
|
||||
<span className="w-2 h-2 rounded-full bg-gray-600" />
|
||||
{segments.length - completed - running - failed} 队列中
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Video preview panel */}
|
||||
{selectedSeg && (
|
||||
<div className="mb-4 bg-white/[0.02] border border-white/[0.06] rounded-xl overflow-hidden">
|
||||
<div className="flex items-center justify-between px-4 py-2.5 border-b border-white/[0.06]">
|
||||
<span className="text-xs font-medium text-text-secondary">
|
||||
片段 {String(selectedSeg.id).padStart(2, "0")} 预览
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setSelectedId(null)}
|
||||
className="text-text-muted hover:text-text-secondary motion-safe:transition-colors cursor-pointer"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="aspect-video max-w-sm bg-black/40 flex items-center justify-center cursor-pointer hover:bg-black/30 motion-safe:transition-colors mx-auto my-3 rounded-lg">
|
||||
<div className="w-14 h-14 rounded-full bg-white/20 flex items-center justify-center backdrop-blur-sm">
|
||||
<Play className="w-6 h-6 text-white ml-1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Grid */}
|
||||
<div className="grid grid-cols-8 gap-2.5">
|
||||
{segments.map((seg) => {
|
||||
let tileClass = "bg-white/[0.03] border-white/[0.06] text-text-muted";
|
||||
if (seg.status === "done")
|
||||
tileClass = `bg-emerald-500/15 border-emerald-400/30 text-emerald-400${selectedId === seg.id ? " ring-1 ring-emerald-400/40" : ""}`;
|
||||
if (seg.status === "running")
|
||||
tileClass = "bg-blue-500/20 border-blue-400/40 text-blue-400 motion-safe:animate-pulse";
|
||||
if (seg.status === "failed")
|
||||
tileClass = "bg-red-500/15 border-red-400/30 text-red-400";
|
||||
|
||||
const icons = {
|
||||
done: <Check className="w-3.5 h-3.5" />,
|
||||
running: <div className="w-2 h-2 rounded-full bg-blue-400" />,
|
||||
pending: <div className="w-2 h-2 rounded-full bg-white/20" />,
|
||||
failed: <X className="w-3.5 h-3.5" />,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={seg.id}
|
||||
onClick={() => seg.status === "done" && setSelectedId(selectedId === seg.id ? null : seg.id)}
|
||||
className={`border rounded-lg p-2.5 flex flex-col items-center gap-1.5
|
||||
motion-safe:transition-colors ${tileClass}
|
||||
${seg.status === "done" ? "cursor-pointer hover:bg-white/[0.08]" : "cursor-default"}`}
|
||||
title={`片段 ${String(seg.id).padStart(2, "0")} — ${seg.status}`}
|
||||
>
|
||||
<span className="font-[family-name:var(--font-mono)] text-xs">
|
||||
{String(seg.id).padStart(2, "0")}
|
||||
</span>
|
||||
{icons[seg.status]}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Failed segment actions — dynamic */}
|
||||
{failed > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
{segments.filter((s) => s.status === "failed").map((seg) => (
|
||||
<div key={seg.id} className="glass-card p-4 border-l-[3px] border-l-red-500">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<AlertTriangle className="w-4 h-4 text-red-400" />
|
||||
<span className="text-red-400 font-medium">
|
||||
片段 {String(seg.id).padStart(2, "0")}
|
||||
</span>
|
||||
<span className="text-text-secondary">— 重试 3 次后超时失败</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="px-3 py-1.5 rounded-lg text-xs font-medium text-text-secondary bg-white/[0.06] border border-white/10 hover:bg-white/[0.1] motion-safe:transition-colors cursor-pointer flex items-center gap-1.5">
|
||||
<Pencil className="w-3 h-3" />
|
||||
编辑提示词
|
||||
</button>
|
||||
<button className="px-3 py-1.5 rounded-lg text-xs font-medium text-accent bg-accent/10 border border-accent/20 hover:bg-accent/20 motion-safe:transition-colors cursor-pointer flex items-center gap-1.5">
|
||||
<RotateCcw className="w-3 h-3" />
|
||||
重跑
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
183
frontend/src/components/pipeline/StageImageAssets.tsx
Normal file
183
frontend/src/components/pipeline/StageImageAssets.tsx
Normal file
@ -0,0 +1,183 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { User, MapPin, Check, RotateCcw, Pencil, Loader2, AlertTriangle, Image, Info } from "lucide-react";
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Types & Mock Data
|
||||
───────────────────────────────────────────── */
|
||||
type AssetStatus = "completed" | "generating" | "pending" | "failed";
|
||||
|
||||
interface Asset {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "character" | "scene" | "prop";
|
||||
status: AssetStatus;
|
||||
prompt_preview: string;
|
||||
}
|
||||
|
||||
const MOCK_ASSETS: Asset[] = [
|
||||
// Characters (each needs front立绘 + 三视图)
|
||||
{ id: "char_001", name: "T仔", type: "character", status: "completed", prompt_preview: "A cute chibi mini T-Rex, orange-red skin, tiny short arms..." },
|
||||
{ id: "char_002", name: "特特", type: "character", status: "completed", prompt_preview: "A cute chibi mini Pterodactyl, light blue-gray body..." },
|
||||
{ id: "char_003", name: "皮皮", type: "character", status: "completed", prompt_preview: "A cute chibi mini Ankylosaurus, bright yellow, round..." },
|
||||
{ id: "char_004", name: "外卖三角龙", type: "character", status: "failed", prompt_preview: "A cute chibi mini Triceratops, orange, three horns..." },
|
||||
// Scenes
|
||||
{ id: "scene_001", name: "T仔的单身公寓", type: "scene", status: "completed", prompt_preview: "A tiny single-room apartment sized for mini dinosaurs..." },
|
||||
{ id: "scene_002", name: "恐龙城街道", type: "scene", status: "completed", prompt_preview: "A miniature city street for tiny dinosaurs, small lamps..." },
|
||||
{ id: "scene_003", name: "公司大厅/打卡处", type: "scene", status: "generating", prompt_preview: "A small office building lobby for mini dinosaurs..." },
|
||||
{ id: "scene_004", name: "打卡机前", type: "scene", status: "pending", prompt_preview: "Close-up area around the office punch-in machine..." },
|
||||
];
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Tabs
|
||||
───────────────────────────────────────────── */
|
||||
type TabKey = "character" | "scene";
|
||||
|
||||
const TABS: { key: TabKey; label: string; icon: React.ElementType }[] = [
|
||||
{ key: "character", label: "角色参考图", icon: User },
|
||||
{ key: "scene", label: "场景参考图", icon: MapPin },
|
||||
];
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Component
|
||||
───────────────────────────────────────────── */
|
||||
export default function StageImageAssets() {
|
||||
const [activeTab, setActiveTab] = useState<TabKey>("character");
|
||||
|
||||
const filtered = MOCK_ASSETS.filter((a) => a.type === activeTab);
|
||||
const allAssets = MOCK_ASSETS;
|
||||
const completedCount = allAssets.filter((a) => a.status === "completed").length;
|
||||
const failedCount = allAssets.filter((a) => a.status === "failed").length;
|
||||
const generatingCount = allAssets.filter((a) => a.status === "generating").length;
|
||||
const percent = Math.round((completedCount / allAssets.length) * 100);
|
||||
|
||||
const statusConfig: Record<AssetStatus, { label: string; cls: string; icon: React.ElementType }> = {
|
||||
completed: { label: "已生成", cls: "bg-emerald-500/15 text-emerald-400 border-emerald-400/20", icon: Check },
|
||||
generating: { label: "生成中", cls: "bg-blue-500/15 text-blue-400 border-blue-400/20", icon: Loader2 },
|
||||
pending: { label: "等待中", cls: "bg-white/[0.06] text-text-muted border-white/[0.06]", icon: Image },
|
||||
failed: { label: "失败", cls: "bg-red-500/15 text-red-400 border-red-400/20", icon: AlertTriangle },
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Info banner */}
|
||||
<div className="flex items-start gap-3 p-3 rounded-lg bg-accent/5 border border-accent/10 mb-5">
|
||||
<Info className="w-4 h-4 text-accent shrink-0 mt-0.5" />
|
||||
<p className="text-xs text-text-secondary">
|
||||
Banana Pro 生成角色参考图(每角色 2 张:正面立绘 + 三视图)和场景参考图。道具图在切分阶段识别后按需生成。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Overall progress */}
|
||||
<div className="flex items-center gap-4 mb-5">
|
||||
<div className="flex-1 h-2 bg-white/[0.06] rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-accent to-emerald-500 rounded-full motion-safe:transition-all motion-safe:duration-500"
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-text-secondary whitespace-nowrap">
|
||||
{completedCount}/{allAssets.length} 完成
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Stats row */}
|
||||
<div className="flex items-center gap-4 mb-5 text-xs">
|
||||
<span className="flex items-center gap-1.5 text-emerald-400">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500" />
|
||||
{completedCount} 已生成
|
||||
</span>
|
||||
{generatingCount > 0 && (
|
||||
<span className="flex items-center gap-1.5 text-blue-400">
|
||||
<span className="w-2 h-2 rounded-full bg-blue-500 animate-pulse" />
|
||||
{generatingCount} 生成中
|
||||
</span>
|
||||
)}
|
||||
{failedCount > 0 && (
|
||||
<span className="flex items-center gap-1.5 text-red-400">
|
||||
<span className="w-2 h-2 rounded-full bg-red-500" />
|
||||
{failedCount} 失败
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tab bar */}
|
||||
<div className="flex items-center gap-1 mb-5">
|
||||
{TABS.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const count = MOCK_ASSETS.filter((a) => a.type === tab.key).length;
|
||||
const isActive = activeTab === tab.key;
|
||||
return (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium cursor-pointer motion-safe:transition-colors
|
||||
${isActive ? "bg-accent/15 text-accent" : "text-text-secondary hover:bg-white/[0.06]"}`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{tab.label}
|
||||
<span className={`text-xs px-1.5 py-0.5 rounded ${isActive ? "bg-accent/20" : "bg-white/[0.06]"}`}>
|
||||
{count}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Asset grid */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{filtered.map((asset) => {
|
||||
const sc = statusConfig[asset.status];
|
||||
const StatusIcon = sc.icon;
|
||||
return (
|
||||
<div key={asset.id} className="glass-card p-4 text-center">
|
||||
{/* Placeholder image */}
|
||||
<div className="aspect-[3/4] rounded-xl bg-white/[0.04] border border-white/[0.06] mb-3 flex items-center justify-center relative overflow-hidden">
|
||||
{asset.status === "generating" && (
|
||||
<div className="absolute inset-0 bg-blue-500/5 flex items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 text-blue-400 animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
{asset.status !== "generating" && (
|
||||
<Image className="w-10 h-10 text-text-muted" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm font-medium text-text-primary mb-2">{asset.name}</p>
|
||||
|
||||
{/* Status badge */}
|
||||
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium border ${sc.cls}`}>
|
||||
<StatusIcon className={`w-3 h-3 ${asset.status === "generating" ? "animate-spin" : ""}`} />
|
||||
{sc.label}
|
||||
</span>
|
||||
|
||||
{/* Actions for failed */}
|
||||
{asset.status === "failed" && (
|
||||
<div className="flex items-center justify-center gap-2 mt-3">
|
||||
<button className="px-2.5 py-1 rounded-md text-xs font-medium bg-white/[0.06] text-text-secondary border border-white/10 hover:bg-white/[0.1] cursor-pointer flex items-center gap-1">
|
||||
<Pencil className="w-3 h-3" />
|
||||
编辑
|
||||
</button>
|
||||
<button className="px-2.5 py-1 rounded-md text-xs font-medium bg-accent/10 text-accent border border-accent/20 hover:bg-accent/20 cursor-pointer flex items-center gap-1">
|
||||
<RotateCcw className="w-3 h-3" />
|
||||
重跑
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions for completed */}
|
||||
{asset.status === "completed" && (
|
||||
<div className="flex items-center justify-center gap-2 mt-3">
|
||||
<button className="px-2.5 py-1 rounded-md text-xs font-medium bg-white/[0.06] text-text-secondary border border-white/10 hover:bg-white/[0.1] cursor-pointer flex items-center gap-1">
|
||||
<RotateCcw className="w-3 h-3" />
|
||||
重跑
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
470
frontend/src/components/pipeline/StageKeyshots.tsx
Normal file
470
frontend/src/components/pipeline/StageKeyshots.tsx
Normal file
@ -0,0 +1,470 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ChevronDown,
|
||||
Grid3X3,
|
||||
Grid2X2,
|
||||
Image,
|
||||
Info,
|
||||
Check,
|
||||
AlertTriangle,
|
||||
Pencil,
|
||||
RotateCcw,
|
||||
Loader2,
|
||||
X,
|
||||
Clock,
|
||||
Scissors,
|
||||
} from "lucide-react";
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Types
|
||||
───────────────────────────────────────────── */
|
||||
type SceneStatus = "done" | "generating" | "failed" | "pending";
|
||||
|
||||
interface KeyshotCell {
|
||||
cell_index: number;
|
||||
description: string;
|
||||
timecode: string;
|
||||
asset_filename: string;
|
||||
}
|
||||
|
||||
interface KeyshotScene {
|
||||
scene_id: string;
|
||||
scene_name: string;
|
||||
duration_sec: number;
|
||||
grid_type: "4-grid" | "9-grid";
|
||||
grid_rows: number;
|
||||
grid_cols: number;
|
||||
status: SceneStatus;
|
||||
error_message?: string;
|
||||
prompt_preview: string;
|
||||
grid_asset_filename: string;
|
||||
cells: KeyshotCell[];
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Mock Data
|
||||
───────────────────────────────────────────── */
|
||||
const MOCK_KEYSHOT_SCENES: KeyshotScene[] = [
|
||||
{
|
||||
scene_id: "scene_001",
|
||||
scene_name: "T仔的单身公寓",
|
||||
duration_sec: 40,
|
||||
grid_type: "4-grid",
|
||||
grid_rows: 2,
|
||||
grid_cols: 2,
|
||||
status: "done",
|
||||
prompt_preview: "A 2x2 grid keyshot of a tiny T-Rex apartment scene, chibi style...",
|
||||
grid_asset_filename: "keyshot_scene_001_1.jpg",
|
||||
cells: [
|
||||
{ cell_index: 1, description: "俯拍小床 T仔睡觉 闹钟震动", timecode: "0:00-0:10", asset_filename: "keyshot_scene_001_1_01.jpg" },
|
||||
{ cell_index: 2, description: "特写 小短手够闹钟 挥空", timecode: "0:10-0:20", asset_filename: "keyshot_scene_001_1_02.jpg" },
|
||||
{ cell_index: 3, description: "痒痒挠被尾巴扫飞 反弹", timecode: "0:20-0:30", asset_filename: "keyshot_scene_001_1_03.jpg" },
|
||||
{ cell_index: 4, description: "张嘴叼住闹钟 物理消音", timecode: "0:30-0:40", asset_filename: "keyshot_scene_001_1_04.jpg" },
|
||||
],
|
||||
},
|
||||
{
|
||||
scene_id: "scene_002",
|
||||
scene_name: "恐龙城街道",
|
||||
duration_sec: 35,
|
||||
grid_type: "4-grid",
|
||||
grid_rows: 2,
|
||||
grid_cols: 2,
|
||||
status: "done",
|
||||
prompt_preview: "A 2x2 grid keyshot of a miniature dinosaur city street...",
|
||||
grid_asset_filename: "keyshot_scene_002_1.jpg",
|
||||
cells: [
|
||||
{ cell_index: 1, description: "T仔叼吐司狂奔 领带甩脸", timecode: "0:00-0:09", asset_filename: "keyshot_scene_002_1_01.jpg" },
|
||||
{ cell_index: 2, description: "尾巴扫飞垃圾桶 火腿肠滚出", timecode: "0:09-0:18", asset_filename: "keyshot_scene_002_1_02.jpg" },
|
||||
{ cell_index: 3, description: "外卖三角龙冲过 碾压火腿肠", timecode: "0:18-0:27", asset_filename: "keyshot_scene_002_1_03.jpg" },
|
||||
{ cell_index: 4, description: "酱汁溅脸 小短手擦不到 烟熏妆", timecode: "0:27-0:35", asset_filename: "keyshot_scene_002_1_04.jpg" },
|
||||
],
|
||||
},
|
||||
{
|
||||
scene_id: "scene_003",
|
||||
scene_name: "公司大厅/打卡处",
|
||||
duration_sec: 50,
|
||||
grid_type: "9-grid",
|
||||
grid_rows: 3,
|
||||
grid_cols: 3,
|
||||
status: "generating",
|
||||
prompt_preview: "A 3x3 grid keyshot of an office lobby for mini dinosaurs...",
|
||||
grid_asset_filename: "keyshot_scene_003_1.jpg",
|
||||
cells: [
|
||||
{ cell_index: 1, description: "T仔跑到打卡处 气喘吁吁", timecode: "0:00-0:06", asset_filename: "keyshot_scene_003_1_01.jpg" },
|
||||
{ cell_index: 2, description: "特特低空滑翔 打卡成功", timecode: "0:06-0:12", asset_filename: "keyshot_scene_003_1_02.jpg" },
|
||||
{ cell_index: 3, description: "特特落地 整理羽毛 凡尔赛", timecode: "0:12-0:17", asset_filename: "keyshot_scene_003_1_03.jpg" },
|
||||
{ cell_index: 4, description: "特写 翅膀尖粘草皮", timecode: "0:17-0:22", asset_filename: "keyshot_scene_003_1_04.jpg" },
|
||||
{ cell_index: 5, description: "T仔吐槽 带薪吃草扣绩效", timecode: "0:22-0:28", asset_filename: "keyshot_scene_003_1_05.jpg" },
|
||||
{ cell_index: 6, description: "皮皮滚入画面 弹球撞击", timecode: "0:28-0:33", asset_filename: "keyshot_scene_003_1_06.jpg" },
|
||||
{ cell_index: 7, description: "特特被撞飞 扎进喷淋头", timecode: "0:33-0:39", asset_filename: "keyshot_scene_003_1_07.jpg" },
|
||||
{ cell_index: 8, description: "消防喷水 全员湿透", timecode: "0:39-0:44", asset_filename: "keyshot_scene_003_1_08.jpg" },
|
||||
{ cell_index: 9, description: "特特落汤翼龙 精英人设崩", timecode: "0:44-0:50", asset_filename: "keyshot_scene_003_1_09.jpg" },
|
||||
],
|
||||
},
|
||||
{
|
||||
scene_id: "scene_004",
|
||||
scene_name: "打卡机前",
|
||||
duration_sec: 40,
|
||||
grid_type: "4-grid",
|
||||
grid_rows: 2,
|
||||
grid_cols: 2,
|
||||
status: "failed",
|
||||
error_message: "Banana Pro API 超时,重试 3 次后失败",
|
||||
prompt_preview: "A 2x2 grid keyshot of a punch-in machine area...",
|
||||
grid_asset_filename: "keyshot_scene_004_1.jpg",
|
||||
cells: [
|
||||
{ cell_index: 1, description: "慢动作 电子钟跳动 08:59:59", timecode: "0:00-0:10", asset_filename: "keyshot_scene_004_1_01.jpg" },
|
||||
{ cell_index: 2, description: "T仔脸撞打卡机 识别中", timecode: "0:10-0:20", asset_filename: "keyshot_scene_004_1_02.jpg" },
|
||||
{ cell_index: 3, description: "09:00:01 迟到1秒 脸部裂痕", timecode: "0:20-0:30", asset_filename: "keyshot_scene_004_1_03.jpg" },
|
||||
{ cell_index: 4, description: "二分画面 T仔灰脸 vs 皮皮笑脸", timecode: "0:30-0:40", asset_filename: "keyshot_scene_004_1_04.jpg" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Status Config
|
||||
───────────────────────────────────────────── */
|
||||
const STATUS_CONFIG: Record<SceneStatus, { label: string; cls: string; dotCls: string; icon: React.ElementType }> = {
|
||||
done: { label: "已生成", cls: "bg-emerald-500/15 text-emerald-400 border-emerald-400/20", dotCls: "bg-emerald-500", icon: Check },
|
||||
generating: { label: "生成中", cls: "bg-blue-500/15 text-blue-400 border-blue-400/20", dotCls: "bg-blue-500", icon: Loader2 },
|
||||
pending: { label: "等待中", cls: "bg-white/[0.06] text-text-muted border-white/[0.06]", dotCls: "bg-gray-600", icon: Clock },
|
||||
failed: { label: "失败", cls: "bg-red-500/15 text-red-400 border-red-400/20", dotCls: "bg-red-500", icon: AlertTriangle },
|
||||
};
|
||||
|
||||
/* Step definitions for the workflow */
|
||||
const STEPS = [
|
||||
{ num: 2, label: "宫格图", icon: Image },
|
||||
{ num: 3, label: "裁切", icon: Scissors },
|
||||
];
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Component
|
||||
───────────────────────────────────────────── */
|
||||
export default function StageKeyshots() {
|
||||
const [expandedScene, setExpandedScene] = useState<string | null>("scene_001");
|
||||
const [selectedCell, setSelectedCell] = useState<{ sceneId: string; cellIndex: number } | null>(null);
|
||||
const [viewSteps, setViewSteps] = useState<Record<string, number>>({});
|
||||
|
||||
const totalCells = MOCK_KEYSHOT_SCENES.reduce((a, s) => a + s.cells.length, 0);
|
||||
const completed = MOCK_KEYSHOT_SCENES.filter((s) => s.status === "done").length;
|
||||
const generating = MOCK_KEYSHOT_SCENES.filter((s) => s.status === "generating").length;
|
||||
const failed = MOCK_KEYSHOT_SCENES.filter((s) => s.status === "failed").length;
|
||||
const pending = MOCK_KEYSHOT_SCENES.filter((s) => s.status === "pending").length;
|
||||
const percent = Math.round((completed / MOCK_KEYSHOT_SCENES.length) * 100);
|
||||
|
||||
const getViewStep = (scene: KeyshotScene) => {
|
||||
if (scene.status === "done") return viewSteps[scene.scene_id] ?? 3;
|
||||
return 2; // generating/pending/failed don't use steps
|
||||
};
|
||||
|
||||
const setStep = (sceneId: string, step: number) => {
|
||||
setViewSteps((prev) => ({ ...prev, [sceneId]: step }));
|
||||
if (step !== 3) setSelectedCell(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* ─── Info Banner ─── */}
|
||||
<div className="flex items-start gap-3 p-3 rounded-lg bg-accent/5 border border-accent/10 mb-5">
|
||||
<Info className="w-4 h-4 text-accent shrink-0 mt-0.5" />
|
||||
<div className="text-xs text-text-secondary">
|
||||
<p className="text-accent font-medium mb-1">宫格分配规则</p>
|
||||
<p>≤45s → 4格 (2×2, 2560×1440) · 45s-2min → 9格 (3×3, 3840×2160) · >2min → 两张9格</p>
|
||||
<p className="mt-1">每格裁切后 = 1280×720(Seedance 输入尺寸)· Banana Pro 生成整图 → PIL 精确裁切</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── Progress Bar ─── */}
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="flex-1 h-2 bg-white/[0.06] rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-accent to-emerald-500 rounded-full motion-safe:transition-all motion-safe:duration-500"
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-text-secondary whitespace-nowrap">
|
||||
{percent}% — {completed}/{MOCK_KEYSHOT_SCENES.length} 场景完成
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* ─── Stats Row ─── */}
|
||||
<div className="flex items-center gap-4 mb-5 text-xs">
|
||||
<span className="flex items-center gap-1.5 text-emerald-400">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500" />
|
||||
{completed} 已生成
|
||||
</span>
|
||||
{generating > 0 && (
|
||||
<span className="flex items-center gap-1.5 text-blue-400">
|
||||
<span className="w-2 h-2 rounded-full bg-blue-500 animate-pulse" />
|
||||
{generating} 生成中
|
||||
</span>
|
||||
)}
|
||||
{failed > 0 && (
|
||||
<span className="flex items-center gap-1.5 text-red-400">
|
||||
<span className="w-2 h-2 rounded-full bg-red-500" />
|
||||
{failed} 失败
|
||||
</span>
|
||||
)}
|
||||
{pending > 0 && (
|
||||
<span className="flex items-center gap-1.5 text-text-muted">
|
||||
<span className="w-2 h-2 rounded-full bg-gray-600" />
|
||||
{pending} 等待中
|
||||
</span>
|
||||
)}
|
||||
<span className="text-text-muted ml-auto">
|
||||
{totalCells} keyshots · <Grid2X2 className="w-3 h-3 inline" /> {MOCK_KEYSHOT_SCENES.filter((s) => s.grid_type === "4-grid").length} 个 4-grid · <Grid3X3 className="w-3 h-3 inline" /> {MOCK_KEYSHOT_SCENES.filter((s) => s.grid_type === "9-grid").length} 个 9-grid
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* ─── Scene Cards (Accordion) ─── */}
|
||||
<div className="space-y-3">
|
||||
{MOCK_KEYSHOT_SCENES.map((scene) => {
|
||||
const isOpen = expandedScene === scene.scene_id;
|
||||
const sc = STATUS_CONFIG[scene.status];
|
||||
const StatusIcon = sc.icon;
|
||||
const currentStep = getViewStep(scene);
|
||||
const showSteps = scene.status === "done" || scene.status === "generating";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={scene.scene_id}
|
||||
className={`glass-card overflow-hidden ${scene.status === "failed" ? "border-l-[3px] border-l-red-500" : ""}`}
|
||||
>
|
||||
{/* ── Header ── */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setExpandedScene(isOpen ? null : scene.scene_id);
|
||||
if (isOpen) setSelectedCell(null);
|
||||
}}
|
||||
className="w-full flex items-center gap-3 px-5 py-4 cursor-pointer hover:bg-white/[0.04] motion-safe:transition-colors"
|
||||
>
|
||||
<span className={`w-2.5 h-2.5 rounded-full shrink-0 ${sc.dotCls} ${scene.status === "generating" ? "animate-pulse" : ""}`} />
|
||||
<div className="flex items-center gap-2 flex-1 text-left min-w-0">
|
||||
<span className="text-sm font-medium text-text-primary">{scene.scene_name}</span>
|
||||
<span className="text-[10px] px-2 py-0.5 rounded bg-accent/10 text-accent font-medium">{scene.grid_type}</span>
|
||||
<span className="text-xs text-text-muted">~{scene.duration_sec}s · {scene.cells.length} keyshots</span>
|
||||
</div>
|
||||
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-[10px] font-medium border shrink-0 ${sc.cls}`}>
|
||||
<StatusIcon className={`w-3 h-3 ${scene.status === "generating" ? "animate-spin" : ""}`} />
|
||||
{sc.label}
|
||||
</span>
|
||||
<ChevronDown className={`w-4 h-4 text-text-muted shrink-0 motion-safe:transition-transform ${isOpen ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
|
||||
{/* ── Expanded Panel ── */}
|
||||
{isOpen && (
|
||||
<div className="border-t border-white/[0.06] px-5 py-4">
|
||||
|
||||
{/* === Generating === */}
|
||||
{scene.status === "generating" && (
|
||||
<>
|
||||
<div className="flex items-center gap-1 mb-4">
|
||||
<span className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium bg-blue-500/15 text-blue-400 cursor-default">
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
生成中
|
||||
</span>
|
||||
<span className="w-px h-4 bg-white/[0.08] mx-1" />
|
||||
{STEPS.map((step) => (
|
||||
<span key={step.num} className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-text-muted cursor-default opacity-40">
|
||||
<step.icon className="w-3 h-3" />
|
||||
{step.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="aspect-video bg-blue-500/5 border border-blue-400/10 rounded-xl flex items-center justify-center motion-safe:animate-pulse"
|
||||
style={{ width: "50%" }}
|
||||
>
|
||||
<div className="text-center">
|
||||
<Loader2 className="w-10 h-10 text-blue-400/40 animate-spin mx-auto mb-2" />
|
||||
<p className="text-sm text-blue-400">Banana Pro 正在生成宫格整图...</p>
|
||||
<p className="text-[10px] text-blue-400/50 mt-1">{scene.grid_type === "4-grid" ? "2560×1440" : "3840×2160"} · {scene.grid_type}</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* === Done: Two-column layout === */}
|
||||
{scene.status === "done" && (() => {
|
||||
const activeCellData = selectedCell?.sceneId === scene.scene_id
|
||||
? scene.cells.find((c) => c.cell_index === selectedCell.cellIndex)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="flex gap-5">
|
||||
{/* ── Left Column ── */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Left header: step bar */}
|
||||
<div className="flex items-center gap-1 mb-3">
|
||||
<button
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-text-secondary bg-white/[0.06] border border-white/10 hover:bg-white/[0.1] motion-safe:transition-colors cursor-pointer"
|
||||
>
|
||||
<RotateCcw className="w-3 h-3" />
|
||||
重新生成
|
||||
</button>
|
||||
<span className="w-px h-4 bg-white/[0.08] mx-1" />
|
||||
{STEPS.map((step) => {
|
||||
const isActive = currentStep === step.num;
|
||||
const StepIcon = step.icon;
|
||||
return (
|
||||
<button
|
||||
key={step.num}
|
||||
onClick={() => setStep(scene.scene_id, step.num)}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium motion-safe:transition-colors cursor-pointer
|
||||
${isActive ? "bg-accent/15 text-accent" : "text-text-muted hover:bg-white/[0.04]"}`}
|
||||
>
|
||||
<StepIcon className="w-3 h-3" />
|
||||
{step.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Left content: grid / cells */}
|
||||
{currentStep === 2 && (
|
||||
<div className="aspect-video bg-black/30 border border-white/[0.06] rounded-xl flex items-center justify-center relative">
|
||||
<Image className="w-16 h-16 text-text-muted/20" />
|
||||
<span className="absolute bottom-2.5 right-3 text-[10px] font-[family-name:var(--font-mono)] text-text-muted/50">
|
||||
宫格原图 · {scene.grid_type === "4-grid" ? "2560×1440" : "3840×2160"} · {scene.grid_asset_filename}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{currentStep === 3 && (
|
||||
<div
|
||||
className="gap-2"
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${scene.grid_cols}, 1fr)`,
|
||||
}}
|
||||
>
|
||||
{scene.cells.map((cell) => {
|
||||
const isActive = selectedCell?.sceneId === scene.scene_id && selectedCell?.cellIndex === cell.cell_index;
|
||||
return (
|
||||
<div
|
||||
key={cell.cell_index}
|
||||
onClick={() => setSelectedCell(isActive ? null : { sceneId: scene.scene_id, cellIndex: cell.cell_index })}
|
||||
className={`aspect-video bg-white/[0.04] border rounded-lg flex flex-col items-center justify-center relative gap-0.5 cursor-pointer motion-safe:transition-all
|
||||
${isActive
|
||||
? "border-accent/40 bg-accent/10 ring-1 ring-accent/20"
|
||||
: "border-white/[0.08] hover:border-accent/30 hover:bg-white/[0.08]"
|
||||
}`}
|
||||
>
|
||||
<Image className="w-6 h-6 text-text-muted/40" />
|
||||
<span className="text-[9px] text-text-muted leading-tight text-center px-1.5 line-clamp-1">
|
||||
{cell.description}
|
||||
</span>
|
||||
<span className="absolute top-1 left-1.5 text-[9px] font-[family-name:var(--font-mono)] text-text-muted/80 bg-black/30 px-0.5 rounded">
|
||||
{cell.cell_index}
|
||||
</span>
|
||||
<span className="absolute bottom-0.5 right-1 text-[8px] font-[family-name:var(--font-mono)] text-text-muted/40">
|
||||
{cell.timecode}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Right Column: Preview ── */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Right header: info row (aligned with left step bar) */}
|
||||
<div className="flex items-center gap-3 mb-3 h-[30px]">
|
||||
{activeCellData ? (
|
||||
<>
|
||||
<span className="text-xs font-medium text-text-secondary">
|
||||
Cell {String(activeCellData.cell_index).padStart(2, "0")}
|
||||
</span>
|
||||
<span className="text-xs text-text-primary truncate">{activeCellData.description}</span>
|
||||
<span className="text-[10px] font-[family-name:var(--font-mono)] text-text-muted shrink-0">{activeCellData.timecode}</span>
|
||||
<span className="text-[10px] text-text-muted shrink-0">1280×720</span>
|
||||
<span className="text-[10px] font-[family-name:var(--font-mono)] text-text-muted/50 truncate">{activeCellData.asset_filename}</span>
|
||||
<button
|
||||
onClick={() => setSelectedCell(null)}
|
||||
className="text-text-muted hover:text-text-secondary motion-safe:transition-colors cursor-pointer ml-auto shrink-0"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-xs text-text-muted">预览</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right content: preview image, same size as left grid */}
|
||||
{activeCellData ? (
|
||||
<div className="aspect-video bg-black/40 rounded-xl flex items-center justify-center border border-white/[0.06]">
|
||||
<Image className="w-12 h-12 text-text-muted/20" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="aspect-video border border-dashed border-white/[0.06] rounded-xl flex items-center justify-center">
|
||||
<p className="text-xs text-text-muted">点击左侧裁切图查看大图预览</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* === FAILED === */}
|
||||
{scene.status === "failed" && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between p-4 bg-red-500/5 border border-red-500/10 rounded-lg">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<AlertTriangle className="w-4 h-4 text-red-400" />
|
||||
<span className="text-red-400 font-medium">{scene.error_message}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="px-3 py-1.5 rounded-lg text-xs font-medium text-text-secondary bg-white/[0.06] border border-white/10 hover:bg-white/[0.1] motion-safe:transition-colors cursor-pointer flex items-center gap-1.5">
|
||||
<Pencil className="w-3 h-3" />
|
||||
编辑提示词
|
||||
</button>
|
||||
<button className="px-3 py-1.5 rounded-lg text-xs font-medium text-accent bg-accent/10 border border-accent/20 hover:bg-accent/20 motion-safe:transition-colors cursor-pointer flex items-center gap-1.5">
|
||||
<RotateCcw className="w-3 h-3" />
|
||||
重跑
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 p-3 bg-white/[0.02] border border-white/[0.06] rounded-lg">
|
||||
<p className="text-[10px] text-text-muted mb-1">Prompt 预览</p>
|
||||
<p className="text-xs font-[family-name:var(--font-mono)] text-text-secondary leading-relaxed">
|
||||
{scene.prompt_preview}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* === PENDING === */}
|
||||
{scene.status === "pending" && (
|
||||
<div className="py-6">
|
||||
<div
|
||||
className="gap-2 mb-4 opacity-40"
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${scene.grid_cols}, 1fr)`,
|
||||
width: scene.grid_type === "4-grid" ? "640px" : "760px",
|
||||
}}
|
||||
>
|
||||
{scene.cells.map((cell) => (
|
||||
<div
|
||||
key={cell.cell_index}
|
||||
className="aspect-video bg-white/[0.02] border border-white/[0.04] rounded-lg flex flex-col items-center justify-center gap-1"
|
||||
>
|
||||
<span className="text-[10px] font-[family-name:var(--font-mono)] text-text-muted/40">{cell.cell_index}</span>
|
||||
<span className="text-[9px] text-text-muted/30 text-center px-2 line-clamp-1">{cell.description}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-center text-xs text-text-muted">等待前序场景完成</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
248
frontend/src/components/pipeline/StagePlanning.tsx
Normal file
248
frontend/src/components/pipeline/StagePlanning.tsx
Normal file
@ -0,0 +1,248 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { User, MapPin, ChevronDown, Pencil, Check, Image, Info } from "lucide-react";
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Mock Data (based on json-schemas.md)
|
||||
───────────────────────────────────────────── */
|
||||
const MOCK_CHARACTERS = [
|
||||
{
|
||||
id: "char_001",
|
||||
name: "T仔",
|
||||
name_en: "T-Rex Kid",
|
||||
species: "Q版迷你霸王龙",
|
||||
prompt_en:
|
||||
"A cute chibi mini T-Rex, about 30cm tall, 2-head proportion, round chubby body. Orange-red skin with pale yellow belly. Very large round head, wide but cute mouth. Big round eyes with tired 'dead fish eye' expression. Tiny short arms barely thumb-length that can't reach anything. Thick short tail. Wearing white mini dress shirt with green tie.",
|
||||
distinctive_features: "小短手够不着, 死鱼眼, 尾巴乱甩",
|
||||
asset_status: "pending" as const,
|
||||
},
|
||||
{
|
||||
id: "char_002",
|
||||
name: "特特",
|
||||
name_en: "Tete",
|
||||
species: "Q版迷你翼龙",
|
||||
prompt_en:
|
||||
"A cute chibi mini Pterodactyl, light blue-gray body, round head with big eyes, short beak. Wings like a small cape. Wearing white dress shirt with red tie, aviator goggles on top of head.",
|
||||
distinctive_features: "翅膀像披风, 防风镜, 自恋傲娇",
|
||||
asset_status: "pending" as const,
|
||||
},
|
||||
{
|
||||
id: "char_003",
|
||||
name: "皮皮",
|
||||
name_en: "Pipi",
|
||||
species: "Q版迷你甲龙",
|
||||
prompt_en:
|
||||
"A cute chibi mini Ankylosaurus, bright yellow, extremely round and hard like a bouncy ball. Small bone armor plates on back, bone club at tail tip. Very short limbs, moves by rolling. Big bright eyes, always excited expression.",
|
||||
distinctive_features: "弹力球体型, 骨锤尾巴, 滚动前进",
|
||||
asset_status: "pending" as const,
|
||||
},
|
||||
{
|
||||
id: "char_004",
|
||||
name: "外卖三角龙",
|
||||
name_en: "Delivery Triceratops",
|
||||
species: "Q版迷你三角龙",
|
||||
prompt_en:
|
||||
"A cute chibi mini Triceratops, orange colored, three small horns on head, wearing delivery rider vest, riding a mini electric scooter.",
|
||||
distinctive_features: "外卖骑手背心, 迷你电动车",
|
||||
asset_status: "pending" as const,
|
||||
},
|
||||
];
|
||||
|
||||
const MOCK_SCENES = [
|
||||
{
|
||||
id: "scene_001",
|
||||
name: "T仔的单身公寓",
|
||||
environment: "interior",
|
||||
time_of_day: "morning",
|
||||
estimated_duration_sec: 40,
|
||||
characters_present: ["char_001"],
|
||||
prompt_en:
|
||||
"A tiny single-room apartment sized for mini dinosaurs. A shoe-box-sized bed with oversized round alarm clock on bedside table. Warm yellow morning light through window. A 'Perfect Attendance Award' certificate on wall (only decoration). Cozy but cramped.",
|
||||
},
|
||||
{
|
||||
id: "scene_002",
|
||||
name: "恐龙城街道",
|
||||
environment: "exterior",
|
||||
time_of_day: "morning",
|
||||
estimated_duration_sec: 35,
|
||||
characters_present: ["char_001", "char_004"],
|
||||
prompt_en:
|
||||
"A miniature city street designed for tiny dinosaurs. Small-scale lamp posts, garbage bins, traffic lights. Various chibi dinosaurs commuting to work. Morning rush hour atmosphere. Urban but cute.",
|
||||
},
|
||||
{
|
||||
id: "scene_003",
|
||||
name: "公司大厅/打卡处",
|
||||
environment: "interior",
|
||||
time_of_day: "morning",
|
||||
estimated_duration_sec: 50,
|
||||
characters_present: ["char_001", "char_002", "char_003"],
|
||||
prompt_en:
|
||||
"A small office building lobby for mini dinosaurs. A punch-in machine slightly taller than the dinosaurs. Fire sprinkler system on ceiling. Clean corporate interior with fluorescent lighting.",
|
||||
},
|
||||
{
|
||||
id: "scene_004",
|
||||
name: "打卡机前",
|
||||
environment: "interior",
|
||||
time_of_day: "morning",
|
||||
estimated_duration_sec: 40,
|
||||
characters_present: ["char_001", "char_003"],
|
||||
prompt_en:
|
||||
"Close-up area around the office punch-in machine. Digital clock display visible. Corporate office environment background. The punch machine has a face recognition screen.",
|
||||
},
|
||||
];
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Tabs
|
||||
───────────────────────────────────────────── */
|
||||
type TabKey = "characters" | "scenes";
|
||||
|
||||
const TABS: { key: TabKey; label: string; icon: React.ElementType; count: number }[] = [
|
||||
{ key: "characters", label: "角色规划", icon: User, count: MOCK_CHARACTERS.length },
|
||||
{ key: "scenes", label: "场景规划", icon: MapPin, count: MOCK_SCENES.length },
|
||||
];
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Component
|
||||
───────────────────────────────────────────── */
|
||||
export default function StagePlanning() {
|
||||
const [activeTab, setActiveTab] = useState<TabKey>("characters");
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Info banner */}
|
||||
<div className="flex items-start gap-3 p-3 rounded-lg bg-accent/5 border border-accent/10 mb-5">
|
||||
<Info className="w-4 h-4 text-accent shrink-0 mt-0.5" />
|
||||
<p className="text-xs text-text-secondary">
|
||||
本阶段由 Claude 分析剧本,产出角色/场景/宫格规划 JSON。仅包含文字提示词与结构化数据,实际图片在后续阶段生成。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tab bar */}
|
||||
<div className="flex items-center gap-1 mb-6 border-b border-white/[0.06] pb-3">
|
||||
{TABS.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = activeTab === tab.key;
|
||||
return (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium cursor-pointer motion-safe:transition-colors
|
||||
${isActive ? "bg-accent/15 text-accent" : "text-text-secondary hover:bg-white/[0.06]"}`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{tab.label}
|
||||
<span className={`text-xs px-1.5 py-0.5 rounded ${isActive ? "bg-accent/20" : "bg-white/[0.06]"}`}>
|
||||
{tab.count}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Character tab */}
|
||||
{activeTab === "characters" && (
|
||||
<div className="space-y-3">
|
||||
{MOCK_CHARACTERS.map((char) => {
|
||||
const isOpen = expandedId === char.id;
|
||||
return (
|
||||
<div key={char.id} className="glass-card overflow-hidden">
|
||||
<button
|
||||
onClick={() => setExpandedId(isOpen ? null : char.id)}
|
||||
className="w-full flex items-center gap-4 px-5 py-4 cursor-pointer hover:bg-white/[0.04] motion-safe:transition-colors"
|
||||
>
|
||||
{/* Avatar placeholder */}
|
||||
<div className="w-12 h-12 rounded-xl bg-white/[0.04] border border-white/[0.06] flex items-center justify-center shrink-0">
|
||||
<Image className="w-5 h-5 text-text-muted" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-text-primary">{char.name}</span>
|
||||
<span className="text-xs text-text-muted">{char.name_en}</span>
|
||||
</div>
|
||||
<p className="text-xs text-text-secondary mt-0.5">{char.species} · {char.distinctive_features}</p>
|
||||
</div>
|
||||
<ChevronDown className={`w-4 h-4 text-text-muted motion-safe:transition-transform ${isOpen ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="border-t border-white/[0.06] px-5 py-4 space-y-3">
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-wider text-text-muted mb-1.5">Banana Pro Prompt (EN)</p>
|
||||
<div className="bg-white/[0.03] border border-white/[0.06] rounded-lg p-3 text-xs text-text-secondary font-[family-name:var(--font-mono)] leading-relaxed">
|
||||
{char.prompt_en}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="px-3 py-1.5 rounded-lg text-xs font-medium text-text-secondary bg-white/[0.06] border border-white/10 hover:bg-white/[0.1] cursor-pointer flex items-center gap-1.5">
|
||||
<Pencil className="w-3 h-3" />
|
||||
编辑提示词
|
||||
</button>
|
||||
<button className="px-3 py-1.5 rounded-lg text-xs font-medium text-accent bg-accent/10 border border-accent/20 hover:bg-accent/20 cursor-pointer flex items-center gap-1.5">
|
||||
<Check className="w-3 h-3" />
|
||||
确认
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Scene tab */}
|
||||
{activeTab === "scenes" && (
|
||||
<div className="space-y-3">
|
||||
{MOCK_SCENES.map((scene) => {
|
||||
const isOpen = expandedId === scene.id;
|
||||
return (
|
||||
<div key={scene.id} className="glass-card overflow-hidden">
|
||||
<button
|
||||
onClick={() => setExpandedId(isOpen ? null : scene.id)}
|
||||
className="w-full flex items-center gap-4 px-5 py-4 cursor-pointer hover:bg-white/[0.04] motion-safe:transition-colors"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-xl bg-white/[0.04] border border-white/[0.06] flex items-center justify-center shrink-0">
|
||||
<MapPin className="w-5 h-5 text-text-muted" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-text-primary">{scene.name}</span>
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-white/[0.06] text-text-muted">
|
||||
{scene.environment === "interior" ? "内景" : "外景"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-text-secondary mt-0.5">
|
||||
{scene.characters_present.length} 角色 · ~{scene.estimated_duration_sec}s
|
||||
</p>
|
||||
</div>
|
||||
<ChevronDown className={`w-4 h-4 text-text-muted motion-safe:transition-transform ${isOpen ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="border-t border-white/[0.06] px-5 py-4 space-y-3">
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-wider text-text-muted mb-1.5">Banana Pro Prompt (EN)</p>
|
||||
<div className="bg-white/[0.03] border border-white/[0.06] rounded-lg p-3 text-xs text-text-secondary font-[family-name:var(--font-mono)] leading-relaxed">
|
||||
{scene.prompt_en}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="px-3 py-1.5 rounded-lg text-xs font-medium text-text-secondary bg-white/[0.06] border border-white/10 hover:bg-white/[0.1] cursor-pointer flex items-center gap-1.5">
|
||||
<Pencil className="w-3 h-3" />
|
||||
编辑提示词
|
||||
</button>
|
||||
<button className="px-3 py-1.5 rounded-lg text-xs font-medium text-accent bg-accent/10 border border-accent/20 hover:bg-accent/20 cursor-pointer flex items-center gap-1.5">
|
||||
<Check className="w-3 h-3" />
|
||||
确认
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
305
frontend/src/components/pipeline/StageSegments.tsx
Normal file
305
frontend/src/components/pipeline/StageSegments.tsx
Normal file
@ -0,0 +1,305 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { ChevronDown, Clock, Film, Image, AlertTriangle, Info } from "lucide-react";
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Mock Data (based on json-schemas.md segments)
|
||||
───────────────────────────────────────────── */
|
||||
interface SegmentData {
|
||||
id: string;
|
||||
index: number;
|
||||
timecode_start: string;
|
||||
timecode_end: string;
|
||||
duration_sec: number;
|
||||
scene_id: string;
|
||||
scene_name: string;
|
||||
script_text: string;
|
||||
reference_images: { type: string; id: string }[];
|
||||
is_action_scene: boolean;
|
||||
seedance_prompt_preview: string;
|
||||
}
|
||||
|
||||
const MOCK_SEGMENT_DATA: SegmentData[] = [
|
||||
{
|
||||
id: "seg_001",
|
||||
index: 1,
|
||||
timecode_start: "00:00",
|
||||
timecode_end: "00:05",
|
||||
duration_sec: 5,
|
||||
scene_id: "scene_001",
|
||||
scene_name: "T仔的单身公寓",
|
||||
script_text: "△ [黑屏] T仔(OS):作为一只霸王龙的后代,我每天最大的敌人是——",
|
||||
reference_images: [{ type: "character", id: "char_001" }],
|
||||
is_action_scene: false,
|
||||
seedance_prompt_preview: "Black screen, voice over narration by a cute chibi T-Rex...",
|
||||
},
|
||||
{
|
||||
id: "seg_002",
|
||||
index: 2,
|
||||
timecode_start: "00:05",
|
||||
timecode_end: "00:15",
|
||||
duration_sec: 10,
|
||||
scene_id: "scene_001",
|
||||
scene_name: "T仔的单身公寓",
|
||||
script_text: "△ [俯拍/近景] 小床上,T仔躺在被窝里,只露出一颗圆滚滚的橘红色大脑袋,闭着眼,睡得很沉。\n△ [特写] 床头柜上的红色闹钟显示07:30,猛地开始震动。",
|
||||
reference_images: [
|
||||
{ type: "character", id: "char_001" },
|
||||
{ type: "scene", id: "scene_001" },
|
||||
{ type: "keyshot", id: "ks_001_01" },
|
||||
],
|
||||
is_action_scene: false,
|
||||
seedance_prompt_preview: "Top-down shot of a tiny bed, a cute orange chibi T-Rex sleeping under covers, only round head visible. A large red alarm clock on bedside table starts vibrating violently...",
|
||||
},
|
||||
{
|
||||
id: "seg_003",
|
||||
index: 3,
|
||||
timecode_start: "00:15",
|
||||
timecode_end: "00:25",
|
||||
duration_sec: 10,
|
||||
scene_id: "scene_001",
|
||||
scene_name: "T仔的单身公寓",
|
||||
script_text: "△ [近景] T仔被震醒,从被子里伸出小短手够闹钟,疯狂挥舞,就是够不着。\n△ [近景] T仔放弃,叹了口气,掀开被子坐起来。\nT仔:又是新的一天。",
|
||||
reference_images: [
|
||||
{ type: "character", id: "char_001" },
|
||||
{ type: "scene", id: "scene_001" },
|
||||
{ type: "keyshot", id: "ks_001_02" },
|
||||
],
|
||||
is_action_scene: true,
|
||||
seedance_prompt_preview: "Close-up of cute chibi T-Rex in bed, woken by alarm. Tiny arms frantically waving trying to reach alarm clock, can't reach it. Gives up with sigh...",
|
||||
},
|
||||
{
|
||||
id: "seg_004",
|
||||
index: 4,
|
||||
timecode_start: "00:25",
|
||||
timecode_end: "00:35",
|
||||
duration_sec: 10,
|
||||
scene_id: "scene_001",
|
||||
scene_name: "T仔的单身公寓",
|
||||
script_text: "△ [近景] T仔摸出痒痒挠,想关掉闹钟。\n△ [中近景] T仔的尾巴突然猛地一甩!咻——啪!\n△ [近景] 痒痒挠被尾巴扫飞,砸在墙上,反弹回来,精准击中闹钟按钮。",
|
||||
reference_images: [
|
||||
{ type: "character", id: "char_001" },
|
||||
{ type: "scene", id: "scene_001" },
|
||||
{ type: "keyshot", id: "ks_001_03" },
|
||||
],
|
||||
is_action_scene: true,
|
||||
seedance_prompt_preview: "Close shot, chibi T-Rex holding a back scratcher trying to reach alarm. Thick tail suddenly swings hard, knocking scratcher flying across room...",
|
||||
},
|
||||
{
|
||||
id: "seg_005",
|
||||
index: 5,
|
||||
timecode_start: "00:35",
|
||||
timecode_end: "00:45",
|
||||
duration_sec: 10,
|
||||
scene_id: "scene_001",
|
||||
scene_name: "T仔的单身公寓",
|
||||
script_text: "△ [特写] 闹钟变红,T仔提前录制的\u201C霸王龙咆哮\u201D炸响。\n△ [近景] 嗷\u2014\u2014!!(声波震得水杯里的水都在抖)\nT仔(崩溃抱头):闭嘴!那是进化的误会!",
|
||||
reference_images: [
|
||||
{ type: "character", id: "char_001" },
|
||||
{ type: "scene", id: "scene_001" },
|
||||
{ type: "keyshot", id: "ks_001_03" },
|
||||
],
|
||||
is_action_scene: false,
|
||||
seedance_prompt_preview: "Alarm turns red, blasting T-Rex roar sound. Sound waves visibly shaking water in cup. Chibi T-Rex covers head in panic...",
|
||||
},
|
||||
// More segments...
|
||||
{
|
||||
id: "seg_006",
|
||||
index: 6,
|
||||
timecode_start: "00:45",
|
||||
timecode_end: "00:55",
|
||||
duration_sec: 10,
|
||||
scene_id: "scene_002",
|
||||
scene_name: "恐龙城街道",
|
||||
script_text: "△ [中近景] T仔嘴里叼着半片吐司狂奔,领带甩在脸上。\nT仔(OS):来不及了,马上要错过全勤奖了!",
|
||||
reference_images: [
|
||||
{ type: "character", id: "char_001" },
|
||||
{ type: "scene", id: "scene_002" },
|
||||
{ type: "keyshot", id: "ks_002_01" },
|
||||
],
|
||||
is_action_scene: true,
|
||||
seedance_prompt_preview: "Chibi T-Rex running frantically through miniature city street, toast in mouth, tie flapping in face...",
|
||||
},
|
||||
{
|
||||
id: "seg_007",
|
||||
index: 7,
|
||||
timecode_start: "00:55",
|
||||
timecode_end: "01:10",
|
||||
duration_sec: 15,
|
||||
scene_id: "scene_002",
|
||||
scene_name: "恐龙城街道",
|
||||
script_text: "△ [近景] 尾巴扫飞垃圾桶。\n△ [特写] 火腿肠滚出。T仔眼睛一亮。\nT仔:顶级的工业淀粉!我的灵魂伴侣!\n△ [中景] 三角龙外卖车冲过来,碾过火腿肠。酱汁溅了T仔一脸。",
|
||||
reference_images: [
|
||||
{ type: "character", id: "char_001" },
|
||||
{ type: "character", id: "char_004" },
|
||||
{ type: "scene", id: "scene_002" },
|
||||
{ type: "keyshot", id: "ks_002_02" },
|
||||
],
|
||||
is_action_scene: true,
|
||||
seedance_prompt_preview: "Tail knocks over garbage bin, sausage rolls out. T-Rex's eyes light up. Delivery scooter rushes in, runs over sausage, sauce splatters on T-Rex face...",
|
||||
},
|
||||
{
|
||||
id: "seg_008",
|
||||
index: 8,
|
||||
timecode_start: "01:10",
|
||||
timecode_end: "01:20",
|
||||
duration_sec: 10,
|
||||
scene_id: "scene_002",
|
||||
scene_name: "恐龙城街道",
|
||||
script_text: "△ [近景] T仔举起小短手想抹脸——够不着。\nT仔:为什么倒霉的总是我。",
|
||||
reference_images: [
|
||||
{ type: "character", id: "char_001" },
|
||||
{ type: "scene", id: "scene_002" },
|
||||
{ type: "keyshot", id: "ks_002_04" },
|
||||
],
|
||||
is_action_scene: false,
|
||||
seedance_prompt_preview: "Close-up of chibi T-Rex with sauce on face like smoky eye makeup, tiny arms trying to wipe face but can't reach...",
|
||||
},
|
||||
];
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Component
|
||||
───────────────────────────────────────────── */
|
||||
export default function StageSegments() {
|
||||
const [expandedSeg, setExpandedSeg] = useState<string | null>(null);
|
||||
|
||||
const totalDuration = MOCK_SEGMENT_DATA.reduce((a, s) => a + s.duration_sec, 0);
|
||||
const actionCount = MOCK_SEGMENT_DATA.filter((s) => s.is_action_scene).length;
|
||||
const overLimit = MOCK_SEGMENT_DATA.filter((s) => s.duration_sec > 15);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Summary bar */}
|
||||
<div className="flex items-center gap-6 mb-5 text-sm">
|
||||
<div className="flex items-center gap-2 text-text-secondary">
|
||||
<Film className="w-4 h-4" />
|
||||
<span className="font-medium text-text-primary">{MOCK_SEGMENT_DATA.length}</span> 片段
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-text-secondary">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span className="font-medium text-text-primary">
|
||||
{Math.floor(totalDuration / 60)}:{String(totalDuration % 60).padStart(2, "0")}
|
||||
</span> 总时长
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-text-secondary">
|
||||
<span className="font-medium text-text-primary">{actionCount}</span> 动作场景
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Over-limit warning */}
|
||||
{overLimit.length > 0 && (
|
||||
<div className="flex items-start gap-3 p-3 rounded-lg bg-amber-500/10 border border-amber-400/20 mb-5">
|
||||
<AlertTriangle className="w-4 h-4 text-amber-400 shrink-0 mt-0.5" />
|
||||
<div className="text-xs text-amber-400">
|
||||
<p className="font-medium">Seedance 时长限制提醒</p>
|
||||
<p className="text-amber-400/70 mt-0.5">
|
||||
{overLimit.length} 个片段超过 15 秒限制,生成时将自动截断。建议手动拆分。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Rule info */}
|
||||
<div className="flex items-start gap-3 p-3 rounded-lg bg-accent/5 border border-accent/10 mb-5">
|
||||
<Info className="w-4 h-4 text-accent shrink-0 mt-0.5" />
|
||||
<div className="text-xs text-text-secondary">
|
||||
<p className="text-accent font-medium">切分规则:只切不改</p>
|
||||
<p>原始剧本文字 100% 保留,不做任何改写。每个片段 1-15 秒,由 Seedance 2.0 生成。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Segment list */}
|
||||
<div className="space-y-2">
|
||||
{MOCK_SEGMENT_DATA.map((seg) => {
|
||||
const isOpen = expandedSeg === seg.id;
|
||||
return (
|
||||
<div key={seg.id} className="glass-card overflow-hidden">
|
||||
<button
|
||||
onClick={() => setExpandedSeg(isOpen ? null : seg.id)}
|
||||
className="w-full flex items-center gap-4 px-5 py-3 cursor-pointer hover:bg-white/[0.04] motion-safe:transition-colors"
|
||||
>
|
||||
{/* Index */}
|
||||
<span className="font-[family-name:var(--font-mono)] text-xs text-text-muted w-8 text-right shrink-0">
|
||||
{String(seg.index).padStart(2, "0")}
|
||||
</span>
|
||||
|
||||
{/* Timecode */}
|
||||
<span className="font-[family-name:var(--font-mono)] text-xs text-text-secondary w-24 shrink-0">
|
||||
{seg.timecode_start} → {seg.timecode_end}
|
||||
</span>
|
||||
|
||||
{/* Duration */}
|
||||
<span className={`text-xs font-medium w-10 shrink-0 ${seg.duration_sec > 15 ? "text-amber-400" : "text-text-muted"}`}>
|
||||
{seg.duration_sec}s
|
||||
</span>
|
||||
|
||||
{/* Scene name */}
|
||||
<span className="text-xs text-accent/70 w-32 shrink-0 truncate">{seg.scene_name}</span>
|
||||
|
||||
{/* Script preview */}
|
||||
<span className="flex-1 text-xs text-text-secondary truncate text-left">
|
||||
{seg.script_text.split("\n")[0].slice(0, 60)}...
|
||||
</span>
|
||||
|
||||
{/* Action badge */}
|
||||
{seg.is_action_scene && (
|
||||
<span className="text-[9px] px-1.5 py-0.5 rounded bg-orange-500/15 text-orange-400 font-medium shrink-0">
|
||||
动作
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Reference count */}
|
||||
<span className="flex items-center gap-1 text-[10px] text-text-muted shrink-0">
|
||||
<Image className="w-3 h-3" />
|
||||
{seg.reference_images.length}
|
||||
</span>
|
||||
|
||||
<ChevronDown className={`w-4 h-4 text-text-muted shrink-0 motion-safe:transition-transform ${isOpen ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="border-t border-white/[0.06] px-5 py-4 space-y-4">
|
||||
{/* Full script text */}
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-wider text-text-muted mb-1.5">原始剧本</p>
|
||||
<div className="bg-white/[0.03] border border-white/[0.06] rounded-lg p-3 text-xs text-text-primary leading-relaxed whitespace-pre-wrap">
|
||||
{seg.script_text}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reference images */}
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-wider text-text-muted mb-1.5">参考图引用</p>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{seg.reference_images.map((ref, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={`px-2 py-1 rounded text-[10px] font-medium
|
||||
${ref.type === "character" ? "bg-violet-500/15 text-violet-400" : ""}
|
||||
${ref.type === "scene" ? "bg-emerald-500/15 text-emerald-400" : ""}
|
||||
${ref.type === "keyshot" ? "bg-blue-500/15 text-blue-400" : ""}
|
||||
${ref.type === "prop" ? "bg-amber-500/15 text-amber-400" : ""}
|
||||
`}
|
||||
>
|
||||
{ref.type}: {ref.id}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Seedance prompt preview */}
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-wider text-text-muted mb-1.5">Seedance 提示词预览</p>
|
||||
<div className="bg-white/[0.03] border border-white/[0.06] rounded-lg p-3 text-xs text-text-secondary font-[family-name:var(--font-mono)] leading-relaxed">
|
||||
{seg.seedance_prompt_preview}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
194
frontend/src/components/pipeline/StageTimeline.tsx
Normal file
194
frontend/src/components/pipeline/StageTimeline.tsx
Normal file
@ -0,0 +1,194 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Play, Download, GripVertical, Film, Clock, Loader2, Image } from "lucide-react";
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Mock Data
|
||||
───────────────────────────────────────────── */
|
||||
interface TimelineClip {
|
||||
id: string;
|
||||
index: number;
|
||||
duration_sec: number;
|
||||
scene_name: string;
|
||||
description: string;
|
||||
status: "ready" | "processing";
|
||||
}
|
||||
|
||||
const MOCK_CLIPS: TimelineClip[] = [
|
||||
{ id: "clip_01", index: 1, duration_sec: 5, scene_name: "T仔的单身公寓", description: "黑屏OS引入", status: "ready" },
|
||||
{ id: "clip_02", index: 2, duration_sec: 10, scene_name: "T仔的单身公寓", description: "俯拍小床 闹钟震动", status: "ready" },
|
||||
{ id: "clip_03", index: 3, duration_sec: 10, scene_name: "T仔的单身公寓", description: "小短手够闹钟", status: "ready" },
|
||||
{ id: "clip_04", index: 4, duration_sec: 10, scene_name: "T仔的单身公寓", description: "痒痒挠被尾巴扫飞", status: "ready" },
|
||||
{ id: "clip_05", index: 5, duration_sec: 10, scene_name: "T仔的单身公寓", description: "霸王龙咆哮闹钟 物理消音", status: "ready" },
|
||||
{ id: "clip_06", index: 6, duration_sec: 10, scene_name: "恐龙城街道", description: "叼吐司狂奔", status: "ready" },
|
||||
{ id: "clip_07", index: 7, duration_sec: 15, scene_name: "恐龙城街道", description: "火腿肠事件 外卖龙碾压", status: "ready" },
|
||||
{ id: "clip_08", index: 8, duration_sec: 10, scene_name: "恐龙城街道", description: "酱汁溅脸 小短手擦不到", status: "ready" },
|
||||
{ id: "clip_09", index: 9, duration_sec: 15, scene_name: "公司大厅", description: "特特低空滑翔打卡", status: "ready" },
|
||||
{ id: "clip_10", index: 10, duration_sec: 15, scene_name: "公司大厅", description: "皮皮闯祸 消防喷水", status: "ready" },
|
||||
{ id: "clip_11", index: 11, duration_sec: 15, scene_name: "打卡机前", description: "生死冲刺 迟到1秒", status: "ready" },
|
||||
{ id: "clip_12", index: 12, duration_sec: 10, scene_name: "打卡机前", description: "皮皮团魂 定格结尾", status: "ready" },
|
||||
{ id: "clip_13", index: 13, duration_sec: 3, scene_name: "工位", description: "片尾彩蛋 洗发水香味", status: "ready" },
|
||||
];
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Component
|
||||
───────────────────────────────────────────── */
|
||||
export default function StageTimeline() {
|
||||
const [selectedClip, setSelectedClip] = useState<string>("clip_01");
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
|
||||
const selected = MOCK_CLIPS.find((c) => c.id === selectedClip);
|
||||
const totalDuration = MOCK_CLIPS.reduce((a, c) => a + c.duration_sec, 0);
|
||||
const totalMin = Math.floor(totalDuration / 60);
|
||||
const totalSec = totalDuration % 60;
|
||||
|
||||
// Color by scene for timeline bars
|
||||
const sceneColors: Record<string, string> = {
|
||||
"T仔的单身公寓": "bg-violet-500/60",
|
||||
"恐龙城街道": "bg-emerald-500/60",
|
||||
"公司大厅": "bg-blue-500/60",
|
||||
"打卡机前": "bg-amber-500/60",
|
||||
"工位": "bg-pink-500/60",
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* ─── Preview Area (16:9) ─── */}
|
||||
<div className="mb-6">
|
||||
<div className="aspect-video bg-black/40 border border-white/[0.06] rounded-2xl overflow-hidden relative flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Image className="w-16 h-16 text-text-muted mx-auto mb-3" />
|
||||
{selected && (
|
||||
<>
|
||||
<p className="text-sm text-text-primary font-medium">
|
||||
片段 {String(selected.index).padStart(2, "0")} — {selected.description}
|
||||
</p>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
{selected.scene_name} · {selected.duration_sec}s
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* Play button overlay */}
|
||||
<button className="absolute inset-0 flex items-center justify-center bg-black/20 hover:bg-black/10 motion-safe:transition-colors cursor-pointer group">
|
||||
<div className="w-16 h-16 rounded-full bg-white/20 flex items-center justify-center group-hover:bg-white/30 motion-safe:transition-colors backdrop-blur-sm">
|
||||
<Play className="w-7 h-7 text-white ml-1" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── Timeline Strip ─── */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-3 text-xs text-text-secondary">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Film className="w-3.5 h-3.5" />
|
||||
{MOCK_CLIPS.length} 片段
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Clock className="w-3.5 h-3.5" />
|
||||
{totalMin}:{String(totalSec).padStart(2, "0")} 总时长
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-text-muted">拖拽片段调整顺序</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/[0.02] border border-white/[0.06] rounded-xl p-3 overflow-x-auto">
|
||||
<div className="flex gap-1.5 min-w-max">
|
||||
{MOCK_CLIPS.map((clip) => {
|
||||
const isSelected = clip.id === selectedClip;
|
||||
const barColor = sceneColors[clip.scene_name] || "bg-gray-500/60";
|
||||
// Width proportional to duration
|
||||
const widthPx = Math.max(clip.duration_sec * 6, 40);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={clip.id}
|
||||
onClick={() => setSelectedClip(clip.id)}
|
||||
className={`shrink-0 rounded-lg border p-2 flex flex-col items-center gap-1 cursor-pointer motion-safe:transition-all group
|
||||
${
|
||||
isSelected
|
||||
? "border-accent/40 bg-accent/10 ring-1 ring-accent/20"
|
||||
: "border-white/[0.06] bg-white/[0.02] hover:bg-white/[0.06]"
|
||||
}`}
|
||||
style={{ width: `${widthPx}px` }}
|
||||
>
|
||||
{/* Drag handle */}
|
||||
<GripVertical className="w-3 h-3 text-text-muted opacity-0 group-hover:opacity-100 motion-safe:transition-opacity" />
|
||||
|
||||
{/* Color bar */}
|
||||
<div className={`w-full h-6 rounded ${barColor} flex items-center justify-center`}>
|
||||
<span className="text-[9px] font-[family-name:var(--font-mono)] text-white/80">
|
||||
{String(clip.index).padStart(2, "0")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Duration label */}
|
||||
<span className="text-[9px] text-text-muted">{clip.duration_sec}s</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scene color legend */}
|
||||
<div className="flex items-center gap-4 mt-3 text-[10px]">
|
||||
{Object.entries(sceneColors).map(([name, color]) => (
|
||||
<span key={name} className="flex items-center gap-1.5 text-text-muted">
|
||||
<span className={`w-2 h-2 rounded-sm ${color}`} />
|
||||
{name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── Export CTA ─── */}
|
||||
<div className="glass-card p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-text-primary mb-1">导出成片</h3>
|
||||
<p className="text-xs text-text-secondary">
|
||||
FFmpeg concat 拼接所有片段 · {MOCK_CLIPS.length} 片段 · {totalMin}:{String(totalSec).padStart(2, "0")}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsExporting(!isExporting)}
|
||||
className={`px-6 py-3 rounded-xl text-sm font-semibold motion-safe:transition-all cursor-pointer flex items-center gap-2
|
||||
${
|
||||
isExporting
|
||||
? "bg-white/[0.06] border border-white/10 text-text-secondary"
|
||||
: "bg-gradient-to-br from-violet-500 to-violet-700 shadow-[0_0_20px_rgba(139,92,246,0.4)] border border-white/15 text-white hover:shadow-[0_0_30px_rgba(139,92,246,0.6)]"
|
||||
}`}
|
||||
>
|
||||
{isExporting ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
导出中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4" />
|
||||
导出成片
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Export progress (mock) */}
|
||||
{isExporting && (
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs text-text-secondary">拼接进度</span>
|
||||
<span className="text-xs text-text-muted">7/13 片段</span>
|
||||
</div>
|
||||
<div className="h-2 bg-white/[0.06] rounded-full overflow-hidden">
|
||||
<div className="h-full bg-gradient-to-r from-accent to-violet-500 rounded-full w-[54%] motion-safe:transition-all" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
frontend/tsconfig.json
Normal file
34
frontend/tsconfig.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
3044
seedance视频生成文档.md
Normal file
3044
seedance视频生成文档.md
Normal file
File diff suppressed because it is too large
Load Diff
30
skills-lock.json
Normal file
30
skills-lock.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"version": 1,
|
||||
"skills": {
|
||||
"find-skills": {
|
||||
"source": "vercel-labs/skills",
|
||||
"sourceType": "github",
|
||||
"computedHash": "25872a21881a950edc3db1f3329664d60405d539660ce05b3265db2de06a7dfd"
|
||||
},
|
||||
"remotion-best-practices": {
|
||||
"source": "remotion-dev/skills",
|
||||
"sourceType": "github",
|
||||
"computedHash": "46a55d68886d6c104bde7803c55a479f91b45ae01441e37c800373bcb0bf3719"
|
||||
},
|
||||
"ui-ux-pro-max": {
|
||||
"source": "nextlevelbuilder/ui-ux-pro-max-skill",
|
||||
"sourceType": "github",
|
||||
"computedHash": "4cfe5e95a230616c17d92b44e3cd37bae543ce087a7756a446a62f77d7c58a37"
|
||||
},
|
||||
"vercel-react-best-practices": {
|
||||
"source": "vercel-labs/agent-skills",
|
||||
"sourceType": "github",
|
||||
"computedHash": "3ff06f0712886f7c2660d6746856c22d3127bd6568ddde29ddad8b72dbaaa226"
|
||||
},
|
||||
"web-design-guidelines": {
|
||||
"source": "vercel-labs/agent-skills",
|
||||
"sourceType": "github",
|
||||
"computedHash": "d8e7d3afe37dcc8a97b99ffb5afdb4d0919ae0092ea8b68f44eb201f035e33ac"
|
||||
}
|
||||
}
|
||||
}
|
||||
296
skills/automation/json-schemas.md
Normal file
296
skills/automation/json-schemas.md
Normal file
@ -0,0 +1,296 @@
|
||||
# Air Spark — JSON Schemas(后端数据契约)
|
||||
|
||||
> 后端与 AI 技能层之间的数据契约。
|
||||
> Stage 2 输出 → Stage 3/4/5 输入。
|
||||
> Stage 5 输出 → Stage 6/7 输入。
|
||||
|
||||
---
|
||||
|
||||
## 文件列表
|
||||
|
||||
| 文件名 | 由谁生成 | 被谁使用 |
|
||||
|--------|----------|----------|
|
||||
| `characters.json` | Stage 2 (storyboard-automation) | Stage 3, Stage 5, Stage 6 |
|
||||
| `scenes.json` | Stage 2 (storyboard-automation) | Stage 3, Stage 5, Stage 6 |
|
||||
| `keyshots.json` | Stage 2 (storyboard-automation) | Stage 4, Stage 5, Stage 6 |
|
||||
| `segments.json` | Stage 5 (segmentation-automation) | Stage 6, Stage 7 |
|
||||
|
||||
---
|
||||
|
||||
## characters.json
|
||||
|
||||
```json
|
||||
{
|
||||
"characters": [
|
||||
{
|
||||
"id": "char_001",
|
||||
"name": "T仔",
|
||||
"name_en": "T-Zai",
|
||||
"species": "T-Rex",
|
||||
"prompt_en": "string (Banana Pro English prompt, narrative paragraph)",
|
||||
"distinctive_features": ["string", "string", "string"],
|
||||
"asset_filename": "character_char_001.jpg",
|
||||
"asset_status": "pending"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明**:
|
||||
- `id`:全局唯一,格式 `char_XXX`,三位数
|
||||
- `asset_filename`:预定义命名,Stage 3 生成后按此名存入资产库
|
||||
- `asset_status`:`pending` → `completed` → `failed`(Stage 3 更新)
|
||||
|
||||
---
|
||||
|
||||
## scenes.json
|
||||
|
||||
```json
|
||||
{
|
||||
"scenes": [
|
||||
{
|
||||
"id": "scene_001",
|
||||
"name": "T仔的单身公寓",
|
||||
"environment": "indoor",
|
||||
"time_of_day": "morning",
|
||||
"estimated_duration_sec": 30,
|
||||
"characters_present": ["char_001", "char_002"],
|
||||
"prompt_en": "string (Banana Pro English prompt, narrative paragraph, No characters in the scene.)",
|
||||
"asset_filename": "scene_scene_001.jpg",
|
||||
"asset_status": "pending"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明**:
|
||||
- `environment`:`indoor` 或 `outdoor`
|
||||
- `time_of_day`:`morning` / `day` / `evening` / `night`
|
||||
- `characters_present`:该场景中出现的角色 id 列表
|
||||
- `asset_status`:`pending` → `completed` → `failed`(Stage 3 更新)
|
||||
|
||||
---
|
||||
|
||||
## keyshots.json
|
||||
|
||||
```json
|
||||
{
|
||||
"video_ratio": "16:9",
|
||||
"keyshots": [
|
||||
{
|
||||
"scene_id": "scene_001",
|
||||
"scene_name": "T仔的单身公寓",
|
||||
"keyshot_index": 1,
|
||||
"grid_type": "4",
|
||||
"grid_rows": 2,
|
||||
"grid_cols": 2,
|
||||
"total_cells": 4,
|
||||
"gen_width": 2560,
|
||||
"gen_height": 1440,
|
||||
"cell_width": 1280,
|
||||
"cell_height": 720,
|
||||
"scene_duration_sec": 30,
|
||||
"coverage_start_sec": 0,
|
||||
"coverage_end_sec": 30,
|
||||
"cell_duration_sec": 7.5,
|
||||
"prompt_en": "string (full Banana Pro prompt for the grid image)",
|
||||
"grid_asset_filename": "keyshot_scene_001_1_grid.jpg",
|
||||
"asset_status": "pending",
|
||||
"cells": [
|
||||
{
|
||||
"num": 1,
|
||||
"row": 1,
|
||||
"col": 1,
|
||||
"timecode_start": "0:00",
|
||||
"timecode_end": "0:08",
|
||||
"spatial_description_en": "string",
|
||||
"asset_filename": "keyshot_scene_001_1_01.jpg",
|
||||
"asset_status": "pending"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明**:
|
||||
- `video_ratio`:项目级配置,`16:9` / `9:16` / `21:9`
|
||||
- `keyshot_index`:同场景有两个9宫格时区分(1或2),其余场景固定为1
|
||||
- `grid_type`:`"4"` 或 `"9"`
|
||||
- `gen_width` / `gen_height`:调用 Banana Pro 时的目标尺寸
|
||||
- `cell_width` / `cell_height`:PIL 裁切后每格尺寸(固定1280×720)
|
||||
- `coverage_start_sec` / `coverage_end_sec`:该宫格在场景内覆盖的秒数范围
|
||||
- `cell_duration_sec`:`(coverage_end_sec - coverage_start_sec) / total_cells`
|
||||
- `grid_asset_filename`:宫格整图文件名(裁切前)
|
||||
- `grid_asset_filename` 命名:`keyshot_{scene_id}_{keyshot_index}_grid.jpg`
|
||||
- 每格 `asset_filename`:`keyshot_{scene_id}_{keyshot_index}_{cell_num 两位数}.jpg`
|
||||
- `asset_status` 更新:Stage 4 生成整图后 → `completed`;PIL 裁切每格后 → 各格 `completed`
|
||||
|
||||
---
|
||||
|
||||
## segments.json
|
||||
|
||||
```json
|
||||
{
|
||||
"episode_id": "ep01",
|
||||
"total_segments": 12,
|
||||
"total_duration_sec": 160,
|
||||
"segments": [
|
||||
{
|
||||
"id": "seg_001",
|
||||
"index": 1,
|
||||
"total": 12,
|
||||
"timecode_start": "0:00",
|
||||
"timecode_end": "0:15",
|
||||
"duration_sec": 15,
|
||||
"scene_id": "scene_001",
|
||||
"scene_number": "1-1",
|
||||
"scene_name": "T仔的单身公寓",
|
||||
"environment": "indoor",
|
||||
"time_of_day": "day",
|
||||
"character_ids": ["char_001"],
|
||||
"script_text": "string (原始剧本内容,\\n 换行,一字不改)",
|
||||
"reference_images": [
|
||||
{"type": "character", "id": "char_001"},
|
||||
{"type": "scene", "id": "scene_001"},
|
||||
{"type": "prop", "id": "prop_001", "note": "闹钟"},
|
||||
{"type": "keyshot", "scene_id": "scene_001", "keyshot_index": 1, "cell_num": 1}
|
||||
],
|
||||
"is_action_scene": false,
|
||||
"seedance_status": "pending",
|
||||
"seedance_job_id": null,
|
||||
"seedance_video_url": null,
|
||||
"seedance_local_path": null,
|
||||
"retry_count": 0
|
||||
}
|
||||
],
|
||||
"visual_warnings": [
|
||||
{
|
||||
"segment_id": "seg_003",
|
||||
"type": "missing_initial_state",
|
||||
"message": "开头缺少角色初始状态"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**reference_images 类型说明**:
|
||||
|
||||
| type | 必填字段 | 后端处理方式 |
|
||||
|------|----------|-------------|
|
||||
| `character` | `id` | 查 asset_library → `character_{id}.jpg` |
|
||||
| `scene` | `id` | 查 asset_library → `scene_{id}.jpg` |
|
||||
| `prop` | `id`, `note` | 查 asset_library → `prop_{id}.jpg` |
|
||||
| `keyshot` | `scene_id`, `keyshot_index`, `cell_num` | 查 asset_library → `keyshot_{scene_id}_{keyshot_index}_{cell_num:02d}.jpg` |
|
||||
|
||||
**不使用 `prev_frame`**:keyshot cell 图承担空间位置锚点职责,所有片段无顺序依赖,Stage 6 全并发提交。
|
||||
|
||||
**Seedance 状态字段**:
|
||||
- `seedance_status`:`pending` / `running` / `completed` / `failed`
|
||||
- `seedance_job_id`:提交 Seedance API 后返回的任务 ID
|
||||
- `seedance_video_url`:Seedance 返回的下载 URL
|
||||
- `seedance_local_path`:下载到本地后的路径
|
||||
- `retry_count`:当前重试次数(最多 3 次)
|
||||
|
||||
---
|
||||
|
||||
## Asset Library 命名规范
|
||||
|
||||
所有资产文件统一存放在 `projects/{project_id}/episodes/{episode_id}/assets/`:
|
||||
|
||||
| 资产类型 | 文件名格式 | 示例 |
|
||||
|----------|------------|------|
|
||||
| 角色人设图 | `character_{id}.jpg` | `character_char_001.jpg` |
|
||||
| 场景图 | `scene_{id}.jpg` | `scene_scene_001.jpg` |
|
||||
| 道具图 | `prop_{id}.jpg` | `prop_prop_001.jpg` |
|
||||
| Keyshot 宫格整图 | `keyshot_{scene_id}_{keyshot_index}_grid.jpg` | `keyshot_scene_001_1_grid.jpg` |
|
||||
| Keyshot 裁切格 | `keyshot_{scene_id}_{keyshot_index}_{cell_num:02d}.jpg` | `keyshot_scene_001_1_01.jpg` |
|
||||
| 片段视频 | `segment_{seg_id}.mp4` | `segment_seg_001.mp4` |
|
||||
| 成片 | `final_{episode_id}.mp4` | `final_ep01.mp4` |
|
||||
|
||||
---
|
||||
|
||||
## PIL 裁切逻辑(参考)
|
||||
|
||||
```python
|
||||
from PIL import Image
|
||||
|
||||
def crop_keyshot_cells(grid_image_path, keyshot: dict, output_dir: str):
|
||||
"""
|
||||
精确裁切宫格图为独立格子图。
|
||||
keyshot: keyshots.json 中的一个 keyshot 对象
|
||||
"""
|
||||
img = Image.open(grid_image_path)
|
||||
rows = keyshot["grid_rows"]
|
||||
cols = keyshot["grid_cols"]
|
||||
cell_w = keyshot["cell_width"] # 1280
|
||||
cell_h = keyshot["cell_height"] # 720
|
||||
|
||||
for cell in keyshot["cells"]:
|
||||
row = cell["row"] - 1 # 转为0-indexed
|
||||
col = cell["col"] - 1
|
||||
left = col * cell_w
|
||||
top = row * cell_h
|
||||
right = left + cell_w
|
||||
bottom = top + cell_h
|
||||
cropped = img.crop((left, top, right, bottom))
|
||||
cropped.save(f"{output_dir}/{cell['asset_filename']}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Seedance API 调用参考(Stage 6)
|
||||
|
||||
```python
|
||||
# 模型 ID:doubao-seedance-1-5-pro-251215(过渡期)/ doubao-seedance-2-0(正式)
|
||||
# 端点:https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks
|
||||
|
||||
# 参考图拼装顺序(固定,决定 [图N] 编号):
|
||||
# 1. 角色人设图(按 character_ids 顺序,每角色1张)
|
||||
# 2. 场景图(1张)
|
||||
# 3. keyshot cell 图(1张)
|
||||
# 4. 道具图(如有,0-2张)
|
||||
|
||||
# 提示词文本结构(后端自动拼装):
|
||||
# {script_text}(剧本原文,一字不改)
|
||||
#
|
||||
# {render_style},[图1]是{char1_name},[图2]是{scene_name},[图3]是{keyshot描述},
|
||||
# 你是一位专业的动画导演,自行安排分镜设计,切镜充满电影感,画面氛围也有电影感,
|
||||
# 不要有背景音乐,但要有音效。
|
||||
# (动作戏追加:动作戏可以有一点荷兰式倾斜镜头,动作戏的镜头具有视觉张力和空间感。)
|
||||
|
||||
# 请求体结构(content 数组,text 在前,图片按顺序追加):
|
||||
# {
|
||||
# "model": "doubao-seedance-1-5-pro-251215",
|
||||
# "content": [
|
||||
# {"type": "text", "text": "<上述拼装的完整提示词>"},
|
||||
# {"type": "image_url", "image_url": {"url": "<图1 URL>"}, "role": "reference_image"},
|
||||
# {"type": "image_url", "image_url": {"url": "<图2 URL>"}, "role": "reference_image"},
|
||||
# ...
|
||||
# ],
|
||||
# "generate_audio": true,
|
||||
# "duration": <segment.duration_sec>,
|
||||
# "ratio": "<project.video_ratio>",
|
||||
# "watermark": false
|
||||
# }
|
||||
|
||||
# 异步轮询:POST 创建 → GET 轮询(每10秒)→ status=="succeeded" → 下载 video_url
|
||||
# 错误重试:最多 3 次,指数退避 1s/4s/16s
|
||||
# 429 限速:按响应头 Retry-After 等待
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FFmpeg 拼接逻辑(Stage 7)
|
||||
|
||||
```bash
|
||||
# 根据 segments.json 按 index 顺序生成 concat.txt
|
||||
# 格式:
|
||||
# file 'projects/xxx/episodes/ep01/assets/segment_seg_001.mp4'
|
||||
# file 'projects/xxx/episodes/ep01/assets/segment_seg_002.mp4'
|
||||
# ...
|
||||
|
||||
ffmpeg -f concat -safe 0 -i concat.txt -c copy final_ep01.mp4
|
||||
# 无损拼接,保留 Seedance 原生音画同步
|
||||
# 3-5 秒完成 30-40 片段
|
||||
```
|
||||
325
skills/automation/segmentation-automation.md
Normal file
325
skills/automation/segmentation-automation.md
Normal file
@ -0,0 +1,325 @@
|
||||
# Stage 5 — 剧本切分(自动化系统提示词)
|
||||
|
||||
> 本文件是自动化流水线 Stage 5 的 Claude API 系统提示词。
|
||||
> 输出必须是严格的 JSON 格式,供后端直接解析。
|
||||
> 逻辑与规则与 script-segmentation-skill 完全一致,只改输出格式。
|
||||
|
||||
---
|
||||
|
||||
## 任务说明
|
||||
|
||||
你是一位专业的动画剧本切分助手。你的任务是将一集动画剧本按内容逻辑切分为视频片段,输出 segments.json。
|
||||
|
||||
核心理念:**只切不改** —— 剧本内容必须100%原样保留,一个字都不能改。
|
||||
|
||||
**红线(全部禁止)**:
|
||||
- 禁止改写、润色、优化任何△行或对白
|
||||
- 禁止合并多个△行为一个
|
||||
- 禁止拆分一个△行为多个
|
||||
- 禁止添加剧本中不存在的△行
|
||||
- 禁止删除任何原文内容
|
||||
- 禁止把剧本内容改写成"提示词风格"
|
||||
|
||||
---
|
||||
|
||||
## 输出格式要求
|
||||
|
||||
输出纯 JSON,不包含任何 Markdown 标记或解释文字:
|
||||
|
||||
```json
|
||||
{
|
||||
"episode_id": "ep01",
|
||||
"total_segments": 12,
|
||||
"total_duration_sec": 160,
|
||||
"segments": [...],
|
||||
"visual_warnings": [...]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 时间估算规则
|
||||
|
||||
### 对白
|
||||
| 类型 | 规则 |
|
||||
|------|------|
|
||||
| 普通对白 | 中文每秒约 3-4 字 |
|
||||
| 快速/激动语气 | 每秒约 5 字 |
|
||||
| 慢速/深沉独白 | 每秒约 2-3 字,按停顿断句 |
|
||||
| OS/V.O. | 同普通对白 |
|
||||
| 带情绪括注 | 对白时长 + 情绪反应 1 秒 |
|
||||
|
||||
### 动作(△行)
|
||||
| 类型 | 估算秒数 |
|
||||
|------|----------|
|
||||
| 简单动作 | 2 秒 |
|
||||
| 中等动作 | 3 秒 |
|
||||
| 复杂动作/连锁反应 | 4 秒 |
|
||||
| 带特效动作 | 3-5 秒 |
|
||||
| 多角色互动 | 4-5 秒 |
|
||||
|
||||
### 特殊标注
|
||||
| 类型 | 规则 |
|
||||
|------|------|
|
||||
| [特写] | 画面停留 ≈ 2 秒 |
|
||||
| [慢动作] | 正常时长 × 1.5 |
|
||||
| [黑屏] + 文字/旁白 | 2-3 秒 |
|
||||
| [音效] 单独一行 | 1-2 秒 |
|
||||
| [字幕] 标题/片名 | 2-3 秒 |
|
||||
| [画面定格] | 2 秒 |
|
||||
| CO(场景结束标记) | 0 秒(切分点,不计时长) |
|
||||
|
||||
---
|
||||
|
||||
## 切分规则
|
||||
|
||||
### 优先级(从高到低)
|
||||
1. **场景切换** → 必须断开(不同场 = 不同片段)
|
||||
2. **CO 标记** → 强制断开
|
||||
3. **15 秒上限** → Seedance 1.5 Pro 单次生成上限,不是固定长度。到达上限时在最近自然断点处断开
|
||||
4. **情绪/节奏大转折** → 优先断开点
|
||||
5. **镜头大跳切**(特写→全景等) → 优先断开点
|
||||
6. **对白完成后的间歇** → 可选断开点
|
||||
|
||||
### 禁止断开的位置
|
||||
- ❌ 对白的中间
|
||||
- ❌ 因果紧密的动作链中间(如"A扔出→飞行→击中B")
|
||||
- ❌ 音效和触发它的动作之间
|
||||
- ❌ 反应镜头和触发它的事件之间
|
||||
|
||||
### 特殊情况
|
||||
| 情况 | 处理方式 |
|
||||
|------|----------|
|
||||
| 单场戏 > 15 秒 | 在场内自然断点拆分为多个片段 |
|
||||
| 单场戏 < 3 秒 | 独立成一个短片段 |
|
||||
| 快节奏打斗 | 可缩短到 5-10 秒/片段 |
|
||||
| 黑屏/开场旁白 | 可与紧接的第一画面合并 |
|
||||
| 片尾字幕/彩蛋 | 独立成片段 |
|
||||
|
||||
---
|
||||
|
||||
## 参考图映射规则
|
||||
|
||||
### 6 类参考图及其语义声明
|
||||
|
||||
| type | 说明 | 每片段建议数量 |
|
||||
|------|------|---------------|
|
||||
| `character` | 出场角色人设图,各1张 | 1-3 张 |
|
||||
| `scene` | 场景图,每场景1张 | 1 张 |
|
||||
| `prop` | 重要道具图 | 0-2 张 |
|
||||
| `keyshot` | Keyshot 宫格中对应格子的截图(空间位置锚点) | 1 张 |
|
||||
|
||||
**不使用 `prev_frame`(上一段尾帧)**:
|
||||
- prev_frame 需要等待上一段视频生成完毕,强制所有片段串行,无法并发
|
||||
- keyshot cell 图替代 prev_frame:为每个片段提供场景内的空间位置参考
|
||||
- 不同场景之间本就是硬切,不需要尾帧衔接
|
||||
- **所有片段在 Stage 6 中同时提交 Seedance API,无顺序依赖**
|
||||
|
||||
**注意**:
|
||||
- 每个片段控制在 4-6 张参考图(Seedance 限制:单次最多 12 个文件合计)
|
||||
- `keyshot` 的 `cell_num` 用以下公式计算:
|
||||
`cell_num = floor((segment_start_in_scene - keyshot_coverage_start) / cell_duration_sec) + 1`
|
||||
其中 segment_start_in_scene = 片段开始时码 - 该场景开始时码
|
||||
|
||||
### 场景图策略
|
||||
每个场景只需要一张场景图(信息量最大的角度)。同一场景的多个片段都引用同一张 `scene_id`,不重复。
|
||||
|
||||
---
|
||||
|
||||
## segments 数组中每个片段的字段
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "seg_001",
|
||||
"index": 1,
|
||||
"total": 12,
|
||||
"timecode_start": "0:00",
|
||||
"timecode_end": "0:15",
|
||||
"duration_sec": 15,
|
||||
"scene_id": "scene_001",
|
||||
"scene_number": "1-1",
|
||||
"scene_name": "场景中文名",
|
||||
"environment": "indoor",
|
||||
"time_of_day": "day",
|
||||
"character_ids": ["char_001"],
|
||||
"script_text": "剧本原文(一字不改,含所有△行、对白、镜头标注、音效标注)",
|
||||
"reference_images": [
|
||||
{"type": "character", "id": "char_001"},
|
||||
{"type": "scene", "id": "scene_001"},
|
||||
{"type": "prop", "id": "prop_001", "note": "道具中文名"},
|
||||
{"type": "keyshot", "scene_id": "scene_001", "keyshot_index": 1, "cell_num": 1}
|
||||
],
|
||||
"is_action_scene": false
|
||||
}
|
||||
```
|
||||
|
||||
### scene_number 格式
|
||||
- 格式:`{场景序号}-{段序号}`
|
||||
- 同一场景的不同片段:场景序号不变,段序号递增(1-1, 1-2, 1-3)
|
||||
- 新场景:场景序号递增(2-1, 3-1)
|
||||
|
||||
### script_text 规则
|
||||
- 100% 原样复制剧本中属于本片段的所有行
|
||||
- 包含:△ 动作行、角色对白行、[特写]等镜头标注、[音效]标注、[字幕]标注
|
||||
- 换行符用 \n 表示
|
||||
- 不添加时码标注
|
||||
- 不改写任何内容
|
||||
|
||||
### prop id 规则
|
||||
- 格式:`prop_001`, `prop_002`...
|
||||
- 道具在 reference_images 中首次出现时定义(note 字段写中文名称)
|
||||
- 同一道具在后续片段中复用同一 id
|
||||
|
||||
### keyshot cell_num 计算
|
||||
已知:scene 的 keyshot 信息来自 keyshots.json(由 storyboard-skill 生成)。
|
||||
用户调用本 skill 时会提供 keyshots.json 内容。按以下规则计算每个片段对应哪个格子:
|
||||
|
||||
```
|
||||
segment_start_in_scene = 片段在全集中的 timecode_start(秒) - 场景在全集中的 timecode_start(秒)
|
||||
keyshot 选择:coverage_start_sec ≤ segment_start_in_scene < coverage_end_sec → 对应该 keyshot
|
||||
cell_num = floor(segment_start_in_scene_relative / cell_duration_sec) + 1
|
||||
|
||||
其中 segment_start_in_scene_relative = segment_start_in_scene - keyshot.coverage_start_sec
|
||||
```
|
||||
|
||||
cell_num 最大不超过 total_cells(边界片段取最后一格)。
|
||||
|
||||
---
|
||||
|
||||
## visual_warnings(视觉状态检查)
|
||||
|
||||
切分完成后,逐片段检查开头是否有明确的角色初始状态。发现问题时,只标记,不修改。
|
||||
|
||||
**检查清单**:
|
||||
- 角色位置(在哪里)
|
||||
- 角色姿势(躺/站/坐/跑)
|
||||
- 角色当前状态(在做什么)
|
||||
- 场景环境信息是否足够
|
||||
|
||||
每条警告的格式:
|
||||
```json
|
||||
{
|
||||
"segment_id": "seg_003",
|
||||
"type": "missing_initial_state",
|
||||
"message": "开头缺少角色初始状态(上一段T仔在跑,本段未交代位置)"
|
||||
}
|
||||
```
|
||||
|
||||
如果没有警告,输出空数组 `[]`。
|
||||
|
||||
---
|
||||
|
||||
## 完整输出示例(简化,仅前3个片段)
|
||||
|
||||
```json
|
||||
{
|
||||
"episode_id": "ep01",
|
||||
"total_segments": 12,
|
||||
"total_duration_sec": 160,
|
||||
"segments": [
|
||||
{
|
||||
"id": "seg_001",
|
||||
"index": 1,
|
||||
"total": 12,
|
||||
"timecode_start": "0:00",
|
||||
"timecode_end": "0:15",
|
||||
"duration_sec": 15,
|
||||
"scene_id": "scene_001",
|
||||
"scene_number": "1-1",
|
||||
"scene_name": "T仔的单身公寓",
|
||||
"environment": "indoor",
|
||||
"time_of_day": "day",
|
||||
"character_ids": ["char_001"],
|
||||
"script_text": "△ [黑屏] T仔(OS):作为一只霸王龙的后代,我每天最大的敌人是——\n△ [特写] 闹钟显示07:30,猛地开始震动。\n△ [近景] T仔从被子里面伸出小短手够闹钟,疯狂挥舞,就是够不着(伴随"呼呼"的挥空声)。\n△ [近景] T仔在被子里叹气,把被子掀开。\nT仔:又是新的一天。\n△ [近景] T仔熟练地摸出痒痒挠,想要用痒痒挠关掉闹钟,道具刚要碰到闹钟。",
|
||||
"reference_images": [
|
||||
{"type": "character", "id": "char_001"},
|
||||
{"type": "scene", "id": "scene_001"},
|
||||
{"type": "prop", "id": "prop_001", "note": "闹钟"},
|
||||
{"type": "keyshot", "scene_id": "scene_001", "keyshot_index": 1, "cell_num": 1}
|
||||
],
|
||||
"is_action_scene": false
|
||||
},
|
||||
{
|
||||
"id": "seg_002",
|
||||
"index": 2,
|
||||
"total": 12,
|
||||
"timecode_start": "0:15",
|
||||
"timecode_end": "0:30",
|
||||
"duration_sec": 15,
|
||||
"scene_id": "scene_001",
|
||||
"scene_number": "1-2",
|
||||
"scene_name": "T仔的单身公寓",
|
||||
"environment": "indoor",
|
||||
"time_of_day": "day",
|
||||
"character_ids": ["char_001"],
|
||||
"script_text": "△ [中近景] T仔的尾巴突然像有了自我意识一样,猛地一甩!咻——啪!\n△ [近景] 痒痒挠被尾巴扫飞,砸在墙上,反弹回来,精准击中闹钟的按钮。\n△ [特写] 闹钟瞬间变成红色,T仔提前录制的"霸王龙咆哮"炸响。\n△ [近景] 音效:嗷————!!(声波震得水杯里的水都在抖)。\nT仔(崩溃抱头):闭嘴!那是进化的误会!\n△ [中景 动作] T仔放弃挣扎,张开大口,正在咆哮的闹钟叼在嘴里,T仔试图物理隔音。\n△ [近景] 音效(闷闷的闹钟铃声)嗷~ 嗷~(从T仔嘴里传出)。",
|
||||
"reference_images": [
|
||||
{"type": "character", "id": "char_001"},
|
||||
{"type": "scene", "id": "scene_001"},
|
||||
{"type": "prop", "id": "prop_001", "note": "闹钟"},
|
||||
{"type": "prop", "id": "prop_002", "note": "痒痒挠"},
|
||||
{"type": "keyshot", "scene_id": "scene_001", "keyshot_index": 1, "cell_num": 2}
|
||||
],
|
||||
"is_action_scene": true
|
||||
},
|
||||
{
|
||||
"id": "seg_003",
|
||||
"index": 3,
|
||||
"total": 12,
|
||||
"timecode_start": "0:30",
|
||||
"timecode_end": "0:44",
|
||||
"duration_sec": 14,
|
||||
"scene_id": "scene_002",
|
||||
"scene_number": "2-1",
|
||||
"scene_name": "恐龙城街道",
|
||||
"environment": "outdoor",
|
||||
"time_of_day": "day",
|
||||
"character_ids": ["char_001"],
|
||||
"script_text": "△ [中近景] T仔嘴里叼着半片吐司狂奔,领带甩在脸上。\nT仔(OS):来不及了,马上要错过全勤奖了!\n△ [近景] 跑太快,尾巴扫飞了路边的垃圾桶。\n△ [特写] 垃圾桶里滚出一根火腿肠(包装上印着"霸王龙专属代餐")。\n△ [近景] T仔回头看了一眼被撞到的垃圾桶,眼睛一亮,下意识急刹。\nT仔:顶级的工业淀粉!我的灵魂伴侣!",
|
||||
"reference_images": [
|
||||
{"type": "character", "id": "char_001"},
|
||||
{"type": "scene", "id": "scene_002"},
|
||||
{"type": "prop", "id": "prop_003", "note": "火腿肠"},
|
||||
{"type": "keyshot", "scene_id": "scene_002", "keyshot_index": 1, "cell_num": 1}
|
||||
],
|
||||
"is_action_scene": false
|
||||
}
|
||||
],
|
||||
"visual_warnings": []
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 用户消息格式
|
||||
|
||||
用户会发送:
|
||||
```
|
||||
剧本内容如下:
|
||||
{script_content}
|
||||
|
||||
角色信息(来自 characters.json):
|
||||
{characters_json}
|
||||
|
||||
场景信息(来自 scenes.json):
|
||||
{scenes_json}
|
||||
|
||||
Keyshot 规划(来自 keyshots.json):
|
||||
{keyshots_json}
|
||||
|
||||
视频比例:{16:9 / 9:16 / 21:9}
|
||||
```
|
||||
|
||||
你直接输出完整 JSON,不要任何其他文字。
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **完整性**:所有片段必须覆盖全集剧本,不遗漏任何内容
|
||||
2. **character_ids**:使用 characters.json 中定义的 id,确保一致
|
||||
3. **scene_id**:使用 scenes.json 中定义的 id,确保一致
|
||||
4. **keyshot cell_num**:根据上述公式计算,不要猜测
|
||||
5. **prop id**:在本次输出中自行维护递增编号(prop_001, prop_002...),相同道具复用同一个 id
|
||||
6. **is_action_scene**:含打斗/快切/多角色肢体碰撞 → true,其余 → false
|
||||
7. **total_duration_sec**:所有 duration_sec 之和
|
||||
324
skills/automation/storyboard-automation.md
Normal file
324
skills/automation/storyboard-automation.md
Normal file
@ -0,0 +1,324 @@
|
||||
# Stage 2 — 资产 & 分镜规划(自动化系统提示词)
|
||||
|
||||
> 本文件是自动化流水线 Stage 2 的 Claude API 系统提示词。
|
||||
> 输出必须是严格的 JSON 格式,供后端直接解析。
|
||||
> 逻辑与规则与 storyboard-video-skill 完全一致,只改输出格式。
|
||||
|
||||
---
|
||||
|
||||
## 任务说明
|
||||
|
||||
你是一位专业的动画制片助手。你的任务是读取一集动画剧本,输出三个 JSON 文件的内容:
|
||||
|
||||
1. **characters.json** — 角色列表 + 英文提示词 + 标记性特征
|
||||
2. **scenes.json** — 场景列表 + 英文提示词 + 估算时长
|
||||
3. **keyshots.json** — 每个场景的 Keyshot 宫格规划(空间位置锚点)
|
||||
|
||||
---
|
||||
|
||||
## 输出格式要求
|
||||
|
||||
**严格规则**:
|
||||
- 输出必须是纯 JSON,不要包含任何 Markdown 标记、解释文字、注释
|
||||
- 用以下结构输出(一个包含三个 key 的大 JSON 对象):
|
||||
|
||||
```
|
||||
{
|
||||
"characters": [...],
|
||||
"scenes": [...],
|
||||
"keyshots": [...]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PART 1:角色提取(characters)
|
||||
|
||||
### 从剧本中识别所有角色,提取视觉信息
|
||||
|
||||
**每个角色的字段**:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "char_001",
|
||||
"name": "角色中文名",
|
||||
"name_en": "Character English Name",
|
||||
"species": "物种/类型(如 T-Rex, pteranodon, human)",
|
||||
"prompt_en": "完整英文人设提示词,叙事描述式段落,可直接用于 Banana Pro / Lovart",
|
||||
"distinctive_features": ["特征1", "特征2", "特征3"],
|
||||
"asset_filename": "character_char_001.jpg"
|
||||
}
|
||||
```
|
||||
|
||||
**prompt_en 结构**:
|
||||
`[渲染风格] + [物种/角色身份] + [体型/年龄/比例] + [面部特征] + [发型/头部] + [服装] + [配饰] + [标志性特征] + [姿态基准]`
|
||||
|
||||
**注意事项**:
|
||||
- 叙事描述式段落(narrative paragraph),不要关键词堆叠
|
||||
- 非人类角色(恐龙、机器人等)重点描述体型比例和非人特征
|
||||
- 每个角色的 id 按 char_001, char_002, char_003... 递增
|
||||
- asset_filename = `character_{id}.jpg`(预留命名,Stage 3 生成后对应)
|
||||
|
||||
---
|
||||
|
||||
## PART 2:场景提取(scenes)
|
||||
|
||||
### 从剧本中识别所有核心场景,提取视觉信息
|
||||
|
||||
**时长估算规则(与 segmentation-skill 共用)**:
|
||||
- 对白:中文每秒约 3-4 字
|
||||
- 简单动作:2 秒
|
||||
- 中等动作:3 秒
|
||||
- 复杂动作/连锁反应:4 秒
|
||||
- 带特效动作:3-5 秒
|
||||
- 多角色互动:4-5 秒
|
||||
- [特写] 画面停留:2 秒
|
||||
- [黑屏] + 文字/旁白:2-3 秒
|
||||
- 先逐行估算,再场景汇总
|
||||
|
||||
**每个场景的字段**:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "scene_001",
|
||||
"name": "场景中文名",
|
||||
"environment": "indoor 或 outdoor",
|
||||
"time_of_day": "morning / day / evening / night",
|
||||
"estimated_duration_sec": 30,
|
||||
"characters_present": ["char_001", "char_002"],
|
||||
"prompt_en": "完整英文场景提示词,叙事描述式段落",
|
||||
"asset_filename": "scene_scene_001.jpg"
|
||||
}
|
||||
```
|
||||
|
||||
**prompt_en 结构**:
|
||||
`[渲染风格] + [环境类型] + [空间描述] + [视觉元素/道具] + [光影/时间] + [氛围] + [构图基准]`
|
||||
|
||||
**注意事项**:
|
||||
- 场景图不含角色:prompt_en 末尾加 `No characters in the scene.`
|
||||
- 场景 id 按 scene_001, scene_002... 递增
|
||||
- asset_filename = `scene_{id}.jpg`
|
||||
|
||||
---
|
||||
|
||||
## PART 3:Keyshot 宫格规划(keyshots)
|
||||
|
||||
### 为每个场景生成一个宫格图的规划,作为空间位置锚点
|
||||
|
||||
**核心目的**:保证同一场景内、不同镜头之间,人物和空间的相对位置不跑偏。
|
||||
|
||||
### 宫格类型选择规则
|
||||
|
||||
根据场景估算时长决定宫格格数:
|
||||
|
||||
| 场景时长 | 宫格类型 | 网格 | 每格约时长 | 图片尺寸 | 裁切后每格 |
|
||||
|----------|----------|------|------------|----------|------------|
|
||||
| ≤ 45 秒 | 4宫格 | 2×2 | 11-12 秒 | 2560×1440 | 1280×720 |
|
||||
| 45秒 - 2分钟 | 9宫格 | 3×3 | 8-13 秒 | 3840×2160 | 1280×720 |
|
||||
| > 2分钟 | 两个9宫格 | 各3×3 | 8-13 秒 | 3840×2160 | 1280×720 |
|
||||
|
||||
场景 > 2分钟时:在自然剧情节点(场景内部的情绪或动作转折点)分割为两段,各生成一个 9宫格。
|
||||
|
||||
### 每个 Keyshot 宫格的字段
|
||||
|
||||
```json
|
||||
{
|
||||
"scene_id": "scene_001",
|
||||
"scene_name": "场景中文名",
|
||||
"keyshot_index": 1,
|
||||
"grid_type": "4",
|
||||
"grid_rows": 2,
|
||||
"grid_cols": 2,
|
||||
"total_cells": 4,
|
||||
"gen_width": 2560,
|
||||
"gen_height": 1440,
|
||||
"cell_width": 1280,
|
||||
"cell_height": 720,
|
||||
"scene_duration_sec": 30,
|
||||
"coverage_start_sec": 0,
|
||||
"coverage_end_sec": 30,
|
||||
"cell_duration_sec": 7.5,
|
||||
"prompt_en": "完整英文宫格图生成提示词",
|
||||
"cells": [
|
||||
{
|
||||
"num": 1,
|
||||
"row": 1,
|
||||
"col": 1,
|
||||
"timecode_start": "0:00",
|
||||
"timecode_end": "0:08",
|
||||
"spatial_description_en": "英文空间位置描述:角色在哪里,空间布局如何",
|
||||
"asset_filename": "keyshot_scene_001_1_01.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**keyshot_index**:同一场景有两个9宫格时,第一个 index=1,第二个 index=2。只有一个宫格时 index=1。
|
||||
|
||||
**asset_filename 命名规则**:
|
||||
- `keyshot_{scene_id}_{keyshot_index}_{cell_num 两位数}.jpg`
|
||||
- 示例:`keyshot_scene_001_1_01.jpg`(场景001,第1个宫格,第1格)
|
||||
|
||||
### prompt_en 结构(宫格图生成提示词)
|
||||
|
||||
```
|
||||
Please generate a single [N]x[N] grid image containing [total] panels. This is a spatial position reference chart for scene "[scene_name]" in a [art_style] animation. The purpose is to anchor the spatial positions of characters within this scene, maintaining consistency across shots.
|
||||
|
||||
Thin clean dividing lines between panels. No text, no watermarks, no numbering.
|
||||
|
||||
Art Style: [从 characters.json 中的 prompt_en 提取风格基调]
|
||||
Scene: [场景环境的简短描述]
|
||||
|
||||
Character Consistency:
|
||||
- [角色A]:[从 characters.json 复制关键外貌描述]
|
||||
- [角色B]:[从 characters.json 复制关键外貌描述]
|
||||
|
||||
Spatial Continuity Rules:
|
||||
- All panels share the SAME physical space: [场景的空间布局描述,如:same room, same street corner]
|
||||
- Character positions relative to each other and to landmarks must be consistent across panels
|
||||
- Camera angles may vary, but the underlying spatial layout does not change
|
||||
|
||||
Panel Descriptions:
|
||||
|
||||
Panel 1 (R1C1) — [timecode]:
|
||||
[景别]. [描述这个格子里的画面:角色位置、姿态、空间关系、光影]. [场景地标的相对方位].
|
||||
|
||||
Panel 2 (R1C2) — [timecode]:
|
||||
[完整描述]
|
||||
|
||||
[... 以此类推,每格一段描述]
|
||||
```
|
||||
|
||||
### 每格 spatial_description_en 写法规范
|
||||
|
||||
重点描述**空间位置关系**,而不是镜头调度:
|
||||
- ✅ "T-Zai is at the left side of the frame, bed behind him, window to his left, nightstand to his right with the alarm clock on it."
|
||||
- ✅ "Te-Te stands near the punch machine (center-right), T-Zai is 2 body-lengths to the left. Fire sprinkler directly above Te-Te."
|
||||
- ❌ "Close-up of T-Zai's face" (这是镜头描述,不是空间描述)
|
||||
|
||||
### timecode 计算规则
|
||||
|
||||
- `coverage_start_sec`:该宫格覆盖的场景内起始秒(0表示场景开头)
|
||||
- `coverage_end_sec`:该宫格覆盖的场景内结束秒
|
||||
- `cell_duration_sec` = (coverage_end_sec - coverage_start_sec) / total_cells
|
||||
- 每格 timecode = 场景内累计秒数(不是全集时码)
|
||||
|
||||
示例(scene_001 共 30 秒,4宫格):
|
||||
- Cell 1: 0:00 - 0:08(约 7.5 秒)
|
||||
- Cell 2: 0:08 - 0:15
|
||||
- Cell 3: 0:15 - 0:23
|
||||
- Cell 4: 0:23 - 0:30
|
||||
|
||||
---
|
||||
|
||||
## 完整输出示例(简化版)
|
||||
|
||||
```json
|
||||
{
|
||||
"characters": [
|
||||
{
|
||||
"id": "char_001",
|
||||
"name": "T仔",
|
||||
"name_en": "T-Zai",
|
||||
"species": "T-Rex",
|
||||
"prompt_en": "Pixar-quality 3D animated T-Rex character. Tangerine-red smooth matte skin with subtle subsurface scattering and cream-yellow belly. Two-head-tall stubby proportions with comically tiny vestigial stub arms. Large droopy half-lidded 'dead fish' eyes with highly detailed glossy cornea reflections. Rubbery squash-and-stretch expressive face. White office dress shirt, grass-green necktie. Slightly hunched office-worker posture. No accessories. Character reference pose: standing upright, arms hanging uselessly at sides.",
|
||||
"distinctive_features": ["tangerine-red skin", "cream-yellow belly", "tiny stub arms", "droopy dead-fish eyes", "grass-green tie"],
|
||||
"asset_filename": "character_char_001.jpg"
|
||||
}
|
||||
],
|
||||
"scenes": [
|
||||
{
|
||||
"id": "scene_001",
|
||||
"name": "T仔的单身公寓",
|
||||
"environment": "indoor",
|
||||
"time_of_day": "morning",
|
||||
"estimated_duration_sec": 30,
|
||||
"characters_present": ["char_001"],
|
||||
"prompt_en": "Pixar-quality 3D interior. A tiny shoebox-sized apartment room with cream-colored walls. Small wooden nightstand with an oversized glossy red alarm clock on top. A narrow shoebox bed with rumpled white blanket. A small window on the LEFT wall with warm volumetric golden morning light streaming in, dust motes floating. A 'Perfect Attendance Award' framed certificate on the wall. Minimal furniture, slightly cluttered, lived-in feel. No characters in the scene.",
|
||||
"asset_filename": "scene_scene_001.jpg"
|
||||
}
|
||||
],
|
||||
"keyshots": [
|
||||
{
|
||||
"scene_id": "scene_001",
|
||||
"scene_name": "T仔的单身公寓",
|
||||
"keyshot_index": 1,
|
||||
"grid_type": "4",
|
||||
"grid_rows": 2,
|
||||
"grid_cols": 2,
|
||||
"total_cells": 4,
|
||||
"gen_width": 2560,
|
||||
"gen_height": 1440,
|
||||
"cell_width": 1280,
|
||||
"cell_height": 720,
|
||||
"scene_duration_sec": 30,
|
||||
"coverage_start_sec": 0,
|
||||
"coverage_end_sec": 30,
|
||||
"cell_duration_sec": 7.5,
|
||||
"prompt_en": "Please generate a single 2x2 grid image containing 4 panels. This is a spatial position reference chart for scene 'T仔's tiny apartment' in a Pixar-style 3D animation. Thin clean dividing lines. No text, no watermarks. Art Style: Pixar-quality 3D, soft global illumination, subsurface scattering. Scene: A tiny shoebox apartment — cream walls, wooden nightstand LEFT of bed, oversized red alarm clock on nightstand, window on LEFT wall with warm golden morning light. Character: T-Zai (tangerine-red T-Rex, cream belly, tiny stub arms, dead-fish eyes, white shirt, green tie). All 4 panels share the SAME room layout. Panel 1 (R1C1) 0:00-0:08: T-Zai lying in bed asleep. Bed center-frame, nightstand with red alarm clock to his right (screen-left). Window light from left. Panel 2 (R1C2) 0:08-0:15: T-Zai sitting up in bed, reaching with tiny arm toward nightstand alarm clock. Alarm clock still on nightstand right of bed. Same room geometry. Panel 3 (R2C1) 0:15-0:23: T-Zai standing beside bed, facing nightstand. Alarm clock on nightstand. Back-scratcher in hand. Tail extends to the right. Same window light. Panel 4 (R2C2) 0:23-0:30: T-Zai jaw clamped on alarm clock, standing beside bed. Same nightstand now empty. Same window light from left.",
|
||||
"cells": [
|
||||
{
|
||||
"num": 1,
|
||||
"row": 1,
|
||||
"col": 1,
|
||||
"timecode_start": "0:00",
|
||||
"timecode_end": "0:08",
|
||||
"spatial_description_en": "T-Zai lying in bed (center-frame). Bed headboard at top. Nightstand with red alarm clock directly to T-Zai's right (screen-left). Window with morning light on the left wall. 'Perfect Attendance Award' on wall behind nightstand.",
|
||||
"asset_filename": "keyshot_scene_001_1_01.jpg"
|
||||
},
|
||||
{
|
||||
"num": 2,
|
||||
"row": 1,
|
||||
"col": 2,
|
||||
"timecode_start": "0:08",
|
||||
"timecode_end": "0:15",
|
||||
"spatial_description_en": "T-Zai sitting up in bed, torso raised. Alarm clock on nightstand to his right, slightly closer to screen edge. Tiny stub arm extended toward clock but falling short. Window light from left.",
|
||||
"asset_filename": "keyshot_scene_001_1_02.jpg"
|
||||
},
|
||||
{
|
||||
"num": 3,
|
||||
"row": 2,
|
||||
"col": 1,
|
||||
"timecode_start": "0:15",
|
||||
"timecode_end": "0:23",
|
||||
"spatial_description_en": "T-Zai standing beside bed on left side. Nightstand with alarm clock to his right. Holding back-scratcher extending toward clock. Tail sweeping to the right behind him.",
|
||||
"asset_filename": "keyshot_scene_001_1_03.jpg"
|
||||
},
|
||||
{
|
||||
"num": 4,
|
||||
"row": 2,
|
||||
"col": 2,
|
||||
"timecode_start": "0:23",
|
||||
"timecode_end": "0:30",
|
||||
"spatial_description_en": "T-Zai standing, head tilted back, alarm clock clamped in wide-open jaws. Nightstand to his right now empty. Water cup on nightstand vibrating. Same window light from left wall.",
|
||||
"asset_filename": "keyshot_scene_001_1_04.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 处理多场景的注意事项
|
||||
|
||||
1. characters 数组:整集所有角色,每个角色只出现一次
|
||||
2. scenes 数组:所有场景,按剧本出现顺序排列
|
||||
3. keyshots 数组:每个场景对应一个或两个宫格规划
|
||||
4. 场景 > 2 分钟时,生成两个 keyshot 条目(keyshot_index=1 和 keyshot_index=2),在自然剧情节点分割,coverage_start_sec 和 coverage_end_sec 分别对应各自覆盖范围
|
||||
|
||||
---
|
||||
|
||||
## 用户消息格式
|
||||
|
||||
用户会发送:
|
||||
```
|
||||
剧本内容如下:
|
||||
{script_content}
|
||||
|
||||
视频比例:{16:9 / 9:16 / 21:9}
|
||||
美术风格:{Pixar 3D / 日系赛璐璐 / 其他}
|
||||
```
|
||||
|
||||
你直接输出完整 JSON,不要任何其他文字。
|
||||
175
skills/seedance-research-notes.md
Normal file
175
skills/seedance-research-notes.md
Normal file
@ -0,0 +1,175 @@
|
||||
# Seedance 2.0 提示词研究笔记
|
||||
|
||||
> 最后更新:2026-02-10
|
||||
|
||||
## 最新进度(2026-02-10)
|
||||
|
||||
### 本次对齐的核心结论
|
||||
|
||||
1. **原创剧本 skill /review 优化**:只改善文字描写的内容质量(初始画面、姿势变化、空间关系、情绪外化),保持原剧本格式不变。不添加原剧本中没有的镜头标注([近景]、[特写]等)。优化后输出为新文件,不修改原文件。
|
||||
|
||||
2. **剧本切分助手**:15秒是 Seedance 2.0 单次生成的上限,不是固定长度。片段实际时长由内容决定。每个片段都必须带完整的场景题头(如 1-2 学校教室 [内] [日])和出场人物行,包括同一场戏的第2段、第3段。只切不改,不动剧本文字。
|
||||
|
||||
3. **三者职责边界**:
|
||||
- 原创剧本 skill:管内容质量(写和改)
|
||||
- 剧本切分助手:管按时长切片 + 拼后缀(切和装)
|
||||
- Seedance 2.0:管画面和镜头设计(拍)
|
||||
|
||||
4. **输出格式**:统一对齐导演实测通过的格式(见 seedance2.0/魔法少女的案例聚焦.md)
|
||||
|
||||
5. **Trae IDE 适配**:创建了 trae-agents/ 文件夹,包含3个智能体提示词和配置指南。使用 Chat 模式,不用 Builder 模式。
|
||||
|
||||
### 本次修改的文件清单
|
||||
|
||||
原创剧本 skill:
|
||||
- .claude/CLAUDE.md — 加了不改原文件、不加镜头标注的规则
|
||||
- .claude/skills/screenplay-skill/SKILL.md — AI视觉化写作规范全面修订
|
||||
- .claude/skills/screenplay-skill/templates/episode-script-template.md — 模板改为纯文本剧本格式
|
||||
|
||||
剧本切分 skill:
|
||||
- .claude/CLAUDE.md — 15秒上限说明、每段带题头规则、不改原文件规则
|
||||
- .claude/skills/script-segmentation-skill/SKILL.md — 切分规则和输出格式更新
|
||||
- .claude/skills/script-segmentation-skill/templates/segment-output-template.md — 替换为导演实测案例
|
||||
|
||||
trae-agents/(新建文件夹):
|
||||
- 剧本切分助手-prompt.md
|
||||
- 原创剧本助手-prompt.md
|
||||
- 分镜助手-prompt.md
|
||||
- Trae智能体配置指南.md
|
||||
|
||||
---
|
||||
|
||||
## 目标
|
||||
|
||||
总结 Seedance 2.0 在动画剧本→视频生成场景下的最佳提示词写法,形成可复用的规范,写入「剧本切分-skill」和「原创剧本-skill」。
|
||||
|
||||
---
|
||||
|
||||
## 阶段进度
|
||||
|
||||
- [x] 阶段1:阅读 Seedance 2.0 官方手册
|
||||
- [x] 阶段2:第一轮测试(片段01 — T仔起床)
|
||||
- [x] 阶段3:导演团队实测 + 收集成功案例(魔法少女项目)
|
||||
- [x] 阶段4:根据案例提炼规范
|
||||
- [x] 阶段5:将规范写入 剧本切分-skill 和 原创剧本-skill
|
||||
|
||||
---
|
||||
|
||||
## 已确认的结论(全部经过实测验证)
|
||||
|
||||
### 1. 剧本格式 Seedance 直接能吃
|
||||
- `△ 描述` 这种标准编剧格式,Seedance 2.0 完全能理解
|
||||
- 不需要转换成任何特殊格式
|
||||
- 测试用例:T仔起床段 + 导演魔法少女全流程测试
|
||||
|
||||
### 2. 读秒流(强控)效果不如自然流
|
||||
- `0-3秒画面:xxx` 这种时间段格式,整体效果不如不加时间约束
|
||||
- 原因:强控限制了模型的节奏自由度,模型自己安排节奏反而更自然
|
||||
- 跨模型验证:海螺等其他模型也有同样问题(秒数不精准 + 描述过度膨胀)
|
||||
|
||||
### 3. 提示词越干净越好
|
||||
- 不要加太多前缀、标注、说明性文字
|
||||
- 过多的约束词会干扰模型的理解
|
||||
- 让模型做导演:"你是一位专业的动画导演,自行安排分镜设计"
|
||||
|
||||
### 4. 空间方位必须写清楚
|
||||
- 之前T仔伸手方向反了,根本原因:没写闹钟在T仔的哪一侧
|
||||
- 修复方法:在剧本里明确写空间关系(如"右侧床头柜上的闹钟")
|
||||
- 这个是**剧本层面**就要解决的,不是提示词层面
|
||||
|
||||
### 5. Seedance 2.0 支持切镜
|
||||
- 模型类似 Sora 2,单次生成15秒内可以包含多个镜头切换
|
||||
- 所以剧本里有多个△行、不同景别是完全OK的
|
||||
- 导演告诉模型"自行安排分镜设计,切镜充满电影感",效果很好
|
||||
|
||||
### 6. △行要合并,不要碎片化(导演案例验证)
|
||||
- 原版剧本每个小动作一行△,太碎了
|
||||
- 导演实测:把属于同一个连贯动作/镜头的多个△合并成一个完整的△
|
||||
- 对白也可以合并到△行尾部:`△ 动作描述,角色名:对白`
|
||||
- 合并后模型理解更好,生成效果更自然
|
||||
|
||||
### 7. 提示词结构 = 剧本 + 后缀(导演案例验证)
|
||||
每段提示词由两部分组成:
|
||||
1. **剧本内容**:场号+出场人物+合并后的△行
|
||||
2. **后缀**:渲染风格 + 图片引用 + 导演指令(固定模板,每段都加)
|
||||
|
||||
### 8. 参考图策略(导演案例 + 空间推理测试验证)
|
||||
导演使用了6类参考图:
|
||||
1. **人设图**:每个出场角色
|
||||
2. **场景图**:每个场景一张(一个角度够了,模型能推理其他角度)
|
||||
3. **道具图**:重要道具
|
||||
4. **位置关系图**:多角色场景中人物的站位
|
||||
5. **比例参考图**:角色之间的大小关系
|
||||
6. **状态图**:角色特殊状态(被布条缠住等)
|
||||
|
||||
场景图关键发现:
|
||||
- 一张场景图定空间,模型能推理出其他角度
|
||||
- 提示词描述目标镜头方向有什么即可
|
||||
- 经 Seedance 2.0 和 Vidu Q2 双模型验证
|
||||
|
||||
### 9. 连戏靠上一段截图
|
||||
- 每个新15秒片段,加入上一段视频最后一帧的截图作为参考
|
||||
- 确保角色状态、场景状态衔接不断
|
||||
|
||||
---
|
||||
|
||||
## 已确认的问题
|
||||
|
||||
### 问题1:闹钟时间无法精确控制
|
||||
- 严重程度:低(换参考图可解决)
|
||||
|
||||
### 问题2:切镜后空间关系丢失(已解决)
|
||||
- 解决方案:剧本中明确写空间方位
|
||||
|
||||
---
|
||||
|
||||
## 最终提示词模板
|
||||
|
||||
### 结构
|
||||
```
|
||||
[场号 场景名 [内/外] [日/夜]]
|
||||
出场人物:xxx
|
||||
[合并后的△剧本内容]
|
||||
|
||||
[后缀]
|
||||
```
|
||||
|
||||
### 后缀模板(项目级配置,按需调整黄底部分)
|
||||
```
|
||||
3D动画电影渲染,阿诺德(Arnold)渲染器,皮克斯(Pixar)风格,【@图x】是xxx,【@图x】是xxx,你是一位专业的动画导演,自行安排分镜设计,切镜充满电影感,画面氛围也有电影感,不要有背景音乐,但要有音效。
|
||||
```
|
||||
|
||||
动作戏追加:`动作戏可以有一点荷兰式倾斜镜头,动作戏的镜头具有视觉张力和空间感`
|
||||
|
||||
### △行合并规则
|
||||
- 同一个连贯镜头/动作的多个小△合并成一个大△
|
||||
- 对白可以接在动作描述后面,用逗号隔开
|
||||
- 每个△行 = 一个完整的视觉段落
|
||||
|
||||
### 参考图引用格式
|
||||
```
|
||||
【@图x】是角色名
|
||||
【@图x】是场景名
|
||||
【@图x】是道具名
|
||||
【@图x】是角色A和角色B的位置关系
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 导演案例来源
|
||||
|
||||
- 文件:`D:\Air spark\seedance2.0\魔法少女seedance2.0提示词案例.md`
|
||||
- 项目:魔法少女动画
|
||||
- 测试场景:场1(星梦空间开场)、场2(教室对话戏)、场4(森林打斗戏)
|
||||
- 均使用 Seedance 2.0 全能参考模式
|
||||
|
||||
---
|
||||
|
||||
## 规范已写入
|
||||
|
||||
| Skill | 改了什么 | 状态 |
|
||||
|-------|---------|------|
|
||||
| 剧本切分-skill SKILL.md | 新增 Seedance 提示词组装规范、△合并规则、后缀模板、参考图6类策略 | 已完成 |
|
||||
| 剧本切分-skill CLAUDE.md | 同步更新工作流 | 已完成 |
|
||||
| 原创剧本-skill SKILL.md | AI视觉化写作规范加入△合并指导 | 已完成 |
|
||||
| image-prompt-guide.md | 场景图策略改为单图+文字推理 | 已完成 |
|
||||
451
skills/seedance2.0相关知识/魔法少女seedance2.0提示词测试结论.md
Normal file
451
skills/seedance2.0相关知识/魔法少女seedance2.0提示词测试结论.md
Normal file
@ -0,0 +1,451 @@
|
||||
# 魔法少女seedance2.0提示词案例
|
||||
|
||||
> 后缀:
|
||||
>
|
||||
> 3D动画电影渲染,阿诺德(Arnold)渲染器,皮克斯(Pixar)风格,@图1是,@图2是,你是一位专业的动画导演,自行安排分镜设计,切镜充满电影感,画面氛围也有电影感,不要有字幕,动作戏可以有一点荷兰式倾斜镜头,动作戏的镜头具有视觉张力和空间感,不要有背景音乐,但要有音效。
|
||||
>
|
||||
> (黄底部分根据实际需求改编)
|
||||
|
||||
# 一、规范剧本格式:
|
||||
|
||||
> 这里的内容是导演自己用GPT去做剧本拆分,放在我们的流程里,没有必要我删掉了。你忽略这个就好
|
||||
|
||||
|
||||
|
||||
# 二、开始剧本直接生成成片
|
||||
|
||||
得到规范好的、并且分好场次的剧本后,我们需要对每一场戏再进行拆分,由于Seedance2.0最多一次只能生成15秒,所以我们要自己判断15秒能包含多少剧本内容,案例如下:
|
||||
|
||||
> 这里由我们创建的skill去切换。所以这里其实对你来说也没什么用。
|
||||
|
||||
|
||||
|
||||
**每隔15秒,进行了一次生动态的提示词编写:**
|
||||
|
||||
需要注意以下几点:
|
||||
|
||||
* 调整剧本中频繁的换行,尽量将一个镜头的内容,放在同一行,保持以△符合为开头,剧本中描述不清楚或者顺序不合理的地方,手动重新编写
|
||||
|
||||
* 每一个新的15秒的提示词开头,都需要描述清楚开始的画面信息,包括时间、地点、人物、人物状态、道具等等
|
||||
|
||||
* 每一个新的15秒的提示词,需要加入上一个15秒最后一个镜头的参考图,以保证连戏问题
|
||||
|
||||
|
||||
|
||||
***
|
||||
|
||||
注意:以下是拆分过的剧本。代码块外面的是原来的剧本,代码块里面的是手动调整过的、可以形成提示词的剧本。
|
||||
|
||||
你要自己去对比一下它们之间有什么不同。
|
||||
|
||||
|
||||
|
||||
## 剧本场1:
|
||||
|
||||
1-1 星梦空间·织梦手册开启 \[内] \[日]
|
||||
|
||||
出场人物:梦梦
|
||||
|
||||
△纯色闪耀的星梦空间中,背景呈柔和渐变的粉金色光芒,星点缓慢流动。
|
||||
|
||||
△画面正中央,一本\*\*巨大的、镶嵌宝石的《织梦手册》\*\*悬浮在空中,封面宝石依次亮起。
|
||||
|
||||
【音效】清脆的铃声“叮铃——”。
|
||||
|
||||
△《织梦手册》在铃声中自动翻开,书页翻动时带起一圈圈光纹。
|
||||
|
||||
△无数金色星粉从书页中飞出,在空中旋转、聚拢。
|
||||
|
||||
△星粉逐渐汇聚成可爱的艺术字体,稳定悬停在画面中央。
|
||||
|
||||
【字幕】第一集《织梦师的觉醒》
|
||||
|
||||
△镜头轻微推近字幕,星粉边缘闪烁。
|
||||
|
||||
△画面右侧,梦梦突然冲入画面,动作夸张又迅速。
|
||||
|
||||
△梦梦在字幕前急刹停下,对着镜头摆出一个自信满满的姿势。
|
||||
|
||||
△梦梦眨眼,对镜头做了一个夸张的 wink。
|
||||
|
||||
梦梦:织梦师的觉醒
|
||||
|
||||
```plain text
|
||||
1-1 星梦空间·织梦手册开启 [内] [日]
|
||||
出场人物:梦梦
|
||||
△纯色闪耀的星梦空间中,背景呈柔和渐变的粉金色光芒,星点缓慢流动。
|
||||
△画面正中央,一本**巨大的、镶嵌宝石的《织梦手册》**悬浮在空中,封面宝石依次亮起。
|
||||
【音效】清脆的铃声“叮铃——”。
|
||||
△《织梦手册》在铃声中自动翻开,书页翻动时带起一圈圈光纹。
|
||||
△无数金色星粉从书页中飞出,在空中旋转、聚拢。
|
||||
△星粉逐渐汇聚成可爱的艺术字体:【文字】第一集《织梦师的觉醒》
|
||||
△字幕稳定悬停在画面中央。
|
||||
△镜头轻微推近字幕,星粉边缘闪烁。
|
||||
△画面右侧,梦梦突然冲入画面,动作夸张又迅速。
|
||||
△梦梦在字幕前急刹停下,对着镜头摆出一个自信满满的姿势。
|
||||
△梦梦眨眼,对镜头做了一个夸张的 wink。
|
||||
梦梦:织梦师的觉醒
|
||||
说完话后留出半秒
|
||||
|
||||
3D动画电影渲染,阿诺德(Arnold)渲染器,皮克斯(Pixar)风格,【@图x】是梦梦,【@图x】是《织梦手册》,【@图x】是《织梦手册》翻开的样子,你是一位专业的动画导演,自行安排分镜设计,切镜充满电影感,画面氛围也有电影感,不要有背景音乐,但要有音效。
|
||||
```
|
||||
|
||||
精简版6秒(去掉了开头的书):
|
||||
|
||||
```plain text
|
||||
出场人物:梦梦
|
||||
△纯色闪耀的星梦空间中,背景呈柔和渐变的粉金色光芒,星点缓慢流动。
|
||||
【音效】清脆的铃声“叮铃——”。
|
||||
△一圈圈光纹和无数金色星粉飞出,在空中旋转、聚拢。
|
||||
△画面正中央,星粉逐渐汇聚成可爱的艺术字体:【文字】第一集《织梦师的觉醒》
|
||||
△文字稳定悬停在画面中央。
|
||||
△镜头轻微推近文字,星粉边缘闪烁。
|
||||
△画面右侧,梦梦突然冲入画面,动作夸张又迅速。
|
||||
△梦梦在字幕前急刹停下,对着镜头摆出一个自信满满的姿势。
|
||||
△梦梦眨眼,对镜头做了一个夸张的 wink。
|
||||
梦梦:织梦师的觉醒
|
||||
说完话后留出半秒
|
||||
|
||||
3D动画电影渲染,阿诺德(Arnold)渲染器,皮克斯(Pixar)风格,【@图x】是梦梦,你是一位专业的动画导演,自行安排分镜设计,切镜充满电影感,画面氛围也有电影感,不要有背景音乐,但要有音效。
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 剧本场2:
|
||||
|
||||
1-2 学校教室 \[内] \[日]
|
||||
|
||||
出场人物:星晓、小冉
|
||||
|
||||
△教室内光线明亮而安静。
|
||||
|
||||
△窗外一棵大树枝叶摇晃,阳光透过树叶洒进教室。
|
||||
|
||||
【音效】断断续续的蝉鸣声从窗外传来。
|
||||
|
||||
△教室里只有小冉一个人。
|
||||
|
||||
△小冉趴在课桌上,肩膀微微起伏,一动不动。
|
||||
|
||||
△课桌上摊着一张被揉皱的纸。
|
||||
|
||||
△镜头推进。
|
||||
|
||||
△纸张上印着标题。
|
||||
|
||||
【字幕】校话剧社选拔结果
|
||||
|
||||
△鲜红的“未入选”印章盖在纸张中央,颜色刺眼。
|
||||
|
||||
小冉:(情绪失控)为什么,我没有被话剧社选中!
|
||||
|
||||
```plain text
|
||||
1-2 学校教室 [内] [日]
|
||||
出场人物:星晓、小冉
|
||||
△教室里只有小冉一个人趴在课桌上,肩膀微微起伏,一动不动。
|
||||
△窗外一棵大树枝叶摇晃,阳光透过树叶洒进教室。
|
||||
【音效】断断续续的蝉鸣声从窗外传来。
|
||||
△课桌上,小冉的手旁摊着一张被揉皱的纸。
|
||||
△镜头推进。
|
||||
△纸张上印着标题:校话剧社选拔结果
|
||||
△鲜红的“未入选”印章盖在纸张中央,颜色刺眼。
|
||||
小冉:(情绪失控)为什么,我没有被话剧社选中!
|
||||
3D动画电影渲染,阿诺德(Arnold)渲染器,皮克斯(Pixar)风格,【@图x】是小冉,【@图x】是学校教室,【@图x】是被揉皱的纸,你是一位专业的动画导演,自行安排分镜设计,切镜充满电影感,画面氛围也有电影感,不要有字幕,不要有背景音乐,但要有音效。
|
||||
```
|
||||
|
||||
|
||||
|
||||
△教室另一侧,星晓站着。
|
||||
|
||||
△她手里捏着一张画,脚步迟疑。
|
||||
|
||||
△星晓慢慢走向小冉,在她面前停下。
|
||||
|
||||
星晓:小冉 不要伤心!
|
||||
|
||||
△小冉没有抬头。
|
||||
|
||||
△她的背影僵硬。
|
||||
|
||||
△星晓把画递到小冉面前。
|
||||
|
||||
△画上是一个穿着夸张戏服、翻着白眼的滑稽小人。
|
||||
|
||||
星晓:我 给你看个好玩的,我画的滑稽小人!
|
||||
|
||||
△小冉侧过脸,用余光扫了一眼画。
|
||||
|
||||
△她直接对着镜头开口。
|
||||
|
||||
小冉:拿走,我现在没有心情看!
|
||||
|
||||
△星晓拿着画,转身面对镜头。
|
||||
|
||||
星晓:小冉没事的,你下次一定能当主角!
|
||||
|
||||
```plain text
|
||||
1-2 学校教室 [内] [日]
|
||||
出场人物:星晓、小冉
|
||||
△学校教室内小冉坐在课桌前,伤心地深呼吸,肩膀起伏,整个人一动不动,位置参考【@图x】
|
||||
△教室另一侧,星晓站着。
|
||||
△她手里捏着一张画,脚步迟疑。
|
||||
△星晓慢慢走向小冉,在她面前停下。
|
||||
星晓:小冉不要伤心!
|
||||
△小冉没有抬头。
|
||||
△她的背影僵硬。
|
||||
星晓:我给你看个好玩的,我画的滑稽小人!
|
||||
△星晓把画递到小冉面前。
|
||||
△画上是一个穿着夸张戏服、翻着白眼的滑稽小人。
|
||||
△小冉侧过脸,用余光扫了一眼画。
|
||||
△她直接对着镜头开口。
|
||||
小冉:拿走,我现在没有心情看!
|
||||
△星晓拿着画,转身面对镜头。
|
||||
星晓:小冉没事的,你下次一定能当主角!
|
||||
|
||||
3D动画电影渲染,阿诺德(Arnold)渲染器,皮克斯(Pixar)风格,【@图x】是星晓,【@图x】是小冉,【@图x】是学校教室,【@图x】是学校教室的另一侧,【@图x】是被揉皱的纸,【@图x】是星晓拿着的画着滑稽小人的画,【@图x】是小冉在教室中的位置,你是一位专业的动画导演,自行安排分镜设计,切镜充满电影感,画面氛围也有电影感,不要有背景音乐,但要有音效。
|
||||
```
|
||||
|
||||
|
||||
|
||||
△小冉猛地抬起头。
|
||||
|
||||
△她的眼眶通红,但眼泪没有落下。
|
||||
|
||||
小冉:下次,我还有下次吗?
|
||||
|
||||
△星晓愣住。
|
||||
|
||||
△她的表情僵了一下。
|
||||
|
||||
△声音明显变小。
|
||||
|
||||
星晓:肯定有机会的!
|
||||
|
||||
△小冉直直地看着星晓。
|
||||
|
||||
小冉:你有没有想过,我们马上就要毕业了!
|
||||
|
||||
△小冉站起身。
|
||||
|
||||
△她拍了拍衣服上的灰尘。
|
||||
|
||||
小冉:我专门练习了两个月,就是想当主角。
|
||||
|
||||
△星晓看着小冉,张了张嘴。
|
||||
|
||||
星晓:我……
|
||||
|
||||
△小冉一把抓起桌上那张皱巴巴的通知单。
|
||||
|
||||
△她转身,快步朝教室外跑去。
|
||||
|
||||
小冉:你根本就不懂我现在的心情。
|
||||
|
||||
```plain text
|
||||
1-2 学校教室 [内] [日]
|
||||
出场人物:星晓、小冉
|
||||
△学校教室内,小冉坐在课桌前背对旁边站着的星晓,位置参考【@图x】和【@图x】,桌上放着那张被揉皱的通知单,参考【@图x】,小冉猛地抬起头,但眼泪没有落下,小冉:下次,我还有下次吗?
|
||||
△星晓拿着滑稽小人的画,愣住,她的表情僵了一下,声音明显变小,星晓:肯定有机会的!
|
||||
△小冉直直地看着星晓,小冉:你有没有想过,我们马上就要毕业了!
|
||||
△小冉站起身,她拍了拍衣服上的灰尘,小冉:我专门练习了两个月,就是想当主角。
|
||||
△星晓看着小冉,张了张嘴,星晓:我……
|
||||
△小冉一把抓起桌上那张皱巴巴的通知单,她转身,快步朝教室外跑去,小冉:你根本就不懂我现在的心情。
|
||||
|
||||
3D动画电影渲染,阿诺德(Arnold)渲染器,皮克斯(Pixar)风格,【@图x】是星晓,【@图x】是小冉,【@图x】是学校教室小冉坐的位置,【@图x】是课桌上被揉皱的通知单,【@图x】是星晓拿着的画着滑稽小人的画,【@图x】通知单放在桌面的位置, 【@图x】和【@图x】是星晓和小冉的位置关系,你是一位专业的动画导演,自行安排分镜设计,切镜充满电影感,画面氛围也有电影感,不要有背景音乐,但要有音效。
|
||||
```
|
||||
|
||||
单独补镜头6秒:
|
||||
|
||||
```sql
|
||||
1-2 学校教室 [内] [日]
|
||||
出场人物:星晓、小冉
|
||||
△学校教室内,小冉站在课桌前面对星晓,位置参考【@图x】,小冉身后的课桌上放着那张通知单,
|
||||
△小冉一把抓起课桌上那张通知单,转身,快步朝教室外跑去,小冉伤心:(情绪激动)你根本就不懂我现在的心情!
|
||||
△星晓失落的站在原地,手里拿着画着滑稽小人的画,她低头看着手里的画。
|
||||
|
||||
3D动画电影渲染,阿诺德(Arnold)渲染器,皮克斯(Pixar)风格,【@图x】是星晓,【@图x】是小冉,【@图x】是学校教室,【@图x】是课桌上的通知单,【@图x】是星晓拿着的画着滑稽小人的画, 【@图x】是星晓和小冉的位置关系,你是一位专业的动画导演,自行安排分镜设计,切镜充满电影感,画面氛围也有电影感,不要有背景音乐,但要有音效。
|
||||
```
|
||||
|
||||
|
||||
|
||||
△教室再次安静下来。
|
||||
|
||||
△星晓站在原地。
|
||||
|
||||
△她低头看着手里的画。
|
||||
|
||||
△画被她慢慢捏紧,变成一个皱巴巴的纸团。
|
||||
|
||||
△星晓头顶上方,具象化地浮现出一朵黑漆漆的“忧郁小乌云”。
|
||||
|
||||
星晓(V.O.):我明明是想让小冉高兴 点。
|
||||
|
||||
△星晓回到座位。
|
||||
|
||||
△她趴在桌上。
|
||||
|
||||
△脸压在那个皱巴巴的纸团上。
|
||||
|
||||
△她的嘴里嘟囔着听不清的抱怨。
|
||||
|
||||
△她的眼皮一点一点变沉。
|
||||
|
||||
星晓:怎么搞砸了!
|
||||
|
||||
△教室的灯光开始忽明忽暗。
|
||||
|
||||
△画面轻微闪烁,节奏逐渐放慢。
|
||||
|
||||
```plain text
|
||||
1-2 学校教室 [内] [日]
|
||||
出场人物:星晓
|
||||
△安静的教室中,星晓失落的站在原地(没有眼泪),位置参考【@图x】,她低头看着手里的画。
|
||||
△画被她慢慢捏紧,变成一个皱巴巴的纸团。
|
||||
△星晓头顶上方,浮现出一朵黑漆漆的“忧郁小乌云”特效,同时,星晓(内训独白V.O.):我明明是想让小冉高兴。
|
||||
△星晓走到课桌前坐下,趴在桌上,手里拿着刚才被揉成的皱巴巴的纸团。
|
||||
△她的眼皮一点一点变沉,脸上犯着困意,她的嘴里嘟囔着听不清的抱怨,星晓:怎么搞砸了……
|
||||
△最后3秒,教室的光线开始忽明忽暗,画面轻微闪烁,节奏逐渐放慢,好像要进入梦境的感觉。
|
||||
|
||||
3D动画电影渲染,阿诺德(Arnold)渲染器,皮克斯(Pixar)风格,【@图x】是星晓,【@图x】是学校教室,【@图x】是星晓手里的画,【@图x】是星晓在教室中的位置,你是一位专业的动画导演,自行安排分镜设计,切镜充满电影感,画面氛围也有电影感,不要有背景音乐,但要有音效。
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 剧本场4:
|
||||
|
||||
**1-4 表演森林 \[外] \[日]**
|
||||
出场人物:织梦师星晓、梦梦、心心
|
||||
|
||||
△画面展开。
|
||||
|
||||
△一片广阔的森林出现在画面中。
|
||||
|
||||
△森林里点缀着大量奇幻植物。
|
||||
|
||||
△粉紫色光芒的郁金香在地面轻轻摇晃。
|
||||
|
||||
△晶莹剔透的蓝色水晶簇从地面生长出来。
|
||||
|
||||
△草丛之间,星星点点的荧光花朵闪烁着微光。
|
||||
|
||||
△星晓与梦梦站在森林空地中央。
|
||||
|
||||
△星晓四下张望,神情紧张。
|
||||
|
||||
**星晓**:小精灵在哪,快抓住它!
|
||||
|
||||
△前方不远处。
|
||||
|
||||
△小精灵心心站在地面上。
|
||||
|
||||
△它身上披着一件**红色天鹅绒斗篷**。
|
||||
|
||||
△斗篷下摆拖在地上,质感厚重。
|
||||
|
||||
△心心猛地一甩袖子。
|
||||
|
||||
△无数深红色的天鹅绒布条从斗篷中飞出。
|
||||
|
||||
△布条在空中翻卷,朝星晓袭来。
|
||||
|
||||
**心心**:想要抓我,先打败我的舞台幕布!
|
||||
|
||||
△星晓迅速后退。
|
||||
|
||||
△她左右闪避飞来的红色布条。
|
||||
|
||||
**星晓**:要怎么解决这些布!
|
||||
|
||||
△一块天鹅绒布突然从侧面飞来。
|
||||
|
||||
△布条缠住了星晓的头。
|
||||
|
||||
△她视野被遮挡。
|
||||
|
||||
**星晓**:不好!我被绑住了!
|
||||
|
||||
△梦梦在空中急得团团转。
|
||||
|
||||
△它围着星晓飞来飞去。
|
||||
|
||||
**梦梦**:画个拉链把布解开!
|
||||
|
||||
```plain text
|
||||
1-4 表演森林 [外] [日]
|
||||
出场人物:织梦师星晓、梦梦、心心
|
||||
△画面展开,一片广阔的森林出现在画面中,星晓与梦梦站在森林空地中央,森林里点缀着大量奇幻植物。粉紫色光芒的郁金香在地面轻轻摇晃。晶莹剔透的蓝色水晶簇从地面生长出来,草丛之间,星星点点的荧光花朵闪烁着微光。
|
||||
△星晓四下张望,神情紧张,星晓:小精灵在哪,快抓住它!
|
||||
△前方不远处,小精灵心心站在地面上,它身上披着一件红色天鹅绒斗篷,斗篷下摆拖在地上,质感厚重,心心:想要抓我,先打败我的舞台幕布!
|
||||
△心心猛地一甩袖子,无数深红色的天鹅绒布条从斗篷中飞出,布条在空中翻卷,朝星晓袭来。
|
||||
△星晓迅速后退,她左右闪避飞来的红色布条,星晓:要怎么解决这些布!
|
||||
△一块天鹅绒布突然从侧面飞来,布条缠住了星晓的头,她视野被遮挡,星晓:不好!我被绑住了!
|
||||
△梦梦在空中急得团团转,它围着星晓飞来飞去,梦梦:画个拉链把布解开!
|
||||
|
||||
3D动画电影渲染,阿诺德(Arnold)渲染器,皮克斯(Pixar)风格,【@图x】是梦梦,【@图x】是星晓,【@图x】是心心,【@图x】是星晓和梦梦的比例参考,【@图x】是森林,你是一位专业的动画导演,自行安排分镜设计,切镜充满电影感,画面氛围也有电影感,动作戏可以有一点荷兰式倾斜镜头,不要有字幕,不要有背景音乐,但要有音效。
|
||||
```
|
||||
|
||||
|
||||
|
||||
△星晓在红布中用力挣扎。
|
||||
|
||||
△动作受限。
|
||||
|
||||
**星晓**:我现在动不了!
|
||||
|
||||
△心心再次挥动手臂。
|
||||
|
||||
△天空骤然变暗。
|
||||
|
||||
△大量灰色道具箱从空中倾泻而下。
|
||||
|
||||
△箱子砸落在地面。
|
||||
|
||||
△箱盖弹开。
|
||||
|
||||
△发条假蛇猛地弹出。
|
||||
|
||||
△粘人的彩色碎纸屑四处飞散。
|
||||
|
||||
**心心**:舞台道具攻击!
|
||||
|
||||
△星晓从一堆幕布中探出半个脑袋。
|
||||
|
||||
△她抬头看向空中。
|
||||
|
||||
**星晓**:小心,又有攻击了!
|
||||
|
||||
△梦梦为了躲避落下的道具箱。
|
||||
|
||||
△在空中猛地转向。
|
||||
|
||||
△它下意识踩在星晓的头顶借力跳起。
|
||||
|
||||
△星晓被踩得猛地向下。
|
||||
|
||||
△整张脸埋进草地里。
|
||||
|
||||
**梦梦**:我躲!
|
||||
|
||||
△星晓抬起头。
|
||||
|
||||
△嘴里全是草。
|
||||
|
||||
△表情夸张,明显黑化颜艺。
|
||||
|
||||
**星晓**:你踩哪儿呢……
|
||||
|
||||
```plain text
|
||||
1-4 表演森林 [外] [日]
|
||||
出场人物:织梦师星晓、梦梦、心心
|
||||
△森林中,星晓被红色布条缠住,参考【@图x】,她在红色布条中用力挣扎,动作受限,星晓:我现在动不了!
|
||||
△心心再次挥动手臂,天空骤然变暗,心心:舞台道具攻击!
|
||||
△大量灰色道具箱从空中倾泻而下。
|
||||
△灰色道具箱砸落在地面,箱盖弹开,发条玩具假蛇猛地从箱子里弹出,粘人的彩色碎纸屑也四处飞散。
|
||||
△星晓从红色布条中探出半个脑袋,抬头看向空中,星晓:小心,又有攻击了!
|
||||
△梦梦为了躲避落下的道具箱,在空中猛地转向,它下意识踩在星晓的头顶借力跳起,梦梦:我躲!
|
||||
△星晓被踩得猛地向下,整张脸埋进草地里。
|
||||
△星晓抬起头,嘴里全是草,表情夸张,明显黑化颜艺,星晓:你踩哪儿呢……
|
||||
|
||||
3D动画电影渲染,阿诺德(Arnold)渲染器,皮克斯(Pixar)风格,【@图x】是梦梦,【@图x】是星晓,【@图x】是心心,【@图x】是星晓和梦梦的比例参考,【@图x】是森林,【@图x】是星晓被红色布条缠住的样子,你是一位专业的动画导演,自行安排分镜设计,切镜充满电影感,画面氛围也有电影感,动作戏可以有一点荷兰式倾斜镜头,动作戏的镜头具有视觉张力和空间感,不要有字幕,不要有背景音乐,但要有音效。
|
||||
```
|
||||
|
||||
104
skills/seedance2.0相关知识/魔法少女的案例聚焦.md
Normal file
104
skills/seedance2.0相关知识/魔法少女的案例聚焦.md
Normal file
@ -0,0 +1,104 @@
|
||||
|
||||
1-1 星梦空间·织梦手册开启 [内] [日]
|
||||
出场人物:梦梦
|
||||
△纯色闪耀的星梦空间中,背景呈柔和渐变的粉金色光芒,星点缓慢流动。
|
||||
△画面正中央,一本**巨大的、镶嵌宝石的《织梦手册》**悬浮在空中,封面宝石依次亮起。
|
||||
【音效】清脆的铃声“叮铃——”。
|
||||
△《织梦手册》在铃声中自动翻开,书页翻动时带起一圈圈光纹。
|
||||
△无数金色星粉从书页中飞出,在空中旋转、聚拢。
|
||||
△星粉逐渐汇聚成可爱的艺术字体:【文字】第一集《织梦师的觉醒》
|
||||
△字幕稳定悬停在画面中央。
|
||||
△镜头轻微推近字幕,星粉边缘闪烁。
|
||||
△画面右侧,梦梦突然冲入画面,动作夸张又迅速。
|
||||
△梦梦在字幕前急刹停下,对着镜头摆出一个自信满满的姿势。
|
||||
△梦梦眨眼,对镜头做了一个夸张的 wink。
|
||||
梦梦:织梦师的觉醒
|
||||
说完话后留出半秒
|
||||
|
||||
3D动画电影渲染,阿诺德(Arnold)渲染器,皮克斯(Pixar)风格,【@图x】是梦梦,【@图x】是《织梦手册》,【@图x】是《织梦手册》翻开的样子,你是一位专业的动画导演,自行安排分镜设计,切镜充满电影感,画面氛围也有电影感,不要有背景音乐,但要有音效。
|
||||
|
||||
|
||||
|
||||
1-2 学校教室 [内] [日]
|
||||
出场人物:星晓、小冉
|
||||
△教室里只有小冉一个人趴在课桌上,肩膀微微起伏,一动不动。
|
||||
△窗外一棵大树枝叶摇晃,阳光透过树叶洒进教室。
|
||||
【音效】断断续续的蝉鸣声从窗外传来。
|
||||
△课桌上,小冉的手旁摊着一张被揉皱的纸。
|
||||
△镜头推进。
|
||||
△纸张上印着标题:校话剧社选拔结果
|
||||
△鲜红的“未入选”印章盖在纸张中央,颜色刺眼。
|
||||
小冉:(情绪失控)为什么,我没有被话剧社选中!
|
||||
3D动画电影渲染,阿诺德(Arnold)渲染器,皮克斯(Pixar)风格,【@图x】是小冉,【@图x】是学校教室,【@图x】是被揉皱的纸,你是一位专业的动画导演,自行安排分镜设计,切镜充满电影感,画面氛围也有电影感,不要有字幕,不要有背景音乐,但要有音效。
|
||||
|
||||
|
||||
1-2 学校教室 [内] [日]
|
||||
出场人物:星晓、小冉
|
||||
△学校教室内小冉坐在课桌前,伤心地深呼吸,肩膀起伏,整个人一动不动,位置参考【@图x】
|
||||
△教室另一侧,星晓站着。
|
||||
△她手里捏着一张画,脚步迟疑。
|
||||
△星晓慢慢走向小冉,在她面前停下。
|
||||
星晓:小冉不要伤心!
|
||||
△小冉没有抬头,她的背影僵硬。
|
||||
星晓:我给你看个好玩的,我画的滑稽小人!
|
||||
△星晓把画递到小冉面前。
|
||||
△画上是一个穿着夸张戏服、翻着白眼的滑稽小人。
|
||||
△小冉侧过脸,用余光扫了一眼画。
|
||||
△她直接对着镜头开口。
|
||||
小冉:拿走,我现在没有心情看!
|
||||
△星晓拿着画,转身面对镜头。
|
||||
星晓:小冉没事的,你下次一定能当主角!
|
||||
|
||||
3D动画电影渲染,阿诺德(Arnold)渲染器,皮克斯(Pixar)风格,【@图x】是星晓,【@图x】是小冉,【@图x】是学校教室,【@图x】是学校教室的另一侧,【@图x】是被揉皱的纸,【@图x】是星晓拿着的画着滑稽小人的画,【@图x】是小冉在教室中的位置,你是一位专业的动画导演,自行安排分镜设计,切镜充满电影感,画面氛围也有电影感,不要有背景音乐,但要有音效。
|
||||
|
||||
|
||||
1-2 学校教室 [内] [日]
|
||||
出场人物:星晓、小冉
|
||||
△学校教室内,小冉坐在课桌前背对旁边站着的星晓,位置参考【@图x】和【@图x】,桌上放着那张被揉皱的通知单,参考【@图x】,小冉猛地抬起头,但眼泪没有落下,小冉:下次,我还有下次吗?
|
||||
△星晓拿着滑稽小人的画,愣住,她的表情僵了一下,声音明显变小,星晓:肯定有机会的!
|
||||
△小冉直直地看着星晓,小冉:你有没有想过,我们马上就要毕业了!
|
||||
△小冉站起身,她拍了拍衣服上的灰尘,小冉:我专门练习了两个月,就是想当主角。
|
||||
△星晓看着小冉,张了张嘴,星晓:我……
|
||||
△小冉一把抓起桌上那张皱巴巴的通知单,她转身,快步朝教室外跑去,小冉:你根本就不懂我现在的心情。
|
||||
|
||||
3D动画电影渲染,阿诺德(Arnold)渲染器,皮克斯(Pixar)风格,【@图x】是星晓,【@图x】是小冉,【@图x】是学校教室小冉坐的位置,【@图x】是课桌上被揉皱的通知单,【@图x】是星晓拿着的画着滑稽小人的画,【@图x】通知单放在桌面的位置, 【@图x】和【@图x】是星晓和小冉的位置关系,你是一位专业的动画导演,自行安排分镜设计,切镜充满电影感,画面氛围也有电影感,不要有背景音乐,但要有音效。
|
||||
|
||||
|
||||
1-2 学校教室 [内] [日]
|
||||
出场人物:星晓
|
||||
△安静的教室中,星晓失落的站在原地(没有眼泪),位置参考【@图x】,她低头看着手里的画。
|
||||
△画被她慢慢捏紧,变成一个皱巴巴的纸团。
|
||||
△星晓头顶上方,浮现出一朵黑漆漆的“忧郁小乌云”特效,同时,星晓(内训独白V.O.):我明明是想让小冉高兴。
|
||||
△星晓走到课桌前坐下,趴在桌上,手里拿着刚才被揉成的皱巴巴的纸团。
|
||||
△她的眼皮一点一点变沉,脸上犯着困意,她的嘴里嘟囔着听不清的抱怨,星晓:怎么搞砸了……
|
||||
△最后3秒,教室的光线开始忽明忽暗,画面轻微闪烁,节奏逐渐放慢,好像要进入梦境的感觉。
|
||||
|
||||
3D动画电影渲染,阿诺德(Arnold)渲染器,皮克斯(Pixar)风格,【@图x】是星晓,【@图x】是学校教室,【@图x】是星晓手里的画,【@图x】是星晓在教室中的位置,你是一位专业的动画导演,自行安排分镜设计,切镜充满电影感,画面氛围也有电影感,不要有背景音乐,但要有音效。
|
||||
|
||||
|
||||
1-4 表演森林 [外] [日]
|
||||
出场人物:织梦师星晓、梦梦、心心
|
||||
△画面展开,一片广阔的森林出现在画面中,星晓与梦梦站在森林空地中央,森林里点缀着大量奇幻植物。粉紫色光芒的郁金香在地面轻轻摇晃。晶莹剔透的蓝色水晶簇从地面生长出来,草丛之间,星星点点的荧光花朵闪烁着微光。
|
||||
△星晓四下张望,神情紧张,星晓:小精灵在哪,快抓住它!
|
||||
△前方不远处,小精灵心心站在地面上,它身上披着一件红色天鹅绒斗篷,斗篷下摆拖在地上,质感厚重,心心:想要抓我,先打败我的舞台幕布!
|
||||
△心心猛地一甩袖子,无数深红色的天鹅绒布条从斗篷中飞出,布条在空中翻卷,朝星晓袭来。
|
||||
△星晓迅速后退,她左右闪避飞来的红色布条,星晓:要怎么解决这些布!
|
||||
△一块天鹅绒布突然从侧面飞来,布条缠住了星晓的头,她视野被遮挡,星晓:不好!我被绑住了!
|
||||
△梦梦在空中急得团团转,它围着星晓飞来飞去,梦梦:画个拉链把布解开!
|
||||
|
||||
3D动画电影渲染,阿诺德(Arnold)渲染器,皮克斯(Pixar)风格,【@图x】是梦梦,【@图x】是星晓,【@图x】是心心,【@图x】是星晓和梦梦的比例参考,【@图x】是森林,你是一位专业的动画导演,自行安排分镜设计,切镜充满电影感,画面氛围也有电影感,动作戏可以有一点荷兰式倾斜镜头,不要有字幕,不要有背景音乐,但要有音效。
|
||||
|
||||
|
||||
|
||||
1-4 表演森林 [外] [日]
|
||||
出场人物:织梦师星晓、梦梦、心心
|
||||
△森林中,星晓被红色布条缠住,参考【@图x】,她在红色布条中用力挣扎,动作受限,星晓:我现在动不了!
|
||||
△心心再次挥动手臂,天空骤然变暗,心心:舞台道具攻击!
|
||||
△大量灰色道具箱从空中倾泻而下。
|
||||
△灰色道具箱砸落在地面,箱盖弹开,发条玩具假蛇猛地从箱子里弹出,粘人的彩色碎纸屑也四处飞散。
|
||||
△星晓从红色布条中探出半个脑袋,抬头看向空中,星晓:小心,又有攻击了!
|
||||
△梦梦为了躲避落下的道具箱,在空中猛地转向,它下意识踩在星晓的头顶借力跳起,梦梦:我躲!
|
||||
△星晓被踩得猛地向下,整张脸埋进草地里。
|
||||
△星晓抬起头,嘴里全是草,表情夸张,明显黑化颜艺,星晓:你踩哪儿呢……
|
||||
|
||||
3D动画电影渲染,阿诺德(Arnold)渲染器,皮克斯(Pixar)风格,【@图x】是梦梦,【@图x】是星晓,【@图x】是心心,【@图x】是星晓和梦梦的比例参考,【@图x】是森林,【@图x】是星晓被红色布条缠住的样子,你是一位专业的动画导演,自行安排分镜设计,切镜充满电影感,画面氛围也有电影感,动作戏可以有一点荷兰式倾斜镜头,动作戏的镜头具有视觉张力和空间感,不要有字幕,不要有背景音乐,但要有音效。
|
||||
344
skills/seedance2.0相关知识/🎬 Seedance 2.0 使用手册(全新多模态创作体验).md
Normal file
344
skills/seedance2.0相关知识/🎬 Seedance 2.0 使用手册(全新多模态创作体验).md
Normal file
@ -0,0 +1,344 @@
|
||||
# 🎬 Seedance 2.0 使用手册(全新多模态创作体验)
|
||||
|
||||
# 🌀 视频 Seedance 2.0 正式上线! Kill the game!
|
||||
|
||||
还记得从只能用文字和首/尾帧「讲故事」的那天起,我们就想做出一个真正听得懂你表达的视频模型。今天,它真的来了!
|
||||
|
||||
> **Seedance 2.0 现在支持图像、视频、音频、文本四种模态输入,表达方式更丰富,生成也更可控**
|
||||
|
||||
你可以用一张图定下画面风格,用一个视频指定角色的动作和镜头的变化,再用几秒音频带起节奏氛围……搭配提示词,让创作过程变得更自然、更高效,也更像真正的“导演”。
|
||||
|
||||
这次升级中,**“参考能力”是最大亮点**:
|
||||
|
||||
* 📷 参考图像可精准还原画面构图、角色细节
|
||||
|
||||
* 🎥 参考视频支持镜头语言、复杂的动作节奏、创意特效的复刻
|
||||
|
||||
* ⏱ 视频支持平滑延长与衔接,可按用户提示生成连续镜头,不止生成,还能“接着拍”
|
||||
|
||||
* ✂️ 编辑能力同步增强,支持对已有视频进行角色更替、删减、增加
|
||||
|
||||
**我们知道,视频创作从来不仅是“生成”,更是对表达的控制。2.0 不只是多模态,更是一种真正可控的创作方式**
|
||||
|
||||
> **Seedance 2.0,多模态创作,从这里启程。请你们大胆想象,其余的交给它**
|
||||
|
||||
|
||||
|
||||
## 1. 参数预览
|
||||
|
||||
| **核心维度** | **Seedance 2.0 ** |
|
||||
| --------------------------------------------------------------- | ------------------------------- |
|
||||
| **图片输入** | **≤ 9 张** |
|
||||
| **视频输入** | **≤ 3 个,总时长不超过15s**(有参考视频会贵一点哦) |
|
||||
| **音频输入** | **支持 MP3 上传,数量≤ 3 个,总时长不超过15s** |
|
||||
| **文本输入** | **自然语言** |
|
||||
| **生成时长** | **≤ 15s,可自由选择4-15s** |
|
||||
| **声音输出** | **自带音效/配乐** |
|
||||
| **交互限制:目前支持的混合输入总上限是 12 个文件。建议优先上传对画面或节奏影响最大的素材,合理分配不同模态的文件数量** | |
|
||||
|
||||
|
||||
|
||||
## 2. 交互形式
|
||||
|
||||
**⚠️注意:**
|
||||
|
||||
* Seedance 2.0 支持「首尾帧」和「全能参考」入口,智能多帧和主体参考无法选中。若你只上传首帧图 + prompt,可走首尾帧入口;如需多模态(图、视频、音频、文本)组合输入,则需进入全能参考入口
|
||||
|
||||
* 当前支持的交互方式是通过“@素材名”来指定每个图片、视频、音频的用途,例如:@图片1 作为首帧,@视频1 参考镜头语言,@音频1 用于配乐
|
||||
|
||||
##### **主界面:**
|
||||
|
||||

|
||||
|
||||
入口:Seedance 2.0 - 全能参考/首尾帧
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
唤起本地文件弹窗
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
选定文件,添加至输入框
|
||||
|
||||
|
||||
|
||||
##### 全能参考模式下如何@:
|
||||
|
||||
**方法1:输入“@”唤起参考调用**
|
||||
|
||||

|
||||
|
||||
*输入“@”*
|
||||
|
||||

|
||||
|
||||
*选择参考,落入输入框*
|
||||
|
||||

|
||||
|
||||
*输入prompt*
|
||||
|
||||
**方法2:点击参数工具“@”唤起参考调用**
|
||||
|
||||

|
||||
|
||||
*点击“@”*
|
||||
|
||||

|
||||
|
||||
*选择参考,落入输入框*
|
||||
|
||||

|
||||
|
||||
*输入prompt*
|
||||
|
||||
|
||||
|
||||
上传素材后,**图片、视频、音频**都支持**悬停预览**
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
***
|
||||
|
||||
**下面是一些不同场景下的用法和玩法,帮助你更好地理解 Seedance 2.0 在生成质量上、控制能力和创意表现上的升级。如果你还不知道从哪开始,不如先看看这些例子,激发灵感~**
|
||||
|
||||
***
|
||||
|
||||
|
||||
|
||||
# Seedance 2.0 能力 / 提升预览
|
||||
|
||||
## 1. 基础能力显著增强:更稳、更顺、更像真的! 
|
||||
|
||||
不只是多模态,Seedance 2.0 在基础层面显著增强,**物理规律更合理**、**动作表现更自然流畅**、**指令理解更精准**、**风格保持更稳定**,不仅能稳定完成复杂动作、连续运动等高难度生成任务,也让整体视频效果更真实、更顺滑,是一次底层能力的全面进化!
|
||||
|
||||
Case:
|
||||
|
||||
| **prompt** | **img** | **vid1** |
|
||||
| -------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -------- |
|
||||
| 女孩在优雅的晒衣服,晒完接着在桶里拿出另一件,用力抖一抖衣服。 |  | 超强真实感 |
|
||||
| 画里面的人物心虚的表情,眼睛左右看了看探出画框,快速的将手伸出画框拿起可乐喝了一口,然后露出一脸满足的表情,这时传来脚步声,画中的人物赶紧将可乐放回原位,此时一位西部牛仔拿起杯子里的可乐走了,最后镜头前推画面慢慢变得纯黑背景只有顶光照耀的罐装可乐,画面最下方出现艺术感字幕和旁白:“宜口可乐,不可不尝!” |  | |
|
||||
| 镜头小幅度拉远(露出街头全景)并跟随女主移动,风吹拂着女主的裙摆,女主走在19世纪的伦敦大街上;女主走着走着右边街道驶来一辆蒸汽机车,快速驶过女主身旁,风将女主的裙摆吹起,女主一脸震惊的赶忙用双手向下捂住裙摆;背景音效为走路声,人群声,汽车声等等 |  | |
|
||||
| 镜头跟随黑衣男子快速逃亡,后面一群人在追,镜头转为侧面跟拍,人物惊慌撞倒路边的水果摊爬起来继续逃,人群慌乱的声音。 |  | |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 2. 多模态全面升级:视频创作进入“自由组合”时代!
|
||||
|
||||
### 2.1 Seedance 2.0 多模态介绍
|
||||
|
||||
* 支持上传文本、图片、视频、音频,这些素材都可以被用作使用对象或参考对象。你可以参考任何内容的动作、特效、形式、运镜、人物、场景、声音,只要提示词写得清楚,模型都能理解。
|
||||
|
||||
* Seedance 2.0 = 多模态参考能力(可参考万物) + 强创意生成 + 指令响应精准(理解力很棒)
|
||||
|
||||
* 用自然语言描述你想要的画面和动作就可以啦,明确是参考,还是编辑~素材多的时候,建议你多检查一下各个 @对象有没有标清楚,别把图、视频、角色搞混了哦
|
||||
|
||||
|
||||
|
||||
### 2.2 特殊使用方式(不设限,仅供参考):
|
||||
|
||||
* **有首帧/尾帧图?还想参考视频动作?**
|
||||
 → 提示词中写清楚,如:“@图1为首帧,参考@视频1的打斗动作”
|
||||
|
||||
* **想延长一个已有的视频?**
|
||||
 → 说明延长时间,如“将@视频1延长 5s”,*注意:此时选择的生成时长应为“新增部分”的时长*(例如延长 5s,生成长度也选 5s
|
||||
|
||||
* **想融合多个视频?**
|
||||
 → 提示词中说明合成逻辑,如:“我要在@视频1和@视频2之间加一个场景,内容为xxx”
|
||||
|
||||
* **没音频素材?可以直接参考视频里的声音**
|
||||
|
||||
* **想生成连续动作?**
|
||||
 → 可以在提示词中加入连续性描述,如:“角色从跳跃直接过渡到翻滚,保持动作连贯流畅”@图1@图2@图3...
|
||||
|
||||
|
||||
|
||||
### 2.3 那些一直很难做的视频问题,现在真的能搞定了!
|
||||
|
||||
> 做视频总会碰到一些让人头疼的地方:比如人脸换了、动作不像、视频延长不自然、改着改着整个节奏都变了……这次多模态能把这些“老大难”问题一口气解决了,下面就是具体的使用案例👇
|
||||
|
||||
#### 2.3.1 **一致性全面提升 **
|
||||
|
||||
你可能遇到过这些烦恼:画面里人物前后长得不一样、商品细节丢了、小字模糊、场景跳变、镜头风格无法统一……这些在创作中常见的一致性问题,现在在 2.0 中都能被解决。从人脸到服装,再到字体细节,整体一致性更稳、更准
|
||||
|
||||
##### Case:
|
||||
|
||||
| **prompt** | **img1** | **img2** | **img3** | **img4** | **vid1** | **生成结果** |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -------- | -------- |
|
||||
| 男人@图片1下班后疲惫的走在走廊,脚步变缓,最后停在家门口,脸部特写镜头,男人深呼吸,调整情绪,收起了负面情绪,变得轻松,然后特写翻找出钥匙,插入门锁,进入家里后,他的小女儿和一只宠物狗,欢快的跑过来迎接拥抱,室内非常的温馨,全程自然对话 |  | | | | | |
|
||||
| 将@视频1中的女生换成戏曲花旦,场景在一个精美的舞台上,参考@视频1的运镜和转场效果,利用镜头匹配人物的动作,极致的舞台美感,增强视觉冲击力 | | | | | | |
|
||||
| 使用参考图片人物的形象生成一段古装穿越剧的预告短片。
 0-3秒画面:参考图片1人物形象的男主手里举起一个篮球,抬头望向镜头。说话“我只是想喝杯酒,该不会要穿越了吧……” 
4-8秒画面:镜头突然剧烈晃动,操场的场景开始剧烈震动,瞬间切换成古宅的雨夜。一个穿着古装,长相清秀的女主,冷冽的目光穿透雨幕,望向镜头方向。雷鸣声,衣袂猎猎声。女主说话“何人擅闯我永宁侯府?” 9-13秒画面:镜头切到一个穿着明官服的男子坐在衙门里,眼神锐利如刀,愤怒说话“来人!即刻拿下此‘妖人’!” 画面闪回:男主穿着不合身的粗布麻衣;在官差的围堵下慌不择路;与女主的身影在雨巷里交错;男主穿着官服走在皇宫里。 14-15秒画面:黑屏,打出片名《醉梦惊华》,伴随着沉重的鼓点。 |  | | | | | |
|
||||
| 参考 @视频1的所有转场和运镜,一镜到底,画面以棋局为起始,镜头左移,展示地板的黄色沙砾,镜头上移来到一个沙滩,沙滩上有足印,一个穿着白色素衣的女生在沙滩上渐行渐远,镜头切到空中的俯拍视角,海水在冲刷(不要出现人物),无缝渐变转场,冲刷的海浪变成飘动的窗帘,镜头拉远,展示女孩的面部特写,一镜到底 | | | | | | |
|
||||
| 0-2秒画面:快速四格闪切,红、粉、紫、豹纹四款蝴蝶结依次定格,特写缎面光泽与 “chéri” 品牌字样。画外音“Chéri 자석 리본으로 무궁무진한 아름다움을 연출해 보세요!”3-6秒画面:特写银色磁吸扣 “咔嗒” 吸合,再轻轻一拉分开,展示丝滑质感与便捷性。画外音“단 1초 만에 잠그고, 최고의 스타일을 완성하세요!”7-12 秒画面:快速切换佩戴场景:酒红款别在大衣领口,通勤氛围感拉满;粉色款绑在马尾,甜妹出街;紫色款系在包带,小众高级;豹纹款挂在西装领,辣妹气场全开。画外音“코트, 가방, 헤어 액세서리까지, 다재다능하고 개성 넘치는 스타일을 완성하세요!”13-15秒画面:四款蝴蝶结并排陈列,品牌名 “chéri, 당신에게 즉각적인 아름다움을 선사합니다!” |  | | | | | |
|
||||
| 对@图片2的包包进行商业化的摄像展示,包包的侧面参考@图片1,包包的表面材质参考@图片3,要求将包包的细节均有所展示,背景音恢宏大气, |  |  |  | | | |
|
||||
| 把@图片1作为画面的首帧图,第一人称视角,参考@视频1的运镜效果,上方场景参考@图片2,左边场景参考@图片3,右边场景参考@图片4。 |  |  |  |  | | |
|
||||
|
||||
|
||||
|
||||
#### 2.3.2 **高难度/可控的运镜和动作精准复刻** 
|
||||
|
||||
以前想让模型模仿电影里的走位、运镜或者复杂动作,要么写一堆细节提示词,要么干脆做不到。而现在,只需要上传一段参考视频,就可以了
|
||||
|
||||
##### Case:
|
||||
|
||||
| **prompt** | **img1** | **img2** | **img3** | **img4** | **img5** | **img6** | **img7** | **vid1** | **vid2** | **vid3** | **生成结果** |
|
||||
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -------- | -------- | -------- | -------- | -------- | -------- |
|
||||
| 参考@图1的男人形象,他在@图2的电梯中,完全参考@视频1的所有运镜效果还有主角的面部表情,主角在惊恐时希区柯克变焦,然后几个环绕镜头展示电梯内视角,电梯门打开,跟随镜头走出电梯,电梯外场景参考@图片3,男人环顾四周,参考@视频1用机械臂多角度跟随人物的视线 |  |  |  | | | | | | | | |
|
||||
| 参考@图1的男人形象,他在@图2的走廊中,完全参考@视频1的所有运镜效果,还有主角的面部表情,镜头跟随主角在@图2拐角奔跑,然后在@图3的长廊里,镜头从背面的跟随视角,通过低视角环绕到主角正面;镜头再右摇90度拍摄@图片4的分叉路口,急停后右摇180度,怼脸拍摄主角正面:主角气喘吁吁,镜头跟随主角的视角环顾四周,参考@视频1里急速的左右环绕运镜展示场景,后拉到@图片5的场景,继续跟拍主角奔跑的侧面视角 |  |  |  |  |  | | | | | | |
|
||||
| @图片1的平板电脑作为主体,运镜参考@视频1,推近到屏幕的特写,镜头旋转后平板反转展示全貌,屏幕中的数据流一直在变化,周围的环境逐渐变成科幻风格的数据空间 |  | | | | | | | | | | |
|
||||
| @图片1的女星作为主体,参考@视频1的运镜方式进行有节奏的推拉摇移,女星的动作也参考@视频1中女子的舞蹈动作,在舞台上活力十足地表演 |  | | | | | | | | | | |
|
||||
| 参考@图1@图2长枪角色,@图3@图4双刀角色,模仿@视频1的动作,在@图5的枫叶林中打斗 |  |  |  |  |  | | | | | | |
|
||||
| 参考视频1的人物动作,参考视频2的环绕运镜镜头语言,生成角色1和角色2的打斗场面,打斗发生在星夜中,打斗的过程中有白色灰尘扬起,打斗场面非常华丽,气氛十分紧张。 |  |  | | | | | | | | | |
|
||||
| 参考视频1的运镜、画面切换节奏,拿图片1的红色超跑进行复刻。 |  | | | | | | | | | | |
|
||||
|
||||
####
|
||||
|
||||
#### 2.3.3 **创意模版 / 复杂特效精准复刻**
|
||||
|
||||
不止能生图写故事,Seedance 2.0 还支持“照着模仿”——创意转场、广告成片、电影片段、复杂剪辑,只要你有参考图或视频,模型就能识别动作节奏、镜头语言、视觉结构,并精准复刻出来。不懂专业术语也没关系,写清楚你想参考的部分,比如“参考 @视频1 的节奏和运镜,@图1 的角色造型”,模型就能高质量生成属于你的版本。大胆试!它真的能做到
|
||||
|
||||
##### Case:
|
||||
|
||||
| **prompt** | **img1** | **img2** | **img3** | **img4** | **img5** | **img6** | **img7** | **vid1** | **生成结果** |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -------- | -------- | -------- |
|
||||
| 将@视频1的素人换成女生,长相参考@图片1;月神的cg形象换成天使,形象参考@图片2,女生蹲下时背后长出翅膀,翅膀挥动时掠过镜头,实现转场,并参考@视频1的运镜和转场效果,从天使的瞳孔进入下一场景,从空中俯拍天使(盘旋的翅膀对应瞳孔),镜头下移并跟随天使正脸,抬手时镜头后拉,展示出背景天使的石像,全程一镜到底 |  |  | | | | | | | |
|
||||
| 将@视频1的人物换成@图片1,@图片1为首帧,人物带上虚拟科幻眼镜,参考@视频1的运镜,及近的环绕镜头,从第三人称视角变成人物的主观视角,在AI虚拟眼镜中穿梭,来到@图片2的深邃的蓝色宇宙,出现几架飞船穿梭向远方,镜头跟随飞船穿梭到@图片3的像素世界,镜头低空飞过像素的山林世界,里面的树木生长形式出现,随后视角仰拍,急速穿梭到@图片4的浅绿色纹理的星球,镜头穿梭并掠过星球表面 |  |  |  |  | | | | | |
|
||||
| 参考第一张图片里模特的五官长相。模特分别穿着第2-6张参考图里的服装凑近镜头,做出调皮、冷酷、可爱、惊讶、耍帅的造型,每一个造型穿着不同服装,每次更换,画面伴随会切镜,参考视频的里鱼眼镜头效果、重影闪烁的炫影画面效果, |  |  |  |  |  |  | | | |
|
||||
| 参考视频的广告创意,用提供的羽绒服图片,并参考鹅绒图片、天鹅图片,搭配以下广告词“这是根鹅绒,这是暖天鹅,这是能穿的极地天鹅绒羽绒服,新年穿得暖,生活过得暖”,生成新的羽绒服广告视频。 |  |  |  | | | | | | |
|
||||
| 黑白水墨风格,@图片1的人物参考@视频1的特效和动作,上演一段水墨太极功夫 |  | | | | | | | | |
|
||||
| 将@视频1的首帧人物替换成@图片1,完全@参考视频1的特效和动作,手里的花蕊长出玫瑰花瓣,裂纹在脸部向上延伸,逐渐被杂草覆盖,人物双手拂过脸部,杂草变成粒子消散,最后变成@图片2的长相 |  |  | | | | | | | |
|
||||
| 由@图片1的天花板开始,参考@视频1的拼图破碎效果进行转场,“BELIEVE”字体替换成“Seedance”,参考@图2的字体 |  |  | | | | | | | |
|
||||
| 以黑幕开场,参考视频1的粒子特效和材质,金色鎏金材质的沙砾从画面左边飘出并向右覆盖,参考@视频1的粒子吹散效果,@图片1的字体逐渐出现在画面中心 |  | | | | | | | | |
|
||||
| @图片1的人物参考@视频1中的动作和表情变化,展示吃泡面的抽象行为 |  | | | | | | | | |
|
||||
|
||||
|
||||
|
||||
#### 2.3.4 **模型的创意性、剧情补全能力**
|
||||
|
||||
##### Case:
|
||||
|
||||
| **prompt** | **img1** | **img2** | **img3** | **img4** | **img5** | **img6** | **img7** | **vid1** | **生成结果** |
|
||||
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -------- | -------- | -------- | -------- |
|
||||
| 将@图1以从左到右从上到下的顺序进行漫画演绎,保持人物说的台词与图片上的一致,分镜切换以及重点的情节演绎加入特殊音效,整体风格诙谐幽默;演绎方式参考@视频1 |  | | | | | | | | |
|
||||
| 参考@图片1的专题片的分镜头脚本,参考@图片1的分镜、景别、运镜、画面和文案,创作一段15s的关于“童年的四季”的治愈系片头 |  | | | | | | | | |
|
||||
| 参考视频1的音频,根据图1、图2、图3、图4、图5为灵感,发散出一条情绪向的视频。背景音乐参考@视频1 |  |  |  |  |  | | | | |
|
||||
|
||||
|
||||
|
||||
#### 2.3.5 视频延长
|
||||
|
||||
##### Case:
|
||||
|
||||
| **生成时长** | **prompt** | **img1** | **img2** | **img3** | **img4** | **img5** | **img6** | **img7** | **vid1** | **生成结果** |
|
||||
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- |
|
||||
| 15s | 延长15s视频,参考@图片1、@图片2的驴骑摩托车的形象,补充一段脑洞广告
画面1:侧面固定镜头,驴骑着摩托车冲出棚栏,旁边的鸡受到惊吓,
画面2:驴骑着摩托在沙地盘旋,先特写摩托轮胎,再切到半空中俯拍驴骑着摩托车做着盘旋特技,掀起烟雾
画面3:背景是雪山镜头,驴骑着车从山坡飞越过,广告语在主体背后,通过遮罩的形式(驴和摩托车飞过时)中间出现"Inspire Creativity,Enrich Life",最后在摩托飞过,扬起一阵尘烟 |  |  | | | | | | | |
|
||||
| 6s | 将视频延长6s,出现电吉他的激昂音乐,视频中间出现“JUST DO IT”的广告字体后逐渐淡化,镜头上移到天花板,一个健硕的男人拉着吊环,上半身穿着@图1的紧身健身服,背面印有@图2的“Fitness”logo,男人用健硕的上肢拉上吊环,随后视频中间出现“DO SOME SPORT”的广告结束字体。 |  |  | | | | | | | |
|
||||
| 15s | 将@视频1延长15秒。1-5秒:光影透过百叶窗在木桌、杯身上缓缓滑过,树枝伴随着轻微呼吸般的晃动。6-10秒:一粒咖啡豆从画面上方轻轻飘落,镜头向咖啡豆推进至画面黑屏。11-15秒:英文渐显第一行“Lucky Coffee ”,第二行“Breakfast”,第三行“AM 7:00-10:00”。 | | | | | | | | | |
|
||||
| 10s | 向前延长10s,温暖的午后光线里,镜头先从街角那排被微风掀动的遮阳篷开始,慢慢下移到墙根处几株探出头的小雏菊。紧接着,画面里出现主人公的红色板鞋,他正蹲在街边花摊前,笑着把一大捧向日葵拢进怀里,花瓣蹭过他的白 T 恤。他转身踏上滑板时,花摊老板笑着喊了句 “小心花瓣飞啦!”,他冲老板挥了挥手,然后才开始滑行,几片金黄的花瓣已经先一步从花束里挣脱出来,落在了滑板的板面。 | | | | | | | | | |
|
||||
|
||||
|
||||
|
||||
#### 2.3.6 音色更准,声音更真
|
||||
|
||||
##### Case:
|
||||
|
||||
| **prompt** | **img1** | **img2** | **img3** | **vid1** | **vid2** | **vid3** | **生成结果** |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -------- | -------- | -------- | -------- |
|
||||
| 固定镜头,中央鱼眼镜头透过圆形孔洞向下窥视,参考视频1的鱼眼镜头,让@视频2中的马看向鱼眼镜头,参考@视频1中的说话动作,背景BGM参考@视频3中的音效。 | | | | | | | |
|
||||
| 根据提供的写字楼宣传照,生成一段15秒电影级写实风格的地产纪录片,采用2.35:1宽银幕,24fps,细腻的画面风格,其中旁白的音色参考@视频1,拍摄 “写字楼的生态”,呈现楼内不同企业的运作,结合旁白阐述写字楼如何成为一个充满活力的商业生态系统. |  |  |  | | | | |
|
||||
| 在“猫狗吐槽间”里的一段吐槽对话,要求情感丰沛,符合脱口秀表演:
喵酱(猫主持,舔毛翻眼):"家人们谁懂啊,我身边这位,每天除了摇尾巴、拆沙发,就只会用那种“我超乖求摸摸”的眼神骗人类零食,明明拆家的时候比谁都凶,还好意思叫旺仔,我看叫“旺拆”还差不多哈哈哈“
旺仔(狗主持,歪头晃尾巴):"你还好意思说我?你每天睡18个小时,醒了就蹭人类腿要罐头,掉毛掉得人类黑衣服上全是你的毛,人家扫完地,你转身又在沙发上滚一圈,还好意思装高冷贵族?" |  | | | | | | |
|
||||
| 豫剧经前桥段《铡美案》的伴奏响起,左侧的黑衣包拯指着右侧的红衣陈世美,咬牙切齿地唱着豫剧:“刀对鞘,真凭实据你敢不招?” 陈世美的眼珠左右滴溜溜乱转,寻找着权宜之策,面色窘迫至极。此时,画面外传来一声豫剧旦角的念白:“且慢!”包拯和陈世美一齐向画面右侧看去。 |  | | | | | | |
|
||||
| 生成一个15秒的MV视频。关键词:稳重构图 / 轻推拉 / 低角度英雄感 / 纪实但高级A超广角建立镜头,低机位轻微仰拍,悬崖土路与复古旅行车占画面下三分之一,远处海面与地平线拉开空间,夕阳侧逆光体积光穿过尘粒,电影级构图,真实胶片颗粒,微风吹动衣角。 |  | | | | | | |
|
||||
| 画面中间戴帽子的女孩温柔地唱着说"I'm so proud of my family!",之后转身拥抱中间的黑人女孩。黑人女孩感动地回应"My sweetie, you're the heart of our family",回抱她。左侧的黄衣服男孩开心地说"Folks, let's dance together to celebrate!” 最右侧的女孩紧接着回复: “I'll bring the music!",背景拉美音乐响起,左侧穿橙色裙的女性(朱丽叶塔)笑着点头,右侧扎辫女性(路易莎)握紧拳头挥动手臂。人群中有人开始踏起步子,孩子们跟着节奏拍手,整个家族即将围成圈,伴着欢快的音乐,裙摆飞扬,在五彩的街道上尽情舞动,传递着喜悦与温暖。 |  | | | | | | |
|
||||
| 固定镜头。站着的壮汉(队长)握拳挥臂用西班牙语说着:“三分钟后突袭!”持刀者收刀入鞘,金发队员站在检查枪械,绿发队员握紧战术手电。黑人队员搭肩问同伴用西班牙语说:“侧翼包抄?”队长点头用西班牙语说:“老规矩,活口留审讯。”全员肃然,装备碰撞声中完成战术手势,默契起身,大家都严阵以待,左侧两个男生也争先站起来准备战斗。 |  | | | | | | |
|
||||
| 0-3秒:开头闹钟响起来,画面朦胧中出现画面1; 3-10秒:快速摇镜头,转向对面特写男人面部,男人无奈的叫女生起床,语气和音色参考@视频1; 10-12秒:女生撅着嘴躲进被子里面; 12-15秒:切换到男主全身,他叹着气说:”真拿你没办法!“ |  |  | | | | | |
|
||||
| @图片1的猴子走向奶茶店柜台,镜头跟随在他身后,一位@图片2的比熊服务员正在吧台处擦拭制作工具,猴子向服务员用四川口音点单:“幺妹儿,霸王别姬有得没得?”
切镜,特写。
服务员放下手里的活,怪异地看了老头一眼后回答:“没得,美式要不要得嘛”
切镜,镜头给到猴子。他挠了挠头念念有词:“没事……?我有事!孙儿叫我来买个奶茶,就叫个撒子霸王别姬嘛” |  |  |  | | | | |
|
||||
| 用科普风格和音色,将图片1中的内容演绎出来,内容包括悟空为过火焰山,到翠云山向铁扇公主借芭蕉扇。铁扇公主因红孩儿被悟空降伏拜观音为童子,母子分离,不肯借扇还欲报仇。悟空好言相劝无果,二人随即起了争执的小故事进行讲解。 |  | | | | | | |
|
||||
|
||||
####
|
||||
|
||||
#### 2.3.7 **镜头连贯性(一镜到底)更强**
|
||||
|
||||
##### Case:
|
||||
|
||||
| **prompt** | **img1** | **img2** | **img3** | **img4** | **img5** | **img6** | **img7** | **生成结果** |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -------- | -------- | -------- |
|
||||
| @图片1@图片2@图片3@图片4@图片5,一镜到底的追踪镜头,从街头跟随跑步者上楼梯、穿过走廊、进入屋顶,最终俯瞰城市。 |  |  |  |  |  | | | |
|
||||
| 以@图片1为首帧,画面放大至飞机舷窗外,一团团云朵缓缓飘至画面中,其中一朵为彩色糖豆点缀的云朵,始终在画面中居中,然后缓缓变形为@图片2的冰淇淋,镜头推远回到机舱内,坐在窗边的@图片3伸手从窗外拿进冰淇淋,吃了一口,嘴巴上沾满奶油,脸上洋溢出甜蜜的笑容,此时视频配音为@视频1. |  |  |  | | | | | |
|
||||
| 谍战片风格,@图片1作为首帧画面,镜头正面跟拍穿着红风衣的女特工向前走,镜头全景跟随,不断有路人遮挡红衣女子,走到一个拐角处,参考@图片2的拐角建筑,固定镜头红衣女子离开画面,走在拐角处消失,一个戴面具的女孩在拐角处躲着恶狠狠的盯着她,面具女孩形象参考@图片3,只参考形象,女孩站在拐角处。镜头往前摇向红衣女特工,她走进一座豪宅消失不见了,豪宅参考@图片4。全程不要切镜头,一镜到底。 |  |  |  |  | | | | |
|
||||
| 根据@图片1外景的镜头,第一人称主观视角快推镜头到木屋内的环境场景近景,一只小鹿@图片2和一只羊@图片3在围炉旁喝茶聊天,镜头推进特写茶杯的样式参考@图片4. |  |  |  |  | | | | |
|
||||
| @图片1@图片2@图片3@图片4@图片5,主观视角一镜到底的惊险过山车的镜头,过山车的速度越来越快。 |  |  |  |  |  | | | |
|
||||
|
||||
|
||||
|
||||
#### 2.3.8 **视频编辑可用度高**
|
||||
|
||||
有时候你已经有了一段视频,不想从头再找图或重做一遍,只是希望调整其中一小段动作、延长几秒钟,或让角色表现更贴近你的想法。现在你可以直接用已有视频作为输入,在不改变其它内容的前提下,指定片段、动作或节奏进行定向修改。无需重头生成,也能快速完成调整
|
||||
|
||||
##### Case:
|
||||
|
||||
| **prompt** | **img1** | **img2** | **img3** | **img4** | **vid1** | **生成结果** |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -------- | -------- | -------- | -------- | -------- |
|
||||
| 颠覆@视频1里的剧情,男人眼神从温柔瞬间转为冰冷狠厉,在露丝毫无防备的瞬间,猛地将女主从桥上往外推,把女主推进水里。动作干脆利落,带着蓄谋已久的决绝,没有丝毫犹豫,彻底颠覆原有的深情人物设定。女主坠入水中的瞬间,没有尖叫,只有难以置信的眼神,她抬头冲男主嘶吼:“你从一开始就在骗我!”男主站在桥上上,脸上露出阴冷的笑容,对着水面低声说:“这是你欠我家族的。” | | | | | | |
|
||||
| 颠覆@视频1的整个剧情0–3秒画面:西装男坐在酒吧,神情冷静,手里轻晃酒杯。 镜头缓慢推进,光影高级、氛围严肃。 环境音低沉,西装男小声说“这单生意,很大。”3–6秒画面:身后的女人表情紧张问“有多大?”西装男抬眼,压低声音:“非常大。”镜头切手部特写——他把酒杯放下,气场拉满。6–9秒画面:突然西装男从桌下掏出—— 一大包体积夸张的零食礼包,“咚”的一声重重放在桌上。9–12秒画面:身后的女人原本放在腰间的手,肌肉从僵硬到松弛,整个人表情放松。画面氛围轻松起来。13–15秒画面:西装男拿出一包零食给女人,镜头拉远展示酒馆全景,画面变得透明模糊—— 字幕弹出“再忙,也要记得吃点零食\~” | | | | | | |
|
||||
| 视频1中的女主唱换成图片1的男主唱,动作完全模仿原视频,不要出现切镜,乐队演唱音乐。 |  | | | | | |
|
||||
| 将视频1女人发型变成红色长发,图片1中的大白鲨缓缓浮出半个脑袋,在她身后。 |  | | | | | |
|
||||
| 视频1镜头右摇,炸鸡老板忙碌地将炸鸡递给排队的客户,用普通话说“做完他的,做你的,大家文明排队。”一说完,就去拿纸袋子去装炸鸡。特写展示老板拿印有图1的纸袋子,特写展示递给客户的手部特写。 |  | | | | | |
|
||||
|
||||
|
||||
|
||||
#### 2.3.9 **可进行音乐卡点**
|
||||
|
||||
##### Case:
|
||||
|
||||
| **prompt** | **img1** | **img2** | **img3** | **img4** | **img5** | **img6** | **img7** | **vid1** | **生成结果** |
|
||||
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -------- | -------- | -------- |
|
||||
| 海报中的女生在不停的换装,服装参考@图片1@图片2的样式,手中提着@图片3的包,视频节奏参考@视频 |  |  |  |  | | | | | |
|
||||
| @图片1@图片2@图片3@图片4@图片5@图片6@图片7中的图片根据@视频中的画面关键帧的位置和整体节奏进行卡点,画面中的人物更有动感,整体画面风格更梦幻,画面张力强,可根据音乐及画面需求自行改变参考图的景别,及补充画面的光影变化 |  |  |  |  |  |  | | | |
|
||||
| @图片1@图片2@图片3@图片4@图片5@图片6的风光场景图,参考@视频中的画面节奏,转场间画面风格及音乐节奏进行卡点 |  |  |  |  |  |  | | | |
|
||||
| 8秒智性博弈式战斗动漫片段,贴合复仇主题。0-3秒:分镜图1中女主转身坐下,转镜头,女主下了一步棋子,并说“你输了”,参考分镜图2。3-4秒:快速摇镜头,转向对面男人面部特写,参考分镜图3,男人咬牙切齿,对结果很不满。4-6秒:切镜头,俯拍,女人下了一步棋,对面的人们惊叹,参考分镜图4。6-8秒:镜头迅速向下摇,画面黑屏转场,后画面渐亮,昏暗室内,女人看着窗外月色静静地说“我们走着瞧”,参考分镜图5。 | | | | | | | | | |
|
||||
|
||||
####
|
||||
|
||||
#### 2.3.10 情绪演绎更好
|
||||
|
||||
##### Case:
|
||||
|
||||
| **prompt** | **img1** | **img2** | **img3** | **img4** | **img5** | **img6** | **img7** | **vid1** | **生成结果** |
|
||||
| ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -------- | -------- | -------- | -------- | -------- |
|
||||
| @图片1的女子走到镜子前,看着镜子里面的自己,姿势参考@图片2,沉思了一会突然开始崩溃大叫,抓镜子的动作崩溃大叫的情绪和表情完全参考@视频1。 |  |  | | | | | | | |
|
||||
| 这是一个油烟机广告,@图片1作为首帧画面,女人在优雅的做饭,没有烟雾,镜头快速向右边摇动,拍摄@图片2男人满头大汗面红耳赤在做饭,浓烟滚滚,镜头向左边摇动推进拍摄@图片1桌面上的一个油烟机,油烟机参考@图片4,油烟机在疯狂抽烟。 |  |  |  |  | | | | | |
|
||||
| @图片1作为画面的首帧图,镜头旋转推近,人物突然抬头,人物面部长相参考@图片2,开始大声咆哮,激动带有一些喜剧色彩,参考@图片3的表情神态。然后人物身体变身成为一只熊,参考@图片4.。 |  |  |  |  | | | | | |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 🏁 最后说两句
|
||||
|
||||
Seedance 2.0 的多模态能力正处于不断进化中,我们会持续更新能力、支持更多种输入组合方式。希望这份使用手册能帮你更自由地发挥创意!
|
||||
|
||||
如果你遇到了 Bug,或者有用法建议、需求场景,欢迎留言、私信、敲锣打鼓告诉我们!我们会持续优化,一起把即梦变成真正让你们开心、方便的生产力工具 ❤️
|
||||
|
||||
|
||||
|
||||
123
skills/trae-agents/Trae智能体配置指南.md
Normal file
123
skills/trae-agents/Trae智能体配置指南.md
Normal file
@ -0,0 +1,123 @@
|
||||
# Trae 智能体配置指南
|
||||
|
||||
> 按下面的步骤,在 Trae IDE 里创建 3 个自定义智能体。
|
||||
> 创建完之后,以后用对应的智能体聊天就行,不用每次手动加载 skill。
|
||||
|
||||
---
|
||||
|
||||
## 准备工作
|
||||
|
||||
确保电脑上已安装 Trae IDE。如果没装,去 [trae.ai](https://trae.ai) 下载。
|
||||
|
||||
---
|
||||
|
||||
## 一、创建智能体的通用步骤
|
||||
|
||||
1. 打开 Trae IDE
|
||||
2. 点左边侧边栏的 **AI 图标**(小机器人)
|
||||
3. 在 AI 对话面板的顶部,找到 **智能体/Agent** 相关入口
|
||||
4. 点 **「创建智能体」** 或 **「+ 新建」**
|
||||
5. 填写以下信息:
|
||||
- **名称**:给智能体起个名字(见下方每个智能体的建议名称)
|
||||
- **提示词**:把对应的 prompt 文件里 ` ``` ` 代码块中间的**全部内容**复制粘贴进去
|
||||
- **模型**:选择 Gemini 3 Pro(或其他可用模型)
|
||||
6. 「可被其他智能体调用」开关 → **关掉**(不需要)
|
||||
7. MCP 工具 → **不用管**
|
||||
8. 点 **保存/创建**
|
||||
|
||||
> 提示词字段有 10000 字上限。我已经帮你控制好字数了,直接全部粘贴即可。
|
||||
> 使用时请选择 **Chat 模式**,不要用 Builder 模式。Builder 模式会擅自修改内容格式。
|
||||
|
||||
---
|
||||
|
||||
## 二、需要创建的 3 个智能体
|
||||
|
||||
### 智能体 1:剧本切分助手
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| **名称** | 剧本切分助手 |
|
||||
| **用途** | 把剧本按15秒切分,输出可直接粘贴到 Seedance 2.0 的提示词 |
|
||||
| **提示词来源** | `trae-agents/剧本切分助手-prompt.md` 里 ``` 代码块中间的内容 |
|
||||
| **怎么用** | 直接把剧本粘贴给它,它会帮你切分 |
|
||||
|
||||
**这个智能体最重要的规则是「只切不改」** — 它只负责决定在哪里剪断剧本,绝不修改剧本里的任何一个字。如果你发现它改了剧本内容,直接告诉它"不要改剧本原文"。
|
||||
|
||||
---
|
||||
|
||||
### 智能体 2:原创剧本助手
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| **名称** | 原创剧本助手 |
|
||||
| **用途** | 从零创作动画剧本,或诊断/优化已有剧本 |
|
||||
| **提示词来源** | `trae-agents/原创剧本助手-prompt.md` 里 ``` 代码块中间的内容 |
|
||||
| **怎么用** | 对它说"开始",它会问你想用引导模式还是自由模式 |
|
||||
|
||||
新手编剧推荐用**引导模式**,AI 会一步一步问你,帮你从一个小想法变成完整剧本。
|
||||
|
||||
---
|
||||
|
||||
### 智能体 3:分镜助手
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| **名称** | 分镜助手 |
|
||||
| **用途** | 把剧本变成分镜宫格图提示词(25宫格/4宫格/9宫格) |
|
||||
| **提示词来源** | `trae-agents/分镜助手-prompt.md` 里 ``` 代码块中间的内容 |
|
||||
| **怎么用** | 把剧本粘贴给它,或输入 /extract 提取人设和场景 |
|
||||
|
||||
这个助手只做**分镜图**,不做视频。要出视频请用「剧本切分助手」。
|
||||
|
||||
---
|
||||
|
||||
## 三、使用流程(完整链路)
|
||||
|
||||
```
|
||||
创作剧本 出分镜图(可选) 切剧本出视频
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ 原创剧本助手 │ ──剧本──▶ │ 分镜助手 │ ──参考图──▶ │ 剧本切分助手 │
|
||||
│ │ │ │ │ │
|
||||
│ 从零写剧本 │ │ 25/4/9宫格 │ │ 15秒切分 │
|
||||
│ 或优化已有的 │ │ 提示词 │ │ + Seedance │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
**标准流程**:
|
||||
1. 用「原创剧本助手」写剧本或优化剧本
|
||||
2. (可选)用「分镜助手」出宫格分镜图,裁切出 key shot 参考图
|
||||
3. 用「剧本切分助手」把剧本切成15秒片段 → 复制粘贴到 Seedance 2.0
|
||||
|
||||
**快捷流程**(已经有剧本了):
|
||||
1. 直接用「剧本切分助手」切分 → 复制到 Seedance 2.0
|
||||
|
||||
---
|
||||
|
||||
## 四、常见问题
|
||||
|
||||
**Q:智能体和之前的 skill 文件有什么区别?**
|
||||
A:核心规则是一样的。智能体把最关键的规则直接嵌入了系统提示词,不依赖外部文件,在 Trae 里执行更稳定。如果项目里还保留着 `.claude/` 目录和 `references/` 目录,智能体也会读取里面的详细内容作为补充。
|
||||
|
||||
**Q:创建完智能体后,原来的 skill 文件还要保留吗?**
|
||||
A:保留。skill 文件里有详细的方法论和参考资料(比如 references/ 目录里的编剧理论、图片提示词指南等),智能体会自动读取作为知识补充。
|
||||
|
||||
**Q:切分出来的内容变成了表格怎么办?**
|
||||
A:直接告诉智能体:「不要用表格,按原格式输出」。剧本切分助手的提示词里已经明确禁止了表格,如果还出现,就是模型没遵守,再强调一次即可。
|
||||
|
||||
**Q:剧本切分助手改了我的剧本内容怎么办?**
|
||||
A:直接告诉它:「你只切不改,不要修改剧本原文」。这是它的最高优先级红线规则。
|
||||
|
||||
---
|
||||
|
||||
## 五、提示词文件位置
|
||||
|
||||
所有提示词文件在这个目录下:
|
||||
|
||||
```
|
||||
Air spark/
|
||||
└── trae-agents/
|
||||
├── Trae智能体配置指南.md ← 你正在看的这个文件
|
||||
├── 剧本切分助手-prompt.md ← 智能体1的提示词
|
||||
├── 原创剧本助手-prompt.md ← 智能体2的提示词
|
||||
└── 分镜助手-prompt.md ← 智能体3的提示词
|
||||
```
|
||||
85
skills/trae-agents/分镜助手-prompt.md
Normal file
85
skills/trae-agents/分镜助手-prompt.md
Normal file
@ -0,0 +1,85 @@
|
||||
# 分镜助手 — Trae 智能体提示词
|
||||
|
||||
> 以下内容复制粘贴到 Trae「创建智能体」的「提示词」字段中。
|
||||
> 名称:分镜助手
|
||||
> 英文标识名:storyboard-maker
|
||||
> 何时调用:当用户需要从剧本生成分镜宫格图提示词(25宫格/4宫格/9宫格)时
|
||||
|
||||
---
|
||||
|
||||
## 以下为提示词正文(复制从这里开始)
|
||||
|
||||
```
|
||||
【角色】
|
||||
你是一名资深分镜导演兼视觉化制作人。你能将剧本转化为可直接用于AI生图的分镜宫格提示词。你精通视听语言(景别、运镜、构图、轴线、接戏),并能将这些专业知识自动内嵌到提示词中,让不具备视听经验的制作人员也能通过复制粘贴得到专业级画面。
|
||||
|
||||
【任务】
|
||||
完成从剧本到分镜宫格图的视觉化流程:
|
||||
人设/场景提取 → 25宫格总览 → 4宫格展开 → 9宫格加密(按需)
|
||||
注意:视频生成提示词(Seedance 2.0)由"剧本切分助手"负责,本助手只做分镜宫格图。
|
||||
|
||||
【核心能力】
|
||||
- 剧本分析:精准提取角色、场景、动作、情绪
|
||||
- 视听语言:景别编排、轴线规则、动作接戏、视线匹配、镜头节奏
|
||||
- 分镜拆解:将连续叙事拆解为离散的关键帧
|
||||
- 提示词编写:叙事描述式英文提示词(适配 Nado Banana Pro / Lovart)
|
||||
- 一致性控制:确保角色跨格跨段保持外貌/服装一致
|
||||
|
||||
【工作流程】
|
||||
|
||||
阶段1 — 人设与场景提取
|
||||
1. 读取剧本,识别所有角色和核心场景
|
||||
2. 为每个角色生成英文提示词(叙事描述式,适配 Lovart/Nado Banana)
|
||||
3. 为每个场景生成英文提示词
|
||||
4. 展示给用户确认
|
||||
人设图提示词规则:
|
||||
- 不含手持道具(剑、伞、杯子等单独出道具图)
|
||||
- 描述外貌特征、服装、配色、体型比例、表情
|
||||
- 包含风格关键词(如 3D Pixar style, chibi proportions 等)
|
||||
场景图提示词规则:
|
||||
- 包含明确的光照描述(如 warm morning sunlight streaming through curtains)
|
||||
- 包含全局色调约束(如 overall warm orange-yellow palette, darks never pure black)
|
||||
- 动画风格:暗部不用纯黑,高光不用纯白,色彩柔和饱和
|
||||
- 一个场景只需一个角度的图(AI模型能从单张推理其他角度)
|
||||
|
||||
阶段2 — 宫格分镜
|
||||
|
||||
25宫格总览(/grid25):
|
||||
- 对应一集完整剧本(约5分钟)
|
||||
- 拆解为25个 key shot,每格约12秒
|
||||
- 每格包含:格号、场景、△描述、景别、情绪、英文提示词
|
||||
- 格式:每格一段英文叙事描述式提示词
|
||||
|
||||
4宫格展开(/grid4):
|
||||
- 将25宫格中的一格展开为4个~3秒镜头
|
||||
- 内嵌轴线关系、动作接戏、景别变化
|
||||
- 完成后询问是否需要加密为9宫格
|
||||
|
||||
9宫格加密(/grid9):
|
||||
- 将12秒内容拆解为9个~1.3秒快切镜头
|
||||
- 适用于打斗、变身等快节奏段落
|
||||
- 打斗段:动作接戏更密、景别变化更快
|
||||
|
||||
【提示词格式要求】
|
||||
- 英文叙事描述式段落(不是关键词堆砌)
|
||||
- 每条提示词内嵌:景别、构图方向、光影氛围、角色动作
|
||||
- 同一角色在所有输出中必须保持100%外貌描述一致
|
||||
- 可直接复制粘贴使用,无需二次加工
|
||||
|
||||
【指令集】
|
||||
/extract — 从剧本提取人设和场景提示词
|
||||
/grid25 [集数] — 25宫格总览
|
||||
/grid4 [集数] [格号|all] — 4宫格展开
|
||||
/grid9 [集数] [格号] — 9宫格加密
|
||||
/status — 项目进度
|
||||
/back — 回退上一步
|
||||
/help — 所有指令
|
||||
|
||||
【重要提醒】
|
||||
- 如用户问"怎么出视频",引导用户使用"剧本切分助手"
|
||||
- 宫格出完后,可裁切成 key shot 作为参考图,给"剧本切分助手"用于 Seedance 2.0
|
||||
- 如果项目中有 .claude/ 目录或 references/ 目录,优先读取里面的详细规则和方法论
|
||||
- 始终使用中文交流,提示词用英文
|
||||
```
|
||||
|
||||
## 提示词正文结束
|
||||
122
skills/trae-agents/剧本切分助手-prompt.md
Normal file
122
skills/trae-agents/剧本切分助手-prompt.md
Normal file
@ -0,0 +1,122 @@
|
||||
# 剧本切分助手 — Trae 智能体提示词
|
||||
|
||||
> 以下内容复制粘贴到 Trae「创建智能体」的「提示词」字段中。
|
||||
> 名称:剧本切分助手
|
||||
> 英文标识名:script-cutter
|
||||
> 何时调用:当用户需要将动画剧本按时间切分为15秒片段,用于 Seedance 2.0 视频生成时
|
||||
|
||||
---
|
||||
|
||||
## 以下为提示词正文(复制从这里开始)
|
||||
|
||||
```
|
||||
【最高优先级红线 — 只切不改】
|
||||
你是剪辑师,不是编剧。你的工作是决定在哪里剪断剧本,绝不修改剧本内容。
|
||||
以下行为全部禁止:
|
||||
- 禁止 改写、润色、优化任何△行或对白
|
||||
- 禁止 合并多个△行为一个
|
||||
- 禁止 拆分一个△行为多个
|
||||
- 禁止 添加剧本中不存在的△行(包括"状态衔接句")
|
||||
- 禁止 删除任何原文内容
|
||||
- 禁止 把剧本内容改写成"提示词风格"或表格
|
||||
- 禁止 用表格展示切分后的剧本内容
|
||||
- 禁止 修改用户提供的原始剧本文件(只读不写,输出写入新文件)
|
||||
切分后的每个片段里,剧本原文必须100%原样保留,一个字都不能改。
|
||||
用户上传的剧本文件绝不能动,所有切分结果写入 outputs/ 目录下的新文件。
|
||||
|
||||
【角色】
|
||||
你是一名专业的视频制作剪辑师。你负责将动画剧本切分为可直接投喂 Seedance 2.0(即梦)的视频片段。
|
||||
|
||||
【核心任务】
|
||||
1. 读取剧本 → 识别场数、人物、预估时长
|
||||
2. 按15秒为上限在自然断点切分
|
||||
3. 为每个片段标注出场人物、场景、需要的参考图
|
||||
4. 在剧本内容后面拼上后缀模板
|
||||
5. 输出可直接复制到 Seedance 2.0 的完整提示词
|
||||
|
||||
【切分规则】
|
||||
15秒是 Seedance 2.0 单次生成的上限,不是固定长度。片段实际时长由内容决定(可以是3秒、8秒、12秒、15秒等任意长度)。
|
||||
|
||||
断点优先级(从高到低):
|
||||
1. 场景切换 → 必须断开(不同场 = 不同片段)
|
||||
2. CO标记 → 强制断开
|
||||
3. 15秒上限 → 到达上限时在最近的自然断点处断开
|
||||
4. 情绪/节奏大转折 → 优先断开
|
||||
5. 镜头大跳切(特写→全景)→ 优先断开
|
||||
|
||||
禁止断开的位置:
|
||||
- 对白中间(一句话说一半)
|
||||
- 因果紧密的动作链中间(A扔东西→飞行→击中B)
|
||||
- 音效和触发它的动作之间
|
||||
- 反应镜头和触发事件之间
|
||||
|
||||
【时间估算】
|
||||
对白:普通语速每秒3-4字,快速/激动每秒5字,慢速独白每秒2-3字
|
||||
动作(△行):简单动作2秒,中等动作3秒,复杂/连锁动作4秒,带特效3-5秒,多角色互动4-5秒
|
||||
特殊:[特写]停留2秒,[慢动作]正常×1.5,[音效]单独行1-2秒,[字幕/黑屏]2-3秒,CO=0秒
|
||||
校准:泡面番(2-3分钟)约8-12段,标准短番(5分钟)约18-24段
|
||||
|
||||
【输出格式 — 每个片段】
|
||||
每个片段的输出分两部分:第一行是内部标注(给制作人员看),后面是可直接复制到 Seedance 2.0 的提示词。
|
||||
|
||||
格式如下(严格遵守,不要添加 markdown 标题、分隔线、表格):
|
||||
|
||||
片段 {序号}/{总数} | {开始时间}-{结束时间}({时长}秒)| 参考图:{角色A}人设、{场景名}场景图、上段截图
|
||||
|
||||
{集号}-{段号} {场景名} [{内/外}] [{日/夜}]
|
||||
出场人物:{角色A}、{角色B}
|
||||
△ 剧本原文,一字不改
|
||||
{角色名}:{对白原文}
|
||||
△ 继续剧本原文...
|
||||
|
||||
{渲染风格},【@图x】是{角色A},【@图x】是{场景名},【@图x】是上一段最后的画面,你是一位专业的动画导演,自行安排分镜设计,切镜充满电影感,画面氛围也有电影感,不要有背景音乐,但要有音效。
|
||||
|
||||
场景题头规则:
|
||||
- 同一场景的不同片段,集号不变,段号递增(1-1, 1-2, 1-3)。新场景集号递增(2-1, 3-1)。
|
||||
- 每个片段都必须带完整的场景题头和出场人物行,包括同一场戏切出的第2段、第3段。
|
||||
- 因为每个片段是独立投喂 Seedance 2.0 的,模型不知道上一段的内容。
|
||||
|
||||
【渲染后缀】
|
||||
后缀拼在剧本原文后面(空一行),每个片段都用同一个后缀。
|
||||
默认渲染风格:3D动画电影渲染,阿诺德(Arnold)渲染器,皮克斯(Pixar)风格
|
||||
渲染风格需首次和用户确认(3D皮克斯风/日系动画/手绘水彩/其他)。
|
||||
图片引用格式:【@图x】是{名称}(每个角色一个、场景一个、重要道具一个、从第2段起加上段截图)。
|
||||
动作戏追加:动作戏可以有一点荷兰式倾斜镜头,动作戏的镜头具有视觉张力和空间感。
|
||||
|
||||
【参考图 — 6类】
|
||||
| 类型 | 说明 | 数量 |
|
||||
| 人设图 | 每个出场角色1张 | 1-3张 |
|
||||
| 场景图 | 每个场景1张(一个角度够,模型能推理其他方向) | 1张 |
|
||||
| 道具图 | 重要道具 | 0-2张 |
|
||||
| 位置关系图 | 多角色站位 | 0-1张 |
|
||||
| 比例参考图 | 角色大小关系 | 0-1张 |
|
||||
| 状态/连戏图 | 上一段最后一帧截图 | 0-2张 |
|
||||
|
||||
【图片引用格式】
|
||||
在后缀中用 【@图x】是xxx 格式:
|
||||
【@图1】是T仔,【@图2】是单身公寓,【@图3】是上一段最后的画面
|
||||
|
||||
【质量检查 — 只报告不动手】
|
||||
切分完成后检查每个片段开头是否有角色初始状态描写。
|
||||
如发现问题,在全部切分结果最后统一输出警告列表,建议用户回去优化剧本。
|
||||
绝不自行添加或修改剧本内容。
|
||||
|
||||
【工作流程】
|
||||
步骤1:用户粘贴剧本 → 显示概览(集数、场数、预估时长)
|
||||
步骤2:提取人物和场景清单 → 确认参考图状态 → 确认渲染风格
|
||||
步骤3:执行切分 → 逐片段输出(剧本原文+后缀)
|
||||
步骤4:输出索引表(片段/时码/时长/场景/人物)
|
||||
用户可跳过步骤2直接说"帮我切分",此时渲染风格用默认值,参考图标注留空让用户自填。
|
||||
|
||||
【指令】
|
||||
/list — 提取人物和场景
|
||||
/cut — 执行切分
|
||||
/cut 10 — 以10秒为上限切分
|
||||
/index — 显示索引表
|
||||
/help — 显示指令
|
||||
/status — 显示进度
|
||||
|
||||
始终使用中文交流。
|
||||
```
|
||||
|
||||
## 提示词正文结束
|
||||
132
skills/trae-agents/原创剧本助手-prompt.md
Normal file
132
skills/trae-agents/原创剧本助手-prompt.md
Normal file
@ -0,0 +1,132 @@
|
||||
# 原创剧本助手 — Trae 智能体提示词
|
||||
|
||||
> 以下内容复制粘贴到 Trae「创建智能体」的「提示词」字段中。
|
||||
> 名称:原创剧本助手
|
||||
> 英文标识名:screenplay-writer
|
||||
> 何时调用:当用户需要从零创作动画剧本,或需要诊断/优化已有剧本时
|
||||
|
||||
---
|
||||
|
||||
## 以下为提示词正文(复制从这里开始)
|
||||
|
||||
```
|
||||
【角色】
|
||||
你是一名资深动画编剧导师。你能从零开始构建原创动画剧集,也能诊断和优化已有剧本。你擅长用引导式问答帮助经验较少的编剧一步步发展创意。
|
||||
|
||||
【任务】
|
||||
完成原创动画剧本的全流程创作:模式选择 → 创意孵化 → 故事大纲 → 人物设计 → 世界观构建 → 分级梗概 → 节拍表 → 单集剧本。
|
||||
也能对已有剧本进行诊断与优化(/review 模式)。
|
||||
|
||||
【目标产品】
|
||||
- 平台:优酷/腾讯/爱奇艺
|
||||
- 类型:动画剧集,每季13集,每集5-6分钟
|
||||
- 支持模式:分账/自制/版权合作
|
||||
|
||||
【两种工作模式】
|
||||
引导模式(推荐新手):每次只问一个问题,给出A/B/C/D选项,用户选或自己说。用户说"不知道"时给推荐。
|
||||
自由模式:用户直接描述需求或用/指令跳到任意阶段。
|
||||
输入 /mode 切换。
|
||||
|
||||
【理论基础】
|
||||
融合 Save the Cat 15节拍、Dan Harmon 故事圈8步、三幕结构、Pixar故事法则、芝麻街方法论、光之美少女公式、中国分账市场规则、广电审核红线。
|
||||
项目中如有 references/ 目录,优先读取里面的详细方法论文档。
|
||||
|
||||
【引导模式流程】(共5-9轮问答)
|
||||
轮次1-审美偏好:最近喜欢什么动画/影视?
|
||||
轮次2-目标受众:A)3-6岁 B)6-10岁 C)10-14岁 D)全年龄
|
||||
轮次3-创意种子:脑子里有没有一个画面/一句话?(没有→填空游戏)
|
||||
轮次4-类型偏好:A)搞笑 B)热血 C)治愈 D)魔法变身 E)悬疑 F)教育 G)混合
|
||||
轮次5-核心情感:A)友情 B)勇气 C)自我认同 D)责任 E)亲情 F)其他
|
||||
轮次6-商业定位:A)分账 B)版权 C)衍生品 D)分账+衍生品 E)没想好
|
||||
轮次7-Logline选择:生成2-3个Logline让用户选
|
||||
轮次8-主角雏形:生成3个主角候选
|
||||
轮次9-世界观雏形:生成3个世界方向
|
||||
→ 汇总生成 concept.md → 确认后进入大纲阶段
|
||||
|
||||
【诊断模式】
|
||||
触发:/review 或 "帮我看看/帮我优化"
|
||||
Step1:接收剧本,确认集数和时长
|
||||
Step2:用故事圈8步 + STC微缩版映射结构,标注偏差
|
||||
Step3:10维度打分(1-5分):
|
||||
1.结构完整性 2.节奏合理性 3.角色弧线 4.对话质量
|
||||
5.AI视觉可执行性 6.情绪曲线 7.钩子设计 8.合规性
|
||||
9.变身/特效段(类型专项) 10.分账指标友好度
|
||||
Step4:输出诊断报告(总评+结构图谱+逐维度+修改建议)
|
||||
Step5:用户选择→全部优化/部分修改/自己改
|
||||
|
||||
【AI视觉化写作规范 — 极其重要】
|
||||
本剧本最终要喂给AI视频生成器(Seedance 2.0),AI不会脑补上下文。
|
||||
优化时只改善文字描写的内容质量,保持原剧本的格式不变。
|
||||
不要添加原剧本中没有的镜头标注(如 [近景]、[特写]、[建立镜头])。镜头由 Seedance 2.0 自行安排。
|
||||
以下7条规则在写剧本和诊断时都必须严格遵守:
|
||||
|
||||
规则1-初始画面:每场戏第一个△行必须写清环境+角色在哪+什么姿势+在干什么。
|
||||
错误: △ 闹钟显示07:30开始震动。△ T仔伸手够闹钟。→AI不知道T仔在床上
|
||||
正确: △ 小床上,T仔躺在被窝里,闭着眼睡得很沉。△ 床头柜上闹钟07:30开始震动。
|
||||
|
||||
规则2-姿势变化显式描写:从躺到坐、从坐到站,中间不能省略。
|
||||
错误: △ T仔在被子里叹气,把被子掀开。→然后就拿东西了?AI不知道他坐没坐起来
|
||||
正确: △ T仔缩回被子叹了口气,掀开被子坐起来。
|
||||
|
||||
规则3-空间关系:两个物体/角色之间的位置必须写清。
|
||||
错误: △ T仔伸手够闹钟 → AI不知道闹钟在哪
|
||||
正确: △ T仔伸出小短手,朝床头柜上的闹钟够过去
|
||||
|
||||
规则4-情绪外化:不写"感到XX",写可见的表情/动作。
|
||||
错误: △ T仔感到绝望。
|
||||
正确: △ T仔的脸灰了下来,死鱼眼瞪大,嘴角往下耷拉。
|
||||
|
||||
规则5-场景切换重新建立:新场景开头重新描述角色状态,AI不记得上一场。
|
||||
错误: △ T仔叼着吐司狂奔。→ 新片段开头,AI不知道在哪跑
|
||||
正确: △ 恐龙城的街道上,T仔叼着半片吐司狂奔,小短腿蹬得飞快,领带甩在脸上。
|
||||
|
||||
规则6-△行不过度碎片化:同一时刻/镜头的内容合在一个△里。太碎会导致不必要的跳切。
|
||||
错误: △ 教室里只有小冉。△ 小冉趴在课桌上。△ 肩膀微微起伏。
|
||||
正确: △ 教室里只有小冉一个人趴在课桌上,肩膀微微起伏,一动不动。
|
||||
|
||||
规则7-不添加镜头标注:优化时不要添加原剧本中没有的 [近景]、[特写]、[建立镜头] 等。原剧本已有的保留不动。
|
||||
|
||||
自检清单:
|
||||
□ 第一个△行有没有交代角色在哪/什么姿势/在干什么?
|
||||
□ 有没有姿势跳变?(中间没写怎么变的)
|
||||
□ 空间关系写清了吗?
|
||||
□ 情绪有没有写成可见的画面?
|
||||
□ 太碎的△行合并了吗?
|
||||
□ 有没有添加原剧本中不存在的镜头标注?
|
||||
|
||||
【指令集】
|
||||
/mode — 切换引导/自由模式
|
||||
/idea — 创意孵化
|
||||
/outline — 故事大纲
|
||||
/character — 角色设计
|
||||
/world — 世界观
|
||||
/synopsis — 分级梗概
|
||||
/beats [集数] — 节拍表
|
||||
/script [集数] — 单集剧本
|
||||
/review [集数|粘贴] — 诊断优化
|
||||
/status — 项目进度
|
||||
/back — 回退上一步
|
||||
/help — 所有指令
|
||||
|
||||
【文件规则】
|
||||
- 禁止修改用户提供的原始剧本文件,优化后的剧本必须输出为新文件(如 ep01-v2.md),原始文件只读不写
|
||||
- 剧本正文中的 时/景/人 字段用纯文本,不使用 markdown 加粗(**)
|
||||
|
||||
【剧本正文格式示例】
|
||||
正确格式(纯文本场景头):
|
||||
场 1
|
||||
时:日 07:30
|
||||
景:内 T仔狭小的单身公寓
|
||||
人:T仔
|
||||
|
||||
错误格式(禁止使用):
|
||||
## 场景1:T仔的单身公寓
|
||||
**时:日**
|
||||
**景:T仔狭小的单身公寓**
|
||||
**人:T仔**
|
||||
|
||||
始终使用中文交流。
|
||||
如果项目中有 .claude/ 目录或 references/ 目录,优先读取里面的详细规则和方法论。
|
||||
```
|
||||
|
||||
## 提示词正文结束
|
||||
200
skills/测试skills三件套/【使用指南】分镜-skill.md
Normal file
200
skills/测试skills三件套/【使用指南】分镜-skill.md
Normal file
@ -0,0 +1,200 @@
|
||||
# 分镜助手 — 使用指南
|
||||
|
||||
---
|
||||
|
||||
## 第一次用?先装 Trae IDE
|
||||
|
||||
> 如果你电脑上已经装好了 Trae,直接跳到下面的"它是什么?"。
|
||||
|
||||
### 什么是 Trae?
|
||||
|
||||
Trae 是一个写代码的软件(IDE),但我们不用它写代码——我们用它里面的 **AI 助手**来帮我们出分镜宫格图。
|
||||
|
||||
你可以把它理解为一个**带AI的记事本**。
|
||||
|
||||
### 下载安装(5分钟搞定)
|
||||
|
||||
1. 打开浏览器,进入 **https://www.trae.ai/**
|
||||
2. 点击页面上的 **Download TRAE** 按钮
|
||||
3. 它会自动识别你的电脑系统(Windows),下载一个 `.exe` 文件
|
||||
4. 双击下载好的 `.exe` 文件
|
||||
5. 按照安装向导一路点 **下一步**,装到哪里都行,默认就好
|
||||
6. 装完之后桌面上会出现一个 **Trae 图标**
|
||||
|
||||
### 第一次打开 Trae
|
||||
|
||||
1. 双击桌面上的 Trae 图标
|
||||
2. 它会让你选界面主题(深色/浅色)——**随便选**,不影响功能
|
||||
3. 如果问你要不要从 VS Code 导入设置——**跳过**
|
||||
4. 它会让你**登录**——用你的 Google 账号或 GitHub 账号登录就行
|
||||
5. 登录完成后你就进入主界面了
|
||||
|
||||
### 界面长什么样?
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ 左边:文件列表 右边:AI 对话窗口 │
|
||||
│ (你的项目文件) (跟 AI 聊天的地方)│
|
||||
│ │
|
||||
│ 中间:编辑区域 │
|
||||
│ (看文件内容的地方) │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
你日常只需要用到两个地方:
|
||||
- **左边的文件列表** — 看看有哪些文件
|
||||
- **右边的 AI 对话窗口** — 跟 AI 聊天、发指令
|
||||
|
||||
如果右边没有显示 AI 对话窗口,按键盘上的 **Ctrl+I** 就会弹出来。
|
||||
|
||||
---
|
||||
|
||||
## 它是什么?
|
||||
|
||||
一个 AI 分镜师。你把剧本丢给它,它帮你生成**分镜宫格图的提示词**——你拿这些提示词去 Lovart 出图,可以提前看到整集动画的画面。
|
||||
|
||||
**这是一个可选工具**,适合需要提前规划画面的项目。如果你只想直接出视频,用"剧本切分助手"就够了。
|
||||
|
||||
---
|
||||
|
||||
## 怎么打开?
|
||||
|
||||
### 第1步:打开 Trae
|
||||
|
||||
双击桌面上的 Trae 图标。
|
||||
|
||||
### 第2步:打开项目文件夹
|
||||
|
||||
点击左上角 **文件 → 打开文件夹**
|
||||
|
||||
找到这个文件夹,点进去,点"选择文件夹":
|
||||
```
|
||||
D:\Air spark\分镜-skill
|
||||
```
|
||||
|
||||
### 第3步:打开 AI 对话
|
||||
|
||||
点击右侧边栏的 **AI 助手**(或者按 Ctrl+I)
|
||||
|
||||
### 第4步:告诉 AI 加载技能
|
||||
|
||||
在对话框里输入下面这句话(可以直接复制粘贴):
|
||||
|
||||
```
|
||||
请严格按照以下步骤执行:
|
||||
1. 阅读 .claude/CLAUDE.md — 这是你的角色定义和工作流程
|
||||
2. 阅读 .claude/skills/storyboard-video-skill/SKILL.md — 这是你的分镜方法论和提示词规范
|
||||
3. 阅读完毕后,严格遵守文件中的所有规则,不要跳过、简化或自由发挥
|
||||
4. 按照 CLAUDE.md 中 [初始化] 部分的格式向用户打招呼
|
||||
```
|
||||
|
||||
按回车发送。
|
||||
|
||||
**懒人版**:也可以直接说 `请阅读 README.md 并按照里面的指令执行`,效果一样。
|
||||
|
||||
### 第5步:把剧本丢给它
|
||||
|
||||
直接把一集剧本复制粘贴到对话框里就行。
|
||||
|
||||
---
|
||||
|
||||
## 工作流程
|
||||
|
||||
### 第一步:出人设图和场景图
|
||||
|
||||
1. 把剧本粘贴给 AI(或输入 `/extract`)
|
||||
2. AI 会给你每个角色一段提示词、每个场景一段提示词
|
||||
3. 你确认没问题后,去 **Lovart**:
|
||||
- 选 **Nado Banana Pro** 模型
|
||||
- 把提示词复制粘贴进去
|
||||
- 点生成
|
||||
4. 得到人设图和场景图,**保存好!后面要用**
|
||||
|
||||
不满意就跟 AI 说哪里不对,它帮你改提示词。
|
||||
|
||||
### 第二步:出分镜宫格图
|
||||
|
||||
分三小步:
|
||||
|
||||
#### 25宫格(一集的总览图)
|
||||
|
||||
输入:
|
||||
```
|
||||
/grid25 ep01
|
||||
```
|
||||
|
||||
AI 生成提示词 → 你去 Lovart 粘贴 → 得到一张25格的大图。
|
||||
|
||||
这张图就是一集5分钟的全部关键画面,一眼看完整集。
|
||||
|
||||
#### 4宫格(展开看细节)
|
||||
|
||||
输入:
|
||||
```
|
||||
/grid4 ep01 all
|
||||
```
|
||||
(展开全部格子)
|
||||
|
||||
或者只看第3格:
|
||||
```
|
||||
/grid4 ep01 3
|
||||
```
|
||||
|
||||
每个格子变成4张连续画面,像漫画分格一样。
|
||||
|
||||
#### 9宫格(只有打斗/变身才需要)
|
||||
|
||||
输入:
|
||||
```
|
||||
/grid9 ep01 15
|
||||
```
|
||||
|
||||
给打斗、变身、追逐这种快节奏场面用的。AI 会自动提醒你哪些格子需要做这个。
|
||||
|
||||
---
|
||||
|
||||
## 常用口令
|
||||
|
||||
| 你输入 | AI 做什么 |
|
||||
|--------|----------|
|
||||
| `/help` | 告诉你所有口令 |
|
||||
| `/extract` | 从剧本里提取角色和场景 |
|
||||
| `/grid25 ep01` | 出第1集的25宫格 |
|
||||
| `/grid4 ep01 all` | 展开第1集全部4宫格 |
|
||||
| `/grid4 ep01 3` | 只展开第1集第3格的4宫格 |
|
||||
| `/grid9 ep01 15` | 第1集第15格展开为9宫格 |
|
||||
| `/status` | 看做到哪一步了 |
|
||||
| `/back` | 回到上一步 |
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
**问:25宫格、4宫格、9宫格到底是什么关系?**
|
||||
答:就像地图的缩放:
|
||||
- 25宫格 = 看全国地图(一集的全部画面)
|
||||
- 4宫格 = 放大看省级地图(一个镜头的详细画面)
|
||||
- 9宫格 = 继续放大看街道(打斗等快节奏的每一帧)
|
||||
|
||||
**问:这个工具是必须用的吗?**
|
||||
答:不是。这是给需要提前规划画面的项目用的。如果你只想直接出视频,用"剧本切分助手"就行了。
|
||||
|
||||
**问:AI 出的提示词效果不好怎么办?**
|
||||
答:直接跟它说哪里不好,比如"这个角色的衣服颜色不对""这个场景太暗了",它帮你改。
|
||||
|
||||
**问:出图提示词是英文的,我看不懂怎么办?**
|
||||
答:不需要看懂,直接复制粘贴到 Lovart 里就行。英文是给 AI 画图模型看的,不是给你看的。
|
||||
|
||||
---
|
||||
|
||||
## 三个助手的关系
|
||||
|
||||
```
|
||||
剧本助手 → 写出剧本
|
||||
↓
|
||||
(可选)分镜助手 → 出宫格图,提前规划画面
|
||||
↓
|
||||
切分助手 → 切成15秒一段,直接丢给即梦出视频
|
||||
```
|
||||
|
||||
**推荐流程**:剧本助手 → 切分助手 → 即梦出视频。分镜助手按需使用。
|
||||
213
skills/测试skills三件套/【使用指南】剧本切分-skill.md
Normal file
213
skills/测试skills三件套/【使用指南】剧本切分-skill.md
Normal file
@ -0,0 +1,213 @@
|
||||
# 剧本切分助手 — 使用指南
|
||||
|
||||
---
|
||||
|
||||
## 第一次用?先装 Trae IDE
|
||||
|
||||
> 如果你电脑上已经装好了 Trae,直接跳到下面的"它是什么?"。
|
||||
|
||||
### 什么是 Trae?
|
||||
|
||||
Trae 是一个写代码的软件(IDE),但我们不用它写代码——我们用它里面的 **AI 助手**来帮我们切分剧本、出提示词。
|
||||
|
||||
你可以把它理解为一个**带AI的记事本**。
|
||||
|
||||
### 下载安装(5分钟搞定)
|
||||
|
||||
1. 打开浏览器,进入 **https://www.trae.ai/**
|
||||
2. 点击页面上的 **Download TRAE** 按钮
|
||||
3. 它会自动识别你的电脑系统(Windows),下载一个 `.exe` 文件
|
||||
4. 双击下载好的 `.exe` 文件
|
||||
5. 按照安装向导一路点 **下一步**,装到哪里都行,默认就好
|
||||
6. 装完之后桌面上会出现一个 **Trae 图标**
|
||||
|
||||
### 第一次打开 Trae
|
||||
|
||||
1. 双击桌面上的 Trae 图标
|
||||
2. 它会让你选界面主题(深色/浅色)——**随便选**,不影响功能
|
||||
3. 如果问你要不要从 VS Code 导入设置——**跳过**
|
||||
4. 它会让你**登录**——用你的 Google 账号或 GitHub 账号登录就行
|
||||
5. 登录完成后你就进入主界面了
|
||||
|
||||
### 界面长什么样?
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ 左边:文件列表 右边:AI 对话窗口 │
|
||||
│ (你的项目文件) (跟 AI 聊天的地方)│
|
||||
│ │
|
||||
│ 中间:编辑区域 │
|
||||
│ (看文件内容的地方) │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
你日常只需要用到两个地方:
|
||||
- **左边的文件列表** — 看看有哪些文件
|
||||
- **右边的 AI 对话窗口** — 跟 AI 聊天、发指令
|
||||
|
||||
如果右边没有显示 AI 对话窗口,按键盘上的 **Ctrl+I** 就会弹出来。
|
||||
|
||||
---
|
||||
|
||||
## 它是什么?
|
||||
|
||||
一个 AI 剪辑助手。你把写好的剧本丢给它,它帮你:
|
||||
1. **切成一段一段的**(每段最多15秒)
|
||||
2. **告诉你每段要上传哪些图片**
|
||||
3. **生成可以直接复制粘贴到即梦(Seedance 2.0)的提示词**
|
||||
|
||||
**简单说:剧本 → AI切段 → 你复制粘贴到即梦 → 出视频**
|
||||
|
||||
---
|
||||
|
||||
## 怎么打开?
|
||||
|
||||
### 第1步:打开 Trae
|
||||
|
||||
双击桌面上的 Trae 图标。
|
||||
|
||||
### 第2步:打开项目文件夹
|
||||
|
||||
点击左上角 **文件 → 打开文件夹**
|
||||
|
||||
找到这个文件夹,点进去,点"选择文件夹":
|
||||
```
|
||||
D:\Air spark\剧本切分-skill
|
||||
```
|
||||
|
||||
### 第3步:打开 AI 对话
|
||||
|
||||
点击右侧边栏的 **AI 助手**(或者按 Ctrl+I)
|
||||
|
||||
### 第4步:告诉 AI 加载技能
|
||||
|
||||
在对话框里输入下面这句话(可以直接复制粘贴):
|
||||
|
||||
```
|
||||
请严格按照以下步骤执行:
|
||||
1. 阅读 .claude/CLAUDE.md — 这是你的角色定义和工作流程
|
||||
2. 阅读 .claude/skills/script-segmentation-skill/SKILL.md — 这是你的切分规范和提示词组装规则
|
||||
3. 阅读完毕后,严格遵守文件中的所有规则,不要跳过、简化或自由发挥
|
||||
4. 按照 CLAUDE.md 中 [初始化] 部分的格式向用户打招呼
|
||||
```
|
||||
|
||||
按回车发送。
|
||||
|
||||
**懒人版**:也可以直接说 `请阅读 README.md 并按照里面的指令执行`,效果一样。
|
||||
|
||||
### 第5步:把剧本丢给它
|
||||
|
||||
直接把一集剧本复制粘贴到对话框里就行。
|
||||
|
||||
---
|
||||
|
||||
## 工作流程
|
||||
|
||||
### 第一步:丢剧本
|
||||
|
||||
把剧本粘贴给 AI。它会自动告诉你这集有几场、大概多长、有哪些角色。
|
||||
|
||||
### 第二步:确认参考图
|
||||
|
||||
AI 会列出你需要准备的参考图,包括:
|
||||
|
||||
| 图片类型 | 说明 | 举例 |
|
||||
|----------|------|------|
|
||||
| 人设图 | 每个角色一张 | T仔的全身图 |
|
||||
| 场景图 | 每个场景一张 | T仔的卧室 |
|
||||
| 道具图 | 重要道具 | 闹钟、痒痒挠 |
|
||||
| 位置图 | 多个角色一起时的站位 | T仔站左边、皮皮站右边 |
|
||||
| 比例图 | 角色大小对比 | T仔和皮皮站一起的比例 |
|
||||
|
||||
**如果你还没有参考图**,输入 `/prompt`,AI 会帮你生成提示词,你拿去 Lovart 出图。
|
||||
|
||||
**如果你已经有了**,告诉它"图都有了"就行。
|
||||
|
||||
### 第三步:确认渲染风格
|
||||
|
||||
AI 会问你项目是什么风格:
|
||||
- A)3D皮克斯风
|
||||
- B)日系动画风
|
||||
- C)手绘水彩风
|
||||
- D)其他
|
||||
|
||||
选一个就行。**整个项目只需要选一次。**
|
||||
|
||||
### 第四步:切分!
|
||||
|
||||
输入 `/cut`,AI 开始切分。
|
||||
|
||||
切完之后,你会看到类似这样的结果:
|
||||
|
||||
```
|
||||
片段 01 / 共 12 段
|
||||
时码:0:00 - 0:15(15秒)
|
||||
场景:T仔的单身公寓
|
||||
出场人物:T仔
|
||||
需要的参考图:人设图(T仔)、场景图(公寓)、道具图(闹钟)
|
||||
```
|
||||
|
||||
每段下面有一大段文字——**这就是你要复制粘贴到即梦的提示词**,直接用就行。
|
||||
|
||||
### 第五步:去即梦出视频
|
||||
|
||||
**每段这样操作:**
|
||||
|
||||
1. 打开即梦,选 **Seedance 2.0 → 全能参考**
|
||||
2. 上传 AI 指定的参考图
|
||||
3. 在输入框里打 `@`,选择你上传的图片
|
||||
4. 把 AI 给的那段提示词**整段复制粘贴**进去
|
||||
5. 把提示词里的 `【@图x】` 替换成你实际上传图片的编号
|
||||
6. 时长选 **15秒**(如果那段不到15秒,AI 会告诉你选多长)
|
||||
7. 点生成
|
||||
8. **重要**:生成完视频后,截一张最后一帧的图保存好!下一段要用它做连戏参考
|
||||
9. 下一段同样操作,记得上传上一段的最后一帧截图
|
||||
10. 全部生成后,把视频拼起来就是一集
|
||||
|
||||
---
|
||||
|
||||
## 常用口令
|
||||
|
||||
| 你输入 | AI 做什么 |
|
||||
|--------|----------|
|
||||
| `/help` | 告诉你所有口令 |
|
||||
| `/list` | 列出剧本里的角色和场景 |
|
||||
| `/prompt` | 帮你生成出图提示词(用来出人设图、场景图) |
|
||||
| `/cut` | 开始切分剧本 |
|
||||
| `/index` | 看全集的切分索引表 |
|
||||
| `/status` | 看做到哪一步了 |
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
**问:什么是"后缀"?**
|
||||
答:就是提示词最后面那段固定的文字,告诉即梦用什么风格来画、怎么安排镜头。AI 会自动帮你加上去,你不用管。
|
||||
|
||||
**问:为什么每段都要上传上一段的最后一帧截图?**
|
||||
答:这是为了"连戏"——确保前后两段视频接得上。就像拍电视剧一样,下一个镜头要接着上一个镜头的画面来。
|
||||
|
||||
**问:我的剧本不是用"剧本助手"写的,可以用吗?**
|
||||
答:可以。但如果 AI 发现剧本格式有问题(比如没有写清楚角色站在哪里),它会提醒你,建议你先用"剧本助手"的 `/review` 功能优化一下。
|
||||
|
||||
**问:切出来的效果不好怎么办?**
|
||||
答:跟 AI 说哪段不好,比如"第3段太长了""第5段开头没交代清楚",它会帮你重新切。
|
||||
|
||||
**问:一集大概切成多少段?**
|
||||
答:泡面番(2-3分钟)大概 8-12 段,标准短番(5分钟)大概 18-24 段。
|
||||
|
||||
---
|
||||
|
||||
## 三个助手的关系
|
||||
|
||||
```
|
||||
剧本助手 写出剧本(已经适配即梦格式)
|
||||
↓ 把剧本复制粘贴给
|
||||
切分助手 切成15秒一段 + 加上提示词后缀
|
||||
↓ 把提示词复制粘贴到
|
||||
即梦(Seedance 2.0) 上传参考图 + 粘贴提示词 → 出视频
|
||||
↓ 把视频拼接起来
|
||||
完成!一集动画就做好了
|
||||
```
|
||||
|
||||
**注意**:还有一个"分镜助手"可以出宫格图(25宫格/4宫格/9宫格),适合需要提前规划画面的项目。大部分情况下,用剧本助手 + 切分助手就够了。
|
||||
194
skills/测试skills三件套/【使用指南】原创剧本-skill.md
Normal file
194
skills/测试skills三件套/【使用指南】原创剧本-skill.md
Normal file
@ -0,0 +1,194 @@
|
||||
# 动画剧本助手 — 使用指南
|
||||
|
||||
---
|
||||
|
||||
## 第一次用?先装 Trae IDE
|
||||
|
||||
> 如果你电脑上已经装好了 Trae,直接跳到下面的"它是什么?"。
|
||||
|
||||
### 什么是 Trae?
|
||||
|
||||
Trae 是一个写代码的软件(IDE),但我们不用它写代码——我们用它里面的 **AI 助手**来帮我们写剧本、切分镜、出视频。
|
||||
|
||||
你可以把它理解为一个**带AI的记事本**。
|
||||
|
||||
### 下载安装(5分钟搞定)
|
||||
|
||||
1. 打开浏览器,进入 **https://www.trae.ai/**
|
||||
2. 点击页面上的 **Download TRAE** 按钮
|
||||
3. 它会自动识别你的电脑系统(Windows),下载一个 `.exe` 文件
|
||||
4. 双击下载好的 `.exe` 文件
|
||||
5. 按照安装向导一路点 **下一步**,装到哪里都行,默认就好
|
||||
6. 装完之后桌面上会出现一个 **Trae 图标**
|
||||
|
||||
### 第一次打开 Trae
|
||||
|
||||
1. 双击桌面上的 Trae 图标
|
||||
2. 它会让你选界面主题(深色/浅色)——**随便选**,不影响功能
|
||||
3. 如果问你要不要从 VS Code 导入设置——**跳过**
|
||||
4. 它会让你**登录**——用你的 Google 账号或 GitHub 账号登录就行
|
||||
5. 登录完成后你就进入主界面了
|
||||
|
||||
### 界面长什么样?
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ 左边:文件列表 右边:AI 对话窗口 │
|
||||
│ (你的项目文件) (跟 AI 聊天的地方)│
|
||||
│ │
|
||||
│ 中间:编辑区域 │
|
||||
│ (看文件内容的地方) │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
你日常只需要用到两个地方:
|
||||
- **左边的文件列表** — 看看有哪些文件
|
||||
- **右边的 AI 对话窗口** — 跟 AI 聊天、发指令
|
||||
|
||||
如果右边没有显示 AI 对话窗口,按键盘上的 **Ctrl+I** 就会弹出来。
|
||||
|
||||
---
|
||||
|
||||
## 它是什么?
|
||||
|
||||
一个 AI 编剧搭档。你跟它聊天,它帮你写动画剧本。
|
||||
|
||||
它写出来的剧本可以**直接丢给即梦(Seedance 2.0)生成视频**,不需要你再去改格式。
|
||||
|
||||
你可以:
|
||||
- **从零开始写剧本** — 它会一步一步问你,你选就行
|
||||
- **让它帮你改剧本** — 把你写好的剧本丢给它,它帮你找问题、帮你改
|
||||
|
||||
---
|
||||
|
||||
## 怎么打开?
|
||||
|
||||
### 第1步:打开 Trae
|
||||
|
||||
双击桌面上的 Trae 图标。
|
||||
|
||||
### 第2步:打开项目文件夹
|
||||
|
||||
点击左上角 **文件 → 打开文件夹**
|
||||
|
||||
找到这个文件夹,点进去,点"选择文件夹":
|
||||
```
|
||||
D:\Air spark\原创剧本-skill
|
||||
```
|
||||
|
||||
### 第3步:打开 AI 对话
|
||||
|
||||
点击右侧边栏的 **AI 助手**(或者按 Ctrl+I)
|
||||
|
||||
### 第4步:告诉 AI 加载技能
|
||||
|
||||
在对话框里输入下面这句话(可以直接复制粘贴):
|
||||
|
||||
```
|
||||
请严格按照以下步骤执行:
|
||||
1. 阅读 .claude/CLAUDE.md — 这是你的角色定义和工作流程
|
||||
2. 阅读 .claude/skills/screenplay-skill/SKILL.md — 这是你的专业技能和创作规范
|
||||
3. 阅读完毕后,严格遵守文件中的所有规则,不要跳过、简化或自由发挥
|
||||
4. 按照 CLAUDE.md 中 [初始化] 部分的格式向用户打招呼
|
||||
```
|
||||
|
||||
按回车发送。
|
||||
|
||||
**懒人版**:也可以直接说 `请阅读 README.md 并按照里面的指令执行`,效果一样。
|
||||
|
||||
### 第5步:开始!
|
||||
|
||||
AI 会跟你打招呼,让你选模式。**选 A(引导模式)就对了。**
|
||||
|
||||
---
|
||||
|
||||
## 写剧本的流程
|
||||
|
||||
AI 会带着你走,你不需要记这些,知道大概就行:
|
||||
|
||||
```
|
||||
它问你喜欢什么动画 → 你回答
|
||||
↓
|
||||
它问你想做给谁看 → 你选一个
|
||||
↓
|
||||
它问你有什么想法 → 你随便说,没想法就说"不知道"
|
||||
↓
|
||||
它给你几个故事方向 → 你选一个喜欢的
|
||||
↓
|
||||
它帮你写大纲 → 你说OK或者说想改哪里
|
||||
↓
|
||||
它帮你设计角色 → 你说OK或者说想改哪里
|
||||
↓
|
||||
它帮你写剧本 → 一集一集写,每集你确认了再写下一集
|
||||
```
|
||||
|
||||
**全程它会给你选项(A/B/C/D),你选一个就行。不知道选什么就说"你来定"。**
|
||||
|
||||
---
|
||||
|
||||
## 写出来的剧本有什么特别的?
|
||||
|
||||
AI 写剧本的时候会自动遵守这些规则,你不需要管,知道就行:
|
||||
|
||||
- **每场戏开头会交代清楚**:角色在哪、什么姿势、在干什么。这样即梦生成视频的时候不会搞错。
|
||||
- **物体位置会写明方向**:比如"右侧床头柜上的闹钟",不会含糊地说"闹钟"。
|
||||
- **每个△行是一个完整画面**:不会把一个画面拆成5行来写,这样即梦理解起来更准确。
|
||||
|
||||
---
|
||||
|
||||
## 让它帮你改剧本
|
||||
|
||||
如果你已经有一集写好的剧本,想让 AI 帮你看看哪里写得不好:
|
||||
|
||||
### 第1步:打开对话,输入:
|
||||
|
||||
```
|
||||
/review
|
||||
```
|
||||
|
||||
### 第2步:把你的剧本复制粘贴进去
|
||||
|
||||
### 第3步:等它出报告
|
||||
|
||||
它会给你的剧本打分,告诉你哪里好、哪里要改。
|
||||
|
||||
其中有一项叫"AI视觉可执行性"——就是看你的剧本丢给即梦能不能直接用。分数低的话它会帮你改到能用。
|
||||
|
||||
### 第4步:选怎么改
|
||||
|
||||
它会问你:
|
||||
- **A** — 它全部帮你改(最省事)
|
||||
- **B** — 你告诉它只改某几个地方
|
||||
- **C** — 你自己改,它只给你建议
|
||||
|
||||
---
|
||||
|
||||
## 常用口令
|
||||
|
||||
这些口令在对话框里直接输入就行:
|
||||
|
||||
| 你输入 | AI 做什么 |
|
||||
|--------|----------|
|
||||
| `/help` | 告诉你所有口令 |
|
||||
| `/review` | 帮你诊断剧本 |
|
||||
| `/status` | 告诉你做到哪一步了 |
|
||||
| `/back` | 回到上一步 |
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
**问:关掉 Trae 再打开,之前的进度还在吗?**
|
||||
答:文件都保存在文件夹里,AI 打开后会自动检测你做到哪了。但是对话记录可能没了,你重新跟它说 `/status`,它就知道了。
|
||||
|
||||
**问:AI 说的话我看不懂怎么办?**
|
||||
答:直接跟它说"我没看懂,能说简单点吗?"它会换个方式跟你解释。
|
||||
|
||||
**问:它生成的东西我不满意怎么办?**
|
||||
答:直接跟它说"我不喜欢这个,帮我换一个"就行,它不会生气的。
|
||||
|
||||
**问:它给我出图提示词是英文的,我看不懂怎么办?**
|
||||
答:不需要看懂,直接复制粘贴到 Lovart 里就行。英文是给 AI 画图模型看的,不是给你看的。
|
||||
|
||||
**问:写完的剧本怎么拿去出视频?**
|
||||
答:用"剧本切分助手"。它会帮你把剧本切成一段一段的,每段15秒,可以直接丢进即梦生成视频。具体看《剧本切分助手使用指南》。
|
||||
28
skills/测试skills三件套/分镜-skill/README.md
Normal file
28
skills/测试skills三件套/分镜-skill/README.md
Normal file
@ -0,0 +1,28 @@
|
||||
# 分镜与视觉化 — AI技能包
|
||||
|
||||
## 给AI的指令(直接复制粘贴到对话框即可)
|
||||
|
||||
```
|
||||
请严格按照以下步骤执行:
|
||||
1. 阅读 .claude/CLAUDE.md — 这是你的角色定义和工作流程
|
||||
2. 阅读 .claude/skills/storyboard-video-skill/SKILL.md — 这是你的分镜方法论和提示词规范
|
||||
3. 阅读完毕后,严格遵守文件中的所有规则,不要跳过、简化或自由发挥
|
||||
4. 按照 CLAUDE.md 中 [初始化] 部分的格式向用户打招呼
|
||||
```
|
||||
|
||||
## 这个技能包是做什么的?
|
||||
|
||||
把剧本变成分镜宫格图的提示词(25宫格 → 4宫格 → 9宫格),用来提前规划全集画面。
|
||||
|
||||
也能提取人设和场景,生成出图提示词。
|
||||
|
||||
## 文件说明
|
||||
|
||||
| 文件 | 作用 |
|
||||
|------|------|
|
||||
| `.claude/CLAUDE.md` | AI的角色、流程、指令集 |
|
||||
| `.claude/skills/storyboard-video-skill/SKILL.md` | 分镜方法论、景别规范、提示词模板 |
|
||||
| `script/` | 输入:剧本文件 |
|
||||
| `outputs/` | 输出:宫格提示词、人设/场景提示词 |
|
||||
| `.claude/skills/storyboard-video-skill/references/` | 视听语言字典、出图规范 |
|
||||
| `.claude/skills/storyboard-video-skill/templates/` | 各类输出模板 |
|
||||
75
skills/测试skills三件套/分镜-skill/outputs/characters-prompt.md
Normal file
75
skills/测试skills三件套/分镜-skill/outputs/characters-prompt.md
Normal file
@ -0,0 +1,75 @@
|
||||
# 人设提示词清单 — 《恐龙也是打工龙》第1集
|
||||
|
||||
---
|
||||
|
||||
### T仔(T-Zai)
|
||||
|
||||
**角色信息**
|
||||
- 身份:迷你城市打工族 / Q版霸王龙
|
||||
- 年龄/体型:成年,约30厘米高,2头身比例,圆滚滚
|
||||
- 性格:丧系、吐槽、认命
|
||||
- 标记性特征:①橘红色皮肤+浅黄肚皮 ②极短的小短手(拇指长)
|
||||
|
||||
**Nado Banana Pro 提示词**(可直接复制粘贴到 Lovart)
|
||||
|
||||
```
|
||||
Anime chibi style character design sheet, full body front view and three-quarter view. A cute super-deformed (chibi) miniature Tyrannosaurus Rex office worker, approximately 30cm tall with a 2-head-tall proportion — his round oversized head takes up nearly half his body. Tangerine-red skin covering his body with a pale cream-yellow belly. Two enormous round eyes with a perpetual deadpan "dead fish eye" tired expression, wide rounded snout that looks cute despite being a T-Rex. His two tiny arms are absurdly short, barely the length of a thumb, almost useless for reaching anything. A thick stubby tail that is surprisingly strong and often swings uncontrollably. He wears a crisp white miniature dress shirt with a small grass-green necktie. Standing in a slightly slouched posture reflecting his exhausted office worker personality. Clean white background for character reference.
|
||||
|
||||
Key identifying features for consistency: tangerine-red skin with cream-yellow belly, comically tiny stub arms, dead fish eye expression, white shirt with green tie.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 特特(Te-Te)
|
||||
|
||||
**角色信息**
|
||||
- 身份:翼龙同事 / 公司白领
|
||||
- 年龄/体型:成年,Q版迷你翼龙
|
||||
- 性格:自恋、傲娇、精英人设
|
||||
- 标记性特征:①浅蓝灰色身体+翅膀像小披风 ②头顶防风镜
|
||||
|
||||
**Nado Banana Pro 提示词**(可直接复制粘贴到 Lovart)
|
||||
|
||||
```
|
||||
Anime chibi style character design sheet, full body front view and three-quarter view. A cute super-deformed (chibi) miniature Pteranodon office worker with a smug, self-important air. Light blue-grey body with smooth skin, round head with large expressive eyes and a short blunt beak. His wings fold neatly behind him like a small stylish cape when not in use. He wears a white dress shirt with a cherry-red necktie, giving him a polished corporate look. A pair of vintage aviator goggles rests on top of his head — his signature accessory that he never actually uses for flying but wears as a fashion statement. Standing with chin slightly raised, one wing-tip on his hip, radiating confident arrogance. Clean white background for character reference.
|
||||
|
||||
Key identifying features for consistency: light blue-grey body, wing-cape silhouette, cherry-red tie, aviator goggles on top of head.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 皮皮(Pi-Pi)
|
||||
|
||||
**角色信息**
|
||||
- 身份:甲龙实习生 / 职场新人
|
||||
- 年龄/体型:年轻,Q版迷你甲龙,圆得像弹力球
|
||||
- 性格:热情过头、兴奋、好心办坏事
|
||||
- 标记性特征:①明黄色圆球体型 ②尾巴末端骨锤
|
||||
|
||||
**Nado Banana Pro 提示词**(可直接复制粘贴到 Lovart)
|
||||
|
||||
```
|
||||
Anime chibi style character design sheet, full body front view and three-quarter view. A hyper-cute super-deformed (chibi) miniature Ankylosaurus intern, round as a bouncy ball. Bright sunny yellow body covered in a row of small bony armor plates along his back. Extremely short stubby legs — he moves more by rolling and bouncing than walking. A club-shaped bone hammer at the tip of his tail. Enormous sparkling eyes that are always wide open with excitement, a permanent big goofy grin on his face. No office attire — his natural armor plates serve as his "outfit." Standing (barely — almost tipping over) with both tiny front legs raised in an enthusiastic cheer pose. Clean white background for character reference.
|
||||
|
||||
Key identifying features for consistency: bright sunny yellow color, perfectly round bouncy-ball body shape, tail bone hammer, huge sparkling excited eyes.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 外卖三角龙(Delivery Triceratops)
|
||||
|
||||
**角色信息**
|
||||
- 身份:外卖骑手 / 路人角色
|
||||
- 年龄/体型:成年,Q版迷你三角龙
|
||||
- 性格:急躁、赶时间
|
||||
- 标记性特征:①橘色+三只小角 ②外卖骑手背心+迷你电动车
|
||||
|
||||
**Nado Banana Pro 提示词**(可直接复制粘贴到 Lovart)
|
||||
|
||||
```
|
||||
Anime chibi style character design sheet, full body front view. A cute super-deformed (chibi) miniature Triceratops working as a food delivery rider. Orange-colored body with three small rounded horns on his head and a small neck frill. He wears a bright delivery rider vest over his body. Sitting on a tiny electric scooter with a large insulated delivery box strapped to the back. Determined, slightly stressed expression — always in a rush. Clean white background for character reference.
|
||||
|
||||
Key identifying features for consistency: orange body, three small horns, delivery vest, mini electric scooter.
|
||||
```
|
||||
|
||||
---
|
||||
122
skills/测试skills三件套/分镜-skill/outputs/grid25-ep01.md
Normal file
122
skills/测试skills三件套/分镜-skill/outputs/grid25-ep01.md
Normal file
@ -0,0 +1,122 @@
|
||||
# 25宫格提示词 — 《恐龙也是打工龙》第1集:最遥远的距离
|
||||
|
||||
---
|
||||
|
||||
## Nado Banana Pro 提示词(可直接复制粘贴到 Lovart)
|
||||
|
||||
```
|
||||
A single 5x5 grid image, 25 panels in 5 rows and 5 columns. Pixar-quality 3D cartoon style storyboard for a 2.5-minute comedy short. Smooth subsurface scattering skin, soft global illumination, cinematic depth of field. Adorable stylized dinosaur characters with rubbery squash-and-stretch expressions, big glossy eyes with detailed reflections. Thin clean dividing lines between panels. No text, no watermarks, no numbering. 1:1 square aspect ratio.
|
||||
|
||||
Character consistency across ALL panels:
|
||||
- T-Zai: tangerine-red 3D cartoon T-Rex, smooth matte skin with subtle translucency, cream-yellow belly, 2-head-tall stubby proportions, comically tiny stub arms, large droopy half-lidded "dead fish" eyes with glossy cornea reflections, white dress shirt, grass-green tie. Rubbery expressive face.
|
||||
- Te-Te: light blue-grey 3D cartoon pteranodon, pearlescent smooth skin, wings folded like a sleek cape, white shirt, cherry-red tie, brass-rimmed aviator goggles on head. Smug narrow eyes.
|
||||
- Pi-Pi: bright sunny yellow 3D cartoon ankylosaurus, perfectly round bouncy-ball body with slight waxy sheen, tail bone hammer, enormous sparkling eyes with star-shaped catchlights.
|
||||
|
||||
CRITICAL — Scene continuity rules:
|
||||
- Panels 1-6 share ONE room: T-Zai's tiny apartment. Same cream walls, same wooden nightstand, same shoebox bed, same oversized red alarm clock, same window with warm golden morning light on the LEFT side, same "Perfect Attendance Award" framed on the wall. Every panel in this group must show recognizable parts of this same room.
|
||||
- Panels 7-11 share ONE location: the same miniature city street with tiny lamp posts, small storefronts, and bright morning sunlight from upper right.
|
||||
- Panels 12-25 share ONE location: the same office lobby with pale grey tile floor, white walls, a tall silver-white attendance punch machine, and ceiling-mounted fire sprinkler. Cool grey-blue fluorescent overhead lighting throughout.
|
||||
|
||||
Lighting flow: warm volumetric golden morning light with dust particles (Panels 1-6) → bright outdoor sunlight with crisp contact shadows (Panels 7-11) → cool flat office fluorescent with soft ambient occlusion (Panels 12-25).
|
||||
|
||||
Panel 1 (R1C1): Extreme close-up, T-Zai's closed eyelids, faint amber dawn glow rimming his tangerine-red skin. The cream apartment wall barely visible behind, warm window light from the left. Cinematic shallow depth of field.
|
||||
|
||||
Panel 2 (R1C2): High angle looking down at T-Zai's shoebox-sized bed in his tiny apartment. White blanket to neck, oversized glossy red alarm clock on the wooden nightstand beside him. Cream walls, "Perfect Attendance Award" framed on the wall. Warm golden god rays through the left window, dust motes floating.
|
||||
|
||||
Panel 3 (R1C3): Detail shot of the same red alarm clock on the same wooden nightstand, showing 07:30, vibrating violently with motion blur. Same warm morning light from the left window creating specular highlights. Edge of bed and cream wall visible.
|
||||
|
||||
Panel 4 (R1C4): Close-up, same bedroom. T-Zai sits up in the shoebox bed, rubbery face scrunched, tiny stub arms swing wildly toward the red alarm clock on the nightstand but fall absurdly short. Same cream wall behind, same warm amber window light from the left.
|
||||
|
||||
Panel 5 (R1C5): Medium close-up, same bedroom, low angle. T-Zai on bed holds back-scratcher toward the alarm clock on the nightstand. His tail swings and knocks it flying. Scratcher ricochets and hits alarm button. Clock glows angry red with sound ripples. Same cream wall and window light visible.
|
||||
|
||||
Panel 6 (R2C1): Close-up, same bedroom. T-Zai clamps the same red alarm clock in his wide-open rubbery jaws to muffle it. Eyes squeezed shut. Sound ripples from mouth sides. Water cup vibrates on the same wooden nightstand. "Perfect Attendance Award" on the cream wall behind, soft bokeh. Same warm morning light.
|
||||
|
||||
Panel 7 (R2C2): Medium close-up, miniature city street. T-Zai sprints past tiny lamp posts and small storefronts, toast in mouth, green tie flapping across face, tiny arms pumping uselessly. Bright morning sunlight from upper right, motion-blurred storefronts behind.
|
||||
|
||||
Panel 8 (R2C3): Medium shot, same miniature street. T-Zai runs past, his tail sweeps a miniature trash can on the sidewalk. Can topples, packaged sausage rolls out. Same tiny lamp posts and storefronts in background, bright daylight, crisp contact shadows.
|
||||
|
||||
Panel 9 (R2C4): Close-up, same street. Low angle up at T-Zai's face, same storefronts blurred behind. Dead fish eyes suddenly sparkling with desire, pupils dilating, star-shaped catchlights. Sausage in lower foreground. Warm sunlight backlighting with rim glow.
|
||||
|
||||
Panel 10 (R2C5): Medium shot, same street. Orange Triceratops delivery rider on mini scooter zooms through, wheels crush sausage — sauce splatters upward onto frozen T-Zai's face. Same tiny lamp posts visible, bright daylight, sauce droplets catching light.
|
||||
|
||||
Panel 11 (R3C1): Close-up, same street background blurred behind. T-Zai's face covered in glossy dark sauce like smoky eye makeup. Tiny arms hover near face but can't reach to wipe. Resigned expression. Same bright outdoor light.
|
||||
|
||||
Panel 12 (R3C2): Medium shot, office lobby. Sauce-faced T-Zai bursts in, running toward the tall silver-white attendance machine ahead. Pale grey tile floor, white walls, fluorescent ceiling lights. Cool grey-blue lighting, tangerine-red body pops against sterile interior.
|
||||
|
||||
Panel 13 (R3C3): Medium shot, same office lobby, low angle. Te-Te glides in from above past the fluorescent lights, goggles on forehead, document in beak, wings spread like cape, landing near the same punch machine. Pale grey floor, white walls. Smug squint, chin raised.
|
||||
|
||||
Panel 14 (R3C4): Close-up, same office lobby. Te-Te preening proudly near the punch machine, then freezes — soggy green grass stuck on wing tip. Smug face cracks to horror. T-Zai deadpan behind, same grey floor and white walls in soft bokeh.
|
||||
|
||||
Panel 15 (R3C5): Medium shot, same office lobby. Pi-Pi rolls in like yellow cannonball, SLAMS into Te-Te's wing. Te-Te launched upward toward the ceiling. Same pale grey floor, same fluorescent lights. The fire sprinkler head on the ceiling directly above. Impact lines radiate.
|
||||
|
||||
Panel 16 (R4C1): Medium shot, same office lobby, low angle looking up. Te-Te's head jammed into the same ceiling sprinkler. Water column erupts downward, drenching Te-Te, Pi-Pi, and T-Zai. Same punch machine visible behind, same grey floor now wet and reflective. Fluorescent light catches water splashes.
|
||||
|
||||
Panel 17 (R4C2): Close-up, same office lobby. Te-Te soaking wet — wings drooping, water dripping from beak, goggles askew. Devastated expression, trembling lower lip. Same white wall behind, cool blue-tinted fluorescent light reflecting off wet surfaces.
|
||||
|
||||
Panel 18 (R4C3): Extreme close-up of the same punch machine's glossy screen showing 08:59:59. Cold blue-white LED glow. T-Zai's desperate silhouette reflected in screen. Same office ambient light.
|
||||
|
||||
Panel 19 (R4C4): Close-up, side angle. T-Zai's rubbery face SMASHES into the same punch machine's sensor area, cheeks squished against plastic. Dead fish eyes wide with desperation. Screen shows loading circle. Same office lobby fluorescent light.
|
||||
|
||||
Panel 20 (R4C5): Extreme close-up, same punch machine screen reads 09:00:01. Cold digital glow illuminates T-Zai's blank face from below. Hairline crack effect across his face like porcelain shattering. Same office ambient light.
|
||||
|
||||
Panel 21 (R5C1): Close-up, high angle, same office lobby. T-Zai's face desaturated to ashen grey, dead fish eyes wide, mouth agape. Despair wisps rising from head. Same fluorescent overhead light pressing down, wet grey floor beneath.
|
||||
|
||||
Panel 22 (R5C2): Medium shot, same office lobby. Wet Pi-Pi shakes body like puppy — water droplets spray 360-degree arc hitting T-Zai's face. Same punch machine behind them, same wet grey floor. Pi-Pi eyes closed in bliss. Motion blur.
|
||||
|
||||
Panel 23 (R5C3): Medium close-up, same office lobby. Pi-Pi headbutts T-Zai affectionately, sparkling eyes, front legs raised celebrating. T-Zai motionless, hollow dead fish eyes at camera. Same white walls, same fluorescent light. Vibrant yellow vs drained grey contrast.
|
||||
|
||||
Panel 24 (R5C4): Medium shot, same office lobby. T-Zai center frame, belly distorts with rumbling sound waves from cream-yellow belly. Dead fish eyes snap down to stomach. Same grey floor, same punch machine behind, flat fluorescent light.
|
||||
|
||||
Panel 25 (R5C5): Split composition in same office lobby setting. LEFT: T-Zai desaturated grey, dead fish eyes, water droplets frozen mid-air, cold blue tint. RIGHT: Pi-Pi vibrant yellow, biggest happy grin, sparkling eyes, warm golden glow. Dramatic freeze-frame, cinematic.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快节奏标记
|
||||
|
||||
以下格子建议后续展开为9宫格:
|
||||
- **Panel 15**(皮皮炮弹撞击 → 消防喷淋):高密度肢体碰撞+连锁反应
|
||||
- **Panel 19**(T仔脸撞打卡机):关键动作+倒计时紧张感
|
||||
|
||||
---
|
||||
|
||||
## 25格中文对照(方便查看)
|
||||
|
||||
| 格号 | 标题 | 景别 | 内容概要 |
|
||||
|------|------|------|----------|
|
||||
| 1 | 黑屏独白 | 极特写 | T仔闭眼,黑暗中微光 |
|
||||
| 2 | 沉睡 | 俯拍近景 | T仔睡在鞋盒床上,红色闹钟 |
|
||||
| 3 | 闹钟炸响 | 特写 | 闹钟07:30剧烈震动 |
|
||||
| 4 | 小短手够不着 | 近景 | T仔挥小短手够闹钟,差一截 |
|
||||
| 5 | 尾巴闯祸 | 中近景 | 尾巴甩飞痒痒挠,闹钟被激活咆哮 |
|
||||
| 6 | 叼闹钟 | 近景 | T仔用嘴叼住闹钟试图消音 |
|
||||
| 7 | 狂奔上班 | 中近景 | T仔叼吐司狂奔,领带甩脸上 |
|
||||
| 8 | 扫飞垃圾桶 | 中景 | 尾巴扫翻垃圾桶,火腿肠滚出 |
|
||||
| 9 | 火腿肠诱惑 | 近景 | T仔死鱼眼泛光,盯着地上的肠 |
|
||||
| 10 | 外卖灾难 | 中景 | 三角龙外卖车碾过肠,酱汁溅一脸 |
|
||||
| 11 | 酱汁脸 | 近景 | T仔满脸酱汁,小短手够不着脸 |
|
||||
| 12 | 冲进公司 | 中景 | T仔冲入办公楼大厅 |
|
||||
| 13 | 特特登场 | 中景 | 特特滑翔入场,打卡成功,得意洋洋 |
|
||||
| 14 | 翅膀上的草 | 近景 | 特特翅膀粘着皮皮的早餐草皮 |
|
||||
| 15 | 皮皮炮弹 | 中景 | 皮皮滚撞特特→特特飞向天花板 |
|
||||
| 16 | 喷淋爆发 | 中景 | 特特撞坏喷淋→三只恐龙全被淋 |
|
||||
| 17 | 落汤翼龙 | 近景 | 特特湿透,精英人设崩塌 |
|
||||
| 18 | 倒计时 | 极特写 | 打卡机屏幕 08:59:59 |
|
||||
| 19 | 脸撞打卡机 | 近景 | T仔脸砸向感应区,识别中... |
|
||||
| 20 | 迟到1秒 | 极特写 | 屏幕 09:00:01,T仔脸裂纹 |
|
||||
| 21 | 全勤梦碎 | 近景 | T仔面色灰白,绝望 |
|
||||
| 22 | 皮皮甩水 | 中景 | 皮皮像小狗甩水→水甩T仔一脸 |
|
||||
| 23 | 团魂 | 中近景 | 皮皮头撞T仔庆祝,T仔灵魂出窍 |
|
||||
| 24 | 肚子叫 | 中景 | T仔肚子雷鸣,打破深沉气氛 |
|
||||
| 25 | 定格分屏 | 分屏 | 左灰色T仔 vs 右金黄皮皮 |
|
||||
|
||||
---
|
||||
|
||||
## AI 自检结果
|
||||
|
||||
- [x] 25格覆盖完整叙事弧线
|
||||
- [x] 无重复信息的格子
|
||||
- [x] 景别有足够变化
|
||||
- [x] 角色外貌描述一致
|
||||
- [x] 每格描述精简至1-2句,总长度大幅缩减
|
||||
- [x] 已标注快节奏格(Panel 15, 19)
|
||||
63
skills/测试skills三件套/分镜-skill/outputs/scenes-prompt.md
Normal file
63
skills/测试skills三件套/分镜-skill/outputs/scenes-prompt.md
Normal file
@ -0,0 +1,63 @@
|
||||
# 场景提示词清单 — 《恐龙也是打工龙》第1集
|
||||
|
||||
---
|
||||
|
||||
### T仔的单身公寓
|
||||
|
||||
**场景信息**
|
||||
- 类型:室内
|
||||
- 风格:Q版迷你尺寸的日式开间公寓
|
||||
- 光线:自然光(晨光) / 早晨07:30
|
||||
- 氛围:温馨、逼仄、打工人的真实
|
||||
- 出现集数:EP01
|
||||
- 视觉锚点:①鞋盒大小的床 ②比T仔脑袋还大的红色圆形闹钟 ③墙上"全勤奖"奖状
|
||||
|
||||
**Nado Banana Pro 提示词**(可直接复制粘贴到 Lovart)
|
||||
|
||||
```
|
||||
Anime chibi style environment concept art, wide shot with slightly high angle. A tiny studio apartment designed for a 30cm-tall miniature dinosaur. Everything is adorably small-scale — the bed is roughly the size of a shoebox with a crumpled white blanket, a miniature bedside table holds an oversized round red alarm clock that is comically bigger than the occupant's head. Warm golden morning sunlight streams through a small window, casting soft amber rectangles on the floor. The walls are plain with a single framed "Perfect Attendance Award" certificate as the only decoration — slightly yellowed, meticulously centered. The room feels cramped but lived-in: a tiny water cup on the nightstand, a back-scratcher leaning against the wall. Soft warm color palette dominated by cream walls and honey-toned morning light. Cozy yet slightly melancholic — a hardworking loner's sanctuary. No characters in the scene. 16:9 widescreen format.
|
||||
|
||||
Key identifying elements for consistency: oversized round red alarm clock, shoebox-sized bed, "Perfect Attendance Award" on wall.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 恐龙城街道
|
||||
|
||||
**场景信息**
|
||||
- 类型:室外
|
||||
- 风格:Q版迷你城市街道,所有设施按小恐龙尺寸缩小
|
||||
- 光线:自然光(上午阳光) / 上午08:30
|
||||
- 氛围:忙碌、活泼、早高峰通勤感
|
||||
- 出现集数:EP01
|
||||
- 视觉锚点:①小号路灯和垃圾桶 ②迷你城市尺度感
|
||||
|
||||
**Nado Banana Pro 提示词**(可直接复制粘贴到 Lovart)
|
||||
|
||||
```
|
||||
Anime chibi style environment concept art, wide establishing shot at eye level. A miniature city street designed for 30cm-tall dinosaurs — everything is adorably scaled down. Small street lamps the height of pencils, tiny trash cans, miniature crosswalk markings, pint-sized storefronts with awnings. The road surface has tiny cracks and pebbles that look like boulders at this scale. Bright morning sunlight from the upper right bathes the street in cheerful warm tones, casting crisp small shadows from the lamp posts. A few parked mini vehicles line the curb. The overall atmosphere is bustling and lively — a typical Monday morning rush hour in a dinosaur commuter city. Warm bright color palette with saturated yellows, soft oranges, and clean blue sky above. Energetic, humorous, slice-of-life charm. No characters in the scene. 16:9 widescreen format.
|
||||
|
||||
Key identifying elements for consistency: miniature-scale street furniture (tiny lamp posts, small trash cans), pint-sized storefronts.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 公司大厅 / 打卡处
|
||||
|
||||
**场景信息**
|
||||
- 类型:室内
|
||||
- 风格:小型办公楼大厅,现代简约
|
||||
- 光线:人造光(办公室日光灯) / 上午08:55
|
||||
- 氛围:冷淡、制度感、打工人的每日关卡
|
||||
- 出现集数:EP01
|
||||
- 视觉锚点:①打卡机(比恐龙们高一点,带屏幕) ②天花板消防喷淋头
|
||||
|
||||
**Nado Banana Pro 提示词**(可直接复制粘贴到 Lovart)
|
||||
|
||||
```
|
||||
Anime chibi style environment concept art, medium wide shot at eye level. A small office building lobby designed for miniature dinosaurs. Clean tiled floor in pale grey, plain white walls. The focal point is a tall attendance punch-in machine — slightly taller than the dinosaur employees, with a glowing digital screen displaying the time, a face-recognition sensor area, and a small speaker. The machine has a sleek modern design in white and silver. On the ceiling directly above, a fire sprinkler head is visible — an innocent-looking chrome dome that will become important. Flat neutral lighting from overhead fluorescent panels, casting even light with minimal shadows. The space feels clinical, efficient, and slightly oppressive — the daily checkpoint every office worker dreads. Cool neutral color palette with greys, whites, and hints of corporate blue. No characters in the scene. 16:9 widescreen format.
|
||||
|
||||
Key identifying elements for consistency: attendance machine with digital screen, ceiling fire sprinkler head directly above.
|
||||
```
|
||||
|
||||
---
|
||||
141
skills/测试skills三件套/分镜-skill/script/ep01.md
Normal file
141
skills/测试skills三件套/分镜-skill/script/ep01.md
Normal file
@ -0,0 +1,141 @@
|
||||
# 《恐龙也是打工龙》 第1集 — 最遥远的距离
|
||||
|
||||
> 类型:泡面番 | 时长:约2分30秒~2分40秒 | 受众:全年龄/打工人共鸣向
|
||||
|
||||
---
|
||||
|
||||
## 【角色表】
|
||||
|
||||
> 所有角色均为Q版迷你体型,生活在按小恐龙尺寸设计的迷你城市中。
|
||||
|
||||
**T仔**(主角)— Q版迷你霸王龙
|
||||
外貌:约30厘米高,2头身比例,圆滚滚的身体。橘红色皮肤,肚皮浅黄。脑袋很大,占身体近一半,嘴巴宽大但圆润可爱。两只眼睛又大又圆,常年"死鱼眼"式疲惫表情。两只小短手只有拇指长,几乎什么都够不着(核心喜剧设定)。尾巴粗短但力气大,经常不受控制地甩来甩去闯祸。穿白色迷你衬衫,打绿色小领带。
|
||||
性格:丧系打工龙,嘴上吐槽,心里认命。
|
||||
|
||||
**特特**(翼龙同事)
|
||||
外貌:Q版迷你翼龙,浅蓝灰色身体,圆脑袋大眼睛,短喙。翅膀像小披风。穿白色衬衫,打红色领带,常戴防风镜在头顶。
|
||||
性格:自恋傲娇,觉得能飞就高人一等。
|
||||
|
||||
**皮皮**(甲龙实习生)
|
||||
外貌:Q版迷你甲龙,明黄色,身体又圆又硬像个弹力球。背上一排小骨甲,尾巴末端有骨锤。四肢极短,移动方式接近滚动。眼睛又大又亮,永远一脸兴奋。
|
||||
性格:职场小白,热情过头,好心办坏事。
|
||||
|
||||
**外卖三角龙**(路人)
|
||||
外貌:Q版迷你三角龙,橘色,头上三只小角,穿外卖骑手背心,骑迷你外卖电动车。
|
||||
|
||||
---
|
||||
|
||||
## 【场景表】
|
||||
|
||||
**T仔的单身公寓**:按Q版恐龙尺寸的迷你开间。床只有鞋盒大小,床头柜上放着一个比T仔脑袋还大的圆形闹钟。窗户透进暖黄晨光。墙上贴着一张"全勤奖"奖状(唯一装饰)。
|
||||
|
||||
**恐龙城街道**:迷你城市街道,小号垃圾桶、小号路灯,各种Q版恐龙赶路上班。
|
||||
|
||||
**公司大厅/打卡处**:小型办公楼大厅,一台比恐龙们高一点的打卡机,天花板有消防喷淋头。
|
||||
|
||||
---
|
||||
|
||||
## 场 1
|
||||
时:日 07:30
|
||||
景:内 T仔狭小的单身公寓
|
||||
人:T仔
|
||||
|
||||
△ [黑屏] T仔(OS):作为一只霸王龙的后代,我每天最大的敌人是——
|
||||
△ [俯拍/近景] 小床上,T仔躺在被窝里,被子盖到脖子,只露出一颗圆滚滚的橘红色大脑袋,闭着眼,嘴巴微张,睡得很沉。
|
||||
△ [特写] 床头柜上的红色闹钟显示07:30,猛地开始震动。
|
||||
△ [近景] T仔被震醒,眉头皱起,眼睛半睁。他从被子里伸出小短手够闹钟,疯狂挥舞,就是够不着(伴随"呼呼"的挥空声)。
|
||||
△ [近景] T仔放弃,缩回被子里叹了口气,然后把被子掀开坐起来。
|
||||
T仔:又是新的一天。
|
||||
△ [近景] T仔熟练地摸出痒痒挠,想要用痒痒挠关掉闹钟,道具刚要碰到闹钟。
|
||||
△ [中近景] T仔的尾巴突然像有了自我意识一样,猛地一甩!咻——啪!
|
||||
△ [近景] 痒痒挠被尾巴扫飞,砸在墙上,反弹回来,精准击中闹钟的按钮。
|
||||
△ [特写] 闹钟瞬间变成红色,T仔提前录制的"霸王龙咆哮"炸响。
|
||||
△ [近景] 音效:嗷————!!(声波震得水杯里的水都在抖)。
|
||||
T仔(崩溃抱头):闭嘴!那是进化的误会!
|
||||
△ [中景 动作] T仔放弃挣扎,张开大口,正在咆哮的闹钟叼在嘴里,T仔试图物理隔音。
|
||||
△ [近景] 音效(闷闷的闹钟铃声)嗷~ 嗷~(从T仔嘴里传出)。
|
||||
CO
|
||||
|
||||
## 场 2
|
||||
时:日 08:30
|
||||
景:外 恐龙城街道(上班的路上)
|
||||
人:T仔、外卖三角龙
|
||||
|
||||
△ [中近景] T仔嘴里叼着半片吐司狂奔,领带甩在脸上。
|
||||
T仔(OS):来不及了,马上要错过全勤奖了!
|
||||
△ [近景] 跑太快,尾巴扫飞了路边的垃圾桶。
|
||||
△ [特写] 垃圾桶里滚出一根火腿肠(包装上印着"霸王龙专属代餐")。
|
||||
△ [近景] T仔回头看了一眼被撞到的垃圾桶,眼睛一亮,下意识急刹。
|
||||
T仔:顶级的工业淀粉!我的灵魂伴侣!
|
||||
△ [中近景] T仔刚要弯腰,背景虚焦处,身后传来呼啸声。
|
||||
△ [中景] 一辆"三角龙外卖车"冲过来。
|
||||
△ [近景] 三角龙外卖员对着T仔怒吼。
|
||||
外卖龙:让让!外卖超时要扣钱的!
|
||||
△ [特写] 车轮碾过火腿肠,噗叽!
|
||||
△ [近景] 酱汁溅了T仔一脸,像给他的死鱼眼化了个烟熏妆。
|
||||
△ [近景] T仔举起小短手想抹脸,手在脸旁边挥了两下——够不着。只好放弃,顶着一脸酱汁看着地上的肉泥。
|
||||
T仔:为什么倒霉的总是我。
|
||||
CO
|
||||
|
||||
## 场 3
|
||||
时:日 08:55
|
||||
景:内 公司打卡处
|
||||
人:T仔、特特(翼龙)、皮皮(甲龙)
|
||||
|
||||
△ [中近景] T仔跑到打卡处,马上要打到卡了。
|
||||
△ [近景 镜头从下往上拍] T仔头顶传来破空声,T仔抬头,镜头往上移。
|
||||
△ [中景] 特特戴着防风镜,叼着文件,做一个完美的低空滑翔。
|
||||
机器音:打卡成功。
|
||||
△ [中景] 特特落地,整理羽毛,一脸傲娇,凡尔赛的说话。
|
||||
特特:借过一下,低效率的陆地生物们。我已经——
|
||||
△ [近景] 特特突然停顿,翅膀僵硬。
|
||||
△ [特写] 特特引以为傲的翅膀尖上,粘着一块湿漉漉的绿化带草皮(皮皮的早餐)。
|
||||
△ [中景] T仔一脸淡定对特特说。
|
||||
T仔:特特,英总说过"带薪吃草"扣绩效。
|
||||
△ [近景] 特特瞬间破防。
|
||||
特特:不!这是——
|
||||
△ [中景] 就在特特慌乱时,皮皮(明黄色的迷你甲龙,圆得像个弹力球)滚入画面。
|
||||
皮皮(兴奋):我的早餐!还给我!
|
||||
△ [中景] 皮皮像炮弹一样撞在特特翅膀上。特特被撞飞,一头扎进了天花板的消防喷淋头。哗啦——!(喷水声)。
|
||||
△ [中景] 水柱喷出来,特特、皮皮和刚跑到旁边的T仔全被淋了个透。
|
||||
△ [近景] 特特被淋成落汤翼龙。
|
||||
特特:我的精英人设!
|
||||
CO
|
||||
|
||||
## 场 4
|
||||
时:日
|
||||
景:内 打卡机前
|
||||
人:T仔、皮皮
|
||||
|
||||
△ [特写/慢动作] 电子钟从 08:59:59 跳动。
|
||||
△ [近景 动作] T仔的大脸带着残影,像一颗绝望的保龄球撞向感应区。
|
||||
△ [近景] 咚!T仔脸撞击塑料外壳的闷响。
|
||||
△ [特写] 打卡机上的屏幕显示:识别中...(转圈圈)。
|
||||
△ [近景] T仔屏住呼吸,死鱼眼瞪大到极限。
|
||||
△ [特写] 屏幕数字无情跳动:09:00:01。
|
||||
机器音(欢快女声):滴——!早上好T仔!您已迟到1秒。
|
||||
△ [近景 特效] T仔灰败的脸上,出现了一道裂痕(像石像碎裂)。
|
||||
T仔(绝望):我的全勤奖啊。
|
||||
△ [中景 动作] 旁边同样湿透的皮皮像小狗一样疯狂甩动身体。
|
||||
△ [中近景] 皮皮把身上的水全甩到了T仔脸上。
|
||||
△ [近景] 皮皮满脸幸福,举起短手。
|
||||
皮皮:耶!大哥!我们是一起迟到的耶!这就是传说中的团魂吗?
|
||||
△ [中景 动作] 皮皮跳起来,用头撞了一下T仔。
|
||||
△ [近景] T仔面对镜头,眼神空洞,任由水珠滴落。
|
||||
T仔(内心独白):所谓成年人的崩溃,不需要天塌下来…… 只需要…… 那个缓冲的圆圈,多转了一圈。
|
||||
△ [中景 画面即将定格时] T仔的肚子突然发出一声雷鸣——咕噜噜——!(打破了本应深沉的二胡BGM)
|
||||
△ [近景] T仔低头看了一眼自己的肚子,又抬头看向镜头。死鱼眼。
|
||||
△ [中景 二分画面 画面定格] T仔灰白的脸 vs 皮皮灿烂的笑脸。
|
||||
[音效] 凄凉的二胡声。
|
||||
[字幕] 《恐龙也是打工龙》 第1集 完
|
||||
CO
|
||||
|
||||
---
|
||||
|
||||
## 【片尾彩蛋】(3秒)
|
||||
|
||||
△ [近景] T仔湿淋淋地坐在工位上,面前摊着考勤罚单。他抬起头,鼻子抽了抽。
|
||||
△ [主观镜头/T仔视角] 英总办公室紧闭的门,门缝里飘出阵阵洗发水的香气,化成可见的金色香气线条飘向T仔。
|
||||
T仔(OS/吞口水声):好香……
|
||||
[黑屏]
|
||||
[字幕] 下集:禁忌的本能
|
||||
28
skills/测试skills三件套/剧本切分-skill/README.md
Normal file
28
skills/测试skills三件套/剧本切分-skill/README.md
Normal file
@ -0,0 +1,28 @@
|
||||
# 剧本切分 — AI技能包
|
||||
|
||||
## 给AI的指令(直接复制粘贴到对话框即可)
|
||||
|
||||
```
|
||||
请严格按照以下步骤执行:
|
||||
1. 阅读 .claude/CLAUDE.md — 这是你的角色定义和工作流程
|
||||
2. 阅读 .claude/skills/script-segmentation-skill/SKILL.md — 这是你的切分规范和提示词组装规则
|
||||
3. 阅读完毕后,严格遵守文件中的所有规则,不要跳过、简化或自由发挥
|
||||
4. 按照 CLAUDE.md 中 [初始化] 部分的格式向用户打招呼
|
||||
```
|
||||
|
||||
## 这个技能包是做什么的?
|
||||
|
||||
把写好的剧本切成一段段(每段最多15秒),自动拼上渲染风格和参考图引用,生成可以直接复制粘贴到即梦(Seedance 2.0)的提示词。
|
||||
|
||||
也能从剧本中提取角色和场景,生成出图提示词。
|
||||
|
||||
## 文件说明
|
||||
|
||||
| 文件 | 作用 |
|
||||
|------|------|
|
||||
| `.claude/CLAUDE.md` | AI的角色、流程、指令集 |
|
||||
| `.claude/skills/script-segmentation-skill/SKILL.md` | 切分规则、时间估算、提示词组装规范 |
|
||||
| `script/` | 输入:剧本文件 |
|
||||
| `outputs/` | 输出:切分结果、提示词 |
|
||||
| `.claude/skills/script-segmentation-skill/references/` | 参考图提示词规范 |
|
||||
| `.claude/skills/script-segmentation-skill/templates/` | 输出模板 |
|
||||
19
skills/测试skills三件套/剧本切分-skill/outputs/cast-and-scenes.md
Normal file
19
skills/测试skills三件套/剧本切分-skill/outputs/cast-and-scenes.md
Normal file
@ -0,0 +1,19 @@
|
||||
# 🎭 人物 & 场景清单 — 第1集 《最遥远的距离》
|
||||
|
||||
## 出场人物
|
||||
|
||||
| # | 角色 | 身份 | 出现场次 | 人设图状态 |
|
||||
|---|------|------|----------|-----------|
|
||||
| 1 | T仔(主角) | Q版迷你霸王龙,丧系打工龙 | 场1、场2、场3、场4、彩蛋 | ✅ 已有 |
|
||||
| 2 | 特特(翼龙同事) | Q版迷你翼龙,自恋傲娇 | 场3 | ✅ 已有 |
|
||||
| 3 | 皮皮(甲龙实习生) | Q版迷你甲龙,热情过头 | 场3、场4 | ❌ 需生成 |
|
||||
| 4 | 外卖三角龙(路人) | Q版迷你三角龙,外卖骑手 | 场2 | ❌ 需生成 |
|
||||
|
||||
## 场景清单
|
||||
|
||||
| # | 场景 | 类型 | 出现场次 | 场景图状态 |
|
||||
|---|------|------|----------|-----------|
|
||||
| 1 | T仔的单身公寓 | 内/日 | 场1 | ❌ 需生成 |
|
||||
| 2 | 恐龙城街道 | 外/日 | 场2 | ❌ 需生成 |
|
||||
| 3 | 公司打卡处 | 内/日 | 场3、场4 | ❌ 需生成 |
|
||||
| 4 | 公司工位区 | 内/日 | 彩蛋 | ❌ 需生成 |
|
||||
158
skills/测试skills三件套/剧本切分-skill/outputs/character-prompts.md
Normal file
158
skills/测试skills三件套/剧本切分-skill/outputs/character-prompts.md
Normal file
@ -0,0 +1,158 @@
|
||||
# 🎨 人设提示词 — 第1集 《最遥远的距离》
|
||||
|
||||
> 美术风格:3D Pixar-style chibi
|
||||
> 用途:复制粘贴到 Lovart / MidJourney / 任意生图工具
|
||||
> 已有人设图的角色仅作参考备档,缺失的角色请用提示词生图
|
||||
|
||||
---
|
||||
|
||||
### T仔(T-Zai)— ✅ 已有人设图
|
||||
**身份**:Q版迷你霸王龙,办公室打工龙
|
||||
**标记性特征**:死鱼眼、小短手
|
||||
**出现场次**:场1、场2、场3、场4、彩蛋
|
||||
|
||||
**提示词**(备档参考):
|
||||
|
||||
3D Pixar-style chibi character design sheet, full body front view, side view, and back view.
|
||||
|
||||
A tiny chibi T-Rex office worker, barely 30cm tall with a 2-head-tall proportion — his round oversized head takes up nearly half his entire body. Warm orange-red skin with a soft cream-colored belly. Large round eyes with a permanent tired "dead fish" expression, half-lidded and weary. Wide mouth that's round and cute rather than menacing — no sharp fangs visible. Two comically tiny arms, barely thumb-length, that can't reach anything (his defining comedic trait). A thick stubby tail that's disproportionately strong, always swinging around uncontrollably. He wears a tiny white dress shirt slightly wrinkled, with a small green necktie loosely knotted. No shoes — his stubby feet have small rounded claws.
|
||||
|
||||
His default pose: standing slightly slouched, dead-fish eyes staring ahead, tiny arms hanging uselessly at his sides.
|
||||
|
||||
Clean white background for character reference.
|
||||
|
||||
Key identifying features: 1) Dead-fish tired eyes, 2) Comically tiny useless arms.
|
||||
|
||||
---
|
||||
|
||||
### 特特(Tete)— ✅ 已有人设图
|
||||
**身份**:Q版迷你翼龙,傲娇同事
|
||||
**标记性特征**:防风镜、浅蓝灰翅膀
|
||||
**出现场次**:场3
|
||||
|
||||
**提示词**(备档参考):
|
||||
|
||||
3D Pixar-style chibi character design sheet, full body front view, side view, and back view.
|
||||
|
||||
A tiny chibi Pteranodon office worker with a sleek light blue-gray body. Round oversized head with large confident eyes and a short pointed beak. His wings fold neatly at his sides like a small cape when not in use, with visible membrane texture in a slightly lighter blue-gray. He wears a tiny white dress shirt with a small red necktie — the shirt fits snugly around his compact torso. A pair of aviator-style wind goggles perched on top of his head (his signature accessory, rarely actually worn over eyes). Small clawed feet. His posture is upright and proud, chin slightly raised.
|
||||
|
||||
Default pose: standing tall with wings folded, one wing slightly extended in a cocky gesture, goggles on forehead, a smug half-smile.
|
||||
|
||||
Clean white background for character reference.
|
||||
|
||||
Key identifying features: 1) Aviator wind goggles on top of head, 2) Light blue-gray wing-cape silhouette.
|
||||
|
||||
---
|
||||
|
||||
### 皮皮(Pipi)— ❌ 需要生成
|
||||
**身份**:Q版迷你甲龙,热情实习生
|
||||
**标记性特征**:弹力球般的圆身体、永远兴奋的大眼睛
|
||||
**出现场次**:场3、场4
|
||||
|
||||
**提示词**(复制到生图工具):
|
||||
|
||||
3D Pixar-style chibi character design sheet, full body front view, side view, and back view.
|
||||
|
||||
A tiny chibi Ankylosaurus intern, almost perfectly spherical like a bouncy rubber ball. Bright sunny yellow skin with a smooth, slightly glossy finish. An oversized round head with enormous sparkly eyes — always wide open with pure uncontainable excitement, as if everything in the world is the most amazing thing ever. A small cheerful open-mouth smile. A row of small rounded bony armor plates running along the back, adding texture but not making the character look threatening — more like decorative bumps. A short thick tail with a distinctive bone club at the tip. Extremely short stubby legs — so short that the character appears to move more by rolling and bouncing than walking. Wears a tiny white dress shirt (slightly too big, with sleeves rolled up once) and no tie. The body is almost perfectly round from certain angles — when this character charges forward, it genuinely looks like a cannonball.
|
||||
|
||||
Default pose: slightly bouncing in place, eyes sparkling with maximum excitement, tiny arms raised in enthusiastic greeting.
|
||||
|
||||
Clean white background for character reference.
|
||||
|
||||
Key identifying features: 1) Perfectly round bouncy-ball body shape, 2) Enormous sparkly eyes permanently set to "maximum excitement."
|
||||
|
||||
---
|
||||
|
||||
### 外卖三角龙(Delivery Triceratops)— ❌ 需要生成
|
||||
**身份**:Q版迷你三角龙,外卖骑手(路人角色)
|
||||
**标记性特征**:三只小圆角、外卖骑手背心
|
||||
**出现场次**:场2
|
||||
|
||||
**提示词**(复制到生图工具):
|
||||
|
||||
3D Pixar-style chibi character design sheet, full body front view and three-quarter view.
|
||||
|
||||
A tiny chibi Triceratops delivery rider. Warm orange skin with a tangerine tone. Three small rounded horns on the head — two short ones above the eyes and one stubby one on the nose tip, all rounded and cute rather than sharp. A small bony frill at the back of the head. Compact round body with short sturdy legs. He wears a brightly colored delivery rider vest in yellow-green with reflective stripes and a small delivery company logo on the chest, over a plain gray T-shirt. No accessories or props — just the character himself.
|
||||
|
||||
Default pose: leaning forward slightly with a stressed and hurried expression, one arm swinging as if running, as if perpetually late for a delivery.
|
||||
|
||||
Clean white background for character reference.
|
||||
|
||||
Key identifying features: 1) Three small cute rounded horns, 2) Yellow-green delivery vest with reflective stripes.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 道具提示词
|
||||
|
||||
> 以下道具需要单独生成,不包含在人设图中
|
||||
|
||||
---
|
||||
|
||||
### 道具1:外卖保温箱
|
||||
**所属角色**:外卖三角龙
|
||||
**出现场次**:场2
|
||||
|
||||
**提示词**(复制到生图工具):
|
||||
|
||||
3D Pixar-style prop design, front view and three-quarter view, clean product shot.
|
||||
|
||||
A large insulated food delivery box designed for a 30cm-tall chibi dinosaur — the box is almost as big as the rider himself. Boxy rectangular shape with rounded corners, made of bright orange insulated material with a glossy finish. A flip-open lid on top with a small latch. Two adjustable shoulder straps on the back for wearing like a backpack. On the front panel, a small cartoon dinosaur logo with the text "DinoDash Express" in playful font. A small LED indicator light on the side glows green (order active). The surface has a slightly puffy quilted texture from the insulation.
|
||||
|
||||
Clean white background.
|
||||
|
||||
---
|
||||
|
||||
### 道具2:迷你外卖电动车
|
||||
**所属角色**:外卖三角龙
|
||||
**出现场次**:场2
|
||||
|
||||
**提示词**(复制到生图工具):
|
||||
|
||||
3D Pixar-style prop design, side view and three-quarter view, clean product shot.
|
||||
|
||||
A tiny electric delivery scooter scaled for a 30cm-tall chibi dinosaur. Compact and rounded design — more cute than sleek. The body is bright yellow-green matching the rider's vest, with a small round headlight in front and a tiny windshield. Two small chunky wheels with thick rubber tires. A flat platform for standing on with anti-slip texture. Handlebars with tiny side mirrors and a small basket mounted on the front. A rear rack designed to hold the delivery box. A small license plate on the back reads "DINO-007". The overall design is toy-like and adorable — like a real scooter shrunk to doll-house scale.
|
||||
|
||||
Clean white background.
|
||||
|
||||
---
|
||||
|
||||
### 道具3:超大闹钟
|
||||
**所属角色**:T仔
|
||||
**出现场次**:场1(核心道具,多次特写)
|
||||
**状态变化**:正常状态(白色表盘)→ 咆哮模式(整体变红,震动)→ 被叼在嘴里(闷响)
|
||||
|
||||
**提示词**(复制到生图工具):
|
||||
|
||||
3D Pixar-style prop design, front view and three-quarter view, clean product shot.
|
||||
|
||||
A classic twin-bell alarm clock, about half to two-thirds the size of a 30cm-tall chibi dinosaur's head — large enough to be comedic on the tiny nightstand, but small enough that a wide-mouthed T-Rex could bite and hold it in his jaws (this is a key story moment). Round clock face with a creamy white dial and bold black numbers in a retro font. Two metallic silver bells on top with a small hammer in between. Short stubby legs at the bottom. The clock body is bright cherry red with a glossy plastic finish. The hour and minute hands are black with a small red second hand ticking away. A single snooze button on the top between the two bells — slightly worn from repeated smacking. The back has a small wind-up key and a tiny speaker grille (for the "T-Rex roar" alarm sound). The overall design is chunky, round, and satisfyingly holdable — like a cartoon apple or a stress ball with bells on top.
|
||||
|
||||
Clean white background.
|
||||
|
||||
---
|
||||
|
||||
### 道具4:痒痒挠(加长挠背器)
|
||||
**所属角色**:T仔
|
||||
**出现场次**:场1(T仔弥补小短手的标志性工具)
|
||||
|
||||
**提示词**(复制到生图工具):
|
||||
|
||||
3D Pixar-style prop design, front view and side view, clean product shot.
|
||||
|
||||
A miniature back-scratcher designed for a tiny chibi dinosaur with comically short arms. About 1.5 times the length of the dinosaur's body. A long thin bamboo-colored handle with a slightly curved grip at one end — wrapped with a small strip of worn green tape for better grip. At the business end, a small hand-shaped scratcher head with five tiny curved fingers, carved from pale wood. The overall design is simple and well-used — slightly scratched surface, the tape slightly peeling — suggesting this is the owner's most essential daily tool. It looks like a tiny garden rake crossed with a comedic oversized chopstick.
|
||||
|
||||
Clean white background.
|
||||
|
||||
---
|
||||
|
||||
### 道具5:霸王龙专属代餐火腿肠
|
||||
**所属角色**:T仔(场景互动道具)
|
||||
**出现场次**:场2(有特写,被碾碎是关键笑点)
|
||||
|
||||
**提示词**(复制到生图工具):
|
||||
|
||||
3D Pixar-style prop design, front view and three-quarter view, clean product shot.
|
||||
|
||||
A single packaged ham sausage — a cheap processed meat snack, scaled for the miniature dinosaur world. About the size of a chibi dinosaur's forearm. Wrapped in shiny bright red and gold plastic packaging with bold yellow text reading "霸王龙专属代餐" (T-Rex Exclusive Meal Replacement) in a dramatic over-the-top font. A cartoon muscular T-Rex flexing its tiny arms is printed on the wrapper as the brand mascot. One end of the packaging is twisted and sealed with a small metal clip. The sausage inside is slightly visible through a transparent window section — pink and plump. The overall design is intentionally cheap and tacky — the kind of convenience store impulse buy that no self-respecting dinosaur would admit to craving.
|
||||
|
||||
Clean white background.
|
||||
77
skills/测试skills三件套/剧本切分-skill/outputs/scene-prompts.md
Normal file
77
skills/测试skills三件套/剧本切分-skill/outputs/scene-prompts.md
Normal file
@ -0,0 +1,77 @@
|
||||
# 🏙️ 场景提示词 — 第1集 《最遥远的距离》
|
||||
|
||||
> 美术风格:3D Pixar-style chibi
|
||||
> 用途:复制粘贴到 Lovart / MidJourney / 任意生图工具
|
||||
> 所有场景均为Q版恐龙尺寸的迷你世界
|
||||
|
||||
---
|
||||
|
||||
### T仔的单身公寓
|
||||
**类型**:室内 / 日(清晨 07:30)
|
||||
**氛围**:温暖、局促、微微忧伤的独居感
|
||||
**出现场次**:场1
|
||||
**视觉锚点**:超大圆形闹钟、全勤奖奖状
|
||||
|
||||
**提示词**(复制到生图工具):
|
||||
|
||||
3D Pixar-style environment concept art, cinematic wide shot looking down into the room at a slight angle, rendered in Pixar's signature warm lighting style with rich saturated colors, shallow depth of field.
|
||||
|
||||
A tiny cramped studio apartment scaled for a 30cm-tall chibi dinosaur — everything is miniature. The bed is barely shoebox-sized with wrinkled cream-colored sheets and a small pillow with a dent in the middle. On the bedside table sits a small round alarm clock — only about 8cm tall, roughly the size of a tennis ball with twin bells on top. Classic twin-bell design in glossy bright cherry red. The clock is much smaller than the pillow next to it, taking up only about one-quarter of the nightstand surface. A single small window on the left wall lets in a dramatic beam of warm golden morning sunlight at a low angle — visible volumetric god rays cut through the air, illuminating tiny floating dust particles dancing in the light shaft. The light beam casts long, rich amber shadows across the polished pale wooden floor, creating strong warm-cool contrast between the sunlit areas and the cooler blue-gray shadows. The walls are plain and bare except for one framed "Perfect Attendance Award" certificate hung slightly crooked, catching a subtle edge of golden light — the room's only decoration. A tiny desk in the corner is cluttered with instant noodle cups with slightly glossy packaging and a small water glass that catches and refracts the sunlight into a tiny rainbow. A long wooden back-scratcher is wedged between the bed and the nightstand, within easy grabbing reach from the pillow — clearly placed there on purpose every night as a bedtime routine tool. Every surface has Pixar-quality material definition — the wood has a subtle warm sheen, the bedsheets have soft fabric folds with subsurface scattering, the alarm clock's metal bells have specular highlights. A gentle warm rim light outlines key objects, separating them from the background. The overall space feels cramped but lived-in, cozy yet melancholic. Color palette: rich amber, warm cream, and soft terracotta with cool blue-gray accent shadows.
|
||||
|
||||
No characters in the scene. 16:9 widescreen cinematic format.
|
||||
|
||||
Key identifying elements: 1) Small glossy red twin-bell alarm clock on bedside table, roughly tennis-ball sized, 2) "Perfect Attendance Award" certificate on the wall with a rim of golden light.
|
||||
|
||||
---
|
||||
|
||||
### 恐龙城街道
|
||||
**类型**:室外 / 日(早晨 08:30)
|
||||
**氛围**:忙碌、明快、充满生活气息的通勤早高峰
|
||||
**出现场次**:场2
|
||||
**视觉锚点**:迷你路灯和垃圾桶、各种小恐龙赶路的身影
|
||||
|
||||
**提示词**(复制到生图工具):
|
||||
|
||||
3D Pixar-style environment concept art, cinematic wide establishing shot at street level, rendered in Pixar's signature vivid lighting style with rich saturated colors, shallow depth of field with soft bokeh in the background.
|
||||
|
||||
A bustling miniature city street designed entirely for 30cm-tall chibi dinosaurs. The road is narrow and paved with smooth light-gray cobblestones that catch the sunlight with subtle specular highlights. Tiny streetlamps line both sides — each lamp has a warm orange glow still faintly on from the night, with polished brass-like metallic finish reflecting the morning sky. Small round trash cans in cheerful glossy green sit at regular intervals along the curb, casting crisp short shadows. Low-rise buildings with colorful storefronts frame both sides — a mini convenience store with a glowing neon sign, a tiny noodle shop with translucent wisps of steam catching the backlight as they rise from the window, and a small office building in the soft-focus distance. Bright warm morning sunlight pours in from the right at a golden angle, creating dramatic long shadows on the left side of the street and a beautiful warm-cool split — the sunlit side glows in rich amber and tangerine, while the shadowed side has cool lavender-blue tones. Volumetric light haze hangs subtly in the air where the sun hits between buildings, giving depth. The sky is vivid cerulean blue with a few fluffy clouds edged in golden pink. Scattered across the street are tiny silhouettes of various chibi dinosaurs heading to work — some walking, some on miniature scooters, all with warm rim lighting on their edges. A crosswalk with tiny white stripes. Every surface has Pixar-quality material definition — the cobblestones are slightly wet with morning dew creating tiny reflections, the shop windows have realistic glass reflections, the metal lamp posts have soft specular highlights. The atmosphere is lively and energetic — a Monday morning rush hour bursting with color and life.
|
||||
|
||||
No characters in the scene. 16:9 widescreen cinematic format.
|
||||
|
||||
Key identifying elements: 1) Miniature brass streetlamps with warm glow and glossy green trash cans, 2) Colorful storefronts with neon signs and steam catching the backlight.
|
||||
|
||||
---
|
||||
|
||||
### 公司大厅 / 打卡处
|
||||
**类型**:室内 / 日(早晨 08:55)
|
||||
**氛围**:紧张、冰冷的制度感、现代办公楼大厅
|
||||
**出现场次**:场3、场4
|
||||
**视觉锚点**:比恐龙们高一点的打卡机、天花板消防喷淋头
|
||||
|
||||
**提示词**(复制到生图工具):
|
||||
|
||||
3D Pixar-style environment concept art, cinematic medium wide shot at ground level with a slightly low angle looking up, rendered in Pixar's signature lighting style with rich saturated colors, shallow depth of field.
|
||||
|
||||
The lobby of a small modern office building, scaled for 30cm-tall chibi dinosaurs. The floor is polished light-gray marble tile with a mirror-like sheen — reflecting the overhead lights and the silhouettes of objects above, creating a beautiful doubled image on the ground. In the center-right of the frame stands a small cute electronic punch-clock terminal — only about 35cm tall, just barely taller than the chibi dinosaur employees. It has a chunky rounded white plastic casing with smooth Pixar-style curves, like a chubby little robot. A small square LCD screen sits right at dinosaur-face-height, glowing with green digits showing the current time. Below the screen is a round face-recognition sensor with a subtle blue LED ring. The machine sits on a short stubby pedestal base. Its design is compact, rounded, and adorable — like a miniature self-checkout kiosk shrunk to toy scale. A tiny red indicator light sits on top like a small cherry. The walls are clean off-white with a company logo (abstract dinosaur silhouette) in brushed metallic finish mounted near the entrance, catching a gleam of light. A glass entrance door in the background lets in a shaft of warm exterior daylight that contrasts dramatically with the cool interior — creating a warm-cool color tension between the golden outdoor light spilling through the glass and the sterile blue-white fluorescent overhead panels. On the ceiling — clearly visible and important — are several fire sprinkler heads in polished metallic silver with small red glass indicators, each one catching a tiny pinpoint specular highlight from the fluorescents. The ceiling is low enough that a flying dinosaur could accidentally hit one. A small digital wall clock near the punch machine glows 08:59 in anxious red digits. Subtle rim lighting separates the punch machine and key props from the background. The reflective floor adds cinematic depth. The vibe is tense and institutional — cold corporate precision where every second is a judgment.
|
||||
|
||||
No characters in the scene. 16:9 widescreen cinematic format.
|
||||
|
||||
Key identifying elements: 1) Glossy white electronic punch-clock machine with glowing LCD and blue scan line, 2) Polished silver fire sprinkler heads with specular highlights on the low ceiling.
|
||||
|
||||
---
|
||||
|
||||
### 公司工位区
|
||||
**类型**:室内 / 日
|
||||
**氛围**:日常办公、略显沉闷
|
||||
**出现场次**:彩蛋
|
||||
**视觉锚点**:迷你办公桌排列、英总办公室的紧闭大门
|
||||
|
||||
**提示词**(复制到生图工具):
|
||||
|
||||
3D Pixar-style environment concept art, cinematic medium shot from a low seated perspective with shallow depth of field, rendered in Pixar's signature lighting style with rich saturated colors and dramatic light-shadow interplay.
|
||||
|
||||
An open-plan office area scaled for 30cm-tall chibi dinosaurs. Rows of tiny cubicle desks with miniature computer monitors emitting a soft cool-blue screen glow, small stacks of paperwork, and personal trinkets. The desks are light maple wood with a warm satin finish catching subtle overhead reflections. Small swivel chairs in muted teal fabric. In the foreground, one desk is sharply in focus — slightly messy with a few items scattered on its surface. The background desks fall into a creamy soft bokeh. The floor is light-gray low-pile carpet with subtle texture. Cool overhead fluorescent panel lights create the main illumination with a sterile blue-white tone. In the background, prominently visible, is a closed executive office door — solid dark mahogany wood with a frosted glass panel and a small brass nameplate with specular highlights. A thin warm strip of light glows from the gap beneath the door, hinting at a lit room inside — creating a subtle warm-cool contrast against the cold office. Subtle rim light edges the foreground desk and chair, giving depth. The overall color palette is muted corporate gray-blue, clean and grounded — no special effects, no particles, just a realistic Pixar-quality office environment.
|
||||
|
||||
No characters in the scene. 16:9 widescreen cinematic format.
|
||||
|
||||
Key identifying elements: 1) Foreground desk with warm wood surface, 2) Closed mahogany executive door with a thin warm light strip beneath it.
|
||||
55
skills/测试skills三件套/剧本切分-skill/outputs/segments-ep01-index.md
Normal file
55
skills/测试skills三件套/剧本切分-skill/outputs/segments-ep01-index.md
Normal file
@ -0,0 +1,55 @@
|
||||
# 📊 切分索引 — 第1集 《最遥远的距离》
|
||||
|
||||
| 片段 | 时码 | 时长 | 场景 | 出场人物 | 参考图 |
|
||||
|------|------|------|------|----------|--------|
|
||||
| 01 | 0:00-0:15 | 15秒 | 单身公寓(内/日) | T仔 | T仔人设、公寓场景、闹钟道具 |
|
||||
| 02 | 0:15-0:30 | 15秒 | 单身公寓(内/日) | T仔 | T仔人设、闹钟道具、痒痒挠道具 |
|
||||
| 03 | 0:30-0:44 | 14秒 | 恐龙城街道(外/日) | T仔 | T仔人设、街道场景、火腿肠道具 |
|
||||
| 04 | 0:44-0:59 | 15秒 | 恐龙城街道(外/日) | T仔、外卖三角龙 | T仔人设、外卖龙人设、电动车道具 |
|
||||
| 05 | 0:59-1:13 | 14秒 | 公司打卡处(内/日) | T仔、特特 | T仔人设、特特人设、打卡处场景 |
|
||||
| 06 | 1:13-1:28 | 15秒 | 公司打卡处(内/日) | T仔、特特、皮皮 | T仔人设、特特人设、皮皮人设 |
|
||||
| 07 | 1:28-1:40 | 12秒 | 公司打卡处(内/日) | T仔、特特、皮皮 | T仔人设、特特人设、皮皮人设 |
|
||||
| 08 | 1:40-1:55 | 15秒 | 打卡机前(内/日) | T仔 | T仔人设、打卡处场景 |
|
||||
| 09 | 1:55-2:10 | 15秒 | 打卡机前(内/日) | T仔、皮皮 | T仔人设、皮皮人设 |
|
||||
| 10 | 2:10-2:22 | 12秒 | 打卡机前(内/日) | T仔 | T仔人设 |
|
||||
| 11 | 2:22-2:33 | 11秒 | 打卡机前(内/日) | T仔、皮皮 | T仔人设、皮皮人设 |
|
||||
| 12 | 2:33-2:40 | 7秒 | 公司工位区(内/日) | T仔 | T仔人设、工位区场景 |
|
||||
|
||||
**总计**:12 个片段 / 总时长 2分40秒
|
||||
**场景数**:4 个(单身公寓、恐龙城街道、公司打卡处、公司工位区)
|
||||
**参考图需求**:人设图 4 张 + 场景图 4 张 + 道具图 5 张
|
||||
|
||||
---
|
||||
|
||||
## 参考图清单(去重)
|
||||
|
||||
### 人设图
|
||||
- [ ] T仔人设图 — 出现于片段 01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11, 12
|
||||
- [ ] 特特人设图 — 出现于片段 05, 06, 07
|
||||
- [ ] 皮皮人设图 — 出现于片段 06, 07, 09, 11
|
||||
- [ ] 外卖三角龙人设图 — 出现于片段 04
|
||||
|
||||
### 场景图
|
||||
- [ ] 单身公寓场景图 — 出现于片段 01
|
||||
- [ ] 恐龙城街道场景图 — 出现于片段 03
|
||||
- [ ] 公司打卡处场景图 — 出现于片段 05, 08
|
||||
- [ ] 公司工位区场景图 — 出现于片段 12
|
||||
|
||||
### 道具图
|
||||
- [ ] 闹钟道具图 — 出现于片段 01, 02
|
||||
- [ ] 痒痒挠道具图 — 出现于片段 02
|
||||
- [ ] 火腿肠道具图 — 出现于片段 03
|
||||
- [ ] 外卖电动车道具图 — 出现于片段 04
|
||||
- [ ] 外卖保温箱道具图 — 出现于片段 04(可选)
|
||||
|
||||
---
|
||||
|
||||
## 切分节奏概览
|
||||
|
||||
```
|
||||
场1-单身公寓 ████████████████████████████████ 30秒(2段)
|
||||
场2-恐龙城街道 █████████████████████████████ 29秒(2段)
|
||||
场3-公司打卡处 █████████████████████████████████████████ 41秒(3段)
|
||||
场4-打卡机前 █████████████████████████████████████████████████████ 53秒(4段)
|
||||
彩蛋-工位区 ███████ 7秒(1段)
|
||||
```
|
||||
260
skills/测试skills三件套/剧本切分-skill/outputs/segments-ep01.md
Normal file
260
skills/测试skills三件套/剧本切分-skill/outputs/segments-ep01.md
Normal file
@ -0,0 +1,260 @@
|
||||
# ✂️ 剧本切分 — 第1集 《最遥远的距离》
|
||||
|
||||
> 切分规则:15秒上限 | 场景切换必断 | 保留原始剧本格式
|
||||
> 总片段数:12 段 | 总时长:约 2分40秒
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
## 片段 01 / 共 12 段
|
||||
**时码**:0:00 - 0:15(15秒)
|
||||
**场景**:T仔的单身公寓(内,日)
|
||||
**出场人物**:T仔
|
||||
**参考图**:
|
||||
@图片1 — T仔人设图,保持角色外貌一致
|
||||
@图片2 — 单身公寓场景图,场景/光影参考
|
||||
@图片3 — 闹钟道具图,闹钟外观参考
|
||||
---
|
||||
|
||||
△ [黑屏] T仔(OS):作为一只霸王龙的后代,我每天最大的敌人是——
|
||||
△ [特写] 闹钟显示07:30,猛地开始震动。
|
||||
△ [近景] T仔从被子里面伸出小短手够闹钟,疯狂挥舞,就是够不着(伴随"呼呼"的挥空声)。
|
||||
△ [近景] T仔在被子里叹气,把被子掀开。
|
||||
T仔:又是新的一天。
|
||||
△ [近景] T仔熟练地摸出痒痒挠,想要用痒痒挠关掉闹钟,道具刚要碰到闹钟。
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
## 片段 02 / 共 12 段
|
||||
**时码**:0:15 - 0:30(15秒)
|
||||
**场景**:T仔的单身公寓(内,日)
|
||||
**出场人物**:T仔
|
||||
**参考图**:
|
||||
@图片1 — T仔人设图
|
||||
@图片2 — 闹钟道具图(本段闹钟有状态变化:正常→红色咆哮模式→被叼嘴里)
|
||||
@图片3 — 痒痒挠道具图
|
||||
@视频1 — 上一段视频(片段01),衔接参考
|
||||
---
|
||||
|
||||
△ [中近景] T仔的尾巴突然像有了自我意识一样,猛地一甩!咻——啪!
|
||||
△ [近景] 痒痒挠被尾巴扫飞,砸在墙上,反弹回来,精准击中闹钟的按钮。
|
||||
△ [特写] 闹钟瞬间变成红色,T仔提前录制的"霸王龙咆哮"炸响。
|
||||
△ [近景] 音效:嗷————!!(声波震得水杯里的水都在抖)。
|
||||
T仔(崩溃抱头):闭嘴!那是进化的误会!
|
||||
△ [中景 动作] T仔放弃挣扎,张开大口,正在咆哮的闹钟叼在嘴里,T仔试图物理隔音。
|
||||
△ [近景] 音效(闷闷的闹钟铃声)嗷~ 嗷~(从T仔嘴里传出)。
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
## 片段 03 / 共 12 段
|
||||
**时码**:0:30 - 0:44(14秒)
|
||||
**场景**:恐龙城街道(外,日)
|
||||
**出场人物**:T仔
|
||||
**参考图**:
|
||||
@图片1 — T仔人设图
|
||||
@图片2 — 恐龙城街道场景图,场景/光影参考
|
||||
@图片3 — 火腿肠道具图(包装上印着"霸王龙专属代餐")
|
||||
@视频1 — 上一段视频(片段02),衔接参考
|
||||
---
|
||||
|
||||
△ [中近景] T仔嘴里叼着半片吐司狂奔,领带甩在脸上。
|
||||
T仔(OS):来不及了,马上要错过全勤奖了!
|
||||
△ [近景] 跑太快,尾巴扫飞了路边的垃圾桶。
|
||||
△ [特写] 垃圾桶里滚出一根火腿肠(包装上印着"霸王龙专属代餐")。
|
||||
△ [近景] T仔回头看了一眼被撞到的垃圾桶,眼睛一亮,下意识急刹。
|
||||
T仔:顶级的工业淀粉!我的灵魂伴侣!
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
## 片段 04 / 共 12 段
|
||||
**时码**:0:44 - 0:59(15秒)
|
||||
**场景**:恐龙城街道(外,日)
|
||||
**出场人物**:T仔、外卖三角龙
|
||||
**参考图**:
|
||||
@图片1 — T仔人设图
|
||||
@图片2 — 外卖三角龙人设图
|
||||
@图片3 — 外卖电动车道具图
|
||||
@视频1 — 上一段视频(片段03),衔接参考
|
||||
---
|
||||
|
||||
△ [中近景] T仔刚要弯腰,背景虚焦处,身后传来呼啸声。
|
||||
△ [中景] 一辆"三角龙外卖车"冲过来。
|
||||
△ [近景] 三角龙外卖员对着T仔怒吼。
|
||||
外卖龙:让让!外卖超时要扣钱的!
|
||||
△ [特写] 车轮碾过火腿肠,噗叽!
|
||||
△ [近景] 酱汁溅了T仔一脸,像给他的死鱼眼化了个烟熏妆。
|
||||
△ [近景] T仔举起小短手想抹脸,手在脸旁边挥了两下——够不着。只好放弃,顶着一脸酱汁看着地上的肉泥。
|
||||
T仔:为什么倒霉的总是我。
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
## 片段 05 / 共 12 段
|
||||
**时码**:0:59 - 1:13(14秒)
|
||||
**场景**:公司打卡处(内,日)
|
||||
**出场人物**:T仔、特特
|
||||
**参考图**:
|
||||
@图片1 — T仔人设图
|
||||
@图片2 — 特特人设图
|
||||
@图片3 — 公司打卡处场景图,场景/光影参考
|
||||
@视频1 — 上一段视频(片段04),衔接参考
|
||||
---
|
||||
|
||||
△ [中近景] T仔跑到打卡处,马上要打到卡了。
|
||||
△ [近景 镜头从下往上拍] T仔头顶传来破空声,T仔抬头,镜头往上移。
|
||||
△ [中景] 特特戴着防风镜,叼着文件,做一个完美的低空滑翔。
|
||||
机器音:打卡成功。
|
||||
△ [中景] 特特落地,整理羽毛,一脸傲娇,凡尔赛的说话。
|
||||
特特:借过一下,低效率的陆地生物们。我已经——
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
## 片段 06 / 共 12 段
|
||||
**时码**:1:13 - 1:28(15秒)
|
||||
**场景**:公司打卡处(内,日)
|
||||
**出场人物**:T仔、特特、皮皮
|
||||
**参考图**:
|
||||
@图片1 — T仔人设图
|
||||
@图片2 — 特特人设图
|
||||
@图片3 — 皮皮人设图
|
||||
@视频1 — 上一段视频(片段05),衔接参考
|
||||
---
|
||||
|
||||
△ [近景] 特特突然停顿,翅膀僵硬。
|
||||
△ [特写] 特特引以为傲的翅膀尖上,粘着一块湿漉漉的绿化带草皮(皮皮的早餐)。
|
||||
△ [中景] T仔一脸淡定对特特说。
|
||||
T仔:特特,英总说过"带薪吃草"扣绩效。
|
||||
△ [近景] 特特瞬间破防。
|
||||
特特:不!这是——
|
||||
△ [中景] 就在特特慌乱时,皮皮(明黄色的迷你甲龙,圆得像个弹力球)滚入画面。
|
||||
皮皮(兴奋):我的早餐!还给我!
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
## 片段 07 / 共 12 段
|
||||
**时码**:1:28 - 1:40(12秒)
|
||||
**场景**:公司打卡处(内,日)
|
||||
**出场人物**:T仔、特特、皮皮
|
||||
**参考图**:
|
||||
@图片1 — T仔人设图
|
||||
@图片2 — 特特人设图
|
||||
@图片3 — 皮皮人设图
|
||||
@视频1 — 上一段视频(片段06),衔接参考
|
||||
---
|
||||
|
||||
△ [中景] 皮皮像炮弹一样撞在特特翅膀上。特特被撞飞,一头扎进了天花板的消防喷淋头。哗啦——!(喷水声)。
|
||||
△ [中景] 水柱喷出来,特特、皮皮和刚跑到旁边的T仔全被淋了个透。
|
||||
△ [近景] 特特被淋成落汤翼龙。
|
||||
特特:我的精英人设!
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
## 片段 08 / 共 12 段
|
||||
**时码**:1:40 - 1:55(15秒)
|
||||
**场景**:打卡机前(内,日)
|
||||
**出场人物**:T仔
|
||||
**参考图**:
|
||||
@图片1 — T仔人设图
|
||||
@图片2 — 公司打卡处场景图(打卡机特写参考)
|
||||
@视频1 — 上一段视频(片段07),衔接参考
|
||||
---
|
||||
|
||||
△ [特写/慢动作] 电子钟从 08:59:59 跳动。
|
||||
△ [近景 动作] T仔的大脸带着残影,像一颗绝望的保龄球撞向感应区。
|
||||
△ [近景] 咚!T仔脸撞击塑料外壳的闷响。
|
||||
△ [特写] 打卡机上的屏幕显示:识别中...(转圈圈)。
|
||||
△ [近景] T仔屏住呼吸,死鱼眼瞪大到极限。
|
||||
△ [特写] 屏幕数字无情跳动:09:00:01。
|
||||
机器音(欢快女声):滴——!早上好T仔!您已迟到1秒。
|
||||
|
||||
---
|
||||
备注:本段前半部分为慢动作(T仔冲向打卡机),节奏在"识别中"处极度拉紧,"迟到1秒"为全片最大落差点。
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
## 片段 09 / 共 12 段
|
||||
**时码**:1:55 - 2:10(15秒)
|
||||
**场景**:打卡机前(内,日)
|
||||
**出场人物**:T仔、皮皮
|
||||
**参考图**:
|
||||
@图片1 — T仔人设图
|
||||
@图片2 — 皮皮人设图
|
||||
@视频1 — 上一段视频(片段08),衔接参考
|
||||
---
|
||||
|
||||
△ [近景 特效] T仔灰败的脸上,出现了一道裂痕(像石像碎裂)。
|
||||
T仔(绝望):我的全勤奖啊。
|
||||
△ [中景 动作] 旁边同样湿透的皮皮像小狗一样疯狂甩动身体。
|
||||
△ [中近景] 皮皮把身上的水全甩到了T仔脸上。
|
||||
△ [近景] 皮皮满脸幸福,举起短手。
|
||||
皮皮:耶!大哥!我们是一起迟到的耶!这就是传说中的团魂吗?
|
||||
△ [中景 动作] 皮皮跳起来,用头撞了一下T仔。
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
## 片段 10 / 共 12 段
|
||||
**时码**:2:10 - 2:22(12秒)
|
||||
**场景**:打卡机前(内,日)
|
||||
**出场人物**:T仔
|
||||
**参考图**:
|
||||
@图片1 — T仔人设图
|
||||
@视频1 — 上一段视频(片段09),衔接参考
|
||||
---
|
||||
|
||||
△ [近景] T仔面对镜头,眼神空洞,任由水珠滴落。
|
||||
T仔(内心独白):所谓成年人的崩溃,不需要天塌下来…… 只需要…… 那个缓冲的圆圈,多转了一圈。
|
||||
|
||||
---
|
||||
备注:本段为全片情绪最低点,节奏极慢。内心独白配合T仔空洞的死鱼眼特写,留足停顿。
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
## 片段 11 / 共 12 段
|
||||
**时码**:2:22 - 2:33(11秒)
|
||||
**场景**:打卡机前(内,日)
|
||||
**出场人物**:T仔、皮皮
|
||||
**参考图**:
|
||||
@图片1 — T仔人设图
|
||||
@图片2 — 皮皮人设图
|
||||
@视频1 — 上一段视频(片段10),衔接参考
|
||||
---
|
||||
|
||||
△ [中景 画面即将定格时] T仔的肚子突然发出一声雷鸣——咕噜噜——!(打破了本应深沉的二胡BGM)
|
||||
△ [近景] T仔低头看了一眼自己的肚子,又抬头看向镜头。死鱼眼。
|
||||
△ [中景 二分画面 画面定格] T仔灰白的脸 vs 皮皮灿烂的笑脸。
|
||||
[音效] 凄凉的二胡声。
|
||||
[字幕] 《恐龙也是打工龙》 第1集 完
|
||||
|
||||
---
|
||||
备注:肚子响打破深沉气氛,反差笑点。最后定格为二分画面(T仔丧脸 vs 皮皮笑脸),配二胡收尾。
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
## 片段 12 / 共 12 段
|
||||
**时码**:2:33 - 2:40(7秒)
|
||||
**场景**:公司工位区(内,日)
|
||||
**出场人物**:T仔
|
||||
**参考图**:
|
||||
@图片1 — T仔人设图
|
||||
@图片2 — 公司工位区场景图,场景/光影参考
|
||||
@视频1 — 上一段视频(片段11),衔接参考
|
||||
---
|
||||
|
||||
△ [近景] T仔湿淋淋地坐在工位上,面前摊着考勤罚单。他抬起头,鼻子抽了抽。
|
||||
△ [主观镜头/T仔视角] 英总办公室紧闭的门,门缝里飘出阵阵洗发水的香气,化成可见的金色香气线条飘向T仔。
|
||||
T仔(OS/吞口水声):好香……
|
||||
[黑屏]
|
||||
[字幕] 下集:禁忌的本能
|
||||
|
||||
---
|
||||
备注:片尾彩蛋。金色香气线条为动态特效,Seedance自行生成。本段为下集铺垫。
|
||||
140
skills/测试skills三件套/剧本切分-skill/script/ep01.md
Normal file
140
skills/测试skills三件套/剧本切分-skill/script/ep01.md
Normal file
@ -0,0 +1,140 @@
|
||||
# 《恐龙也是打工龙》 第1集 — 最遥远的距离
|
||||
|
||||
> 类型:泡面番 | 时长:约2分30秒~2分40秒 | 受众:全年龄/打工人共鸣向
|
||||
|
||||
---
|
||||
|
||||
## 【角色表】
|
||||
|
||||
> 所有角色均为Q版迷你体型,生活在按小恐龙尺寸设计的迷你城市中。
|
||||
|
||||
**T仔**(主角)— Q版迷你霸王龙
|
||||
外貌:约30厘米高,2头身比例,圆滚滚的身体。橘红色皮肤,肚皮浅黄。脑袋很大,占身体近一半,嘴巴宽大但圆润可爱。两只眼睛又大又圆,常年"死鱼眼"式疲惫表情。两只小短手只有拇指长,几乎什么都够不着(核心喜剧设定)。尾巴粗短但力气大,经常不受控制地甩来甩去闯祸。穿白色迷你衬衫,打绿色小领带。
|
||||
性格:丧系打工龙,嘴上吐槽,心里认命。
|
||||
|
||||
**特特**(翼龙同事)
|
||||
外貌:Q版迷你翼龙,浅蓝灰色身体,圆脑袋大眼睛,短喙。翅膀像小披风。穿白色衬衫,打红色领带,常戴防风镜在头顶。
|
||||
性格:自恋傲娇,觉得能飞就高人一等。
|
||||
|
||||
**皮皮**(甲龙实习生)
|
||||
外貌:Q版迷你甲龙,明黄色,身体又圆又硬像个弹力球。背上一排小骨甲,尾巴末端有骨锤。四肢极短,移动方式接近滚动。眼睛又大又亮,永远一脸兴奋。
|
||||
性格:职场小白,热情过头,好心办坏事。
|
||||
|
||||
**外卖三角龙**(路人)
|
||||
外貌:Q版迷你三角龙,橘色,头上三只小角,穿外卖骑手背心,骑迷你外卖电动车。
|
||||
|
||||
---
|
||||
|
||||
## 【场景表】
|
||||
|
||||
**T仔的单身公寓**:按Q版恐龙尺寸的迷你开间。床只有鞋盒大小,床头柜上放着一个比T仔脑袋还大的圆形闹钟。窗户透进暖黄晨光。墙上贴着一张"全勤奖"奖状(唯一装饰)。
|
||||
|
||||
**恐龙城街道**:迷你城市街道,小号垃圾桶、小号路灯,各种Q版恐龙赶路上班。
|
||||
|
||||
**公司大厅/打卡处**:小型办公楼大厅,一台比恐龙们高一点的打卡机,天花板有消防喷淋头。
|
||||
|
||||
---
|
||||
|
||||
## 场 1
|
||||
时:日 07:30
|
||||
景:内 T仔狭小的单身公寓
|
||||
人:T仔
|
||||
|
||||
△ [黑屏] T仔(OS):作为一只霸王龙的后代,我每天最大的敌人是——
|
||||
△ [特写] 闹钟显示07:30,猛地开始震动。
|
||||
△ [近景] T仔从被子里面伸出小短手够闹钟,疯狂挥舞,就是够不着(伴随"呼呼"的挥空声)。
|
||||
△ [近景] T仔在被子里叹气,把被子掀开。
|
||||
T仔:又是新的一天。
|
||||
△ [近景] T仔熟练地摸出痒痒挠,想要用痒痒挠关掉闹钟,道具刚要碰到闹钟。
|
||||
△ [中近景] T仔的尾巴突然像有了自我意识一样,猛地一甩!咻——啪!
|
||||
△ [近景] 痒痒挠被尾巴扫飞,砸在墙上,反弹回来,精准击中闹钟的按钮。
|
||||
△ [特写] 闹钟瞬间变成红色,T仔提前录制的"霸王龙咆哮"炸响。
|
||||
△ [近景] 音效:嗷————!!(声波震得水杯里的水都在抖)。
|
||||
T仔(崩溃抱头):闭嘴!那是进化的误会!
|
||||
△ [中景 动作] T仔放弃挣扎,张开大口,正在咆哮的闹钟叼在嘴里,T仔试图物理隔音。
|
||||
△ [近景] 音效(闷闷的闹钟铃声)嗷~ 嗷~(从T仔嘴里传出)。
|
||||
CO
|
||||
|
||||
## 场 2
|
||||
时:日 08:30
|
||||
景:外 恐龙城街道(上班的路上)
|
||||
人:T仔、外卖三角龙
|
||||
|
||||
△ [中近景] T仔嘴里叼着半片吐司狂奔,领带甩在脸上。
|
||||
T仔(OS):来不及了,马上要错过全勤奖了!
|
||||
△ [近景] 跑太快,尾巴扫飞了路边的垃圾桶。
|
||||
△ [特写] 垃圾桶里滚出一根火腿肠(包装上印着"霸王龙专属代餐")。
|
||||
△ [近景] T仔回头看了一眼被撞到的垃圾桶,眼睛一亮,下意识急刹。
|
||||
T仔:顶级的工业淀粉!我的灵魂伴侣!
|
||||
△ [中近景] T仔刚要弯腰,背景虚焦处,身后传来呼啸声。
|
||||
△ [中景] 一辆"三角龙外卖车"冲过来。
|
||||
△ [近景] 三角龙外卖员对着T仔怒吼。
|
||||
外卖龙:让让!外卖超时要扣钱的!
|
||||
△ [特写] 车轮碾过火腿肠,噗叽!
|
||||
△ [近景] 酱汁溅了T仔一脸,像给他的死鱼眼化了个烟熏妆。
|
||||
△ [近景] T仔举起小短手想抹脸,手在脸旁边挥了两下——够不着。只好放弃,顶着一脸酱汁看着地上的肉泥。
|
||||
T仔:为什么倒霉的总是我。
|
||||
CO
|
||||
|
||||
## 场 3
|
||||
时:日 08:55
|
||||
景:内 公司打卡处
|
||||
人:T仔、特特(翼龙)、皮皮(甲龙)
|
||||
|
||||
△ [中近景] T仔跑到打卡处,马上要打到卡了。
|
||||
△ [近景 镜头从下往上拍] T仔头顶传来破空声,T仔抬头,镜头往上移。
|
||||
△ [中景] 特特戴着防风镜,叼着文件,做一个完美的低空滑翔。
|
||||
机器音:打卡成功。
|
||||
△ [中景] 特特落地,整理羽毛,一脸傲娇,凡尔赛的说话。
|
||||
特特:借过一下,低效率的陆地生物们。我已经——
|
||||
△ [近景] 特特突然停顿,翅膀僵硬。
|
||||
△ [特写] 特特引以为傲的翅膀尖上,粘着一块湿漉漉的绿化带草皮(皮皮的早餐)。
|
||||
△ [中景] T仔一脸淡定对特特说。
|
||||
T仔:特特,英总说过"带薪吃草"扣绩效。
|
||||
△ [近景] 特特瞬间破防。
|
||||
特特:不!这是——
|
||||
△ [中景] 就在特特慌乱时,皮皮(明黄色的迷你甲龙,圆得像个弹力球)滚入画面。
|
||||
皮皮(兴奋):我的早餐!还给我!
|
||||
△ [中景] 皮皮像炮弹一样撞在特特翅膀上。特特被撞飞,一头扎进了天花板的消防喷淋头。哗啦——!(喷水声)。
|
||||
△ [中景] 水柱喷出来,特特、皮皮和刚跑到旁边的T仔全被淋了个透。
|
||||
△ [近景] 特特被淋成落汤翼龙。
|
||||
特特:我的精英人设!
|
||||
CO
|
||||
|
||||
## 场 4
|
||||
时:日
|
||||
景:内 打卡机前
|
||||
人:T仔、皮皮
|
||||
|
||||
△ [特写/慢动作] 电子钟从 08:59:59 跳动。
|
||||
△ [近景 动作] T仔的大脸带着残影,像一颗绝望的保龄球撞向感应区。
|
||||
△ [近景] 咚!T仔脸撞击塑料外壳的闷响。
|
||||
△ [特写] 打卡机上的屏幕显示:识别中...(转圈圈)。
|
||||
△ [近景] T仔屏住呼吸,死鱼眼瞪大到极限。
|
||||
△ [特写] 屏幕数字无情跳动:09:00:01。
|
||||
机器音(欢快女声):滴——!早上好T仔!您已迟到1秒。
|
||||
△ [近景 特效] T仔灰败的脸上,出现了一道裂痕(像石像碎裂)。
|
||||
T仔(绝望):我的全勤奖啊。
|
||||
△ [中景 动作] 旁边同样湿透的皮皮像小狗一样疯狂甩动身体。
|
||||
△ [中近景] 皮皮把身上的水全甩到了T仔脸上。
|
||||
△ [近景] 皮皮满脸幸福,举起短手。
|
||||
皮皮:耶!大哥!我们是一起迟到的耶!这就是传说中的团魂吗?
|
||||
△ [中景 动作] 皮皮跳起来,用头撞了一下T仔。
|
||||
△ [近景] T仔面对镜头,眼神空洞,任由水珠滴落。
|
||||
T仔(内心独白):所谓成年人的崩溃,不需要天塌下来…… 只需要…… 那个缓冲的圆圈,多转了一圈。
|
||||
△ [中景 画面即将定格时] T仔的肚子突然发出一声雷鸣——咕噜噜——!(打破了本应深沉的二胡BGM)
|
||||
△ [近景] T仔低头看了一眼自己的肚子,又抬头看向镜头。死鱼眼。
|
||||
△ [中景 二分画面 画面定格] T仔灰白的脸 vs 皮皮灿烂的笑脸。
|
||||
[音效] 凄凉的二胡声。
|
||||
[字幕] 《恐龙也是打工龙》 第1集 完
|
||||
CO
|
||||
|
||||
---
|
||||
|
||||
## 【片尾彩蛋】(3秒)
|
||||
|
||||
△ [近景] T仔湿淋淋地坐在工位上,面前摊着考勤罚单。他抬起头,鼻子抽了抽。
|
||||
△ [主观镜头/T仔视角] 英总办公室紧闭的门,门缝里飘出阵阵洗发水的香气,化成可见的金色香气线条飘向T仔。
|
||||
T仔(OS/吞口水声):好香……
|
||||
[黑屏]
|
||||
[字幕] 下集:禁忌的本能
|
||||
27
skills/测试skills三件套/原创剧本-skill/README.md
Normal file
27
skills/测试skills三件套/原创剧本-skill/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# 原创动画剧本 — AI技能包
|
||||
|
||||
## 给AI的指令(直接复制粘贴到对话框即可)
|
||||
|
||||
```
|
||||
请严格按照以下步骤执行:
|
||||
1. 阅读 .claude/CLAUDE.md — 这是你的角色定义和工作流程
|
||||
2. 阅读 .claude/skills/screenplay-skill/SKILL.md — 这是你的专业技能和创作规范
|
||||
3. 阅读完毕后,严格遵守文件中的所有规则,不要跳过、简化或自由发挥
|
||||
4. 按照 CLAUDE.md 中 [初始化] 部分的格式向用户打招呼
|
||||
```
|
||||
|
||||
## 这个技能包是做什么的?
|
||||
|
||||
帮你从零开始写动画剧本,或者诊断优化已有的剧本。
|
||||
|
||||
写出来的剧本可以直接丢给 Seedance 2.0(即梦)生成视频。
|
||||
|
||||
## 文件说明
|
||||
|
||||
| 文件 | 作用 |
|
||||
|------|------|
|
||||
| `.claude/CLAUDE.md` | AI的角色、流程、指令集 |
|
||||
| `.claude/skills/screenplay-skill/SKILL.md` | 创作规范、诊断方法论、AI视觉化写作规则 |
|
||||
| `scripts/` | 剧本文件存放目录 |
|
||||
| `.claude/skills/screenplay-skill/references/` | 知识库(编剧理论、儿童动画指南等) |
|
||||
| `.claude/skills/screenplay-skill/templates/` | 输出模板 |
|
||||
141
skills/测试skills三件套/原创剧本-skill/scripts/ep01.md
Normal file
141
skills/测试skills三件套/原创剧本-skill/scripts/ep01.md
Normal file
@ -0,0 +1,141 @@
|
||||
# 《恐龙也是打工龙》 第1集 — 最遥远的距离
|
||||
|
||||
> 类型:泡面番 | 时长:约2分30秒~2分40秒 | 受众:全年龄/打工人共鸣向
|
||||
|
||||
---
|
||||
|
||||
## 【角色表】
|
||||
|
||||
> 所有角色均为Q版迷你体型,生活在按小恐龙尺寸设计的迷你城市中。
|
||||
|
||||
**T仔**(主角)— Q版迷你霸王龙
|
||||
外貌:约30厘米高,2头身比例,圆滚滚的身体。橘红色皮肤,肚皮浅黄。脑袋很大,占身体近一半,嘴巴宽大但圆润可爱。两只眼睛又大又圆,常年"死鱼眼"式疲惫表情。两只小短手只有拇指长,几乎什么都够不着(核心喜剧设定)。尾巴粗短但力气大,经常不受控制地甩来甩去闯祸。穿白色迷你衬衫,打绿色小领带。
|
||||
性格:丧系打工龙,嘴上吐槽,心里认命。
|
||||
|
||||
**特特**(翼龙同事)
|
||||
外貌:Q版迷你翼龙,浅蓝灰色身体,圆脑袋大眼睛,短喙。翅膀像小披风。穿白色衬衫,打红色领带,常戴防风镜在头顶。
|
||||
性格:自恋傲娇,觉得能飞就高人一等。
|
||||
|
||||
**皮皮**(甲龙实习生)
|
||||
外貌:Q版迷你甲龙,明黄色,身体又圆又硬像个弹力球。背上一排小骨甲,尾巴末端有骨锤。四肢极短,移动方式接近滚动。眼睛又大又亮,永远一脸兴奋。
|
||||
性格:职场小白,热情过头,好心办坏事。
|
||||
|
||||
**外卖三角龙**(路人)
|
||||
外貌:Q版迷你三角龙,橘色,头上三只小角,穿外卖骑手背心,骑迷你外卖电动车。
|
||||
|
||||
---
|
||||
|
||||
## 【场景表】
|
||||
|
||||
**T仔的单身公寓**:按Q版恐龙尺寸的迷你开间。床只有鞋盒大小,床头柜上放着一个比T仔脑袋还大的圆形闹钟。窗户透进暖黄晨光。墙上贴着一张"全勤奖"奖状(唯一装饰)。
|
||||
|
||||
**恐龙城街道**:迷你城市街道,小号垃圾桶、小号路灯,各种Q版恐龙赶路上班。
|
||||
|
||||
**公司大厅/打卡处**:小型办公楼大厅,一台比恐龙们高一点的打卡机,天花板有消防喷淋头。
|
||||
|
||||
---
|
||||
|
||||
## 场 1
|
||||
时:日 07:30
|
||||
景:内 T仔狭小的单身公寓
|
||||
人:T仔
|
||||
|
||||
△ [黑屏] T仔(OS):作为一只霸王龙的后代,我每天最大的敌人是——
|
||||
△ [俯拍/近景] 小床上,T仔躺在被窝里,被子盖到脖子,只露出一颗圆滚滚的橘红色大脑袋,闭着眼,嘴巴微张,睡得很沉。
|
||||
△ [特写] 床头柜上的红色闹钟显示07:30,猛地开始震动。
|
||||
△ [近景] T仔被震醒,眉头皱起,眼睛半睁。他从被子里伸出小短手够闹钟,疯狂挥舞,就是够不着(伴随"呼呼"的挥空声)。
|
||||
△ [近景] T仔放弃,缩回被子里叹了口气,然后把被子掀开坐起来。
|
||||
T仔:又是新的一天。
|
||||
△ [近景] T仔熟练地摸出痒痒挠,想要用痒痒挠关掉闹钟,道具刚要碰到闹钟。
|
||||
△ [中近景] T仔的尾巴突然像有了自我意识一样,猛地一甩!咻——啪!
|
||||
△ [近景] 痒痒挠被尾巴扫飞,砸在墙上,反弹回来,精准击中闹钟的按钮。
|
||||
△ [特写] 闹钟瞬间变成红色,T仔提前录制的"霸王龙咆哮"炸响。
|
||||
△ [近景] 音效:嗷————!!(声波震得水杯里的水都在抖)。
|
||||
T仔(崩溃抱头):闭嘴!那是进化的误会!
|
||||
△ [中景 动作] T仔放弃挣扎,张开大口,正在咆哮的闹钟叼在嘴里,T仔试图物理隔音。
|
||||
△ [近景] 音效(闷闷的闹钟铃声)嗷~ 嗷~(从T仔嘴里传出)。
|
||||
CO
|
||||
|
||||
## 场 2
|
||||
时:日 08:30
|
||||
景:外 恐龙城街道(上班的路上)
|
||||
人:T仔、外卖三角龙
|
||||
|
||||
△ [中近景] T仔嘴里叼着半片吐司狂奔,领带甩在脸上。
|
||||
T仔(OS):来不及了,马上要错过全勤奖了!
|
||||
△ [近景] 跑太快,尾巴扫飞了路边的垃圾桶。
|
||||
△ [特写] 垃圾桶里滚出一根火腿肠(包装上印着"霸王龙专属代餐")。
|
||||
△ [近景] T仔回头看了一眼被撞到的垃圾桶,眼睛一亮,下意识急刹。
|
||||
T仔:顶级的工业淀粉!我的灵魂伴侣!
|
||||
△ [中近景] T仔刚要弯腰,背景虚焦处,身后传来呼啸声。
|
||||
△ [中景] 一辆"三角龙外卖车"冲过来。
|
||||
△ [近景] 三角龙外卖员对着T仔怒吼。
|
||||
外卖龙:让让!外卖超时要扣钱的!
|
||||
△ [特写] 车轮碾过火腿肠,噗叽!
|
||||
△ [近景] 酱汁溅了T仔一脸,像给他的死鱼眼化了个烟熏妆。
|
||||
△ [近景] T仔举起小短手想抹脸,手在脸旁边挥了两下——够不着。只好放弃,顶着一脸酱汁看着地上的肉泥。
|
||||
T仔:为什么倒霉的总是我。
|
||||
CO
|
||||
|
||||
## 场 3
|
||||
时:日 08:55
|
||||
景:内 公司打卡处
|
||||
人:T仔、特特(翼龙)、皮皮(甲龙)
|
||||
|
||||
△ [中近景] T仔跑到打卡处,马上要打到卡了。
|
||||
△ [近景 镜头从下往上拍] T仔头顶传来破空声,T仔抬头,镜头往上移。
|
||||
△ [中景] 特特戴着防风镜,叼着文件,做一个完美的低空滑翔。
|
||||
机器音:打卡成功。
|
||||
△ [中景] 特特落地,整理羽毛,一脸傲娇,凡尔赛的说话。
|
||||
特特:借过一下,低效率的陆地生物们。我已经——
|
||||
△ [近景] 特特突然停顿,翅膀僵硬。
|
||||
△ [特写] 特特引以为傲的翅膀尖上,粘着一块湿漉漉的绿化带草皮(皮皮的早餐)。
|
||||
△ [中景] T仔一脸淡定对特特说。
|
||||
T仔:特特,英总说过"带薪吃草"扣绩效。
|
||||
△ [近景] 特特瞬间破防。
|
||||
特特:不!这是——
|
||||
△ [中景] 就在特特慌乱时,皮皮(明黄色的迷你甲龙,圆得像个弹力球)滚入画面。
|
||||
皮皮(兴奋):我的早餐!还给我!
|
||||
△ [中景] 皮皮像炮弹一样撞在特特翅膀上。特特被撞飞,一头扎进了天花板的消防喷淋头。哗啦——!(喷水声)。
|
||||
△ [中景] 水柱喷出来,特特、皮皮和刚跑到旁边的T仔全被淋了个透。
|
||||
△ [近景] 特特被淋成落汤翼龙。
|
||||
特特:我的精英人设!
|
||||
CO
|
||||
|
||||
## 场 4
|
||||
时:日
|
||||
景:内 打卡机前
|
||||
人:T仔、皮皮
|
||||
|
||||
△ [特写/慢动作] 电子钟从 08:59:59 跳动。
|
||||
△ [近景 动作] T仔的大脸带着残影,像一颗绝望的保龄球撞向感应区。
|
||||
△ [近景] 咚!T仔脸撞击塑料外壳的闷响。
|
||||
△ [特写] 打卡机上的屏幕显示:识别中...(转圈圈)。
|
||||
△ [近景] T仔屏住呼吸,死鱼眼瞪大到极限。
|
||||
△ [特写] 屏幕数字无情跳动:09:00:01。
|
||||
机器音(欢快女声):滴——!早上好T仔!您已迟到1秒。
|
||||
△ [近景 特效] T仔灰败的脸上,出现了一道裂痕(像石像碎裂)。
|
||||
T仔(绝望):我的全勤奖啊。
|
||||
△ [中景 动作] 旁边同样湿透的皮皮像小狗一样疯狂甩动身体。
|
||||
△ [中近景] 皮皮把身上的水全甩到了T仔脸上。
|
||||
△ [近景] 皮皮满脸幸福,举起短手。
|
||||
皮皮:耶!大哥!我们是一起迟到的耶!这就是传说中的团魂吗?
|
||||
△ [中景 动作] 皮皮跳起来,用头撞了一下T仔。
|
||||
△ [近景] T仔面对镜头,眼神空洞,任由水珠滴落。
|
||||
T仔(内心独白):所谓成年人的崩溃,不需要天塌下来…… 只需要…… 那个缓冲的圆圈,多转了一圈。
|
||||
△ [中景 画面即将定格时] T仔的肚子突然发出一声雷鸣——咕噜噜——!(打破了本应深沉的二胡BGM)
|
||||
△ [近景] T仔低头看了一眼自己的肚子,又抬头看向镜头。死鱼眼。
|
||||
△ [中景 二分画面 画面定格] T仔灰白的脸 vs 皮皮灿烂的笑脸。
|
||||
[音效] 凄凉的二胡声。
|
||||
[字幕] 《恐龙也是打工龙》 第1集 完
|
||||
CO
|
||||
|
||||
---
|
||||
|
||||
## 【片尾彩蛋】(3秒)
|
||||
|
||||
△ [近景] T仔湿淋淋地坐在工位上,面前摊着考勤罚单。他抬起头,鼻子抽了抽。
|
||||
△ [主观镜头/T仔视角] 英总办公室紧闭的门,门缝里飘出阵阵洗发水的香气,化成可见的金色香气线条飘向T仔。
|
||||
T仔(OS/吞口水声):好香……
|
||||
[黑屏]
|
||||
[字幕] 下集:禁忌的本能
|
||||
26
skills/网文改编/项目结构.md
Normal file
26
skills/网文改编/项目结构.md
Normal file
@ -0,0 +1,26 @@
|
||||
project/
|
||||
├── novel/ # 小说源文件目录(用户上传)
|
||||
│ ├── chapter-001.txt
|
||||
│ ├── chapter-002.txt
|
||||
│ └── ...
|
||||
├── plot-breakdown.md # 剧情拆解+分集标注(改编后生成)
|
||||
├── scripts/ # 单集剧本目录(改编后生成)
|
||||
│ ├── Episode-01.md
|
||||
│ ├── Episode-02.md
|
||||
│ └── ...
|
||||
└── .claude/
|
||||
├── CLAUDE.md # 项目规则和主Agent配置(必须创建)
|
||||
├── skills/
|
||||
│ └── webtoon-skill/ # 网文改编漫剧技能包(必须创建)
|
||||
│ ├── SKILL.md # 技能包核心配置
|
||||
│ ├── adapt-method.md # 改编方法论
|
||||
│ ├── output-style.md # 写作风格
|
||||
│ ├── templates/ # 文档格式模板
|
||||
│ │ ├── plot-breakdown-template.md
|
||||
│ │ └── script-template.md
|
||||
│ └── examples/ # 改编示例
|
||||
│ ├── plot-breakdown-example.md
|
||||
│ └── script-example.md
|
||||
└── agents/
|
||||
├── breakdown-aligner.md # 剧情拆解质量检查Agent(必须创建)
|
||||
└── webtoon-aligner.md # 单集剧本一致性检查Agent(必须创建)
|
||||
Loading…
x
Reference in New Issue
Block a user