Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/BOARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@

| 功能 | 状态 | 备注 |
|---|---|---|
| 分镜 / Shot 节点(脚本→分镜→序列) | 🔵 | Shot 编辑器落地(`3ba33d3`);**首切片完成**:「用本镜备注生成图像」(shot_notes→image config 节点+narrative 边)+ FAB/空态 shot 创建入口 + addNode typeConfig 透传。**SB-5 链排序 util 落地**(`canvas/util/narrative_order.dart`——E1/E4 公共地基,同时解锁 SB-6 序列预览与 EX-1′ 导出叙事序)。**SB-3 落地**:shot 面板预期时长/预期运镜(意图口径,不按 provider 能力钳制;运镜文案抽 `util/camera_labels.dart` 双面板共享)。**SB-6 序列预览落地**(`features/storyboard`:顶栏「序列预览」→ 按叙事链把整条分镜从头播一遍;图片按时长、视频按真实进度推进,无产物镜显 notes 占位同计时)。后续:脚本解析(SB-1,**D-M4-1 待拍板**)、shot→video 直达(SB-4) |
| 分镜 / Shot 节点(脚本→分镜→序列) | 🔵 | Shot 编辑器落地(`3ba33d3`);**首切片完成**:「用本镜备注生成图像」(shot_notes→image config 节点+narrative 边)+ FAB/空态 shot 创建入口 + addNode typeConfig 透传。**SB-5 链排序 util 落地**(`canvas/util/narrative_order.dart`——E1/E4 公共地基,同时解锁 SB-6 序列预览与 EX-1′ 导出叙事序)。**SB-3 落地**:shot 面板预期时长/预期运镜(意图口径,不按 provider 能力钳制;运镜文案抽 `util/camera_labels.dart` 双面板共享)。**SB-6 序列预览落地**(`features/storyboard`:顶栏「序列预览」→ 按叙事链把整条分镜从头播一遍;图片按时长、视频按真实进度推进,无产物镜显 notes 占位同计时)。**SB-4 落地**(shot→video 直达,镜头级参数一并带过去)。**D-M4-1 拍板 A 规则拆分**(2026-08-08)→ **SB-1 落地**(`storyboard/util/script_splitter.dart` 纯规则零 LLM,无 API key 也能用)。后续:脚本导入对话框 + 批量建链(SB-2,依赖 SB-1 已解锁)——补上后「粘贴脚本→分镜链」这条入口即通 |
| 视频导出 / 拼接 | 🔵 | **服务层落地**:`VideoExportService.concat`(项目相对路径进出)→ `FfmpegVideoExportService`(concat demuxer 流拷贝 `-c copy`,不转码,要求输入同编码参数)+ `FfmpegLocator`(INKFRAME_FFMPEG env → PATH 探测,命中缓存)+ 最小 `ProcessRunner` 抽象(fake 可注入);`FileResolverService.resolveInProject`(项目根边界安全校验,导出落 `exports/`)。**不打包 ffmpeg 二进制**(体积/许可评估延后):系统有 ffmpeg 才真拼接,没有 → `LocalIOError(reason=ffmpeg_not_found)`。真 ffmpeg 集成测 `@Tags(['ffmpeg'])` TEST_FFMPEG=1 门控。**UI 入口落地**:`features/export`——画布顶栏「导出视频」按钮(无 video result 时禁用+说明 tooltip)→ 导出对话框(按 position.x 升序默认全选、复选/上下移手动排序、输出名本地预校验同 `_assertPlainFileName` 规则、busy 态 indeterminate、成功 snackbar+复制绝对路径、ffmpeg_not_found 专门文案 `exportVideoFfmpegMissing`);`ExportController` 承担画布相对→项目相对路径换算(补 `canvases/<canvasId>/` 前缀)。**EX-1′ 落地**:默认序改 narrative 链序(经 `export/util/export_order.dart` + `canvas/util/node_artifacts.dart`,无链退回 position.x)+ 行首 16:9 缩略图。后续:转码/分辨率归一(EX-2,D-M4-6 待拍板)、打包二进制评估 |
| 本地素材 / 产物画廊(借鉴 InvokeAI) | 🔵 | **首切片落地**:`features/gallery`——project 维度只读聚合(result 节点 image_url/video_url + batch_results 成功 slot,`listSuccessByProject` 一次跨画布查询 + 与节点主图同路径去重,createdAt 倒序)→ 网格 UI(图片 tile 走 fileResolver + 图片 lightbox;视频 tile 图标+时长占位);入口=Studio 项目卡菜单「Gallery」,路由走 `currentGalleryProjectProvider`(同 canvasId 语义)。**视频缩略图/播放(GA-1/2/7,PR-3 #208)与筛选/搜索+存为角色(GA-3/4,PR-4)已随 Polish Wave 1 落地**;后续:拖入画布(GA-5,D-M4-2 待拍板)、删除(GA-6,D-M4-3 待拍板);视频时长已全链路打通(#177 写侧 + XM-1b 存量启动回填);fake `listSuccessByProject` join 派生列由种子行提供(契约见注释,PG 集成测兜真语义) |
| 模型扩展(自定义 Provider,BYO-key) | 🔵 | **首切片落地**:`custom_providers.json`(逐条校验 + 损坏兜底)→ 协议模板 `openai-image` 派生 capabilities → `OpenAICompatibleImageProvider`(extends SyncProviderBase) → 启动期一次性注册(改 json 重启生效);key 复用 `provider.custom:<id>.api_key`,设置页/门控/banner 零改动生效。PROVIDER-API §13 已重写为唯一方案,ADR-0009 已修订(2026-07-02)。后续:设置页编辑 UI、运行时增删(registry 变异非 invalidate)、更多协议模板 |
Expand Down Expand Up @@ -114,7 +114,9 @@
| **SB-5 narrative 链排序 util(M4 E1/E4 公共地基;MASTERPLAN 关键路径首件)**:纯函数 `orderByNarrativeChain({nodes, edges, include})`——只认 narrative 边且两端在场(悬空边忽略);链头=参与图且入度 0,按 (dx,dy,id);显式栈 DFS 前序,同源多出边按 (sortOrder,边 id);visited **出栈时**标记(菱形汇合不重复输出、环不死循环);遍历走不到的(纯环+孤立)按 (dx,dy,id) 追加——**任何形态都产出确定性全序,输出恒为输入的一个排列**(少一节点=导出少一镜);`include` 只滤输出不打断链序(滤掉链中间节点,两侧仍保持链相对序——EX-1′ 用法)。15 例覆盖单链/多链/分叉/汇合/环/尾环/孤立/悬空边/include/全序不变量。**解锁 SB-6 序列预览 + EX-1′ 导出叙事序** | #219 |
| **EX-1′ 导出 narrative 自动排序 + 行首缩略图(M4 E4;E4「完整态」三件之一收口)**:导出对话框默认序从 position.x 改为 narrative 链序。**关键判断**:video result 节点挂在 config 节点下(sourceNodeId)、**自己不在 narrative 链上**——直接把 result 集合喂给 `orderByNarrativeChain` 一条边都找不到,会静默退化成 position.x("看起来能跑,实际卡等于没做")。正解=先按链排 config/shot 节点,再经**新建的 artifacts util** 映射到各自产物。产出:①`canvas/util/node_artifacts.dart`(`resultsFor` 新→旧 + `latestResultFor`;**SB-6 只消费不再建**——卡面裁决)②`export/util/export_order.dart`(`orderVideoNodesForExport` + `exportableVideoNodes` 从 widget 迁入,消除 util→widgets 层级倒置)③`CanvasNode.createdAt`(`row_reader` 加 `optDateTime`;**取最新产物不能取列表序末位**——`listByCanvas` 主序是 z_index,由用户拖动层级决定,与生成时间无关)④行首 16:9 缩略图(GA-1 同源读 `thumbnail_url`,ME-26 按宽解码,缺失/解码失败回退图标)⑤排序移出对话框(两入口顶栏+命令面板同一条路径)。**只改顺序不改候选集**:同一镜多 take 仍全部列出、成组跟在该镜位置新→旧(收窄默认选中是产品行为变更,超出卡面,另记债)。+19 例 | #220 |
| **SB-3 镜头级参数(M4 E1;解锁 SB-6 最后一张前置)**:shot 面板加「预期时长」(固定档 3/5/10/15s → `duration_ms`,与 video 面板同键同单位,SB-4 直达视频原样透传)与「预期运镜」(**列全量 CameraMovement**)。**语义分界**:shot 记的是导演意图,此刻还没选 provider——所以不按 `supportedCameras`/`supportedDurations` 钳制(video 面板在无能力表时整段隐藏,shot 面板必须照列),能不能真做到由生成时 video inspector 的现有钳制收口;面板加一行说明把这条讲给用户。运镜文案抽 `canvas/util/camera_labels.dart` 双面板共享(video inspector 改 import,行为零变化)。两处健壮性:不在档位表的历史值 / 认不出的枚举名一律当未设置,不硬塞进 `DropdownButton.value`(会命中「value 不在 items」断言崩面板);选回「未设置」**显式写 null**——`patchTypeConfig` 是 `type_config \|\| patch::jsonb` 合并,省略键会保留旧值,用户点了清不掉。ARB +4 键。+8 例 | #221 |
| **SB-6 序列预览(M4 E1;分镜线首个「打开就能看见」的成果)**:新 `features/storyboard`——`models/sequence_shot.dart` + `util/sequence_builder.dart`(纯函数,全单测)+ `widgets/sequence_preview_dialog.dart`;排序复用 SB-5、产物查找复用 EX-1′ 首建的 artifacts util(**本卡只消费,不再造第二套**——卡面裁决)。**关键设计=「折叠」**:`_generateImageFromNotes` 建出的真实拓扑是 shot──narrative──>image config──sourceNodeId──>image result,**config 节点也在链上**;照单每节点一镜会把 3 镜播成 6 条(占位/出图交替),而那是当前把图接进分镜链的**唯一**入口——不折叠等于本卡不可用。规则=节点把产物借给前面的 shot 后不再单独成镜,已借走的产物不被第二个 shot 再借(多 shot 指向同一 config 时)。**推进两套**:图片/占位走定时器(缺省 3s);视频以真实播放进度推进(position≥duration)**外加兜底定时器**——打不开文件/进度流不动/时长拿不到时不至于永远卡死在一镜(余量 +1.5s)。**media_kit 生命周期**(卡面点名泄漏高发):全对话框只 create 一个 handle、换镜靠 open 换源、只在 initState 建只在 dispose 放、**纯图片序列完全不 create**;`_epoch` 计数器防迟到回调推进已翻过的镜。入口=顶栏「序列预览」(有 narrative 边才 enable,可用性只 watch 边,拖节点不重建顶栏)。ARB +11 键。+30 例(折叠/时长优先级/handle 生命周期/推进语义/入口门控);顺带被仓库硬编码视觉值闸门抓到一处 `InkSpacing.xs / 2`,改整档而非放松守卫 | 本 PR |
| **SB-6 序列预览(M4 E1;分镜线首个「打开就能看见」的成果)**:新 `features/storyboard`——`models/sequence_shot.dart` + `util/sequence_builder.dart`(纯函数,全单测)+ `widgets/sequence_preview_dialog.dart`;排序复用 SB-5、产物查找复用 EX-1′ 首建的 artifacts util(**本卡只消费,不再造第二套**——卡面裁决)。**关键设计=「折叠」**:`_generateImageFromNotes` 建出的真实拓扑是 shot──narrative──>image config──sourceNodeId──>image result,**config 节点也在链上**;照单每节点一镜会把 3 镜播成 6 条(占位/出图交替),而那是当前把图接进分镜链的**唯一**入口——不折叠等于本卡不可用。规则=节点把产物借给前面的 shot 后不再单独成镜,已借走的产物不被第二个 shot 再借(多 shot 指向同一 config 时)。**推进两套**:图片/占位走定时器(缺省 3s);视频以真实播放进度推进(position≥duration)**外加兜底定时器**——打不开文件/进度流不动/时长拿不到时不至于永远卡死在一镜(余量 +1.5s)。**media_kit 生命周期**(卡面点名泄漏高发):全对话框只 create 一个 handle、换镜靠 open 换源、只在 initState 建只在 dispose 放、**纯图片序列完全不 create**;`_epoch` 计数器防迟到回调推进已翻过的镜。入口=顶栏「序列预览」(有 narrative 边才 enable,可用性只 watch 边,拖节点不重建顶栏)。ARB +11 键。+30 例(折叠/时长优先级/handle 生命周期/推进语义/入口门控);顺带被仓库硬编码视觉值闸门抓到一处 `InkSpacing.xs / 2`,改整档而非放松守卫 | #222 |
| **SB-4 shot→video 直达(M4 E1)**:shot 面板第二个生成入口——以本镜备注为 prompt 建 **video** config 节点 + narrative 边,并把 SB-3 的预期时长/预期运镜一并写进去(省得在 video 面板重填)。**带的是意图不是承诺**:video 面板按所选 provider 的 supportedDurations/supportedCameras 钳制,不支持的值落地即丢回未设置——本卡不做任何能力判断(此刻还没选 provider);未设置的参数**不写空键**,让 video 面板照常按 provider 取默认而非被显式 null 顶掉。两个生成入口的公共主体抽 `_generateFromNotes(type, extraConfig)`——原先只有图片一条,现在两条,复制一遍迟早发散(notifier/文案 await 前取好、节点与连线失败分开报这些约束一处都不能漏)。ARB +1 键。+5 例 | #223 |
| **SB-1 规则脚本拆分器(M4 E1;D-M4-1 拍板后首件)**:`storyboard/util/script_splitter.dart` 纯 Dart 零 Flutter 依赖——`ShotDraft{label,notes}` + `splitScript(text, strategy)`,策略 blankLine(空行分段)/perLine。**剥行首编号**是本卡最易写漏处:用户粘来的文本十有八九带 `1.`/`镜头1`/`第3镜`/`SHOT 1`/`# `,留着会污染 prompt(「1. 山径破晓」让模型去画数字);但不能剥过头——「1920 年代的街道」「3D 渲染质感」里的数字是内容,故**光秃秃的数字必须跟分隔符才算编号**,有词锚的(镜头/第N镜/shot/scene)才允许无分隔符。markdown 井号单独一趟剥(可独立出现也可叠在编号前,`### Shot 1 dawn`)。只剥**段首那一行**(「2 号机位跟拍」是内容不是第 2 镜);剥完为空的行丢弃让后续行顶上。CRLF 与单独 CR 都归一 LF;「空行」按 trim 判定(Word/网页粘贴常带空格 tab);label=段首行截 60 字而 notes 恒为全文。22 例覆盖两策略/六类编号/不误伤/退化输入/顺序稳定 | 本 PR |

## M1 补遗(审计发现的悬空项)

Expand Down
2 changes: 1 addition & 1 deletion docs/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ lib/
│ │ └── widgets/ # StartupErrorView (full-screen error + retry + open-log-dir)
│ ├── storyboard/ # Sequence preview (SB-6) — play the narrative chain end to end
│ │ ├── models/ # sequence_shot.dart (what each shot shows, and for how long)
│ │ ├── util/ # sequence_builder.dart (nodes+edges → playlist; pure)
│ │ ├── util/ # sequence_builder.dart (nodes+edges → playlist) + script_splitter.dart (SB-1 rule-based, no LLM); both pure
│ │ └── widgets/ # sequence_preview_dialog.dart (playback + advance only)
│ └── studio/ # Project / workspace shell (home + open-canvas + first-run onboarding dialog; ON-1/ON-2)
│ ├── studio_home_screen.dart
Expand Down
8 changes: 6 additions & 2 deletions docs/MASTERPLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,8 @@ beta 全周期零数据事故;官网稳定;SLA 演练过一次。
| D-4 数据升级政策(=U7) ✅ **已拍→ADR-0012** | QG-4/5、SCRAM 覆盖面、**每个 schema PR**、LB-12 manifest 策略 | ~~下一个 schema 变更前~~ 2026-07-08 拍板 |
| D-5 Win 安装物(=U6)/ D-6 Win 证书(=U2) | PKG-4、beta 准入 | PKG-4 实测后即拍 |
| D-7 画布保真度 d4-d7 / D-8 ⌘K ✅ **已拍(2026-07-09)** | ~~CV-1~~ 已解锁 | ~~CV-1 开工前~~ 已拍板 |
| D-M4-1~8 | GA-5/6、AG-4/5、EX-2、SB-1、CH-3 | M4 对应 Wave 开工前 |
| D-M4-1 脚本拆分 ✅ **已拍 A(2026-08-08)** | ~~SB-1~~ 已解锁(SB-1 落地,SB-2 可开) | ~~SB-1 开工前~~ 已拍板 |
| D-M4-2~8(其余七项) | GA-5/6、AG-4/5、EX-2、CH-3 | M4 对应 Wave 开工前 |
| D-BE-1/2 | LB-13A、LB-11/12 | 对应卡开工前 |
| D-9 任务状态载体 / D-10 数据目录选址 ✅ **D-10 已拍(2026-07-09)** | 规划运维 / ~~LB-10~~ 已解锁(新增迁移卡) | 首个 M4/M5 卡完成前 / ~~LB-10 前~~ 已拍板 |

Expand Down Expand Up @@ -343,7 +344,10 @@ beta 全周期零数据事故;官网稳定;SLA 演练过一次。
- **D-8 ⌘K 命令面板** ✅ **已拍(2026-07-09):A 做真**(首版 ≤6 动作;已随 #174 交付 PL-1)。

**功能模块系(D-M4-1~8,详见 features 明细文档;开 M4 Wave 前拍)**:
- **D-M4-1 脚本拆分**:A 规则拆分(推荐)/ B LLM / C A 先行后加 AI 按钮。
- **D-M4-1 脚本拆分**:✅ **已拍 A 规则拆分**(2026-08-08;SB-1 落地
`storyboard/util/script_splitter.dart`)。理由:纯规则零 LLM——没有 API key 也能用、
行为可断言可单测不花钱;LLM 辅助质量不可断言且成本 ≥L,留作 AG-5 chat 模板后的可选增强
(即 C 档,不废弃只延后)。**解锁 SB-1 → SB-2**。
- **D-M4-2 画廊→画布形态**:A 菜单动作(推荐)/ B 画布内抽屉真拖拽(后续)/ C OS 拖放。
- **D-M4-3 画廊删除语义**:A 永久删除 行+文件(推荐)/ B 仅隐藏 / C 软删保文件。
- **D-M4-4 聚合器生效时机**:A 编辑+重启(推荐先做)→ B 追加运行时新增 / C 全运行时(永不)。
Expand Down
20 changes: 20 additions & 0 deletions lib/features/storyboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,29 @@
```
models/sequence_shot.dart 播放清单条目(一镜放什么、放多久)
util/sequence_builder.dart 画布节点+边 → 播放清单(纯函数,全单测)
util/script_splitter.dart 粘贴的文本 → 分镜草稿(纯 Dart,无 Flutter 依赖)
widgets/sequence_preview_dialog.dart 只负责"放"与"推进"
```

## 脚本拆分(SB-1)

`splitScript(text, strategy)` 把一段粘来的文本拆成 `ShotDraft{label, notes}`。
**纯规则,零 LLM**(D-M4-1 拍 A 档)—— 没有 API key 也能用,行为可断言、可单测、不花钱。

两种策略:`blankLine`(空行分段,适合有段落结构的剧本)、`perLine`(每行一镜)。

最容易写漏的是**剥行首编号**。用户粘来的文本十有八九带 `1.` / `镜头1` / `第3镜` /
`SHOT 1` / `# `,留着会污染 prompt("1. 山径破晓" 会让模型去画一个数字 1)。但也不能
剥过头 —— "1920 年代的街道"、"3D 渲染质感"里的数字是内容。分界:

- **有词锚的**(`镜头N` / `第N镜` / `SHOT N` / `Scene N`)→ 分隔符可有可无
- **光秃秃的数字** → **必须**跟分隔符(`.` `)` `、` `:` `-`),否则不认

只剥**段首那一行**:段内的 "2 号机位跟拍" 是内容,不是第 2 镜。markdown 井号单独一趟剥,
因为它既可独立出现(`# 山径破晓`)也可叠在编号前(`### Shot 1 dawn`)。

`label` 是段首行截 60 字(展示用),`notes` 恒为全文 —— 截断不该丢内容。

排序复用 SB-5 的 `canvas/util/narrative_order.dart`,产物查找复用 EX-1′ 首建的
`canvas/util/node_artifacts.dart` —— 本 feature 不再造第二套(卡面裁决)。

Expand Down
Loading
Loading