diff --git a/docs/README.md b/docs/README.md index b9a0be0c9..dd71e8670 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,6 +19,9 @@ - [图片画布编辑器 MVP 接入方案](./technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md) - [图片画布编辑器前端拆分计划](./technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md) +- [画板音乐生成入口设计](./【编辑器】画板音乐生成入口设计-2026-06-18.md) +- [音频生成 Composer 恢复共享分流方案](./project-memory/plans/【前端重构】音频生成面板恢复共享分流方案-2026-08-06.md) +- [BGM 提示词优化 T6 测试与发布门禁](./【实施记录】BGM生成提示词优化T6测试与发布门禁-2026-08-05.md) - [画布 Agent 对话面板](./【编辑器】画布Agent对话面板-2026-07-03.md) - [画布 Agent 会话消息存 OSS](./adr/【ADR】画布Agent会话消息存OSS-2026-07-03.md) - [图片画布撤销范围与操作提示方案](./【图片画布】撤销范围与操作提示方案-2026-07-17.md) diff --git a/docs/project-memory/plans/【前端重构】音频生成面板恢复共享分流方案-2026-08-06.md b/docs/project-memory/plans/【前端重构】音频生成面板恢复共享分流方案-2026-08-06.md new file mode 100644 index 000000000..5abe42b8a --- /dev/null +++ b/docs/project-memory/plans/【前端重构】音频生成面板恢复共享分流方案-2026-08-06.md @@ -0,0 +1,188 @@ +# 音频生成 Composer 恢复共享分流方案 + +日期:`2026-08-06` + +状态:`已实施` + +## 一、目标 + +图片画布的音效与背景音乐恢复使用同一个音频 composer。`ImageCanvasGenerationComposerView.tsx` 内只保留一个 `ImageCanvasAudioGenerationComposerView`,并在组件内定义: + +```ts +const isSoundEffect = dialog.mode === 'audio-sound-effect'; +``` + +`isSoundEffect === true` 渲染现有 SFX 分支,`isSoundEffect === false` 渲染现有 BGM V1 分支。删除完整的独立 BGM composer 文件,但保留确有独立职责的 BGM 纯模型、助手 controller 和预设跑马灯组件。 + +这次只调整前端视图组织方式,不改变任何生成契约、业务规则、请求时序、计费或持久化语义。 + +## 二、范围与非目标 + +### 2.1 必须保留的 BGM 能力 + +- `gpt_description_prompt` 可见原文、Unicode `White_Space` canonicalization 和 200 字生成限制。 +- 30 个预设、字符计数、AI 补全、一键简化和单层交换式撤销。 +- AI 处理锁、同步正式提交锁、`generating` 锁和迟到响应隔离。 +- 稳定 dialog ID、账号 / 项目 scope 校验和按 dialog ID 写回。 +- 固定 `Suno`、当前动态泥点价格和隐藏 `make_instrumental` 的现状。 + +### 2.2 必须保持不变的现有 SFX 能力 + +- 固定 Vidu `audio1.0`,Prompt 同值映射到现有 `prompt + sound` 请求字段。 +- `2–10` 秒、步长 `1` 秒、默认 `5` 秒。 +- 当前 Prompt 规范化、全空白时回退“游戏音效”、1500 字限制、价格和失败态。 +- 现有提交 callback、生成占位和完成链路。 +- SFX 中不出现 Suno、BGM 预设、AI 补全、一键简化、BGM 字符计数或撤销。 + +### 2.3 本次明确不做 + +- 不实现 SFX V2 独有的一键优化、自动中译英、ElevenLabs、自动时长、30 秒、Loop 或 SFX 预设。 +- 不修改 BGM 或 SFX 需求原文。 +- 不修改 BFF、队列、`platform-audio`、External v1、OpenAPI、SpacetimeDB schema、计费或重试。 +- 不把 BGM Prompt controller 泛化为音频通用 controller。 +- 不新建配置驱动的 composer 框架、第二套音频组件或其它顺带重构。 + +## 三、当前问题 + +当前 `ImageCanvasGenerationComposerView.tsx` 分别渲染 SFX 内部组件和 `ImageCanvasBackgroundMusicGenerationComposerView.tsx`。这层完整视图拆分让同一个音频入口形成两套 composer 边界,而 SFX V2 需求已经表明预设、AI 写回、撤销和锁定等交互并非 BGM 永久独占,继续用“BGM 专属交互较多”作为完整组件分叉理由不再成立。 + +同时,最近一次合并把共享架构中的 `isSoundEffect` 条件表达式带回了当前 SFX-only 组件,却没有带回变量定义,形成 `isSoundEffect is not defined`。该问题不是增加一个局部常量后就可以收口的长期架构问题;本次重构应恢复单一音频组件,使变量与它控制的两个分支重新处于同一组件边界。 + +## 四、目标结构 + +```text +ImageCanvasEditorView +└─ useImageCanvasGenerationSurface + └─ ImageCanvasGenerationComposerView + └─ ImageCanvasAudioGenerationComposerView + ├─ isSoundEffect === true → 现有 SFX UI 与行为 + └─ isSoundEffect === false → 现有 BGM V1 UI 与行为 + └─ ImageCanvasBackgroundMusicPresetMarquee +``` + +继续保留的 BGM 专项模块: + +- `ImageCanvasBackgroundMusicPromptModel.ts`:canonicalization、计数、动作资格和 dialog 类型收窄。 +- `useImageCanvasBackgroundMusicPromptAssist.ts`:按 dialog ID 隔离的异步助手状态与提交锁。 +- `ImageCanvasBackgroundMusicPresetModel.ts`:30 个预设与追加规则。 +- `ImageCanvasBackgroundMusicPresetMarquee.tsx`:展开、滚动、hover、触摸和 reduced-motion。 + +删除的完整视图模块: + +- `ImageCanvasBackgroundMusicGenerationComposerView.tsx` +- `ImageCanvasBackgroundMusicGenerationComposerView.test.tsx` + +删除测试文件不等于删除覆盖;其中全部用例必须迁入总 composer 测试。 + +## 五、实现边界 + +### 5.1 单一渲染入口 + +`ImageCanvasGenerationComposerView` 对 `audio-sound-effect` 与 `audio-background-music` 只保留一个渲染条件和一个 `ImageCanvasAudioGenerationComposerView` 调用。组件内部以 `isSoundEffect` 选择两棵现有表单子树,不为本次重构新造配置层。 + +共享组件使用基于 dialog ID 与 mode 的稳定 `key`。连续切换 BGM dialog 时,预设展开、滚动、hover 和触摸状态不得继承;从 SFX 切到 BGM 时,音效时长菜单状态也不得泄漏。 + +### 5.2 Hook 与类型安全 + +- React Hook 不得放进 `isSoundEffect` 条件分支。`useState`、`useRef`、`useId` 和 `useImageCanvasFloatingOptionDismiss` 保持固定调用顺序。 +- `GenerateDialogState` 不是可判别联合。BGM 分支继续通过 `toBackgroundMusicGenerationDialog` 取得带稳定 ID 的 `BackgroundMusicGenerationDialogState`。 +- BGM 缺少稳定 ID 或 `backgroundMusicPromptAssist` 时失败关闭,不渲染不完整的 BGM 表单;SFX 不依赖这两个条件。 + +### 5.3 两套状态写回不得合并 + +- SFX 继续走现有 `setGenerateDialog` 路径,保持按 mode 更新、失败态复位和时长写回语义。 +- BGM 继续走 `updateCanvasGenerationDialogById(dialog.id, updater)`,不能降级成只比较 mode。助手响应、预设、撤销和提交锁仍按稳定 dialog ID 隔离。 +- `backgroundMusicPromptAssist` 可以继续由 surface 传给共享 composer,但只允许 BGM 分支读取或调用。 + +### 5.4 锁定与提交资格不得串用 + +|分支|锁定条件|生成资格| +|---|---|---| +|SFX|沿用现有 `dialog.status === 'generating'`|沿用现有 SFX 提交入口和默认 Prompt 规则| +|BGM|AI processing、`submitting` 或 `generating` 任一成立|沿用 canonical Prompt 的有效字符与 `1–200` code point 规则| + +BGM 使用 `PlatformTextField + readOnly + aria-invalid`;SFX 继续使用 `AutoGrowTextArea + disabled`。本次不统一这两个输入控件,也不改错误、可访问名称或按钮文案。 + +## 六、预计代码改动 + +|文件|最小改动| +|---|---| +|`ImageCanvasGenerationComposerView.tsx`|恢复共享 `ImageCanvasAudioGenerationComposerView` 与 `isSoundEffect`;移入现有 BGM JSX、常量和依赖;删除独立 BGM import 与双渲染入口| +|`ImageCanvasGenerationComposerView.test.tsx`|迁入独立 BGM composer 的全部测试,并增加双 mode 隔离与切换回归| +|`ImageCanvasBackgroundMusicGenerationComposerView.tsx`|删除| +|`ImageCanvasBackgroundMusicGenerationComposerView.test.tsx`|覆盖迁完后删除| +|`useImageCanvasBackgroundMusicPromptAssist.ts`|只修正指向独立 composer 的历史注释;不改 controller 行为| + +以下生产文件预计不改:`useImageCanvasGenerationSurface.tsx` 的现有 props 接线、BGM Prompt / 预设模型、预设跑马灯、submission workflow、submission model、dialog model、`src/index.css` 的现有 BGM 样式,以及全部后端代码。 + +如实施时发现必须超出该清单才能保持现有行为,应先停下并重新确认边界,不能借本次组件归并顺带重构。 + +## 七、测试迁移与回归矩阵 + +### 7.1 BGM 原覆盖完整迁移 + +独立组件测试中的下列覆盖必须逐项迁入 `ImageCanvasGenerationComposerView.test.tsx`: + +- canonical preview 计数、超限展示和不改写输入框。 +- 0 / 1 / 2 个有效字符及 200 / 201 / 2000 / 2001 边界。 +- 补全、简化、撤销和预设按正确 dialog ID 路由。 +- `preparePreset` 拒绝时不写回,预设成功时沿用追加和清快照语义。 +- 助手错误与生成错误的展示顺序。 +- Suno、泥点价格、生成允许 / 拒绝。 +- `completing`、`simplifying`、`submitting`、`generating` 锁定。 +- 完整单层撤销按钮矩阵和现有可访问属性。 + +### 7.2 mode 隔离与切换 + +- SFX 渲染时不存在 BGM 控件,也不存在本次明确排除的 SFX V2 控件。 +- BGM 渲染时不存在 Vidu 和音效时长控件。 +- SFX 仍保持 `audio1.0`、2–10 秒、默认 5 秒、当前价格和既有提交参数。 +- BGM dialog A 展开预设后切到 dialog B,局部展开与滚动状态不继承。 +- SFX 与 BGM 相互切换时,菜单、锁和 Prompt 助手状态不跨 mode 泄漏。 +- 缺少稳定 ID 或 controller 的 BGM 失败关闭;同样条件不影响 SFX 正常渲染。 + +### 7.3 最小验证命令 + +```powershell +npm run test -- src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasBackgroundMusicPromptModel.test.ts src/components/image-editor/ImageCanvasBackgroundMusicPresetModel.test.ts src/components/image-editor/ImageCanvasBackgroundMusicPresetMarquee.test.tsx src/components/image-editor/useImageCanvasBackgroundMusicPromptAssist.test.tsx +npm run test -- src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx +npm run typecheck +npm run check:encoding +git diff --check +``` + +删除旧测试文件后,还要确认测试收集清单中不再引用它。若本次改动触发其它现有图片画布测试失败,只修复由共享 composer 归并直接造成的回归,不扩大到无关模块。 + +## 八、实施顺序 + +1. 在总 composer 内恢复共享音频组件、`isSoundEffect` 和单一音频渲染入口。 +2. 原样迁入 BGM 分支,保留按 ID 写回、锁定、错误顺序、Suno 与预设行为。 +3. 迁移独立 BGM 组件测试,并补齐 SFX/BGM 隔离与切换用例。 +4. 删除独立 BGM composer 及其测试文件,修正相关注释与文档引用。 +5. 运行定向测试、typecheck、编码检查和差异检查;实现完成后再把实际结果补入 T6 后续记录。 + +## 九、完成定义 + +- 两个音频 mode 均从同一个 `ImageCanvasAudioGenerationComposerView` 渲染,且组件内存在唯一的 `isSoundEffect` 分流。 +- 独立完整 BGM composer 文件和独立测试文件已删除,原测试覆盖无遗漏地迁入总 composer。 +- BGM V1 所有已交付能力与正式提交语义不变。 +- 现有 SFX UI、Prompt、Vidu、时长、价格、校验和提交链路无回归。 +- 没有实现任何 SFX V2 独有功能,没有修改需求原文或后端契约。 +- 规定的定向测试、typecheck、编码检查和 `git diff --check` 全部通过。 + +## 十、实施结果 + +2026-08-06 已按本文边界完成: + +- `ImageCanvasGenerationComposerView.tsx` 恢复唯一 `ImageCanvasAudioGenerationComposerView`,由组件内 `isSoundEffect` 分流;BGM 继续按稳定 dialog ID 写回,SFX 继续走原 `setGenerateDialog` 路径。 +- 删除 `ImageCanvasBackgroundMusicGenerationComposerView.tsx`,保留 BGM Prompt / 预设纯模型、助手 controller 和预设跑马灯。 +- 把独立组件全部用例迁入 `ImageCanvasGenerationComposerView.test.tsx` 后删除旧测试文件;总 composer 现有 50 项测试同时覆盖 BGM 完整动作矩阵和 SFX/BGM 控件、状态、dialog 切换隔离。 +- 只修正 `useImageCanvasBackgroundMusicPromptAssist.ts` 的组件归属注释;没有修改 controller、surface、submission workflow、样式、后端、契约或需求原文,也没有实现 SFX V2 独有功能。 + +实际验证: + +- Prompt / 预设 / controller / 总 composer:`121/121`。 +- surface 与 submission workflow:`72/72`。 +- `npm run typecheck`、变更文件 ESLint、Prettier、`npm run check:encoding` 和 `git diff --check`:全部通过。 + +2026-08-06 已执行共享 composer 归并后的浏览器视觉 smoke,未发现阻断性问题;本轮未创建真实音频生成任务或产生扣费。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 3e8f563c2..01e07f9e6 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -6258,6 +6258,46 @@ - 兼容边界:这是基于「截至 2026-07-31 尚无外部第三方存量调用方」接受的 v1 原地 breaking change;一旦出现外部活跃 Key、公开契约或联调方,后续破坏性变更必须保留兼容、经过弃用期或升级 `/api/external/v2`。 - 关联文档:`docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md`、`docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md`、`.codex/skills/genarrative-external-editor-api/SKILL.md`。 +## 2026-08-04 图片画布 BGM Prompt 采用唯一可见规范化文本与面板级同步提交锁 + +- 背景:图片画布背景音乐链路原先会在前端、BFF 和 Suno body 构造阶段执行不一致的 Prompt 清理,并为空值提供默认回退,可能形成输入框不可见的实际提交文本;新增预设和 AI 助手后,需要统一唯一可见 Prompt、首尾 Unicode 空白 canonicalization,以及与 SFX、请求字段和 External v1 的边界。 +- 决策:本规则只适用于 `/editor/canvas` 的 `audio-background-music`,并在该模式内取代 2026-08-03「编辑器持久化的 `prompt` 统一表示规范化用户意图」的泛化表述;其它图片、角色和图标生成语义不变。输入框是唯一 BGM Prompt 真相,预设可见文本和 AI 成功结果写回后都成为当前 Prompt;不得在输入框之外维护或向 Suno 发送另一份隐藏 Prompt。进入预设追加、AI 补全、简化、撤销或正式生成边界前,只允许执行一项 canonicalization:移除首尾属于 Unicode `White_Space` 属性的 code point,并在后续动作前把结果同步写回同一个输入框;TypeScript 不得用会额外移除 U+FEFF 的 `String.trim()` 代替 Unicode `White_Space` 判定。canonical Prompt 内部的空格、CR / LF 和其它 Unicode 空白保持原位,U+200B、U+FEFF、组合字符、ZWJ emoji 等非 `White_Space` code point 即使位于首尾也必须保留;除此之外不得做 NFC、空白折叠、换行转换、标点替换或静默截断。canonical Prompt 满足 `有效字符数 = 0 ⇔ 总字符数 = 0`,所以任意长度的纯 Unicode `White_Space` 原始输入规范化后都禁止补全、简化和正式生成;只有至少含 1 个有效字符且总数超过 200 才允许简化。预设 ID、分组、颜色、助手系统模板和内部引导不得拼入 Suno Prompt;Prompt 助手可以使用服务端固定模板生成可见结果,但正式 Suno 请求只携带输入框可见的 canonical Prompt。200 字上限和有效字符数都以 canonical Prompt 计算,有效字符定义为非 Unicode `White_Space` code point:0 个不能补全或正式生成;1 个且总数不超过 200 时只能正式生成;至少 2 个且总数不超过 200 时可以补全和正式生成;至少含 1 个有效字符且总数超过 200 时禁止补全和正式生成,只允许简化。请求字段继续使用 `gptDescriptionPrompt` / `gpt_description_prompt`,不得改名为 `actualPrompt`;`actual_prompt` 继续保留资源 / 素材审计语义,canonical 输入框、BFF、队列载荷、Suno body、生成记录 `prompt` / `actual_prompt` 和结果响应 `prompt` / `actualPrompt` 必须等值,BGM 成功响应同时填充后两者。AI 补全与 `180 -> 170`、最多两次的一键简化只走登录态内部 BFF,不调用 Suno、不创建任务、不扣正式音乐生成泥点;简化全程冻结入站 canonical `originalPrompt`,第一次以它为 `currentPrompt`、目标 180,第二次优先以第一次响应对象中可提取且包含有效字符的 canonical 字符串 `prompt` 为 `currentPrompt`、目标 170,即使其它 envelope 字段无效;连对象或字符串 `prompt` 都无法提取,或候选 canonicalize 后不含有效字符时回退原文,同时始终用同一 `originalPrompt` 作保真参照。内部 envelope 固定为 `prompt: string`、`isDirectWritebackFormat: boolean`、`isContentComplete: boolean`、`hasObviousFragment: boolean`;候选仅在四字段结构有效、canonical 后包含有效字符且不超过 200 字、后三个字段依次为 `true / true / false` 时通过。`isDirectWritebackFormat` 表示候选只含一条可直接写回的中文 BGM Prompt,不含解释、标题、Markdown、JSON、代码块、字数报告、处理过程或删改说明;程序只解析字段、canonicalize、计数和执行布尔结果,不用关键词或未定义正则猜测语义判断。程序不截断,也不硬编码内容保留规则,成功后只保存一层 canonical Prompt 交换快照。正式生成在点击事件内、任何 `await` 前同步锁定当前 BGM generation dialog,并完成 canonicalization、写回、校验和请求值冻结,后续重复点击忽略;拒绝时保留 canonical Prompt 和既有快照,接受后进入现有 `queued/generating` 占位,不锁整个画布或其它 dialog。 +- T2 助手补充决策:补全与简化共用 `prompt: string`、`isDirectWritebackFormat: boolean`、`isContentComplete: boolean`、`hasObviousFragment: boolean` 四字段内部 envelope;补全候选也只有在结构与 canonical 字符校验通过、三个判断为 `true / true / false` 时才成功。助手显式使用现有 OpenAI Chat 协议,envelope 只允许由完整 `response.text` 中唯一一个 JSON object 承载;服务端仅用 `serde_json` 对完整文本全量解析,允许 object 外围 JSON whitespace,但不接受代码块、前后解释、多个 JSON 值、子串提取或自动修复。请求不发送 function tools,不做运行时双协议 fallback,响应出现 tool call 也按结构非法处理。补全固定一个业务语义轮,简化固定最多两个业务语义轮;单轮内部由现有 `LlmClient` 执行的 transport retry 不计入业务语义轮数。简化第一轮最终发生 transport、超时或上游失败时直接失败,不进入 170 字内容修复轮;只有成功取得第一轮响应但候选不合格时才派生第二轮。第二轮只有在第一轮完整文本已全量解析为单个 JSON object 后,才可从 object 读取字符串 `prompt`;禁止从未完整解析的响应中捞取候选。助手成功响应只暴露 canonical `prompt` 和程序计算的 `charCount`,失败响应不得暴露任一未通过候选、可提取的 `prompt`、内部 envelope 或判断字段。 +- 影响范围:画板音乐权威设计、BGM composer 与临时状态模型、`editorProjectClient`、`shared-contracts` 内部助手 DTO、`api-server` 登录态 Prompt 助手与正式 BGM BFF、正式 generation queue 载荷、`platform-audio` Suno body builder,以及 BGM 提交与端到端测试。SFX 继续使用 Vidu `audio1.0`、现有规范化、默认 Prompt、1500 字限制和时长契约,不应用 BGM 的 canonicalization 或 0 / 1 / 2 有效字符规则;本规则不修改 SpacetimeDB schema,也不改变或扩展 External v1 / OpenAPI 的背景音乐请求、异步语义和路由,Suno 三字段 body、固定模型 / 泥点展示和现有 LLM 原文日志策略不变。 +- 验证方式:TypeScript 与 Rust 对 CR / LF、CRLF、组合字符、ZWJ emoji、U+0085、U+200B、U+FEFF 和 199 / 200 / 201 code point 得出一致结果;首尾 U+0085 等 Unicode `White_Space` 被移除并同步反映到输入框,U+FEFF 与零宽字符不被误删。状态测试分别锁定全空白、0 / 1 / 2 个有效字符、预设追加、补全 / 简化失败不覆盖 canonical Prompt、单层交换撤销、迟到响应和同 dialog 双击;端到端断言 canonical 输入框、BFF、队列载荷、Suno body、记录和响应等值,且不存在隐藏 Prompt。SFX 请求体、默认 Prompt、1500 字限制和时长不变,External v1 契约测试无差异。文档阶段运行 `npm run check:encoding` 和 `git diff --check`。 +- 关联文档:`docs/【编辑器】画板音乐生成入口设计-2026-06-18.md`、`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。 + +--- + +## 2026-08-04 BGM Prompt 助手补齐输入、完成状态与埋点边界 + +- 背景:BGM Prompt 助手基线已经固定四字段 envelope、完整 `response.text` JSON 解析和 `180 → 170` 简化轮次,但权威设计仍把所有 201 字以上输入视为可简化,也没有明确 OpenAI Chat 未完成响应、HTTP body 上限及成功路由埋点;这会让超长文本消耗共享模型额度,并可能把 `finish_reason = length / content_filter` 的偶然闭合 JSON 当作完整候选。 +- 决策:本条补充并取代上一条中“总数超过 200 即只允许简化”的无上限表述。一键简化只接受至少含 1 个有效字符且总数为 `201–2000` 的 canonical Prompt,最大值等于正式生成 200 字上限的 10 倍;超过 2000 时保留完整文本但禁用 AI 补全、一键简化和正式生成,服务端以现有 `400 BAD_REQUEST` 和字段 `currentPrompt` 拒绝。两个助手路由分别设置 `32 KiB` HTTP body 上限,body 超限保持 Axum `413 PAYLOAD_TOO_LARGE`,字符上限与 body 上限独立校验。 +- LLM 完成状态:在解析 envelope、canonicalize 候选或提取重试 `prompt` 前,使用 `platform-llm` 现有 API-kind-aware 未完成原因判断检查 OpenAI Chat `finish_reason`;去除外围空白并忽略 ASCII 大小写后的 `length` / `content_filter` 均不可信,即使正文形成合法 JSON 也不得接受或提取。补全遇到两者均直接失败;简化第一轮 `length` 只以冻结的 `originalPrompt` 进入 170 字轮,第一轮 `content_filter` 直接失败,第二轮出现任一未完成原因都最终失败。缺失、空值或未知自定义 reason 不因该字段单独拒绝,继续执行其余门禁,不采用 `stop` 白名单。`platform-llm` 只公开复用 predicate,不改变其它普通文本调用方的降级行为。 +- 限流与埋点:不增加 Prompt 助手专属的用户级、IP 级、时间窗口、令牌桶或本地额度限流,不新增功能级 `429` / `Retry-After`;现有 api-server 全局并发背压、前端防重复操作、Nginx 保护和上游真实 `429` 安全映射不变。两个成功路由进入 `tracking.rs` 静态映射:补全为 `editor_background_music_prompt_completion`,简化为 `editor_background_music_prompt_simplification`,两者均使用 `module_key = editor`、User scope;普通 route tracking 继续只记录成功响应并走现有本机 outbox,助手 handler 不同步写 SpacetimeDB。 +- 影响范围:BGM composer 动作状态与 client、`api-server` Prompt 助手路由和候选验收、`platform-llm` 公共未完成原因 predicate、route tracking 静态映射及相应测试;不修改 SFX、正式 BGM 200 字限制、canonicalization 算法、SpacetimeDB schema、External v1 / OpenAPI 或 LLM 原文日志策略。 +- 验证方式:覆盖 canonical 2000 / 2001、两个路由 `32 KiB` / `413`、补全与两轮简化的 `length` / `content_filter`、缺失和未知 reason 兼容、连续合法请求无功能级 `429`,以及两个成功路由的 event key、`editor` module 和 User scope;运行 `api-server` 与 `platform-llm` 定向测试、`npm run check:encoding` 和 `git diff --check`。 +- 关联文档:`docs/【编辑器】画板音乐生成入口设计-2026-06-18.md`。 + +--- + +## 2026-08-05 BGM 正式提交保持站内 canonical 载荷与分层回调所有权 + +- 决策:登录态站内 BGM queue / inline 在分流前生成唯一 canonical payload,queue serializer 与 Suno body 只消费其中的 canonical `gptDescriptionPrompt` 并固定 `makeInstrumental=true`。正式 BGM POST 遇到 retryable HTTP 状态或 transport error 不由客户端自动重发,避免一次点击产生重复任务或扣费。 +- 回调所有权:正式任务接受后,钱包刷新只校验账号;任务列表通知同时校验账号与项目;dialog、canvas、asset 和 layer 写回校验账号、项目、scope version 与原 BGM dialog。dialog 删除或同账号切项目不应阻止账号级钱包刷新,账号切换即使暂时保留相同 project ID 也不得触发旧账号的任务列表回调。 +- 外部边界:上述 canonical payload 只属于登录态站内链路。External v1 继续保留调用方原始 BGM payload,并按原始 payload 执行既有 Idempotency-Key 等值语义;不能把 canonical 等价值误判为相同重放。 +- 影响范围:图片画布 BGM 正式提交与回调门禁;不新增持久提交锁、自动重试、计费设计或 External v1 契约变化。 +- 关联文档:`docs/【编辑器】画板音乐生成入口设计-2026-06-18.md`。 + +--- + +## 2026-08-04 BGM 撤销按钮按快照存在性显示、按面板锁定禁用 + +- 背景:需求《BGM生成优化需求 V1.0》第三节要求“AI 开始处理”时撤销按钮显示但禁用,权威设计也只要求处理中禁用撤销,但没有写明可见性;T4 界面方案据此把渲染条件收窄为“存在可撤销快照”,而状态模型在发起 AI 操作时会把可撤销快照转为本次临时快照,两者叠加会让撤销按钮在处理期间消失,与需求不一致。 +- 决策:撤销按钮的可见条件为存在可撤销快照或本次 AI 操作的临时快照,启用条件为可见且当前 BGM 面板未处于 `completing`、`simplifying`、`submitting` 或现有 `generating` 锁定状态。因此 AI 处理期间显示并禁用,AI 失败、点击预设和完全无快照时隐藏,AI 成功、手动编辑 AI 结果和连续撤销互换时显示并启用,`submitting` 期间有快照显示并禁用、无快照隐藏,解除锁定后按快照恢复。禁用必须使用真实禁用态并保留按钮在 DOM 与可访问树中的位置,不得用隐藏、透明度或其它视觉伪装代替,也不得因锁定改变按钮占位。发起 AI 操作时仍以当时的 canonical Prompt 替换旧快照,不得为了让处理期间按钮可见而保留旧快照;那会与本日第一条“AI 失败不恢复已被替换的旧快照”冲突,并让失败后的撤销指向不相关内容。视图只按该矩阵决定显示与启用,是否真正执行撤销仍由状态层在非 `idle` 或无快照时拒绝,两处不得各写一套判定。 +- 影响范围:`/editor/canvas` 的 `audio-background-music` 面板撤销按钮与其定向测试;不改变单层交换快照语义、canonicalization、提交锁、Suno 契约或后端 Prompt 助手,也不修改状态模型字段,`temporaryPromptSnapshot` 已在公开 dialog 状态中且只在 `completing` / `simplifying` 期间非空。本条不适用于 SFX,V1.0 不改动 SFX 的一键优化与撤销行为。 +- 验证方式:按矩阵逐行覆盖初始隐藏、首次与再次 AI 处理期间显示并禁用、成功启用、失败隐藏、手动编辑后仍启用、点击预设隐藏、`submitting` 有无快照的两种表现、解除锁定后恢复,以及连续撤销互换保持启用;并断言处理期间按钮仍在可访问树中且为真实禁用态。 +- 关联文档:`docs/【编辑器】画板音乐生成入口设计-2026-06-18.md`。 ## 2026-08-03 完美像素对账判据改看 dialog 收口状态,网关合成响应归入未知结果 - 缺陷一(对账把真成功判成失败):对账用「同 ID 的 generation-dialog 是否还在权威快照里」判定成败,而服务端成功回填时**保留**该 dialog 并就地改写——`apply_editor_canvas_generation_items` 置 `status: "idle"`、`composerOpen: false`、写入 `generatedLayerId`、清掉 `errorMessage`,该行为另有服务端测试断言 `dialog["generatedLayerId"]` 钉住。所以响应丢失但服务端其实已完成时,判据反向:用户被告知「画布未收到完美像素结果,请确认素材库」,而结果早已在画布上,重做一遍就造出第二份;这条分支还刻意不套用快照,本地也看不到那个新图层。 @@ -6697,3 +6737,10 @@ - 图片类最终 `generationInputs.references` 不信任客户端输入;队列 payload、完美像素及直接创建资源 / 素材入口删除客户端 references,worker / inline 路径按本次真实参考源与 owner 范围内的项目资源、账号素材重建 `refType/refId`。只有 owned objectKey 但没有正式行时不生成伪 provenance。完美像素继续使用升级前 canonical 客户端输入计算 operation fingerprint;新操作只持久化权威重建值,历史同 task/resource 重放复用服务端既存 metadata 通过精确比较。 - 升级前 External 幂等任务可能仍在 payload 中保留客户端 references;重放比较只对白名单内已迁移的图片生成、图片修改、去背景、图标图集和 UI 提取任务,在旧侧有 references、当前侧已删除时移除旧字段,其他字段变化仍返回 `409`。音频 / 视频 / 角色动作等未迁移 job kind 始终完整比较,不能扩大兼容面。 - 本次复用既有 `external_generation_job.dedupe_key` 唯一索引和 `spacetime-client` 查询,不改 SpacetimeDB schema、迁移或 bindings。 + +## 2026-08-06 音效与背景音乐恢复共享音频 Composer + +- 决策:图片画布的 `audio-sound-effect` 与 `audio-background-music` 只保留一个 `ImageCanvasAudioGenerationComposerView`,组件内以 `isSoundEffect = dialog.mode === 'audio-sound-effect'` 分流。撤销的是完整 `ImageCanvasBackgroundMusicGenerationComposerView` 这一层视图拆分,不撤销 BGM Prompt 纯模型、助手 controller、预设模型或预设跑马灯的独立职责。 +- 业务隔离:共享组件不等于共享规则。SFX 继续使用 Vidu `audio1.0`、2–10 秒、默认 5 秒、现有 Prompt 回退、1500 字限制、价格和提交链路;BGM 继续使用 canonical Prompt、200 字生成限制、30 个预设、AI 补全 / 简化、单层撤销、提交锁和 Suno。BGM 按 dialog ID 写回,SFX 继续走现有 `setGenerateDialog`,两条路径不得互换。 +- 非目标:本次只规划视图归并,不实现 SFX V2 的 ElevenLabs、中译英、自动时长、30 秒、Loop、一键优化或预设,不修改任何后端、External v1、Schema、计费或需求原文,也不新建配置驱动的 composer 框架。 +- 实施状态:已恢复共享音频 composer,独立完整 BGM composer 及其测试文件已删除,原覆盖完整迁入总 composer。Prompt / 预设 / controller / 总 composer `121/121`、surface 与 submission workflow `72/72` 通过,typecheck、变更文件 ESLint、Prettier、编码检查和差异检查通过;没有修改后端、契约或需求原文,也没有实现 SFX V2 独有功能。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 3574369ec..f16e77123 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -4318,6 +4318,12 @@ - 验证:自动测试使用真实公开 Host/Origin 执行 `initialize`;部署后再从公网域名完成带 Key 的 `initialize`、`tools/list`、`resources/list`、Skill resource 读取和至少一个只读业务 tool 调用。loopback 成功只能证明 MCP 实现和 Key 可用,不能替代公网 Host 验收。 - 关联:`server-rs/crates/api-server/src/external_mcp.rs`、`docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md`。 +## 异步任务接受后的刷新回调不能统一套用 dialog 所有权(2026-08-05) + +- 现象:正式生成任务已被后端接受,用户随后删除 dialog 或切换项目,任务仍继续并可能扣费,但钱包和任务列表没有刷新;反向问题是账号切换时若 project ID 暂时相同,旧任务可能刷新新账号的任务列表。 +- 原因:把 dialog / canvas 的完整 UI 所有权同时用于账号级钱包和账号内项目级任务列表,或者任务列表只比较 project ID,没有校验账号。 +- 处理:按副作用分层校验。钱包只比较账号;任务列表比较账号加项目;dialog、canvas、asset 和 layer 写回继续比较账号、项目、scope version 与原 dialog。正式请求已接受后,删除 UI 状态不等于取消后端任务。 +- 验证:分别覆盖删除 dialog、同账号切项目、账号 A 切到账号 B 且 project ID 保持相同,以及原账号原项目原 dialog 仍有效的正常回写。 ## GUI owner 锁不能替代逐 boot 的事件接收端登记(2026-08-05) - 现象:GUI 首次启动后 manifest 事件转发正常,但 Runner 被替换为新 boot 后只剩 owner 锁和 endpoint 可用,后台更新不再到达 GUI;或者 attach 响应只确认 owner,客户端却误记当前 boot 已完整登记,后续 ensure 不再重试。 @@ -4338,3 +4344,10 @@ - 原因:客户端虽在重试中复用 `x-request-id`,队列入口却用随机 job id 生成 dedupe key;前端允许无限追加,api-server 和 provider 用 `.take(...)` 静默截断;`generationInputs.references` 被当成可信持久 provenance。 - 处理:主站生成 POST 禁止自动重试,把显式复用的稳定 request id 接到队列唯一键并校验 replay payload;所有边界显式拒绝超限,前端还要预留主图槽位、统计在途上传,并在上传完成前拒绝模型切换、画布选图、提交生成、关联源图删除 / 剪切 / 素材删除和面板切换 / 关闭;reservation 必须绑定原面板上下文,批量部分失败时不能丢弃已经持久化的成功项。入队、完美像素及直接创建资源 / 素材时删除客户端 references,执行时按真实参考源和 owner 资源记录重建权威引用。历史任务比较必须兼容仅差已删除 references 的旧 payload,不能只保留旧 hash 却让 payload 比较误报冲突。 - 验证:覆盖同键同 payload / 不同 payload、普通图片第 6 张、带主图的 GPT-image-2 第 5 张额外引用、provider 6 / 15 张边界、伪造引用删除和 owned 资源 / 素材重建。 + +## 共享音频 Composer 架构冲突不能按单行选边(2026-08-06) + +- 现象:master 的音频 composer 同时承载 SFX 与 BGM,并在组件内定义 `isSoundEffect`;功能分支把 BGM 拆成独立组件后,原组件变成 SFX-only。合并时只把 master 的条件占位表达式带回 SFX-only 组件,没有带回变量定义,最终在测试渲染阶段报 `isSoundEffect is not defined`。 +- 原因:冲突两侧代表不同组件架构,逐行保留看似有用的 JSX 会把一个架构中的局部条件拼进另一个架构。import 排序、格式检查和只覆盖单一 mode 的测试都不能证明这种组合成立。 +- 处理:先确定权威组件边界,再按完整调用链解决冲突。图片画布音频入口当前决策是恢复一个共享 `ImageCanvasAudioGenerationComposerView`,由组件内 `isSoundEffect` 分流;BGM/SFX 的 validator、写回、锁和提交契约仍分别保持。不要只补一个常量后继续维持已经废弃的双 composer 边界。 +- 验证:同时渲染 `audio-sound-effect` 与 `audio-background-music`,覆盖两个 mode 的正向控件和互斥负向断言、dialog / mode 切换、BGM 稳定 ID 与 controller 缺失的失败关闭,并运行 `ImageCanvasGenerationComposerView.test.tsx` 与 typecheck。 diff --git a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md index a1f56d560..dd42aa144 100644 --- a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md +++ b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md @@ -19,7 +19,7 @@ - 鼠标中键拖拽始终平移画布;长按 Space 临时进入抓手模式,松开后恢复原工具。 - 图片拖拽时显示水平 / 垂直吸附参考线,吸附到其它图层、生成占位框或画板的边缘与中心线;当移动元素接近两个同轴元素形成的等距位置时,支持横向或纵向等距吸附。 - 生成资源右上角显示元数据按钮,点击打开独立元数据窗口。图片信息页不展示后端组装后的生图 Prompt,也不提供复制 Prompt;只展示该图片生成时用户在面板里提交的输入快照,包括普通生成提示词、规范表单字段、角色设定、图标素材描述、快速编辑提示词、重绘提示词,以及角色规范 / 常规参考图 / 图标规范 / 编辑参考图等参考图卡片,并提供“复制信息”复制当前可见字段。参考图输入快照只保存 `refType/refId` 行引用,其中 `refType="project-resource"` 指向 `editor_project_resource.resourceId`,`refType="asset"` 指向 `editor_asset.assetId`;不得把图片 Data URL、普通 URL 或 `objectKey` 写入 `generationInputs.references`。旧数据或上传图片没有输入快照时显示 `-`,禁止回退展示内部 Prompt。 -- 对生成资源执行重绘时,在右侧创建新的生成结果图层,并自动调整视图显示原图和新图;重绘面板不因提交成功自动关闭,便于连续改提示词。重绘 / 改造输入框只允许从 `generationInputs.fields` 中恢复用户可见输入快照,例如普通生成提示词、视频描述、音效 `prompt`、背景音乐 `gpt_description_prompt`、角色设定、UI 用户输入、图标素材描述、规范表单和宣发素材字段;禁止回退展示资源 `prompt` / `actualPrompt` 中的后端拼接 Prompt、固定生成模板或模型默认提示词。没有用户输入快照的旧图层打开改造时保持空输入,等待用户重新填写。 +- 对生成资源执行重绘时,在右侧创建新的生成结果图层,并自动调整视图显示原图和新图;重绘面板不因提交成功自动关闭,便于连续改提示词。重绘 / 改造输入框只允许从 `generationInputs.fields` 中恢复用户可见输入快照,例如普通生成提示词、视频描述、音效 `prompt`、背景音乐 `gpt_description_prompt`、角色设定、UI 用户输入、图标素材描述、规范表单和宣发素材字段;禁止回退展示资源 `prompt` / `actualPrompt` 中可能存在的后端拼接 Prompt、固定生成模板或模型默认提示词。当前 BGM 是字段语义上的例外:其新记录中的 `prompt` 与 `actualPrompt` 必须等于输入框已经写回的 canonical `gpt_description_prompt`,不得包含隐藏内容;但改造输入仍只从 `generationInputs.fields` 恢复,不能因此放宽为从资源审计字段回退。没有用户输入快照的旧图层打开改造时保持空输入,等待用户重新填写。 - 图片生成 / 修改统一经 api-server BFF 接入 VectorEngine。普通生成、生成规范和重绘保留既有 `gpt-image-2` 路径;图片快速编辑统一打开框选区域 + 单提示词 + 模型选择面板,默认沿用原图模型,不展示参考图或比例 / 尺寸控件;其中生成规范类图片固定 `16:9`、`2K`、`gpt-image-2`,面板底部用与可编辑面板一致的比例 / 尺寸 / 模型胶囊按钮展示固定参数,但按钮为禁用态,不允许在该面板改比例、尺寸或模型。`生成角色形象` 与 `生成图标素材` 支持 `nanobanana2`(`gemini-3.1-flash-image-preview`)和 `gpt-image-2`,默认 `nanobanana2`,并在两类面板之间沿用用户上次选择的模型;两类面板不展示抠图背景色或抠图模型选择;前端用户路径固定提交 `screenColor=auto` 和 `segModel=birefnet`,由后端自动决策具体抠图背景色,`anime-seg` 作为内部保留能力不在用户界面暴露。`nanobanana2` 走 `/v1beta/models/{model}:generateContent`,请求体写入 `generationConfig.imageConfig.aspectRatio/imageSize`;`gpt-image-2` 走 `/v1/images/generations` 或 `/v1/images/edits`,请求体按 VectorEngine 文档映射 `size`。宣发素材三个工作流(游戏首图、详情五图、运营海报)固定使用 `gpt-image-2`,面板模型胶囊为禁用态,不提供 `nanobanana2` 入口;前端按 workflow 同时提交 `outputSize`、`aspectRatio` 和 `imageSize`,其中游戏首图为 `720x540 / 4:3`、详情单图为 `720x1280 / 9:16`、运营海报为 `1280x720 / 16:9`;后端收到 `kind: "publication-material"` 时也强制归一为 `gpt-image-2` 生成和计费,生成回填图层优先使用生成占位的 `originalWidth/originalHeight`,即使上游回包尺寸漂移也不得把宣发素材卡片变成随机 `1:1` 或 `4:3`。纯文本生成走 `/api/editor/images/generations`,重绘在前端优先复用当前图层 objectKey;尚未登记的本地图片先上传 OSS,再把 objectKey 交给同一图片生成 BFF,并在原图右侧生成一张新图;普通图层重绘作为 `quick-edit` 参考图提交,角色图层重绘必须按 `kind: "character"` 提交,继续套用角色生成器提示词限定、透明 PNG 后处理和角色资产持久化。`生成视频` 走 `/api/editor/videos/generations`,前端模型入口仅展示 Seedance 2.0 Fast / Seedance 2.0 / Kling 3.0 / Kling 3.0 Omni,不展示 Veo 入口,默认 Seedance 2.0 Fast;视频参数按当前正式面板支持的比例、时长、清晰度和声音开关提交,且 Seedance Fast 与 Seedance 标准版必须按各自真实模型 ID 独立映射,不得混用。生成结果以视频图层加入画布。纯文本生成入口采用 Lovart 式画布内占位图 + 锚定生成输入框:点击生成图片后以当前视口世界中心为目标,经统一 placement 避让后创建选中的灰色占位框,输入框跟随占位框显示;普通图片、角色、图标图集、UI 设计图及其重绘 / 改造入口必须在比例或清晰度恢复、切换时同步把占位框 `width/height/originalWidth/originalHeight` 更新为目标像素尺寸,生成中不得继续显示默认 1K 框;UI 素材提取的 1K / 2K 图集占位和旧图片修改入口也分别使用本次目标尺寸与源图真实尺寸。待生成、生成中和失败后保留的占位图都必须继续支持拖动,生成完成时真实生成图或视频落在最新占位框位置,输入框继续跟随新生成图层;占位图失焦时隐藏高亮边框、左上角生成器名称和右上角原始尺寸,重新聚焦时再显示,且名称 / 尺寸在画布缩小时按 viewport 反向缩放保持屏幕尺寸稳定;点击所有图片 / 视频生成入口并确认请求开始后,必须隐藏对应设置面板,只保留画布内占位图或原图预览,并在预览上显示 Lovart 式生成中遮罩,避免“面板仍占屏”或“预览一起消失”。图片快速编辑和重绘在调用图片 BFF 前必须把当前图层图片解析为已上传的 objectKey 或资源 ID;浏览器临时图片需先上传 OSS;视频素材快速编辑走视频生成 BFF,不允许走图片模型;角色动作的 `生成动画` 仍固定使用 `seedance2.0-fast` 动作 / 视频模型,角色动作素材的 `快速编辑` 按当前帧图片走图片编辑。前端不持有 provider 密钥;上游失败或配置缺失时恢复当前生成设置面板展示失败,不创建 mock 成功图。 - 图片画布抠图统一通过唯一、只监听 loopback 的 `bgfilter-worker` 调用 BgFilter provider。手动去除背景面向用户任意图片,仍走登录态同源 BFF `POST /api/editor/images/background-removals` 和外部生成队列;API 在入队前拒绝 `data:` / `blob:` 内联媒体,父流程将稳定引用解析为当前账号已登记且归属已校验的私有 OSS object key,并在同一轮账号项目 / 素材快照读取中同时恢复用户可见源模型,禁止为 object key、所有权和源模型分别重复拉取全量快照;随后只通过一次内部 HTTP RPC 传递 object key、排队预算 `maxQueueWaitMs`、调用预算 `callBudgetMs` 和固定的 `background_mode=complex`、`seg_model=birefnet`、`cross_check=off`,不传源图字节、签名 URL、`file` 或 `screen_color`。子 worker 在每次真实 provider attempt 前签发 600 秒 URL,承担默认 `Q=2048` admission 保险丝、provider 并发 `N=16`、严格最多两次顺序 attempt、响应字节与图片尺寸校验,并把成功图片作为内部 HTTP 二进制 body 直接返回;父流程同步等待该响应且不重试已被 worker 接收的内部 RPC(连接从未建立时按调度方案 §5.1 有界重连)。排队只消耗 `maxQueueWaitMs`,取得 provider permit 后才启动 `callBudgetMs`;attempt 按 `N × est × 2`、调用预算按 `2 × attempt + 1s` 派生,冻结 `est=5000ms` 时分别为 `160s / 321s`。complex 的真实 provider 失败会累计并打开自身熔断,但与 flat 状态隔离;complex 任意失败或熔断仍直接返回父流程失败,不接入阿里云 / 本地键色降级。provider 配置继续统一使用 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL` 和 `GENARRATIVE_EDITOR_BGFILTER_TOKEN`,父子共同使用 `GENARRATIVE_BGFILTER_WORKER_CONCURRENCY` 与 `GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS` 派生预算;旧 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN` 只作为 provider token 兼容别名;内部调用另使用 `GENARRATIVE_BGFILTER_WORKER_BASE_URL` 和独立内部 Token。所有令牌只在服务端注入,前端不持有令牌。成功字节返回父流程后,仍由父流程完成最终处理、OSS / asset object 持久化、结果图层与最新项目快照写回;接口只向前端返回 `queueState`,有项目上下文时前端同时创建去背景生成占位并把 `canvasCompletion` 交给后端。 - 编辑器自己生成的标准纯色背景抠图资产在保存源图后统一以 `background_mode=flat` 调用内部 `bgfilter-worker`。角色形象生成、图标 spritesheet 生成、UI 设计图素材提取和角色动作的用户路径都固定使用 `screenColor=auto`,但用户可见 `generationInputs.fields` 不记录 `抠图背景色` 或 `抠图模型`;api-server 在组装 prompt 前调用背景决策模块,从 12 个候选色中选择具体 hex,最多重试 3 次,失败后兜底 `#CFEFFF`。后端仍保留手动 hex 解析能力供内部兼容。最终生图 prompt、动作视频实色背景和子 worker 发往 provider 的 `screen_color` multipart 字段只接收解析后的具体 hex,不透传 `auto`。角色、图标 spritesheet 和 UI 设计图素材提取的同源画布请求由前端自动提交默认 `segModel=birefnet`,api-server 负责 allowlist 校验并在缺失时回落默认值;角色动作逐帧去背的 `seg_model` 由后端固定。四条 flat 路径再由 api-server 向 worker 显式传递 `background_mode=flat` 与 `cross_check`:角色形象生成、图标 spritesheet 和角色动作逐帧去背传 `on`,UI 设计图素材提取传 `off`,不依赖 BgFilter 服务端默认值。前端不展示抠图模型选择,`segModel` 不进入 `generationInputs`、响应、搜索、详情或导出;`background_mode`、`cross_check` 只存在于 api-server 到 worker 的内部 RPC。子 worker 为 flat / complex 分别维护独立进程级熔断,并对一次逻辑调用严格最多执行两次顺序 provider attempt;两种模式共享 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD=3` 和 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS=120` 默认值,但失败和成功只更新当前模式;父侧至多让 worker 接收一次内部 RPC(连接从未建立时按调度方案 §5.1 有界重连)。flat 两次失败、熔断、overload、内部 deadline 或断连后,只要父业务预算仍有效,父流程才按同一 object key 进入“阿里云通用抠图 → 本地 `editor_green_screen` 键色”降级;阿里云 fallback 不属于 `bgfilter-worker`。角色动作生成的序列帧背景色已与生图统一:后端把源角色图合成到视觉决策出的具体 hex 后再图生视频;抽帧后逐帧进入同一条 `内部 bgfilter-worker(background_mode=flat,cross_check=on)→ 父侧阿里云 → 父侧本地键色` 链路。 @@ -142,7 +142,7 @@ - 图片生成请求边界:角色生成、图标 spritesheet 和 UI 素材提取的同源画布 request DTO 保留 `segModel`,前端不提供选择控件而是自动提交默认 `birefnet`;api-server 继续校验并在字段缺失时回落默认值。`background_mode`、`cross_check` 与角色动作逐帧去背的 `seg_model` 不属于前端请求字段,只在 api-server 到 loopback worker 的内部 RPC 中传递。请求中的 `segModel` 不进入用户可见 `generationInputs` 或任何普通用户响应。 - `POST /api/editor/images/edits`:按提示词和当前图片的已登记 `objectKey` / `resourceId` 修改图片,返回新的生成图片元数据;图片快速编辑当前只提交 `sourceImageSrc`,不提交隐藏的 `referenceImageSrcs`,并随用户当前选择提交 `model / aspectRatio / imageSize / size`。api-server 必须先归一模型再选择 VectorEngine 协议:`nanobanana2` 调用 `/v1beta/models/{model}:generateContent` 并把原图作为 `inline_data`、比例和清晰度写入 `generationConfig.imageConfig`;`gpt-image-2` 调用 `/v1/images/edits` multipart。gpt-image-2 路径在 provider 边界把目标尺寸和所有 multipart 参考图临时补齐到 16 的倍数;nanobanana2 路径保留 provider 的比例 / 清晰度请求,但两条路径回图后都以统一业务目标尺寸尝试归一。只允许缩小和轻微裁切;回图任意一边小于目标或比例偏差过大时保留 provider 实际回图及尺寸,并返回通用 `warning`,不得放大伪造所选档位。无论是否发生尺寸恢复都只创建一个 project resource / 账号素材,不显示重复“原始输出”。provider 对齐尺寸或原生 K 档像素不得泄漏到正常完成的最终响应、资源或图层 Resolution;变换失败降级时以实际 provider 原图尺寸为准。本地红框标记图必须先上传再提交 objectKey;请求携带 project / asset 上下文时由后端创建新 resource / asset,前端只消费响应快照。 - `POST /api/editor/videos/generations`:按视频描述、模型、比例、时长、分辨率、模式、声音、默认联网搜索标记和泥点价格生成视频。前端可选模型为 `seedance2.0-fast`、`seedance2.0`、`kling3.0`、`kling3.0-omni`,默认 `seedance2.0-fast`;后端必须将 `seedance2.0-fast` 映射到 `doubao-seedance-2-0-fast-260128`,将 `seedance2.0` 映射到 `doubao-seedance-2-0-260128`,两者不得混用。后端允许 6 类比例、4 到 15 秒整数、`480p / 720p / 1080p`,并拒绝 `seedance2.0-fast + 1080p`;`sound=on/off` 映射 Ark `generate_audio=true/false`。后端复用 Ark / VectorEngine content generation task 轮询链路,下载最终视频并持久化到 OSS;请求携带 `projectId` / `assetFolderId` 时同步创建 project resource / 账号素材并返回 `project` / `asset` 快照,基础响应返回 `videoSrc`、尺寸、prompt、model、taskId、durationSeconds、resolution 和 `priceMudPoints`,不返回生成 provider。 -- `POST /api/editor/audios/sound-effects/generations` 与 `POST /api/editor/audios/background-music/generations`:按音效 / 背景音乐参数生成音频并持久化到 OSS;请求携带 `projectId` / `assetFolderId` 时同步创建 project resource / 账号素材并返回 `project` / `resource` / `asset` 快照,基础响应返回 `audioSrc`、prompt、model、taskId、duration、歌词和 `priceMudPoints`,不返回生成 provider。 +- `POST /api/editor/audios/sound-effects/generations` 与 `POST /api/editor/audios/background-music/generations`:按音效 / 背景音乐参数生成音频并持久化到 OSS;请求携带 `projectId` / `assetFolderId` 时同步创建 project resource / 账号素材并返回 `project` / `resource` / `asset` 快照,基础响应返回 `audioSrc`、`prompt`、`actualPrompt`、model、taskId、duration、歌词和 `priceMudPoints`,不返回生成 provider。BGM 成功响应必须同时填充 `prompt` 与 `actualPrompt`,两者都和输入框已经写回的 canonical `gpt_description_prompt` 逐 code point 等值;共享响应类型为兼容 SFX 与历史数据仍可保留 `actualPrompt` 可选,SFX 语义不变。 所有写接口都必须校验 Bearer 登录态和 owner;接口只返回当前用户有权读取的工程与资源。 diff --git a/docs/【实施记录】BGM生成提示词优化T6测试与发布门禁-2026-08-05.md b/docs/【实施记录】BGM生成提示词优化T6测试与发布门禁-2026-08-05.md new file mode 100644 index 000000000..d664b40e7 --- /dev/null +++ b/docs/【实施记录】BGM生成提示词优化T6测试与发布门禁-2026-08-05.md @@ -0,0 +1,152 @@ +# BGM 生成提示词优化 T6 测试与发布门禁实施记录 + +日期:`2026-08-05` + +状态:`实施完成并已提交、推送;本文保留实施时门禁记录` + +## 文档定位 + +本文承接 BGM 生成提示词优化 V1.0 的 T6 实施边界、实际测试覆盖和发布门禁记录。产品与技术规则以[画板音乐生成入口设计](./【编辑器】画板音乐生成入口设计-2026-06-18.md)为准;长期稳定边界同步记录在 `docs/project-memory/shared-memory/decision-log.md` 和 `pitfalls.md`。 + +本文不重新解释原始需求,不把测试便利转化为产品规则,也不作为生产部署授权。 + +## 实施基线与边界 + +- T0–T5 已完成并提交;T5 提交为 `3b69b4886`。 +- 用户已于 2026-08-05 完成人工全链路浏览器测试,并确认未发现阻断性问题。 +- 当前分支通过 merge commit `69426ee61` 包含 `origin/master@0cc257ce6`;实施结束时相对该 ref 为 `0 behind / 12 ahead`。 +- T6 只补测试、共用 fixture、文档和发布门禁。唯一生产代码调整是把既有 inline BGM 响应字段原样抽为同文件私有构造函数,供生产路径与测试共同调用。 + +明确未做: + +- 不修改 BGM / SFX UI、Prompt 业务规则、助手模板、字符或 body 上限、限流、埋点、价格、计费、队列、重试或持久化语义。 +- 不新增 Idempotency-Key、服务端 dedupe、持久提交锁或跨页面恢复机制。 +- 不修改 SpacetimeDB schema、migration、bindings 或表目录。 +- 不修改 External v1 路由、DTO、状态码、异步语义、OpenAPI 或 worker。 +- 不修改 `platform-llm` 原文日志策略。 +- 不挂载或清理历史 `vector_engine_audio_generation/tests.rs`。 +- 不执行生产部署,也不重复创建可能扣费的真实 BGM 任务。 + +## 实际实施 + +### 共用 canonicalization 向量 + +新增: + +```text +packages/shared/test-fixtures/background-music-prompt-canonicalization.json +``` + +共 10 个 case,由 TypeScript 与 Rust 共同消费,覆盖: + +- 完整 Unicode `White_Space` 边界和 U+0085; +- 内部 LF、CRLF 与空白保持; +- U+200B、U+FEFF 保持; +- 组合字符不做 NFC; +- ZWJ emoji 按 code point 计数; +- 标点、数字、ASCII 与 canonicalization 幂等性。 + +代表性 case `representative-complex` 的 canonical `charCount=16`、`effectiveCharCount=13`。同一 case 用于前端正式请求、成功结果 layer、client JSON body、站内 queue serializer、inline 响应和 Suno body 等值断言。 + +### 前端与 SFX 回归 + +只修改既有测试文件: + +- `ImageCanvasBackgroundMusicPromptModel.test.ts` +- `useImageCanvasGenerationSubmissionWorkflow.test.tsx` +- `ImageCanvasGenerationSubmissionModel.test.ts` +- `editorProjectClient.test.ts` + +新增或收紧的证据: + +- Prompt model 对全部共用 fixture 逐项断言 canonical Prompt、总字符数、有效字符数和幂等性。 +- BGM 正式请求与成功 layer 的 `prompt`、`actualPrompt`、唯一 `gpt_description_prompt` 均等于代表性 canonical Prompt。 +- 正式 BGM client JSON body 保持 canonical Prompt 原值。 +- SFX 全空白 Prompt 继续回退“游戏音效”,并保持 `audio1.0` 与默认 5 秒。 + +未修改前端生产实现。 + +### 后端与平台回归 + +修改: + +- `server-rs/crates/platform-audio/tests/vector_engine_audio.rs` +- `server-rs/crates/api-server/src/vector_engine_audio_generation/generation.rs` + +覆盖: + +- `platform-audio` 消费全部共用 fixture,并保留 BGM 长度、资格和 SFX 回归。 +- Suno body 继续精确包含 `mv`、`gpt_description_prompt`、`make_instrumental` 三个字段。 +- 已挂载 generation 模块覆盖 BGM 空字符串、全 Unicode 空白、1 / 200 / 201 code point。 +- 登录态 queue serializer 与 inline 完成响应保持 canonical Prompt 等值。 +- 已挂载 SFX 测试覆盖唯一 `audio1.0`、Prompt 规范化、1500 / 1501、2 / 10 / 1 / 11 秒和动态价格。 + +生产路径只新增同文件私有 `build_editor_background_music_generate_response` 抽取;字段、DTO、分支和业务语义不变。 + +## 验证结果 + +|门禁|结果| +|---|---| +|前端 11 个定向测试文件|`259/259`| +|shared-contracts BGM DTO|`1/1`| +|`platform-audio`|`29/29`| +|`platform-llm` 未完成原因与普通文本降级|两个定向测试均通过| +|`api-server background_music`|`36/36`| +|已挂载 generation 模块|`6/6`| +|`editor_sound_effect`|`2/2`,不再是空过滤器| +|External v1|`7/7`| +|External BGM|`2/2`| +|`cargo check -p api-server --all-targets`|通过| +|TypeScript typecheck|通过| +|变更文件 ESLint|通过| +|Rust 格式|通过| +|编码检查|5190 个文件通过| +|`git diff --check`|通过| + +Rust 输出只有既有 dead-code warning,没有新增失败。 + +## 运行态与人工门禁 + +本轮实际验证: + +|服务|地址|门禁|结果| +|---|---|---|---| +|SpacetimeDB|`http://127.0.0.1:3101`|`GET /v1/ping`|HTTP 200| +|BgFilter worker|`http://127.0.0.1:8083`|`GET /readyz`|HTTP 200| +|api-server|`http://127.0.0.1:8082`|`GET /healthz`|HTTP 200| + +API 安全响应摘要: + +```json +{"ok":true,"service":"genarrative-api-server"} +``` + +本轮启动的进程树已按归属清理,三个端口均已关闭。 + +人工门禁如实记录为:`2026-08-05,用户人工全链路浏览器测试完成,未发现阻断性问题`。T6 没有把该结论扩写为未提供的逐项观察数据,也没有重复创建可能扣费的正式任务。 + +## 发布判定与交接 + +当前工作树相对已核对的 `origin/master@0cc257ce6` 满足 T6 本地发布门禁: + +- TypeScript 与 Rust 共同消费同一 canonicalization fixture。 +- 前端、shared-contracts、Prompt 助手、generation、`platform-llm`、`platform-audio`、SFX 和 External v1 定向门禁通过。 +- 跨层等值证据使用同一代表性 Prompt,没有隐藏字段或二次改写。 +- API 健康检查与用户人工浏览器门禁通过。 +- diff 不包含原始需求文件、SpacetimeDB schema、External OpenAPI 或 `platform-llm` 日志策略修改。 + +本节记录门禁时,相关改动尚未提交;之后已以 `aeb5894b9` 提交并推送。后续 PR、四个 required jobs 和生产部署是独立动作,不由 T6 自动执行。 + +## BGM 助手模型试验 + +2026-08-05 起,补全和简化的请求级模型改为 `gpt-5.6-luna`;画布 Agent 其它调用继续使用 `gpt-5.4-mini`。该调整只作用于两个 BGM Prompt 助手路由,不改变共享编辑器 Agent 的默认模型。 + +- 本地 API mock 链路 `27/27` 通过,并确认请求 body 的 `model` 为 `gpt-5.6-luna`。 +- 真实 VectorEngine smoke 已尝试:`GET /v1/models` 与 `POST /v1/chat/completions` 均在建立 HTTP 连接前以 `fetch failed` 结束,没有返回状态码或模型响应;该结果只能说明当前环境网络不可达,不能判定 `gpt-5.6-luna` 被上游拒绝或支持。 +- 网络恢复后重试成功:补全和简化各发送一条最小 Chat Completions 请求,均返回 HTTP 200、`model=gpt-5.6-luna`、`finish_reason=stop`,并解析出完整四字段 JSON object;补全候选 `126` code points,简化候选 `79` code points。由此确认当前模型和两条助手请求形态可以跑通。 + +## 2026-08-06 组件架构后续修订 + +本文前述 T6 数字与文件范围是当时实施完成后的历史记录,不因后续组件归并而改写。新的权威方向是让 SFX 与 BGM 恢复使用 `ImageCanvasGenerationComposerView.tsx` 内同一个音频 composer,并由 `isSoundEffect` 分流;详见[音频生成 Composer 恢复共享分流方案](./project-memory/plans/【前端重构】音频生成面板恢复共享分流方案-2026-08-06.md)。 + +该修订已于 2026-08-06 实施:`ImageCanvasGenerationComposerView.tsx` 恢复唯一共享音频 composer 和 `isSoundEffect` 分流,独立完整 BGM composer 及其测试文件已删除,原用例全部迁入总 composer。Prompt / 预设 / controller / 总 composer `121/121`、surface 与 submission workflow `72/72` 通过,typecheck、变更文件 ESLint、Prettier、编码检查和差异检查通过。现有 SFX 的 Vidu、2–10 秒、默认 5 秒、Prompt 回退、1500 字、价格与提交路径未改;本轮未实现 SFX V2、未修改后端或需求原文,也未执行浏览器视觉测试或创建真实付费任务。 diff --git a/docs/【编辑器】画板音乐生成入口设计-2026-06-18.md b/docs/【编辑器】画板音乐生成入口设计-2026-06-18.md index 0572907cc..9800aa1c4 100644 --- a/docs/【编辑器】画板音乐生成入口设计-2026-06-18.md +++ b/docs/【编辑器】画板音乐生成入口设计-2026-06-18.md @@ -2,10 +2,16 @@ 日期:`2026-06-18` +更新时间:`2026-08-06` + ## 范围 本次只在 `/editor/canvas` 图片画布编辑器内新增底部 `生成音乐` 入口,用于生成完整游戏音效或游戏背景音乐。该入口属于画板生成类工具,不新增平台玩法入口、不进入作品发布链路,也不修改现有视觉小说音频生成开关。 +2026-08-04 起,本文增加 BGM Prompt 优化 V1.0 口径。该切片只修改 `audio-background-music`;`audio-sound-effect` 的 UI、Prompt 回退、Vidu `audio1.0` 请求、`2-10` 秒时长和 1500 字限制全部保持不变。音效与背景音乐使用同一个音频 composer,并由组件内的 `isSoundEffect = dialog.mode === 'audio-sound-effect'` 隔离行为;不得把 BGM 规则扩散到 SFX。 + +2026-08-06 的组件架构修订只撤销完整 BGM composer 的独立视图边界。共享音频 composer 的 SFX 分支保留当前 Vidu、时长、默认 Prompt、1500 字和价格行为,BGM 分支保留本文规定的预设、计数、AI 补全 / 简化、撤销、锁定、canonical Prompt 和 Suno 行为。BGM 纯状态模型、助手 controller 与预设跑马灯仍可保持独立职责;本轮不实现 SFX V2 的 ElevenLabs、中译英、自动时长、30 秒、Loop、一键优化或预设等独有能力。具体实施边界见[音频生成 Composer 恢复共享分流方案](./project-memory/plans/【前端重构】音频生成面板恢复共享分流方案-2026-08-06.md)。 + ## 入口与交互 1. 底部 AI 画布工具栏新增 `生成音乐`。 @@ -13,7 +19,7 @@ - `生成游戏音效` - `生成游戏背景音乐` 3. 选择某一项后创建独立 `generation-dialog` 画布生成对象,并通过现有 placement 模型避让已有图层和占位。 -4. 面板 UI 复用 `生成角色形象` 的紧凑结构:上方为字段区,底部为参数 / 模型 / 生成按钮区,不写规则说明类文案。音效参数按钮靠左下角,固定模型胶囊紧贴生成按钮;背景音乐同样在右下角显示固定模型胶囊并紧贴生成按钮。 +4. 面板 UI 复用 `生成角色形象` 的紧凑结构:上方为字段区,底部为参数 / 模型 / 生成按钮区,不写规则说明类文案。音效参数按钮靠左下角,固定模型胶囊紧贴生成按钮;背景音乐字段区增加字符计数、可展开预设词条、AI 补全、一键简化和单层撤销,底部仍保留现有动态泥点价格、固定 `Suno` 模型胶囊和生成按钮。 5. 生成中隐藏设置面板,只保留画布中的音频生成占位;失败后恢复面板并展示短错误。 ## 面板字段 @@ -27,10 +33,176 @@ ### 生成游戏背景音乐 -- `gpt_description_prompt`:用户输入的背景音乐提示词。 +- `gpt_description_prompt`:用户输入框当前文本按本文规则清理首尾 Unicode 空白后形成的背景音乐提示词,是正式 BGM 生成的唯一最终 Prompt。 - `make_instrumental`:固定传 `true`,不在 UI 中展示为可改字段。 - 提交到 VectorEngine 时映射为 Suno 纯音乐模式字段:`mv`、`gpt_description_prompt`、`make_instrumental: true`。`mv` 后端固定使用默认 Suno 模型,UI 以禁用态模型胶囊显示 `Suno`。 -- `gpt_description_prompt` 按 Apifox 契约限制 200 字,超出时由 BFF 返回参数错误。 +- 前端请求继续使用 `gptDescriptionPrompt`,Rust DTO 继续使用 `gpt_description_prompt`;不得因“唯一最终 Prompt”语义把请求字段改名为 `actualPrompt`。响应中的 `actualPrompt` / `actual_prompt` 继续保留既有生成审计语义。 +- `gpt_description_prompt` 按 Apifox 契约限制 200 个 Unicode code point。前端、BFF 和 `platform-audio` 允许且必须执行同一套首尾 Unicode 空白清理;除此之外不得执行 Unicode 规范化、内部空白折叠、内部换行转换、标点替换或静默截断。 + +## BGM Prompt 优化 V1.0 + +### 唯一可见最终 Prompt、首尾空白与字符口径 + +- 预设和 AI 助手都只修改同一个 BGM 输入框。“唯一最终 Prompt”约束的是语义来源和用户可见性:不得在正式请求中额外拼入用户看不到的前缀、后缀、预设元数据、分组信息、内部模板或系统提示词;它不要求保留编辑缓冲区中的首尾空白。这里的“用户不可见内容”指应用额外注入的语义文本,不指用户输入或粘贴的零宽字符等不可见 Unicode code point。 +- 从当前字符串两端删除属于 Unicode `White_Space` 属性的 code point 后得到的文本,以下称 canonical Prompt(规范化 Prompt);内部空格、内部换行和其它 code point 保持不变。 +- 用户编辑期间输入框可以暂时保留首尾空格、Tab 和换行。字符计数与动作可用状态基于 canonical Prompt 的非写入式预览计算,不得在每次键入时改写输入框;点击预设、AI 补全、一键简化、撤销或正式生成时,前端才在同步操作阶段计算 canonical Prompt 并写回输入框。 +- 正式生成时,写回后的输入框、前端 BFF 请求、队列请求载荷、Suno body、生成记录 `prompt`、生成记录 `actual_prompt` 和结果响应必须逐 code point 完全一致。 +- 首尾空白清理必须跨语言一致,不能直接混用语义不同的原生函数。TypeScript 按 `\p{White_Space}` 删除两端 code point;Rust 使用 `char::is_whitespace` 删除两端 code point。`U+200B` 和 `U+FEFF` 不属于 Unicode `White_Space`,不得被这一步顺带删除。 +- 除首尾 Unicode 空白清理外,不删除零宽字符,不执行 NFC 或其它 Unicode 规范化,不折叠内部空格,不转换或合并内部换行,不替换标点,不静默截断。 +- 总字符数按规范化后最终 Prompt 的 Unicode code point 数计算。TypeScript 使用 `Array.from(finalPrompt).length`,Rust 使用 `final_prompt.chars().count()`;被删除的首尾 Unicode 空白不计入 200 字。 +- “有效字符”只用于动作可用性和参数校验,定义为最终 Prompt 中的非 Unicode 空白 code point。标点、数字、字母和不可见但不属于 Unicode `White_Space` 的 code point 均计为有效字符;内部空格、内部换行和全部不可见 code point 仍计入 200 字总数。 + +| 规范化后的最终 Prompt | AI 补全 | 一键简化 | 正式生成 | +| --- | --- | --- | --- | +| 空字符串(总数 0、有效字符数 0) | 禁止 | 禁止 | 禁止 | +| 1 个有效字符且总数不超过 200 | 禁止 | 禁止 | 允许 | +| 至少 2 个有效字符且总数不超过 200 | 允许 | 禁止 | 允许 | +| 至少 1 个有效字符且总数为 201–2000 | 禁止 | 允许 | 禁止 | +| 总数超过 2000 | 禁止 | 禁止 | 禁止 | + +- canonical Prompt 满足不变量:`有效字符数 = 0` 当且仅当 `总字符数 = 0`。原始输入即使完全由 201 个或更多 Unicode `White_Space` 组成,canonicalization 后仍为空字符串,AI 补全、一键简化和正式生成全部禁止;不得按规范化前的长度把全空白输入归入“可简化”状态。 +- 一键简化最大输入固定为正式生成合法上限的 10 倍,即 `200 × 10 = 2000` 个 canonical Unicode code point。超过 2000 时必须完整保留当前文本供用户手动编辑,不得截断或丢失,但 AI 补全、一键简化和正式生成都不可用。 + +### 预设库与追加规则 + +预设分为三组,仅用于颜色和滚动组织;分组、ID、颜色等隐藏元数据不进入 Prompt、Suno 请求或生成记录。写入输入框的是下表右列完整可见文本。 + +| 分组 | 预设 | 写入输入框的文本 | +| --- | --- | --- | +| 用途 | 菜单待机 | 低干扰、适合菜单待机的背景音乐 | +| 用途 | 休闲消除 | 轻快可爱的休闲消除背景音乐 | +| 用途 | 解谜思考 | 安静专注的解谜思考背景音乐 | +| 用途 | 探索冒险 | 温和推进的探索冒险背景音乐 | +| 用途 | 对白场景 | 克制柔和、留出对白空间的背景音乐 | +| 用途 | 战斗前 | 蓄势待发的战斗前背景音乐 | +| 用途 | 胜利结算 | 明亮满足的胜利结算背景音乐 | +| 用途 | 失败结算 | 克制低落的失败结算背景音乐 | +| 用途 | 日常经营 | 轻松有序的日常经营背景音乐 | +| 用途 | 农场经营 | 自然温暖的农场经营背景音乐 | +| 用途 | 校园日常 | 青春轻松的校园日常背景音乐 | +| 用途 | 美食厨房 | 温暖活泼的美食厨房背景音乐 | +| 氛围 | 温暖治愈 | 柔和明亮的治愈背景音乐 | +| 氛围 | 神秘悬疑 | 克制神秘的悬疑背景音乐 | +| 氛围 | 紧张推进 | 稳定推进、逐渐紧张的背景音乐 | +| 场景 | 森林自然 | 清新自然的森林背景音乐 | +| 场景 | 雨夜静谧 | 雨夜静谧、略带神秘感的背景音乐 | +| 场景 | 海洋漂流 | 开阔舒缓的海洋漂流背景音乐 | +| 场景 | 山野远行 | 自由舒展的山野远行背景音乐 | +| 场景 | 糖果乐园 | 甜美活泼的糖果乐园背景音乐 | +| 场景 | 温馨小屋 | 温暖安静的温馨小屋背景音乐 | +| 场景 | 城市夜晚 | 克制迷人的城市夜晚背景音乐 | +| 场景 | 太空科幻 | 空灵未来感的太空科幻背景音乐 | +| 场景 | 赛博街区 | 冷静律动的赛博街区背景音乐 | +| 场景 | 古风幻想 | 空灵雅致的古风幻想背景音乐 | +| 场景 | 童话花园 | 梦幻轻盈的童话花园背景音乐 | +| 场景 | 海底遗迹 | 深邃神秘的海底遗迹背景音乐 | +| 场景 | 沙漠遗迹 | 苍茫神秘的沙漠遗迹背景音乐 | +| 场景 | 熔岩洞穴 | 炽热压迫的熔岩洞穴背景音乐 | +| 场景 | 奇妙博物馆 | 好奇灵动的奇妙博物馆背景音乐 | + +点击预设时: + +1. 先按统一规则删除输入框文本两端的 Unicode `White_Space` code point,并把结果写回输入框。 +2. 规范化后的字符串为空,直接写入该预设的完整可见文本。 +3. 规范化后的字符串非空,检查其最后一个 Unicode code point。 +4. 最后一个字符属于 Unicode 标点类别时,直接追加预设文本;否则先追加中文句号 `。`,再追加预设文本。 +5. 首尾空白在追加前已经删除;内部空格和内部换行保持原位,不得继续清理。写入输入框的规范化文本、句号和预设可见文本共同构成新的唯一最终 Prompt。 +6. 允许重复点击同一个预设,每次都按同一规则追加。 +7. 点击任意预设后清除旧撤销快照并隐藏撤销按钮。 +8. 追加后允许超过 200 字,完整文本必须保留;超限只复用现有通用 Prompt 过长状态,不增加预设专用警告。 + +预设滚动交互: + +- 展开后全部词条横向、连续、缓慢循环;收起后隐藏词条并停止可见滚动。 +- 桌面端左侧 15% 悬停区快速向左滚动,中间 70% 立即暂停并稳定展示至少 5 个词条,右侧 15% 快速向右滚动;鼠标移出后恢复默认慢速循环。 +- 左右箭头所在区域也必须触发对应方向的加速滚动,避免箭头可见但悬停无反馈。 +- 循环使用多份词条队列无缝衔接,末端不跳回、不留白,也不提供“换一批”。 +- 三组词条使用协调但可区分的颜色;底部细线标记当前悬停控制区。 +- 组件在触摸环境被渲染时支持横向滚动和箭头操作,不依赖 hover 才能选择预设;本需求不恢复移动端图片画布入口。 +- AI 处理或正式提交期间暂停滚动并禁用展开、收起、箭头和词条点击。 + +### AI 补全 + +- 点击 AI 补全时先按统一规则规范化输入框首尾空白并同步写回;至少 2 个有效字符且总字符数不超过 200 时,前端才把这份可见最终 Prompt 传给登录态内部 BFF。 +- Prompt 助手当前使用专用请求模型 `gpt-5.6-luna`,请求级 `reasoning_effort` 固定为 `medium`;画布 Agent 本身仍使用现有编辑器 Agent LLM 配置中的 `gpt-5.4-mini`。两者复用现有 `LlmClient`,不建立新的平台 LLM 能力。 +- 服务端模板必须要求:保留用户明确的主题、场景、风格、情绪、乐器、能量、韵律、时长、循环和避免项;按场景选择性补足场景、氛围、能量、韵律、乐器、旋律、声音设计、循环和避免项,不为凑全方向堆砌形容词。 +- 用户描述已足够完整时,只补充一至两个与主题匹配的具体声音细节。发现冲突时,优先级为“明确避免项和限制 > 明确玩法用途与场景 > 风格、情绪、能量与韵律 > AI 补充细节”。 +- 内部 envelope 的 `prompt` 字段只允许包含一条可直接写回输入框的中文 BGM Prompt;候选文本本身不得包含解释、标题、Markdown、JSON、代码块、具体艺人或歌曲模仿要求。 +- 补全与简化共用同一份内部结构化 envelope:`prompt: string`、`isDirectWritebackFormat: boolean`、`isContentComplete: boolean`、`hasObviousFragment: boolean`。助手显式使用现有 OpenAI Chat 协议,envelope 由完整 `response.text` 中唯一一个 JSON object 承载;服务端只允许 `serde_json` 对完整文本做全量解析,允许 JSON object 外围存在 JSON whitespace,但不接受代码块、前后解释、多个 JSON 值或从字符串中截取 object,也不执行自动修复。助手请求不发送 function tools,不做运行时双协议 fallback;响应出现 tool call 时同样视为结构非法。envelope 只用于服务端解析和判断,不属于候选文本,不写回输入框,也不通过 BFF 暴露给客户端。 +- 服务端必须在解析 envelope、canonicalize 候选或提取任何 `prompt` 前检查 OpenAI Chat `finish_reason`。去除外围空白并忽略 ASCII 大小写后,`length` 和 `content_filter` 都属于未完成响应;即使正文恰好是合法四字段 JSON,也不得接受、解析或提取候选。`finish_reason` 缺失、为空或为未知自定义值时,不因该字段单独拒绝,继续执行其余结构与候选门禁;不得改为只允许 `stop`。 +- 补全遇到 `finish_reason = length` 或 `content_filter` 时均直接失败,不写回、不泄漏响应正文或候选,且仍只执行一个业务语义轮。 +- 补全候选先执行同一套首尾 Unicode 空白规范化,再由程序计算字符数。候选只有在四字段结构有效、canonical `prompt` 包含有效字符且不超过 200 个 Unicode code point,并且三个布尔字段依次为 `true / true / false` 时才通过;程序不使用关键词或未定义正则猜测候选格式、完整性或残句。通过后整段写回输入框并成为唯一最终 Prompt。 +- 补全只执行一个业务语义轮。`LlmClient` 在该轮内部按现有配置执行的 transport retry 不计为新增业务语义轮;该轮最终发生 transport、超时或上游失败时直接返回失败,不再发起内容修复轮。 +- 调用失败、结果为空、结构或判断不合格、格式非法或超限时保留请求前已经写回的规范化 Prompt,不写回部分结果;错误响应不得包含未通过候选或内部 envelope。 +- 补全过程不调用 Suno、不创建正式生成任务、不触发正式音乐生成扣费。 + +### 一键简化 + +- 点击一键简化时先按统一规则规范化输入框首尾空白并同步写回;只在规范化后的当前 Prompt 至少含 1 个有效字符且总字符数为 201–2000 时允许调用。超过 2000 时完整保留文本,但不得调用简化 BFF。点击前保存这份规范化后的完整 Prompt,AI 处理中保持输入框不变。 +- 服务端在本次简化中冻结 `originalPrompt` 为入站 canonical Prompt,最多两个业务语义轮期间始终不变,作为内容保真参照。第一次业务语义轮使用 `currentPrompt = originalPrompt`,目标为 180 字。这里的 `originalPrompt` / `currentPrompt` 是服务端组装 LLM 简化模板时的内部变量;客户端简化 BFF DTO 仍只提交一个 `currentPrompt`,该入站值经 canonicalization 后同时成为内部 `originalPrompt` 和第一次内部 `currentPrompt`。每个业务语义轮内部由 `LlmClient` 按现有配置执行的 transport retry 不增加业务语义轮数。 +- 180 不是硬门槛。简化使用与补全相同的四字段内部结构化 envelope;envelope 不属于候选文本,也不得写回输入框或通过 BFF 暴露。响应不能解析为包含上述正确字段类型的对象时,本次候选不通过。 +- 候选“格式合法”专指 `isDirectWritebackFormat = true`:模型确认 canonical 候选只包含一条可直接写回输入框的中文 BGM Prompt,不包含解释、标题、Markdown、JSON、代码块、字数报告、处理过程或删改说明。它与“内部结构可解析”是两个独立校验项;程序不得另用关键词或未定义的正则推断标题、解释等语义格式。 +- 第一次业务语义轮成功取得上游响应、但候选未通过任一通过条件时,自动进行唯一一个第二业务语义轮,目标为 170 字。只有当完整 `response.text` 已按上述规则全量解析为单个 JSON object 时,才允许从该 object 读取字符串 `prompt`;禁止从未完整解析的文本、代码块、解释或畸形 JSON 中做子串提取。若可提取的字符串 `prompt` canonicalize 后包含有效字符,第二次使用 `currentPrompt = canonicalize(第一次候选)`,即使第一次因其它字段缺失、超限、格式、完整性或残句判断而不通过;若完整响应无法解析为单个 object、object 中没有字符串 `prompt`,或候选 canonicalize 后不含有效字符,第二次回退使用 `currentPrompt = originalPrompt`。第二次业务语义轮中的 `originalPrompt` 始终仍是最初入站 canonical Prompt。 +- 第一轮 `finish_reason = length` 时整份响应不可信,不得接受或提取其中的 `prompt`;它只按“成功取得响应但候选不合格”进入唯一的 170 字轮,第二轮必须使用 `currentPrompt = originalPrompt`。第一轮 `finish_reason = content_filter` 时直接失败,不进入第二轮。第二轮出现 `length` 或 `content_filter` 时最终失败,不得发起第三轮。该规则优先于上一条的一般候选提取与回退规则。 +- 第一次业务语义轮最终发生 transport、超时或上游失败时直接返回失败,不进入 170 字内容修复轮;这类失败只应用该轮内部既有的 `LlmClient` transport retry。 +- 每次候选都先执行同一套首尾 Unicode 空白规范化,再计算实际字符数。第二次仍不合格时返回失败并保留请求前已经写回的规范化 Prompt,提示用户手动精简;错误响应不得包含第一次或第二次未通过候选、可提取的 `prompt` 或内部 envelope。最多两个业务语义轮,任何情况下都不得由程序截断到 200 字。 +- 简化模板必须优先保留明确限制、场景与用途、情绪与风格、核心乐器与速度、能量/韵律/旋律、循环结构、区分度较高的声音设计,再删除次要修饰细节;不得改变专有名词、BPM、调性、时长、数值、乐器和明确限制。 +- 程序不抽取关键词,不对规范化后的输入与候选执行内容硬编码逐项比对。是否为直接写回格式、是否保留重要信息且内容完整、是否形成明显残句由 LLM 在同一次调用的三个布尔字段中分别判断。 +- BFF 成功时只返回已通过校验的 Prompt 和程序计算的字符数,不暴露三个内部判断字段;失败时使用现有 API 错误 envelope,且不返回任何候选或内部判断字段。 +- 候选只有同时满足以下条件才通过:内部结构可解析且四个字段类型正确;canonical 候选包含有效字符;canonical 候选实际字符数不超过 200;`isDirectWritebackFormat = true`;`isContentComplete = true`;`hasObviousFragment = false`。程序负责解析和检查字段类型、canonicalization、有效字符与实际字符数,并执行三个布尔判断结果;字符数由程序计算且不采信模型报告,程序不自行推断三个语义判断。 +- 一键简化不维护或恢复“已选预设”元数据;预设已写入输入框的文本只是当前最终 Prompt 的一部分。 + +### 单层撤销 + +- AI 补全和一键简化成功都必须产生一层 Prompt 快照。发起 AI 操作前,先把当时输入规范化并写回,再以该 canonical Prompt 建立本次临时快照。 +- AI 成功写回后,该快照成为可撤销版本;AI 失败时输入框保留请求前已经写回的 canonical Prompt,清除本次临时快照,不生成新的可撤销版本,也不恢复发起本次操作时已经被替换的旧快照。 +- 用户手动编辑 AI 结果后,撤销仍可用。再次发起 AI 操作时,先规范化并写回当时的当前 Prompt,再以该 canonical Prompt 替换旧快照。 +- 点击预设时先规范化并写回,再清除旧快照;正式提交被后端拒绝时,保留已经写回的 canonical Prompt 和提交前已有快照。 +- 点击撤销时,先把当前输入规范化并写回,再让该 canonical Prompt 与 canonical 快照互换;按钮继续可用,允许用户在两个规范化版本间来回切换。 +- 只保存一层,快照只包含 canonical Prompt,不包含预设滚动位置、展开状态、模型、时长或其它参数。 + +撤销按钮的可见性与可用性: + +- 可见条件为存在可撤销快照或本次 AI 操作的临时快照;启用条件为可见且当前 BGM 面板未处于 `completing`、`simplifying`、`submitting` 或现有 `generating` 锁定状态。发起 AI 操作时可撤销快照被本次临时快照取代,因此“处理中没有可撤销快照”不等于“没有快照”,不得据此隐藏按钮。 +- AI 处理期间按钮显示并禁用,不得隐藏。禁用必须使用真实禁用态并保留按钮在 DOM 与可访问树中的位置,不得用隐藏、透明度或其它视觉伪装代替,也不得在处理前后改变按钮占位。 +- 完全没有快照时隐藏,不显示灰色占位按钮。 + +| 状态 | 可撤销快照 | 本次临时快照 | 撤销按钮 | +| --- | --- | --- | --- | +| 初始进入面板、没有历史快照 | 无 | 无 | 隐藏 | +| AI 开始处理,首次或再次均相同 | 无 | 有 | 显示并禁用 | +| AI 处理成功并写回输入框 | 有 | 无 | 显示并启用 | +| AI 处理失败、原文未改变 | 无 | 无 | 隐藏 | +| 用户手动编辑 AI 结果 | 有 | 无 | 显示并启用 | +| 点击预设词条 | 无 | 无 | 隐藏 | +| 点击生成提交且已有快照 | 有 | 无 | 显示并禁用 | +| 点击生成提交且没有快照 | 无 | 无 | 隐藏 | +| 提交成功或失败后解除锁定 | 有或无 | 无 | 有快照时显示并启用,否则隐藏 | +| 点击撤销 | 有,与当前文本互换 | 无 | 保持显示并启用 | + +- 再次发起 AI 操作时仍按上面的规则用当时的 canonical Prompt 替换旧快照;不得为了让处理期间按钮可见而保留旧快照,那会与“AI 失败不恢复已被替换的旧快照”冲突,并让失败后的撤销指向不相关内容。 +- 视图只按上表决定显示与启用;是否真正执行撤销仍由状态层在非 `idle` 或无快照时拒绝,两处不得各写一套判定。 + +### AI 操作与方案 A 提交锁 + +当前 BGM generation dialog 的 Prompt 助手状态为: + +```text +idle +├─ completing +├─ simplifying +└─ submitting +``` + +- `completing` / `simplifying` 开始时以同步 operation ID 取得当前 dialog 操作权;同一操作的后续双击无效。新操作使旧 operation ID 失效,关闭 dialog 时中止请求,dialog ID 或 operation ID 不匹配的迟到响应必须丢弃。 +- AI 处理中输入框只读,禁用预设展开/收起、预设词条、滚动箭头、AI 补全、一键简化、撤销和生成。这里的“禁用撤销”指显示并禁用,可见性按“单层撤销”一节的矩阵判定,不得隐藏。 +- 点击生成的同步事件内必须在任何 `await` 前依次完成:立即把当前 BGM dialog 切换到 `submitting`、计算 canonical Prompt 并写回输入框、按 canonical Prompt 校验 0 个有效字符和 200 字上限、冻结本次请求值。前端校验失败时立即解除锁并保留写回后的 canonical Prompt,不发送请求。提交中锁定该 dialog 的输入框、预设、展开/收起、滚动箭头、AI 操作、撤销、参数控件和生成按钮,忽略后续重复点击。 +- 锁只作用于当前 BGM dialog,不锁整个画布、其它 generation dialog、画布拖动、缩放、图层操作或其它编辑能力。 +- 后端拒绝正式提交时,解除锁并保留已经写回的 canonical Prompt 与提交前已有撤销快照,继续使用现有正式生成错误展示。原 dialog 仍是当前面板时恢复面板;用户已归档或切走时只记录失败并保持关闭,不自动激活旧面板。 +- 后端接受请求并创建正式生成任务后,`submitting` 结束;原账号、项目和 dialog 仍匹配时进入现有 `queued/generating` 占位并隐藏输入 composer。若 dialog 已删除或账号 / 项目已切换,正式任务继续,但旧回调不得按同名 dialog ID 写入新 scope。 +- 正式任务接受后的异步回调按副作用作用域分别校验:钱包刷新只校验原账号仍是当前账号;任务列表通知同时校验原账号和原项目仍是当前账号与项目;dialog、canvas、asset 和 layer 写回继续校验账号、项目、scope version 与原 BGM dialog。删除 dialog 不得阻止同账号钱包刷新或同账号同项目任务列表通知,切换账号不得让旧任务触发新账号页面回调。 +- “提交成功”仅表示后端已接受请求并创建正式任务,不表示 Suno 已完成音乐生成。 +- 上述“接受后切占位”以现役默认 queue 模式的任务创建响应为观察点;兼容 inline 模式没有中间接受响应,沿用现有 HTTP 终态响应作为客户端可观察结算点,不为此新增协议。 ## 画布数据 @@ -51,10 +223,10 @@ - generated 私有音频资源播放前必须通过 `/api/assets/read-url` 换签;画布卡片不得直接把 `/generated-*` 或 generated OSS 私有地址交给 `