feat(editor): AI 编辑助手悬浮面板、插件权限体系与时间线渲染管线(M4–M6.3) (#411)

* feat(editor): 编辑器实施 M0-M1 - 剪辑入口占位与插件 SDK v2 垂直切片

- detail.tsx 新增剪辑成片视图(editor tab,常驻),概览页插入剪辑步骤卡
- plugin-types.ts 新增 v2 类型(EditorSlotKind/EditorPluginPermission/PluginManifestV2),修复权限数组交叉归约 bug
- editor-slot-registry.ts 插槽注册表(优先级排序、幂等注册、订阅机制)
- plugin-registry.ts 注册器 v1/v2 分支校验,v1 路径零改动
- editor-shell.tsx 垂直切片插件:timeline-panel 插槽真实渲染,验证插件链路全通
- 测试:插槽注册表 5 用例 + 命令测试骨架,v1 插件回归 21 例零回归

* feat(editor): 命令协议黄金文件 - editor-commands 纯函数 reducer + 8 个黄金 op

* feat(editor): 快照撤销历史 - editor-history 有界 200 层 undo/redo

* feat(editor): 命令状态机 store - dispatch/手势预览/1.5s 防抖保存/串行保存链

* fix(editor): createEditorStore 返回 zustand hook 类型修复 typecheck

* feat(editor): 手势 echo 最小件 - 时间线面板拖拽移动/裁剪 + store 接线与本地持久化

* docs(editor): M2 完成状态记录

* feat(editor): M3.1 时间线面板选中联动 - store 选中状态 + 片段高亮/点击取消

* feat(editor): M3.2 预览监视器 - preview-renderer 插槽 + 真实媒体帧/播放头/时间码

* feat(editor): M3.3 片段检查器 - inspector 插槽 + 属性编辑入队可撤销

* feat(editor): M3.4 asset-ingest 素材库插件(addClip 命令入轨)

* feat(editor): M3.4 补充 - host 上下文(项目信息+资产)、editor.tsx 接线、asset-ingest 纯函数、死代码清理

* feat(editor): M3.5 subtitle-tools - SRT import/export + rebuild from node snapshot

* feat(editor): M3.6 transcription - deterministic mock ASR to subtitle track

* feat(editor): M3.7 export - render plan + ffmpeg.wasm fallback export

* feat(editor): M3.8 - render all 8 editor slots in three-pane layout + ai-assistant placeholder

* docs(editor): M3 完成状态 - 8 插件 + 全插槽接线,冒烟待确认

* feat(editor): 编辑器媒体与时间线打磨 - 资产来源分组筛选、时间线全宽布局、拖拽末片段修复、OSS 降级本地存储、Concat 风格控制条与检查器

* fix(plugins): 合并上游后修复插件分类筛选对 v2 清单的类型兼容

* feat(editor): 时间线工具态与轨道操作打磨 - 工具按钮选中仅图标变色、监视器 Concat 式控制条、removeTrack 黄金命令与轨道行头移除按钮、maskClosable 迁移

* fix(api): 资源上传超 MaxBytesReader 上限时报中文大小限制而非英文裸 400

* fix(editor): 监视器控制条 - 修复播放按钮 JSX 模板闭合花括号缺失导致的编译错误

* feat(api): 本地媒体导入分片化对齐 Concat - >50MB 视频不再失败 - 分片会话三接口(POST开始/PUT单片/complete合并)、前端超50MB自动切8MB分片并失败整传重试、默认配额放宽(日200MB→2GB、总2GB→20GB)、UploadResourceFile 豁免单文件限

* feat(editor): 轨道行头新增可见性/静音开关按钮 - 复用现有图标资源,按轨道类型显隐;命令层新增 setTrackFlag 黄金命令(flag 白名单 fail-closed)并补单测

* docs(progress): 记录轨道可见/静音开关改动与复测项

* docs(editor): M4 转写与渲染任务设计 - 复用 Task 表与状态机、独立创建 API、worker 领取类型过滤、whisper HTTP provider(OpenAI 兼容 /v1/audio)

* fix(api): OSS 上传失败自动降级本地存储时 body 已被 Transport 关闭 - newOSSRequest 用 NopCloser 包装请求体,服务端提前返回(签名 403)时不再关闭调用方持有的文件,降级分支 Seek 重读生效;补回归测试

* docs(progress): 记录媒体导入根治(分片+OSS 降级修复)复测项

* feat(api): 视频播放副本转码 H.265→H.264 + variant=playback 服务与启动回填 - 上传视频后异步探测 codec,H.265/AV1 走 moov 快速探测并转码 H.264 播放副本;GET /api/user-data/...?variant=playback 就绪时服务副本(独立 ETag :pb),未就绪回退原件并撤销后缀防 304 黑屏;storeResource 触发、启动幂等回填存量;前端播放失败自动轮询副本就绪后刷新

* fix(editor): 轨道至少保留一条 - 不可移除轨道点击 X 时居中浮现原因提示(2.2s 自动消失, aria-live);修正可见性 toggle 传参 (visible===false 语义) ;轨道行内移除按钮改为常驻(不可移除时点击给提示)

* feat(timeline): M4.1 字幕转写后台任务 - whisper.cpp 本地执行 - 新增 timeline_transcription 任务类型,worker 按类型分叉独立执行器(不走计费与模型渠道);输入 resourceId+language,读取用户资源后 ffmpeg 预处理 16k 单声道 wav,POST 本地 whisper.cpp /inference,结果写回 segments+srt+language;未配置 CANVAS_WHISPER_BASE_URL 等失败均落明确终态;feature 门控;进度分阶段;含解析/SRT/快速失败单元测试

* feat(media): 素材卡片时长与媒体面板打磨 - 导入媒体解析时长(前端 media-metadata + 后端 project_asset durationMs 透出)随卡片展示;媒体面板/监视器/时间线相关样式细节收尾

* docs(progress): 阶段 C 监视器播放复检清单 - H.265 播放副本/真实播放驱动/时长链路待浏览器确认

* feat(timeline): M4.1 转写任务创建 API - POST /api/timeline/transcriptions - service CreateTimelineTranscriptionTask:IsDraining 守护 + feature 门控 + 资源归属与可转写类型校验 + 排队计 active 限额(含任务级/项目级存储估算);local/whisper.cpp 标记,任务不经过模型路由与计费;createTaskWithinStorageQuota 复用与 ErrActiveTaskLimit 用户可读映射;测试覆盖排队成功(owner/type/provider 断言)、跨用户资源拒绝、非音视频拒绝

* feat(timeline): M4.3 字幕转写前端接线 - 真实任务替换 mock - 新增 services/api/timeline-tasks.ts(POST /timeline/transcriptions,复用 apiClient+request);EditorTranscription 改为 创建任务→waitForGenerationTask 轮询(25min)→结果 segments 映射 SrtEntry[]→rebuildSubtitleClips 写入字幕轨道;实时展示 stage/progress,失败/空识别/无字幕轨道分别提示;素材选择支持音频与视频(取 storageKey 中的 resource id 提交)

* fix(media): 播放副本状态收敛与轮询终态 - 无ffmpeg/远端存储/编码不可处理落 none 避免重复探测;前端 none 停轮询回原生播放并加 processing 上限

* feat(timeline): M4.2 时间线渲染任务 - POST /api/timeline/renders 创建(buildRenderPlan 校验含可渲染媒体、配额入队、local/ffmpeg、60min 超时);worker 顶部按类型分叉 processTimelineRender;ffmpeg 合成产物写资源存储并经 ResultJSON 记 resourceId;render 计划/结果测试 PASS

* fix(resource): 转码原子抢占与 OSS 请求体加固 - ClaimPlaybackTranscode 空/none→processing 单实例抢占避免重复转码,runPlaybackTranscode defer recover 落 failed;If-Range 与带 :pb 后缀的 serveETag 比较;无请求体的 OSS 请求不再包 NopCloser(nil)(防 Go 1.26 body 探测 nil 解引用)

* style(editor): 修正字幕回写面板缩进

* docs(timeline): M4 设计文档按实现回写 - 单一 worker 池 + processClaimedTask 顶部按类型分叉(决策 3/4),端点统一为复数 /api/timeline/transcriptions 与 /api/timeline/renders(注册于 RegisterTaskRoutes),ResultJSON/feature gate/ffmpeg env 同步;Runbook M4 表格与状态行更新;auth.go API 前缀白名单补 timeline

* fix(service): 修复上游 v1.2.5 公告配图删除保护自锁与测试夹具缺口

上游 #385 资源引用走查器对公告配图/草稿做了硬性引用检查,导致三个问题:
- UpdateAnnouncement 在更新公告前对旧配图走查,必然命中当前公告自身引用而自锁,
  公告替换配图功能实际不可用;DiscardAnnouncementImage 同理被草稿自身引用阻塞。
  修复:ensureResourceHasNoBusinessReferences 支持忽略指定引用(当前公告/当前草稿),
  其余引用仍按原语义拦截。
- admin DeleteAdminResources 将公告草稿引用视为阻塞,但删除事务会级联清理草稿与
  资源绑定,service 层永不进入事务,回滚审计注入测试无法到达预期路径。修复:service
  预检时过滤公告草稿引用,与 repository 级联语义对齐。
- project_settings_test.go 夹具未建 ResourceReferenceSnapshot 走查的 Task/Session/
  Message/TaskLog/Result/Announcement/AnnouncementImageDraft 表,报 no such table。
  修复:夹具 AutoMigrate 补齐。

后端全量 go test ./... 通过。

* feat(editor): 成片导出接入后端渲染任务(M4.2 前端接线)- timeline-tasks 增 createTimelineRenderTask;editor-export 主路径改 POST /timeline/renders 轮询取 resourceId 内嵌预览/下载,ffmpeg.wasm 降级保留

* feat(editor): M5 插件权限执行(fail-closed)- plugin-permission-check 新建(8 插槽最小权限表/判定/断言);SlotStack 渲染前过滤缺权限插槽改诊断条;editor-shell manifest 补 timeline.command/export.run;测试 10 用例含 editor-shell 真实回归

* feat(editor): M6.1 AI 命令 schema + M6.2 时间线摘要 - ai-command-schema(parse/dry-run 校验/12-op 提示词契约/版本化+golden);timeline-summary(确定性中文摘要/折叠上限);各 20/10 用例全绿

* docs(progress): M6 质检清单 - M6.1 schema/M6.2 摘要标记完成(含测试计数),M6.3 待模型源确认;清理 M5.1 重复条目

* feat(editor): AI 命令 schema 支持只读问答 - commands 空数组合法化(无法执行/纯提问时 reasoning 直接作答),新增 AI_EDITING_MAX_COMMANDS=8 上限防失控编辑,提示词同步

* feat(editor): AI 助手重构为右上悬浮对话面板(M6.3)+ 素材入库原子化收尾

- editor-ai-assistant.tsx: 占位面板替换为 OpenAI Canvas 风格右上悬浮对话面板;
  面板内文本模型选择器(effective.textModels,选择即持久化并覆写请求模型),
  消息流 user/assistant/error/preview 四类气泡 + 思考中占位,空态引导示例 chips
  一键发送,底部输入条;顶部工具栏常驻 Sparkles 入口(aria-pressed 切换)。
  修复上一版重复/截断函数声明(appendToEntry 双定义、settleAnswer 三声明等)。
  宿主链路:buildAiEditingSystemPrompt(summarizeTimeline(project)) 喂上下文 →
  parseAiCommandPlan → validateAiCommandBatch dry-run(任一非法整批拒绝)→
  ≤3 条 dispatch 直执行 / >3 条预览确认卡片;宿主拒绝时 failTurn 回填 FAILED_MARK 可重试。
- editor-asset-ingest/editor-preview-monitor/detail/editor.tsx: 配套接线与打磨
  (编辑器顶栏 AI 入口、素材导入面板、预览监视、项目页顶栏整理)。
- backend: LinkProjectAsset 事务内原子创建资产 + 项目链接(OnConflict 幂等),
  资产首次入库不再预落库,失败整体回滚,消除"有资产无链接"孤儿记录。
- docs: pending-test.mdx / runbook M6.3 状态更新(tsc + 测试 + 构建通过,
  浏览器冒烟待真实文本渠道)。
This commit is contained in:
爱笑的毛毛虫
2026-09-04 14:03:33 +08:00
committed by GitHub
parent 2bd33ea430
commit 7ec51732f7
106 changed files with 10857 additions and 119 deletions

View File

@@ -0,0 +1,34 @@
# ADR-0001: 编辑器整合以 TS 时间线状态机为真相源,不移植 Concat 的 Rust 引擎
状态:已接受。日期:2026-09-01。来源:借鉴 Concat(WolfCut)编辑器能力整合决策。
> 更新记录(2026-09-01):采纳 ADR-0005 后,编辑器 UI 与能力出口改为预设插件贡献(详见 ADR-0005);本 ADR 聚焦的边界因此收窄为:核心状态机仍是宿主 TS 库,但不再是编辑器的一切。
## 背景
Concat(WolfCut)的核心架构原则是"引擎拥有项目模型"(engine doctrine):Rust 引擎持有时间线模型、全部编辑命令(`commands.rs`)、撤销栈、渲染与导出路径、文档格式(wolfcut.json);Tauri host 只是管道,UI 只渲染状态并派发命令。这一形态依赖本地文件系统、ffmpeg 子进程和 GPU 组合器,与其桌面产品定位绑定。
影策是 Web 应用(React 19 + Go 后端 + Node canvas-agent),画布节点模型以 TS 状态机为真相源(localforage user-scope + 后端项目接口持久化)。时间线已有第一期字幕数据落地与第二期类型(`TimelineProject` v2,含轨道/片段/directMedia),第三期规划了 ffmpeg 导出(`timeline-to-ffmpeg.ts` 纯函数规划层已存在)。后端 Go 目前不执行 ffmpeg。
## 决策
借鉴 Concat 的架构**形态**,不移植其**实现**:把"引擎拥有模型"翻译为"TS 时间线状态机拥有编辑真相"。
- 时间线编辑逻辑集中在 `web/src/lib/timeline/`(在现有 build/placement/snap/to-ffmpeg 基础上新增 editor 状态机),UI 组件只渲染状态并派发命令,不直接改写模型。
- 编辑器**UI 与能力出口全部为预设插件贡献**(时间线面板、预览、检查器、转写、导出、字幕工具),核心状态机是插件依赖的宿主协议而非编辑器的一切(见 ADR-0005)。
- 不移植 wolfcut crates、不引入 WASM 版引擎、不引入 Tauri/Electron 壳。
- 媒体重活(转码、导出合成、转写)交给 Go 后端任务队列,浏览器 ffmpeg.wasm 保留为降级路径,两者由同一份滤镜图计划驱动(见 ADR-0003)。
## 考虑过的方案
- **移植/包装 Rust 引擎(wasm32 / 子进程)**:引擎与画布节点模型、localforage 缓存、浏览器预览路径同构成本高,会产生"引擎真相 + 画布真相"双真相源;否决。
- **引入现成 Web 视频编辑器库(Remotion/Editly 等)**:Remotion 是 React 声明式渲染框架而非交互式多轨编辑器,现成编辑器与画布→时间线的 AI 工作流(节点资产入轨、字幕一期能力)耦合成本高于自建;否决。
- **TS 端自建状态机(选定)**:与现有 `timeline.ts` v2、`timeline-build`/`timeline-placement` 等纯函数层一脉相承,可直接复用字幕与导出规划能力。
## 后果
- 撤销、自动保存、命令协议由 TS 端承担,责任随 Concat 的引擎移动到了前端库层;需要用类型与测试约束(见 ADR-0002)。
- 不复制 Concat 的 chains mirror 债务(TS 手写镜像 Rust 滤镜串):滤镜参数只允许单一来源(见 ADR-0003)。
- ffmpeg 执行位置(浏览器 wasm / 后端任务)保持可切换,切换不改变编辑模型与导出参数。
- 编辑器面板(时间线/预览/右侧检查器)复用 `docs/design/workspace-shell-design.mdx` 的设计 token 与三段式布局,不引入新设计体系。
- 新增编辑器能力由预设插件承担(ADR-0005):核心库冻结,只演进命令协议、滤镜图计划与任务协议。

View File

@@ -0,0 +1,28 @@
# ADR-0002: 时间线编辑采用可序列化命令 + 有界快照撤销
状态:已接受。日期:2026-09-01。来源:借鉴 Concat 编辑循环与撤销/自动保存设计。
> 更新记录(2026-09-01):采纳 ADR-0005 后,命令 handler 可经插件注册(时间线工具/效果插件贡献自定义 op),但命令队列、echo、撤销与保存仍是宿主核心;插件不得绕过命令协议改模型。
## 背景
Concat 的编辑循环:UI 把命令推入单队列 → 引擎 `editor.apply(command)` 返回全量视图 → UI 重渲染。拖拽用本地 echo 预演位置,手势结束才提交**一个**命令,撤销粒度是手势而非像素;撤销栈是有界完整克隆(200 层);自动保存 1.5 秒防抖 + 临时文件原子改名。Concat 自述的教训:撤销/重做失败被吞错(§6.4)、重做栈无上限(§6.3)。
影策现有画布数据走 localforage(user-scope)+ 后端项目接口;时间线一期已落地字幕数据,二期按 `TimelineProject` v2 持久化轨道/片段。
## 决策
- 时间线唯一修改入口是**可序列化编辑命令** `{ op, payload }`,经单队列顺序应用到 TS 状态机,每次 apply 返回新的 `TimelineProject` 作为唯一真相;任何 UI 代码不得直接改写模型。
- **命令 handler 可插件化注册**:预设插件(时间线工具、效果、字幕工具)通过宿主 API 贡献自定义 op 及其 reducer/handler;宿主负责校验权限(ADR-0005)后入队执行,插件不持有队列与撤销栈。
- 拖拽/裁剪/变速等手势采用 **echo**:本地预演位置与效果,`release`/手势结束提交一个命令。
- 撤销栈为**有界完整快照**(上限 200 层,与 Concat 一致),重做栈同样限界。
- 保存沿用项目保存链路:localforage user-scope + 后端项目接口,防抖 1.5 秒,写失败必须上报,不吞错、不静默降级。
- 编辑命令对象序列化形式即对外协议:画布 Agent、未来多端协作通过命令日志同步,不经由此协议不得改动时间线。
## 后果
- 快照撤销简单可靠,但内存随项目规模线性增长:快照只存结构化模型,媒体大对象一律以 `directMedia` 引用/存储 key 入快照,不复制二进制。
- 命令队列天然支持幂等重放,为服务端协同与测试(命令黄金文件)留出路径。
- 撤销粒度是手势:移动/裁剪一整段是一次撤销,不提供逐帧回退。
- 与 Concat 不同,命令执行、撤销、自动保存都发生在浏览器进程,无跨进程边界;同步失败的兜底是本地缓存 + 明确的状态标识,不能冒充服务端已保存(遵循 AGENTS.md 写路径约定)。
- 插件贡献的命令 handler 必须保持纯函数、可序列化、可重放:撤销快照与命令黄金文件对插件命令同样生效,插件不能依赖组件实例状态。

View File

@@ -0,0 +1,27 @@
# ADR-0003: 预览与导出分层——交互近似预览 + 单一滤镜图计划
状态:已接受。日期:2026-09-01。来源:借鉴 Concat 预览近似与导出降级设计。
> 更新记录(2026-09-01):采纳 ADR-0005 后,预览渲染器与导出渲染器成为插件贡献点(`registerPreviewRenderer` / `registerExportRenderer`);单一滤镜图计划约束不变,对插件同样强制。
## 背景
Concat 的预览近似层:交互期间用 CSS 滤镜 / canvas 运算渲染近似效果,监视器播放引擎真帧;其原则是"导出只是你已看到内容的更高画质版本"。转场在导出器降级(交叉淡化 → 重叠片段 + 透明度斜坡)。Concat 自述的最大债务是 chains mirror(§6.1):TS 手写镜像 Rust 滤镜链,两侧靠钉死测试维持一致,新增效果成本翻倍。
影策已有 `timeline-to-ffmpeg.ts` 纯函数规划层(trim → gap → concat → subtitle → burn),`timeline-export.ts` 用浏览器 ffmpeg.wasm 执行;后端 Go 暂无 ffmpeg。
## 决策
- **交互预览只做浏览器原生近似**:`<video>` 裁剪/缩放/透明度/音量、CSS 滤镜、canvas 合成近似。近似预览是交互真相,但不是导出像素;UI 上不把两者混为一谈。
- **预览渲染器与导出渲染器为插件贡献点**:预设插件(如转场/滤镜效果插件)通过 `registerPreviewRenderer` / `registerExportRenderer` 贡献渲染实现;宿主按插槽渲染,插件不得绕过插槽直接挂载 DOM。
- **单一滤镜图计划对插件强制生效**:插件贡献的导出渲染必须消费 `buildTimelineRenderPlan` 的输出参数,禁止插件内部自行拼第二套滤镜串(无论多简单)。
- **导出滤镜图只由 `buildTimelineRenderPlan` 单一模块生成**:这是滤镜串的唯一来源。主路径为 Go 后端 ffmpeg 任务(复用任务队列),浏览器 ffmpeg.wasm 为降级路径,二者吃同一份计划。
- **新增效果必须同时产出两份描述,且出自同一参数模型**:`预览近似描述`(CSS/canvas 等价实现)与 `导出滤镜参数`(ffmpeg filtergraph 片段)。禁止在 UI 组件里手写第二套滤镜串——即不复制 Concat 的 chains mirror 债务。
- 转场、滤镜等视觉允许预览与导出存在级差(如预览用近似淡化、导出用 xfade/淡入淡出),但参数必须同源,不允许出现"预览一套、导出另一套"的漂移。
## 后果
- 滤镜参数模型成为编辑器和导出共用的契约:新增效果 = 参数模型 + 两份降级实现,测试围绕参数模型做黄金文件。
- 预览与导出管线各自可演进:服务端派生"预览近似描述"(Concat 式 engine emits preview looks)时,无需改 UI 消费方式。
- 浏览器 wasm 与后端 ffmpeg 的切换不改变编辑模型;wasm 仅承担无后端场景下的降级导出。
- 渲染器插件化后,新增效果仍是「参数模型 + 预览近似 + 导出滤镜」三件套,只是后两者的实现方变为插件;参数模型仍是黄金文件测试的唯一锚点,防止插件间出现参数漂移。

View File

@@ -0,0 +1,27 @@
# ADR-0004: 自动字幕转写作为后端服务任务
状态:已接受。日期:2026-09-01。来源:借鉴 Concat 转写命令集与模型管理设计。
> 更新记录(2026-09-01):采纳 ADR-0005 后,转写提供者为插件贡献点(`registerTranscriptionProvider`),转写面板为预设插件;后端任务与配额协议不变。
## 背景
Concat 的招牌智能功能之一是自动字幕:以 7 个转写命令(转写片段、取消、状态等)驱动本地 whisper 模型,模型文件按需下载并管理。其前提是桌面应用可在本地跑模型与 ffmpeg。
影策是服务端应用:模型渠道统一经后端中转(`custom-channel-relay` 的 `/api/ai/custom`),任务走任务队列与配额体系;素材已服务端化;时间线一期已有字幕数据与 SRT 解析/切分能力(`srt-parser`、`srt-resegment`),字幕轨道已是 `TimelineProject` v2 的一等轨道类型。
## 决策
- 转写由 **Go 后端执行**(whisper.cpp 或服务化 ASR 作为后端任务),前端只提交"素材/片段 → 转写字幕"任务并展示进度,不直接调用模型。
- **转写提供者为插件贡献点**:预设插件经 `registerTranscriptionProvider` 声明前端可用的转写能力与 UI(如自动字幕面板、字幕工具),经宿主校验权限后调用后端任务;第三方插件可扩展新转写来源,但必须走同一任务协议。
- 转写结果以 SRT 形式写回时间线字幕轨道,复用一期 SRT 解析与按句/按词切分能力;前端可预览、编辑、重新转写。
- **不在浏览器运行 whisper**:模型体积、算力与移动端/低端设备不可控,且与既有"模型渠道走后端中转"的安全边界一致。
- 转写任务复用任务队列与配额体系;ASR 引擎可插拔(本地进程 / 云 ASR),后端服务内部切换,前端协议不变。
- TTS/配音等**生成式**语音能力走既有模型渠道(生成素材),与转写(分析已有素材)互不耦合,不在本决策范围内。
## 后果
- 转写是异步任务而非即时命令:前端需要任务状态展示(进行中/成功/失败),失败可重试,重试不重复扣配额。
- 字幕轨道成为时间线一等公民:转写、人工编辑、导出烧录(ADR-0003 的 burn 步骤)共享同一字幕数据源。
- 模型权重与算力要求后端可插拔部署:本地 whisper.cpp 省成本、云 ASR 省运维,两者共用同一个任务协议。
- 前端转写能力集合随插件而变:默认预设提供 whisper 服务转写;第三方插件可增加转写来源,前端任务协议与字幕轨道数据形状保持稳定。

View File

@@ -0,0 +1,40 @@
# ADR-0005: 编辑器预设插件架构——参考 astravia「万物皆可插件」改造
状态:已接受。日期:2026-09-01。来源:参考 astravia(`astravia`)插件体系理念,改造编辑器插件化。
## 背景
**astravia 的「万物皆可插件」理念**:所有能力(含界面)都是插件贡献,宿主核心只保留最小骨架。其实现要点:
- **统一插件形态**:`definePlugin` SDK + `plugin.json` 清单 + Module Federation 打包(`vettaPluginFederation`);react/react-dom/SDK/主题 token 由宿主作为共享单例 external 化,插件不自带运行时。
- **系统插件(presets)与用户插件同构**:内置功能与第三方插件用**同一套 SDK 与清单**,仅分发方式(随 App 发布 vs zip 安装)与权限语义(系统插件自动全量授予且不可删改 vs 用户插件安装时逐项授予)不同。
- **UI 插槽**:workspace view、global slot、file preview、activity tab、input action、tool call slot、shortcut scope 等均为注册式插槽,内置界面与插件界面平等占位。
- **权限执行**:清单声明 + 宿主在 API 调用点校验(fail-closed / warn+noop / 抛错三档),未声明即不可见不可用。
- **能力贡献**:capability-runtime(hub/provider/registry/access)+ capability-sdk,插件以 provider 身份注册能力。
**影策现状(改造对象)**:现有 `yingce.plugin/v1`(`web/src/lib/plugins/`)只有**画布域**贡献(provider / workflow / canvas-node / transform / asset-source 等),存在结构性缺口:无 UI 插槽注册 API(surfaces 只是声明字段)、无编辑器任何贡献类型(时间线/预览/导出/转写没有插件入口)、无预设(系统)插件分发语义、内置插件是 `builtin/` 源码硬编码 import、权限只有清单字符串无宿主执行校验。对"编辑器插件化"这一目标不可直接支撑(即现状"不可理"之处)。
## 决策
- **编辑器全部 UI 与能力以插件贡献形态存在**,作为**预设插件(preset plugins)**随产品发布:时间线面板、预览监视器、右侧检查器、素材入轨、自动字幕(转写)面板、导出面板、字幕工具均为预设插件;默认启用、用户可停用、不可卸载删改,权限自动授予。
- **现有插件体系演进为 v2 形态(`yingce.plugin/v2`),补齐 astravia 式机制,editor 域作为首个完整消费方**:
- 新增**UI 插槽注册 API**:`registerTimelinePanel` / `registerPreviewRenderer` / `registerExportRenderer` / `registerTranscriptionProvider` / `registerSubtitleTool` / `registerShortcutScope` 等;宿主按插槽渲染注册表,内置与第三方插件平等占位。
- 新增**预设插件分发语义**:preset 插件随应用代码分发(沿用 `builtin/` 目录、语义化为 `builtin/editor/` 预设),与用户插件同清单同 SDK,仅权限/可删改性不同。
- 新增**权限执行校验**:编辑命令、转写任务、导出任务、资源读取等宿主 API 调用点按清单校验,fail-closed;未声明不得触发。
- 保持共享单例:react / react-dom / 时间线 SDK / 设计 token(`workspace-shell-design.mdx`)由宿主提供,插件不自带。
- **宿主最小核心保持不变**:时间线状态机(ADR-0001)、编辑命令队列与撤销(ADR-0002)、滤镜图计划(ADR-0003)、任务与配额协议(ADR-0004)是核心骨架,插件只允许在这些协议之上贡献 UI 与 handler,不得绕过命令协议改模型。
- 编辑器内置功能**不做特殊化**:迁入预设插件后与第三方插件走同一条贡献/校验/插槽路径,禁止在核心代码里写"编辑器专属硬编码分支"。
## 考虑过的方案
- **推倒重写插件体系**:现有 v1 已融入画布/生成/素材链路(application.tsx 静态注册 builtin、use-plugin-store 持久化启用态、后端 pluginStates),重写会破坏既有功能且无必要;否决,选择在同一代码库演进。
- **编辑器独立自建面板,不做插件化**:违背"插件化做成预设插件"的产品方向,且重复造轮子;否决。
- **照搬 astravia 包结构(SDK 独立包 + 独立市场)**:影策是单体 web 仓库,先以 v2 形态在 `web/src/lib/plugins/` 内演进,待出现真正的外部插件需求再抽 SDK/打包工具链;当前阶段预设插件足够。
## 后果
- 编辑器成为插件体系第一个完整闭环域:新增编辑器能力 = 写一个预设插件(清单 + 插槽贡献 + handler),不改宿主核心;宿主核心因插件而冻结,只演进协议。
- `yingce.plugin/v1` 升级为 v2:v1 字段(贡献类型/权限声明)保留兼容,新增插槽与分发字段;旧画布插件无需迁移即可继续运行,新增字段可选。
- 权限校验从"声明"变为"执行":编辑器宿主 API 是第一个强制校验点,随后推广到画布/生成链路。
- 与 astravia 的差距被显式承认:暂无外部插件安装/市场/沙箱,预设插件是当前全部范围;待需要时按 astravia 的 SDK/打包/租户选择机制补齐。
- 插件数量与耦合风险:editor 预设插件按功能域拆分(面板/预览/导出/转写/字幕工具各一个),共享同一命令协议与滤镜图计划,避免插件间互相 import。

View File

@@ -0,0 +1,53 @@
# ADR-0006: 语音滤镜与项目模板——Concat 剩余缺口的补充决策
状态:已接受。日期:2026-09-01。来源:Concat 编辑器功能覆盖对照中的两个明确缺口,评估后决定引入。
## 背景
功能覆盖对照(实现规格 §0.4)识别出 Concat 有两个影策方案未覆盖的功能:
- **语音滤镜(Voice Filters)**:Concat 的招牌特色之一,对音频/人声做本地 DSP 处理(降噪、变声、均衡、回声等)。其前提是桌面应用可本地执行音频处理。
- **模板(Templates)**:Concat 提供保存项目为模板(`SaveTemplateDialog`)、模板缩略图(`TemplateThumb`)与从模板新建的流程;模板中心仍在 roadmap。
影策(Web + Go 服务端)此前将两者列为"明确不做"。本 ADR 评估引入方式,结论:**两者都引入,且必须走预设插件形态(ADR-0005),服从单一滤镜图计划约束(ADR-0003)**。
## 决策
### 语音滤镜:预设插件 `editor-audio-effects`
- 新增 `audio-effects` 贡献类型与 `audio.process` 权限;插件注册音频效果(音量、淡入淡出、均衡、回声、降噪、变速变调)。
- **预览近似走 Web Audio API**:`BiquadFilter` / `DynamicsCompressor` / `DelayNode` / `ConvolverNode` 等实时处理,交互真相,非导出像素。
- **导出真相走 ffmpeg 音频滤镜**:`afftdn`(降噪)/ `equalizer` / `aecho` / `acompressor` / `asetrate`+`aresample`(变速变调)等,作为 `buildTimelineRenderPlan` 的 **audio steps** 纳入单一滤镜图计划。
- **参数模型先行**:与视频滤镜同构——同一参数模型 + 预览/导出两份实现;渲染计划黄金文件(§7.2)增加音频夹具,禁止插件手写第二套滤镜串。
- 首版只做基础集(音量、淡入淡出、均衡、回声、简单降噪);变声等复杂 DSP 留作插件扩展点。
### 模板:预设插件 `editor-template`
- 新增 `template-provider` 贡献类型与 `template.read` / `template.write` 权限。
- **模板 = 项目快照**:章节 + 画布 + 时间线(`TimelineProject`)+ 资产引用清单,**不含媒体二进制**;带 `templateSchemaVersion` 版本字段,随项目模型演进。
- "存为模板"从当前项目生成快照;"从模板新建"克隆快照为新项目(复用项目创建链路)。
- 模板存储为**服务端模板表**(归属用户/组织),支持列表 + 缩略图(复用 Concat `TemplateThumb` 概念)。
- **模板中心/市场不实现**:保留扩展点,待组织级模板需求出现时再评估(需组织/审核/订阅体系,超出当前范围)。
### 共同约束
- 两者都是预设插件:随应用分发、默认启用、不可卸载,权限自动授予(ADR-0005)。
- 权限执行校验(实施规格 M5 的 fail-closed)覆盖新增的 `audio.process` / `template.read` / `template.write`。
- 新增效果一律遵守"单一参数模型 + 预览近似 + 导出真相"三件套,音频不例外。
## 考虑过的方案
- **语音滤镜用浏览器 WASM DSP(RNBO/声码器级)**:实时性强、效果上限高,但包体大、实现复杂、与导出滤镜难同源;首版否决,保留为复杂 DSP 插件的未来选项。
- **语音滤镜直接用后端 ffmpeg 滤镜做预览**:网络延迟不可接受,违背"预览近似必须浏览器原生"(ADR-0003);否决。
- **语音滤镜保持缺口(不做)**:Concat 对标的核心卖点之一,且音频效果是实现规格中成本最低的插件化增量;否决。
- **模板 = 复制整个项目含媒体二进制**:两份存储浪费、快照膨胀;否决,快照只存引用清单。
- **模板纯前端 localStorage**:不可跨设备/账号,与项目服务端持久化体系冲突;否决。
- **模板保持缺口(不做)**:"从模板新建"是成片效率工具,直接衔接概览「剪辑成片」流程(§0.2);否决。
## 后果
- `buildTimelineRenderPlan` 从纯视频计划扩展为 **A/V 计划**(video steps + audio steps);ADR-0003 的"单一来源"约束覆盖音频,滤镜图计划的黄金文件增加音频夹具。
- 新增权限 `audio.process` / `template.read` / `template.write`,权限执行校验清单(M5)同步扩展。
- 模板快照与项目模型强耦合:`templateSchemaVersion` 必须随 `TimelineProject` / 画布模型演进,快照兼容测试纳入验证纪律。
- **优先级建议**:模板先于语音滤镜——模板依赖 M2 数据模型与项目创建链路即可落地(M3 后增量窗口),语音滤镜依赖 M3 预览渲染 + M4 导出管线的音频扩展(M4 后增量窗口)。两者均不阻塞 7 个核心预设插件的主线。
- 实现规格 §6 文件规划需追加:`builtin/editor/editor-audio-effects.ts`、`builtin/editor/editor-template.ts`、后端模板表迁移与 `template` handler、`timeline-to-ffmpeg.ts` 音频 steps 扩展。

View File

@@ -0,0 +1,55 @@
# ADR-0007: AI 编辑交互——对话式剪辑作为预设插件
状态:已接受。日期:2026-09-01。来源:编辑器与 AI 交互能力整合决策;建立在 ADR-0002(命令协议)与 ADR-0005(预设插件)之上。
## 背景
编辑器需要与 AI 交互:用户用自然语言表达剪辑意图,AI 直接驱动时间线。Concat 没有对话式 AI 编辑(桌面引擎,AI 只能模拟 UI 操作,脆弱且不可测试);影策因两个既有决策天然支持:
- **命令协议(ADR-0002)**:时间线唯一修改入口是 `{op, payload}` 序列化命令,单队列 apply、可撤销、可重放——AI 只需输出命令 JSON 即可驱动时间线,与人手势编辑完全同构。
- **插件化(ADR-0005)**:AI 交互能力可作为预设插件注册(对话面板插槽 + 权限),不动核心。
- **既有资产**:模型渠道已统一中转(`custom-channel-relay.ts` / `/api/ai/custom`);画布侧已有成熟的 AI 聊天 UI(`canvas-agent-chat-ui.tsx`、`canvas-assistant-panel.tsx`、`canvas-prompt-optimizer-drawer.tsx`);AI 字幕高亮纯函数层已存在(`subtitle-highlight-*`)。
## 决策
### AI 编辑交互 = 预设插件 `editor-ai-assistant`
- 新增 `ai-assistant` 贡献类型与 `ai.text` / `timeline.command` / `timeline.read` 权限;插件注册对话面板插槽(right 或弹层)。
- **对话面板复用画布 agent 聊天 UI 的交互模式**(流式输出、上下文面板、可停止),复用现有组件或同构实现,不复制不移植画布会话协议;编辑器上下文(时间线摘要)由本插件构造。
- AI 是时间线的**一等公民编辑者**:AI 命令与手势命令同构、同队列、同撤销栈。
### AI 命令协议(受约束命令 JSON)
- LLM 输出**受约束的命令序列 JSON**:`{ commands: [{ op, payload }], reasoning?: string }`,`op` 必须来自命令白名单(`editor-commands.ts` 注册的 ops),`payload` 按 per-op schema 校验(新增 `ai-command-schema.ts`,与命令 handler 同源维护)。
- **宿主 fail-closed**:任一命令 schema 校验失败、op 未知、权限未声明,整批拒绝并返回错误给对话(不执行部分命令);失败信息回填对话供 AI 自纠。
- 命令 schema 版本化(`aiCommandSchemaVersion`):模型演进或命令集变更时显式升级,避免 AI 输出与新命令集失配。
### 执行语义:默认直接执行 + 一键撤销,批量改动需确认
- **单条/少量命令(≤3 条)**:解析校验通过后直接入队执行,撤销即回退(符合剪辑工具直觉,与手势编辑一致)。
- **批量改动(>3 条或涉及删除/整体重排)**:先展示命令 diff 预览(将改哪些片段/轨道),用户点「应用」才入队。
- 所有 AI 命令与手势命令共享同一撤销栈,AI 误操作可逐批撤销,不留特殊回退路径。
### AI 只读能力
- 对话可读时间线状态(`timeline.read`)做分析问答("这段节奏怎么样"、"哪里该剪");只读回答不需要 `timeline.command`。
- 时间线摘要为**结构化精简版**(轨道/片段/时长/关键属性,不含媒体二进制),作为 AI 上下文输入;大对象不进 prompt。
### 素材层 AI 不重复决策
- 自动转写(ADR-0004)、AI 字幕高亮(既有能力接入)、AI 生成素材入轨(既有生成链路 + asset-ingest)不属于本 ADR 的对话式编辑范围;`editor-ai-assistant` 可聚合它们的入口,但各自能力边界与协议不变。
## 考虑过的方案
- **AI 直接操作 DOM/UI(模拟点击拖拽)**:脆弱、不可测试、与命令协议割裂;否决。
- **AI 走后端执行编辑**:网络往返破坏交互延迟,撤销/错误回填难做,且后端无时间线真相源;否决。
- **AI 生成完整时间线整体替换**:高风险(覆盖用户已有编辑),无 diff 预览;否决,只允许命令增量。
- **AI 命令立即全量执行不确认**:批量误操作无法挽回;否决,采用上述批量确认语义。
## 后果
- 命令 schema 成为 AI 与宿主的**双契约**:同一 schema 同时服务命令校验(宿主)与 LLM 提示词约束(AI 端);`ai-command-schema.ts` 与命令 handler 同源维护,黄金文件测试扩展到 AI 命令序列。
- 编辑器获得 Concat 不具备的能力:**自然语言驱动的时间线编辑**,且与人编辑共享撤销/可测试/可重放语义。
- 新增预设插件使首版插件数从 7+2 变为 7+3(editor-ai-assistant 加入);插件清单与权限矩阵同步更新。
- 上下文窗口与 token 成本受时间线摘要精简策略影响;摘要生成是纯函数(`timeline-summary.ts`),可测试。
- 画布 agent 聊天 UI 与编辑器 AI 面板并存:两者共享模型渠道与交互模式,但不共享会话上下文(画布会话 ≠ 编辑器会话),避免会话串扰。