diff --git a/.codex/skills/genarrative-external-editor-api/references/api-operations.md b/.codex/skills/genarrative-external-editor-api/references/api-operations.md index e773d8cf3..7d72793d1 100644 --- a/.codex/skills/genarrative-external-editor-api/references/api-operations.md +++ b/.codex/skills/genarrative-external-editor-api/references/api-operations.md @@ -55,7 +55,7 @@ Every generation row requires a stable `Idempotency-Key` header and returns HTTP | UI asset extraction | `/api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` | | Character animation | `/api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `assetFolderId`, `assetLabel`, `canvasCompletion` | | Video generation | `/api/external/v1/editor/videos/generations` | `prompt`, `model`, `aspectRatio`, `durationSeconds`, `resolution`, `mode`, `sound` | `referenceImageSrcs`, `referenceVideoSrcs`, `referenceAudioSrcs`, `webSearchEnabled`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` | -| Sound effect | `/api/external/v1/editor/audios/sound-effects/generations` | `prompt`, `duration` | `model`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` | +| Sound effect | `/api/external/v1/editor/audios/sound-effects/generations` | `prompt` | `model`, `duration`, `loop`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` | | Background music | `/api/external/v1/editor/audios/background-music/generations` | `gptDescriptionPrompt`, `makeInstrumental` | `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` | Poll all eight through: @@ -103,6 +103,7 @@ Use OpenAPI as the final authority; these common values are a routing aid: - Video `aspectRatio`: `16:9`, `9:16`, `1:1`, `4:3`, `3:4`, `21:9`. - Video `resolution`: `480p`, `720p`, `1080p`; `mode`: `std`; `sound`: `on` or `off`. - Character animation uses `model: "seedance2.0-fast"`; `resolution`: `480p` or `720p`; `frameCount`: `32`, `40`, or `48`; `durationSeconds`: `4`, `5`, or `6`; `ratio`: `same`, `1:1`, `4:3`, `16:9`, `9:16`, or `3:4`. +- Sound effect uses canonical model `eleven_text_to_sound_v2`; omit `duration` or send `null` for automatic duration, otherwise send a finite `0.5-30` number. `loop` defaults to `false` and remains independent from Prompt text. - UI extraction uses `aspectRatio: "1:1"`; use `imageSize: "1K"` for normal/small extraction and `2K` for dense designs. Do not hard-code this list as a replacement client schema. In particular, the top-level image `style` field is intentionally extensible; see `requests-and-outputs.md` for its fallback behavior. diff --git a/.codex/skills/genarrative-external-editor-api/references/requests-and-outputs.md b/.codex/skills/genarrative-external-editor-api/references/requests-and-outputs.md index 1de4c8ed6..2a2bf4abf 100644 --- a/.codex/skills/genarrative-external-editor-api/references/requests-and-outputs.md +++ b/.codex/skills/genarrative-external-editor-api/references/requests-and-outputs.md @@ -195,6 +195,7 @@ Do not guess dimensions or pass a temporary signed read URL. See `authentication The completed `result` may contain stable artifact fields such as: - `objectKey`, media type, dimensions, or task ID. +- Sound-effect `durationSeconds` is the probed MP3 duration and `loop` is the frozen request boolean; neither is inferred from Prompt text. - `resource`, `resourceId`, or equivalent canvas reference. - `asset`, `assetId`, or equivalent library reference. - `spritesheetResource`, `spritesheetAsset`, and stable spritesheet metadata. diff --git a/.codex/skills/genarrative-external-editor-api/scripts/genarrative_external_api.py b/.codex/skills/genarrative-external-editor-api/scripts/genarrative_external_api.py index bce78e20e..8cdb21e49 100644 --- a/.codex/skills/genarrative-external-editor-api/scripts/genarrative_external_api.py +++ b/.codex/skills/genarrative-external-editor-api/scripts/genarrative_external_api.py @@ -649,13 +649,19 @@ class GenarrativeExternalClient: idempotency_key=idempotency_key, ) - def generate_sound_effect(self, prompt: str, duration: int, **fields: Any) -> Any: + def generate_sound_effect( + self, + prompt: str, + duration: float | None = None, + loop: bool = False, + **fields: Any, + ) -> Any: self._apply_canvas_session_fields(fields, prompt, 360, 120) prompt = self._apply_art_spec(fields, prompt) idempotency_key = fields.pop("idempotencyKey", None) return self.submit_and_wait_generation( "/api/external/v1/editor/audios/sound-effects/generations", - {"prompt": prompt, "duration": duration, **fields}, + {"prompt": prompt, "duration": duration, "loop": loop, **fields}, idempotency_key=idempotency_key, ) diff --git a/.env.example b/.env.example index 58b202e3d..85a1c3c81 100644 --- a/.env.example +++ b/.env.example @@ -130,6 +130,11 @@ VECTOR_ENGINE_BASE_URL="https://api.vectorengine.cn" VECTOR_ENGINE_API_KEY="" VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS="1000000" +# ElevenLabs editor sound-effect generation is server-side only. +ELEVENLABS_BASE_URL="https://api.elevenlabs.io" +ELEVENLABS_API_KEY="" +ELEVENLABS_REQUEST_TIMEOUT_MS="180000" + # 阿里云 OSS 配置。 # Rust `server-rs` 的 `api-server` 会优先从 `.env` / `.env.local` 读取这些变量, # 用于签发浏览器 PostObject 直传票据,并保持 `/generated-*` 旧路径习惯。 diff --git a/deploy/env/api-server.env.example b/deploy/env/api-server.env.example index 0bcee9522..88f48ed31 100644 --- a/deploy/env/api-server.env.example +++ b/deploy/env/api-server.env.example @@ -86,6 +86,9 @@ VECTOR_ENGINE_BASE_URL=https://api.vectorengine.cn VECTOR_ENGINE_API_KEY= VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=1000000 VECTOR_ENGINE_AUDIO_REQUEST_TIMEOUT_MS=180000 +ELEVENLABS_BASE_URL=https://api.elevenlabs.io +ELEVENLABS_API_KEY= +ELEVENLABS_REQUEST_TIMEOUT_MS=180000 HYPER3D_BASE_URL=https://api.hyper3d.com/api/v2 HYPER3D_API_KEY= diff --git a/docs/README.md b/docs/README.md index 04f98b1d8..e571dde87 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,6 +21,8 @@ - [图片画布编辑器前端拆分计划](./technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md) - [图片画布游戏场景生成链路](./technical/【技术方案】图片画布游戏场景生成链路-2026-08-04.md) - [画板音乐生成入口设计](./【编辑器】画板音乐生成入口设计-2026-06-18.md) +- [SFX 生成优化 V2.0 任务拆解](./project-memory/plans/【实施计划】SFX生成优化V2.0任务拆解-2026-08-06.md) +- [SFX 生成优化 V2.0 T6 测试与发布门禁](./【实施记录】SFX生成优化V2.0T6测试与发布门禁-2026-08-07.md) - [音频生成 Composer 恢复共享分流方案](./project-memory/plans/【前端重构】音频生成面板恢复共享分流方案-2026-08-06.md) - [BGM 提示词优化 T6 测试与发布门禁](./【实施记录】BGM生成提示词优化T6测试与发布门禁-2026-08-05.md) - [画布 Agent 对话面板](./【编辑器】画布Agent对话面板-2026-07-03.md) diff --git a/docs/openapi/genarrative-external-v1.openapi.json b/docs/openapi/genarrative-external-v1.openapi.json index c2d0d6205..74a73eba0 100644 --- a/docs/openapi/genarrative-external-v1.openapi.json +++ b/docs/openapi/genarrative-external-v1.openapi.json @@ -4561,25 +4561,41 @@ "EditorSoundEffectGenerationRequest": { "type": "object", "required": [ - "prompt", - "duration" + "prompt" ], "properties": { "prompt": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 2048, + "description": "用户原始语言音效描述。服务端按 ECMAScript String.trim() 语义删除首尾空白和行终止符(包括 U+FEFF,保留首尾 U+0085),再按 Unicode code point 校验 1-2048。" }, "model": { "type": [ "string", "null" ], - "default": "audio1.0" + "default": "eleven_text_to_sound_v2", + "description": "省略、null、空串、纯 Unicode White_Space 或首尾空白包围的 eleven_text_to_sound_v2 均 canonicalize 为 eleven_text_to_sound_v2;audio1.0 和其它非空值返回 400。" }, "duration": { - "type": "integer", - "minimum": 2, - "maximum": 10 + "anyOf": [ + { + "type": "number", + "minimum": 0.5, + "maximum": 30 + }, + { + "type": "null" + } + ], + "default": null, + "description": "null 或省略表示自动时长;有限数值表示手动时长。服务端不按 UI 0.1 秒步进取整。" + }, + "loop": { + "type": "boolean", + "default": false, + "description": "独立 Loop 参数;服务端不从 Prompt 推断、同步或校验。" }, "projectId": { "type": [ @@ -4741,6 +4757,22 @@ "background-music" ] }, + "durationSeconds": { + "type": [ + "number", + "null" + ], + "exclusiveMinimum": 0, + "maximum": 600, + "description": "SFX V2 为 MP3 探测所得实际时长;不是请求时长。BGM 或历史结果可省略。" + }, + "loop": { + "type": [ + "boolean", + "null" + ], + "description": "SFX V2 返回冻结并发送给 provider 的 Loop;BGM 或历史结果可省略。" + }, "project": { "anyOf": [ { @@ -4786,6 +4818,146 @@ } } }, + "ExternalEditorGenerationCompactResourceReference": { + "type": "object", + "required": [ + "resourceId" + ], + "properties": { + "resourceId": { + "type": "string", + "minLength": 1 + }, + "projectId": { + "type": "string", + "minLength": 1 + }, + "objectKey": { + "type": "string", + "minLength": 1 + }, + "assetObjectId": { + "type": "string", + "minLength": 1 + }, + "sourceResourceId": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": true, + "description": "完成态 compact result 中的稳定画布资源引用;不是完整 resource 快照。" + }, + "ExternalEditorGenerationCompactAssetReference": { + "type": "object", + "required": [ + "assetId" + ], + "properties": { + "assetId": { + "type": "string", + "minLength": 1 + }, + "folderId": { + "type": "string", + "minLength": 1 + }, + "objectKey": { + "type": "string", + "minLength": 1 + }, + "assetObjectId": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": true, + "description": "完成态 compact result 中的稳定素材库引用;不是完整 asset 快照。" + }, + "ExternalEditorSoundEffectCompactResult": { + "type": "object", + "required": [ + "audioKind" + ], + "properties": { + "audioKind": { + "type": "string", + "const": "sound-effect" + }, + "durationSeconds": { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 600, + "description": "SFX V2 保存 MP3 后探测得到的实际时长;不是请求时长。历史完成结果可缺少此字段。" + }, + "loop": { + "type": "boolean", + "description": "SFX V2 冻结并发送给 provider 的 Loop;历史完成结果可缺少此字段。" + }, + "taskId": { + "type": "string", + "minLength": 1 + }, + "objectKey": { + "type": "string", + "minLength": 1, + "description": "持久化音频对象的稳定引用;需要临时下载或预览 URL 时使用 assets/read-url。" + }, + "assetObjectId": { + "type": "string", + "minLength": 1 + }, + "resource": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExternalEditorGenerationCompactResourceReference" + }, + { + "type": "null" + } + ] + }, + "asset": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExternalEditorGenerationCompactAssetReference" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": true, + "description": "External SFX 完成态的 compact result。仅包含稳定产物引用和可安全消费的 SFX 元数据;不包含完整 project/canvas/asset 快照或脱敏的 Prompt、模型与 provider 字段。" + }, + "ExternalEditorGenerationGenericCompactResult": { + "type": "object", + "not": { + "type": "object", + "required": [ + "audioKind" + ], + "properties": { + "audioKind": { + "const": "sound-effect" + } + } + }, + "additionalProperties": true, + "description": "其它生成类型或历史结果的兼容 compact result。背景音乐(audioKind=background-music)与不带 audioKind 的图片、视频、图标序列帧、角色动作和 UI 拆解结果都落在这里。该 fallback 明确排除 audioKind=sound-effect,避免吞掉 SFX 专用分支。" + }, + "ExternalEditorGenerationCompletedResult": { + "oneOf": [ + { + "$ref": "#/components/schemas/ExternalEditorSoundEffectCompactResult" + }, + { + "$ref": "#/components/schemas/ExternalEditorGenerationGenericCompactResult" + } + ], + "description": "External v1 轮询 completed 状态的命名 compact result 联合。两个分支已由 audioKind 的 const 与 not 精确互斥,不使用 discriminator:背景音乐返回 audioKind=background-music,图片、视频、图标序列帧、角色动作、UI 拆解与历史结果根本没有 audioKind,它们都无法映射到具名分支,且 fallback 分支不可能把 audioKind 声明为必填,因此任何 discriminator 映射都不可能覆盖全部合法结果。" + }, "ExternalEditorGenerationSubmissionResponse": { "type": "object", "required": [ @@ -4870,9 +5042,8 @@ "type": "string" }, "result": { - "type": "object", - "description": "completed 时返回的 compact 稳定结果引用;不包含完整 project/canvas、Data URL、Blob URL 或临时签名 URL。", - "additionalProperties": true + "$ref": "#/components/schemas/ExternalEditorGenerationCompletedResult", + "description": "仅在 completed 时返回的 compact 稳定结果引用;queued、running 和 failed 不返回该字段。为兼容历史完成结果,此字段不作为无版本迁移的必填约束。" }, "pollAfterMs": { "type": "integer", diff --git a/docs/project-memory/plans/【实施计划】SFX生成优化V2.0任务拆解-2026-08-06.md b/docs/project-memory/plans/【实施计划】SFX生成优化V2.0任务拆解-2026-08-06.md new file mode 100644 index 000000000..c435a35e6 --- /dev/null +++ b/docs/project-memory/plans/【实施计划】SFX生成优化V2.0任务拆解-2026-08-06.md @@ -0,0 +1,259 @@ +# SFX 生成优化 V2.0 任务拆解 + +日期:`2026-08-06` + +状态:`T1–T6 工程实施已完成;生产配置确认、旧 Vidu 队列 drain、灰度和实际发布仍须按门禁人工执行` + +开发分支:`feat/sound_opt` + +合并基线:`origin/master@281c84b7bf2d` + +权威设计:[`docs/【编辑器】画板音乐生成入口设计-2026-06-18.md`](../../【编辑器】画板音乐生成入口设计-2026-06-18.md) + +决策入口:[`docs/project-memory/shared-memory/decision-log.md`](../shared-memory/decision-log.md) + +> 本文是通过 Git 共享的脱敏实施计划。代码、OpenAPI、测试和运行配置只实现权威设计中冻结的最终口径,不依赖任何未进入仓库的本地资料。 + +## 文档可见性边界 + +- `local-docs/` 只供当前机器本地使用,由本机 Git exclude 排除,不进入仓库;其他开发者通过 Git 无法看到、读取或核验其中任何文件。 +- 除本节用于声明隔离边界外,仓库中的 tracked 文档不得链接、引用、摘录或把 `local-docs/` 中的文件作为来源、证据或前置阅读材料;代码、OpenAPI、测试、配置和提交信息也不得依赖其内容。 +- 所有参与实现、审查、测试和发布所需的规则与证据,必须自包含地写入 tracked 权威设计、决策日志或本共享计划。团队成员不需要、也不应被要求访问本地资料才能开工或验收。 + +## T0 退出条件 + +T0 只冻结设计、决策、任务归属、迁移 / 回滚门禁和安全记录;不要求当前 Vidu V1 代码、OpenAPI 或实际 API 在 T0 与 SFX V2 设计一致。实现差距在 T1–T5 收敛,T6 验收。 + +| T0 条件 | 状态 | 证据 / 剩余动作 | +| --- | --- | --- | +| 权威 SFX V2 设计已进入 tracked `docs/` | 已完成 | 画板音乐生成入口设计的 SFX V2 章节 | +| T1–T6 计划、基线、测试和迁移门禁已通过 Git 共享 | 已完成 | 本文 | +| External v1 `model` 完整矩阵已冻结 | 已完成 | 本文“请求与幂等口径” | +| 英文化具有 LLM 语义判断和程序 Script 门禁 | 已完成 | 本文“Prompt 与 LLM 口径” | +| 一键优化与翻译的 completion tokens 总预算和 `length` 行为已冻结 | 已完成 | 两类请求均为 `2048 × 4 = 8192`,预算包含 reasoning 与可见输出,见本文“Prompt 与 LLM 口径” | +| 旧 Vidu 队列 drain、发布顺序和回滚门禁已冻结 | 已完成 | 本文“发布与回滚” | +| 凭据安全边界已明确 | 已完成 | 秘密值只允许由服务端私密配置注入,不进入 Git、文档、日志或 fixture;凭据轮换不作为本次 T0 仓库门禁 | +| T0 放行状态已确认 | 已完成 | `2026-08-06` 项目负责人明确确认 T0 通过,可以进入 T1 | + +T0 已通过,T1–T5 可以按本文依赖顺序进入实现;T0 通过不表示功能已上线。 + +## 安全边界与 T0 放行记录 + +该记录只保存日期、责任人 / 工单标识和布尔结论,禁止写入账号、密码、Key、Token、Cookie 或任何可恢复凭据的值。 + +| 记录 | 日期 | 责任人 / 工单 | 结论 | +| --- | --- | --- | --- | +| 凭据安全边界 | `2026-08-06` | 项目负责人确认 | 不在仓库记录秘密值;凭据轮换不作为本次 T0 仓库门禁 | +| 本地资料隔离 | `2026-08-06` | 本机 Git exclude | 已确认仅本机可见、未被 Git 跟踪且不作为团队证据源 | +| T0 放行 | `2026-08-06` | 项目负责人确认 | 已通过,可以进入 T1 | + +## 目标和非目标 + +### 目标 + +- 在 `/editor/canvas` 现有 `audio-sound-effect` 分支把新 SFX 任务从 Vidu `audio1.0` 切换为 ElevenLabs `eleven_text_to_sound_v2`。 +- 复用共享音频 composer、现有生成队列、计费、OSS、资源、素材库和画布完成态。 +- 增加 52 个预设、一键优化、单层交换撤销、Worker 内统一英文化、自动 / 手动时长和 Loop。 +- 稳定保存 `prompt = userPrompt`、`actual_prompt = actualPrompt`、实际时长、Loop、模型、provider 和平台 Task ID。 +- 同批演进站内 DTO、External v1 OpenAPI、幂等语义、定价配置、部署配置和测试。 + +### 非目标 + +- 不修改 BGM Suno、BGM Prompt 助手、BGM 预设、提交锁或定价行为。 +- 不新建 SFX 独立页面、平行 composer 或第二套音频业务真相。 +- 不新建平行编辑器音频 DTO、正式生成 handler、BFF 或 `/api/editor/audios/*/generations` 路由;原地演进 `server-rs/crates/shared-contracts/src/assets.rs` 与 `server-rs/crates/api-server/src/vector_engine_audio_generation/generation.rs` 的现有正式链路。 +- 不开放 Prompt Influence UI;服务端固定 `0.3`。 +- 不为新 SFX 任务提供 Vidu fallback,不删除其它未迁移调用方仍使用的 Vidu 通用能力。 +- 不新增 SpacetimeDB 表或列,不向 External v1 暴露 Prompt 优化或翻译助手。 +- 不执行未授权的真实付费生成。 + +## 当前 V1 差距与任务归属 + +| 领域 | 基线状态 | 收敛任务 | +| --- | --- | --- | +| SFX Prompt | `trim()`、空值回退“游戏音效”、1500 上限 | T1 实现 Unicode canonicalization、2048 上限和无默认回退 | +| SFX UI | textarea、Vidu 胶囊、2–10 整数时长 | T4 增加 52 预设、优化 / 撤销、自动 / 手动时长和 Loop | +| SFX DTO | `prompt + model + duration: u8` | T1 / T5 演进固定模型、nullable 小数时长、Loop 和响应字段 | +| Prompt 语义 | `prompt == actual_prompt` | T2 / T5 分离 userPrompt 和 actualPrompt | +| provider | Vidu submit + poll + URL download | T3 增加 ElevenLabs 同步二进制 adapter,T5 接线 | +| 实际时长 | 请求时长同时作为结果时长 | T3 探测 MP3,T5 写回实际值 | +| Loop | 不存在 | T1 契约、T4 UI、T5 持久化与详情 | +| External v1 | nullable model 默认旧 `audio1.0`,duration 为 2–10 integer | T1 类型基础,T5 同批修改 Rust / OpenAPI / 幂等与结果 | +| 定价 | 旧模型键 5 泥点 | T5 增加新模型键并保持 5 泥点 / 次 | +| 详情 | 通用 Prompt / Model / 时长 / Task | T5 增加中英 Prompt、Loop 和历史 Vidu 分支 | + +## 冻结产品与技术口径 + +### Prompt 与 LLM 口径 + +- `userPrompt` 和 `actualPrompt` 上限均为 2048 Unicode code points。 +- 只按 ECMAScript `String.trim()` 删除首尾空白和行终止符,包含 `U+FEFF`、保留首尾 `U+0085`;不做 NFC、内部空白折叠、换行转换、标点替换或静默截断。 +- 一键优化固定 `gpt-5.6-luna`、`reasoning_effort = medium`;Worker 翻译固定同模型、`reasoning_effort = low`。两类请求分别按各自 2048 Unicode code point 候选上限的 4 倍,固定 completion tokens 总预算 `8192`;该预算由隐藏 reasoning tokens 与可见 JSON 输出 tokens 共享,不包含输入 Prompt tokens,不是可见正文保证,也不按实际输入长度缩小。当前 VectorEngine OpenAI Chat wire 固定发送 `max_completion_tokens = 8192`;内部历史字段名 `max_output_tokens` 不是业务语义。两者均不发送 temperature 或 function tools。 +- 翻译 envelope 固定 `prompt / isEnglish / isFaithfulTranslation / isDirectGenerationFormat / hasAddedOrRemovedRequirement`,只接受完整 `response.text` 中的唯一 JSON object。 +- `isEnglish = true` 作为 LLM 语义判断,程序侧另外要求:候选至少含一个 Script=Latin 的 alphabetic code point,且所有 alphabetic code point 的 Script 均为 Latin;Common / Inherited 数字、标点、空白和符号允许。 +- 日文假名、韩文、西里尔、希腊、阿拉伯等非 Latin alphabetic Script 候选失败。测试必须覆盖中文、英文、中英混合输入,以及 actualPrompt 2048 / 2049 边界。 +- 首轮成功响应但候选不合格或 `finish_reason = length` 时,使用同一 userPrompt 唯一重试;首轮 `content_filter` 和 transport 最终失败不开启第二业务语义轮。 + +### 请求与幂等口径 + +- 自动时长默认开启,并预置最近手动值 `5s`;手动范围 `0.5-30s`、UI 步进 `0.1s`。自动模式发送 null 并保留最近手动值。 +- Loop 默认 false,是独立 API 参数;系统不根据 Prompt 推断、同步或校验 Loop。 +- provider body 固定 `text / model_id / duration_seconds / loop / prompt_influence=0.3`,query 固定 `output_format=mp3_44100_128`。 +- provider POST 不 retry,浏览器正式 POST 不 unsafe retry,队列 `max_attempts = 1`,一个平台 job 最多一次 ElevenLabs POST。 + +External v1 `model` 先删除首尾 Unicode `White_Space`,再按大小写敏感矩阵 canonicalize: + +| 输入 | 结果 | canonical queue payload | +| --- | --- | --- | +| omitted / `null` / 空串 / 纯空白 | 接受 | `eleven_text_to_sound_v2` | +| 首尾空白包围的新模型 | 接受 | `eleven_text_to_sound_v2` | +| `eleven_text_to_sound_v2` | 接受 | `eleven_text_to_sound_v2` | +| `audio1.0` | `400 BAD_REQUEST` | 不入队 | +| 其它未知非空值 | `400 BAD_REQUEST` | 不入队 | + +所有接受形态在定价、预扣和 enqueue 前收敛为同一个 model 字段,不得产生不同幂等 payload。拒绝形态必须证明零入队、零预扣、零 LLM 和零 provider。 + +### 结果、计费与数据 + +- 服务端重建 SFX V2 `generation_inputs_json`,不信任客户端的 actualPrompt、实际时长、model 或 Loop。 +- 成功响应的 MP3 必须按现有 `MAX_GENERATED_AUDIO_BYTES = 40 MiB` 有界读取并验证,探测实际时长且只以独立技术异常上限 `600s` 拒绝过长结果;不把请求最大 `30s` 当作响应上限。平台 taskId 使用 operation / queue job ID,不伪造 provider task ID。 +- 新模型按次保持 5 泥点,以后端入队时冻结价格为真相。 +- 不修改 SpacetimeDB schema,复用 `prompt / actual_prompt / generation_inputs_json` 和画布 layout。 + +## 任务包 + +### T0:权威设计、共享计划、安全记录与迁移口径 + +- 仅修改 tracked 文档,不实现功能代码。 +- 完成权威设计、本共享计划、决策日志、对 V1 差距的 T1–T5 归属、drain / 发布 / 回滚门禁。 +- T0 放行记录必须真实且脱敏,不得为凭据轮换、责任人或工单虚构证据。 + +### T1:Prompt 规则、52 预设、共享契约与 metadata 基础 + +- 实现前后端 ECMAScript `String.trim()` 等值 canonicalization、code point 计数、2048 边界和无默认 Prompt 回退。 +- 增加 40 + 12 预设纯模型,锁定数量、ID、分类和可见文案。 +- 原地演进现有 TypeScript / Rust 音频 DTO:fixed model、nullable 小数 duration、Loop、实际时长和 V2 metadata;不得新增同义 DTO 或平行正式生成契约。 +- 实现 External `model` canonicalizer 的纯函数与矩阵测试;实际 OpenAPI / handler 接线属于 T5。 + +实施记录(`2026-08-06`):T1 已完成。前后端共享 canonicalization fixture 已锁定 Unicode 边界与 `2048 / 2049` 行为;52 个预设、最小优化 DTO、固定模型、nullable duration、Loop 默认值、响应结果字段和强类型 V2 metadata 已落地。duration 纯校验接受自动 `null` 与手动 `0.5–30s`,拒绝非有限值和越界值。External `model` 当前只落地纯 canonicalizer 与输入矩阵测试,正式定价、预扣、enqueue、OpenAPI 和副作用测试仍严格归属 T5;在 T5 完成前不得发布当前中间态。 + +补充验收(`2026-08-06`):音频 compact 结果保留完整 DTO 必填的 `provider`,SFX / BGM 均通过真实 compact → Agent reconcile 回归;正式 SFX 提交流程不再执行原生 `trim()` 或默认 Prompt 回退。Rust V2 metadata 只能经校验构造并拒绝错误版本、模型、时长组合与实际时长;共享 fixture 直接锁定 `1 / 2048 / 2049`,52 个预设 ID 和非 `0.1s` 步进小数时长均有固定断言。 + +规则修订(`2026-08-07`):SFX Prompt 边界 canonicalization 改为 ECMAScript `String.trim()`;本条覆盖上段“不得执行原生 `trim()`”的旧口径。TypeScript 直接调用 `String.trim()`,Rust 以等值边界字符集合实现;首尾 `U+FEFF` 删除、首尾 `U+0085` 保留,BGM Prompt 与 External `model` 的 Unicode `White_Space` 规则不变。 + +### T2:一键优化 BFF 和 Worker 翻译 service + +- 增加登录态 SFX Prompt 优化 BFF,固定 Luna + Medium + completion tokens 总预算 `8192`,32 KiB body limit,严格唯一 JSON envelope,不调用音频 provider 或正式计费。 +- 增加仅 Worker 可调用的 Luna + Low 翻译 service,每次业务尝试固定 completion tokens 总预算 `8192`,严格 `isEnglish` + Unicode Script 门禁、保真判断和最多一次业务重试。 +- 测试覆盖中文 / 英文 / 中英混合输入,日文 / 韩文 / 西里尔等非 Latin 字母候选,actualPrompt 2048 / 2049,请求体精确 token 上限,以及优化直接拒绝 `length`、翻译首轮 `length` 重试一次 / 第二轮 `length` 最终失败和 `content_filter / transport` 行为。 + +实施记录(`2026-08-06`,`2026-08-07` 同步 master token 契约):T2 已完成。登录态 `POST /api/editor/audios/sound-effects/prompts/optimizations` 已按 `32 KiB` body limit、Luna + Medium + OpenAI Chat + completion tokens 总预算 `8192` 接入,并注册 User-scope tracking;当前 VectorEngine Chat wire 只发送 `max_completion_tokens=8192`,不发送 `max_tokens` 或 `max_output_tokens`。优化候选只接受完整唯一五字段 JSON object,拒绝 tool call、未完成响应、非 Han、生成参数内容、代码块、解释、额外 / 重复字段和超限结果,错误响应不暴露候选或内部 envelope。现有音频生成模块内已增加不注册 HTTP 路由的 Worker 翻译 service,固定 Luna + Low + completion tokens 总预算 `8192`,使用同一 Chat wire 字段,严格执行保真 / 直接生成格式 / Latin Script 门禁,首轮内容不合格或 `length` 只以原始 `userPrompt` 重试一次,`content_filter` 和 transport 最终失败不进入第二业务语义轮;typed failure 只暴露 `translation_invalid / translation_upstream_failed` 安全分类。T2 只交付可供 T5 调用的内部 service,尚未改变当前 Vidu 正式生成、队列、计费、持久化、External v1 或 OpenAPI,不是可发布切点。 + +### T3:ElevenLabs adapter、配置、二进制与时长探测 + +- 在 `platform-audio` 增加独立 ElevenLabs settings、endpoint normalizer、request builder 和 direct binary client,不伪装 Vidu / Suno poll task。 +- 固定 model、influence、format、header 与 query;按 `40 MiB` 做 Content-Length 预检和 `limit + 1` 流式读取,执行 MIME / MP3 验证和纯 Rust duration probe,并以 `600s` 作为独立技术异常时长上限。 +- 配置增加 `ELEVENLABS_BASE_URL / ELEVENLABS_API_KEY / ELEVENLABS_REQUEST_TIMEOUT_MS`,Key 只在服务端。 +- 测试断言 429 / 5xx / timeout / 读取失败都只有一次 provider POST,不执行真实付费请求。 + +实施记录(`2026-08-07`):T3 已完成。`platform-audio` 已增加独立 ElevenLabs 直接二进制 adapter,固定 endpoint、header、query、model、influence、nullable 小数时长和 Loop;专用 HTTP client 禁止重定向且没有 retry。成功响应先做 `40 MiB` Content-Length 预检,再以 `limit + 1` 有界读取,严格执行 MIME / 真实 MP3 门禁,并以纯 Rust MP3 probe 取得有限正实际时长和独立 `600s` 上限;请求格式 `mp3_44100_128` 不扩展为返回码率硬校验。配置、环境模板和 fail-closed settings guard 已落地,持久化准备已把 provider / file stem 从轮询任务枚举中最小解耦;正式 handler、Worker、计费、OSS 写回、External v1 和 OpenAPI 均未接线,继续归属 T5。 + +### T4:SFX 前端 controller、预设与参数 UI + +- 新增 SFX Prompt 纯模型、预设纯模型和 dialog-scoped controller;抽取音频预设跑马灯内核,BGM / SFX 保留各自 wrapper。 +- 在共享 composer 的 SFX 分支增加计数、52 预设、一键优化、单层交换撤销、自动 / 手动时长、Loop 和 ElevenLabs 胶囊。 +- 在第一个 await 前取得 AI / 提交 operation,只锁当前 SFX dialog;迟到响应和 scope 切换不写新面板。 +- T4 不切换 provider,不是可发布切点;与 T5 同一发布列车。 + +实施记录(`2026-08-07`):T4 已完成。前端增加独立于 BGM 的 dialog-scoped SFX Prompt 状态模型与 controller,优化和提交都在第一个 `await` 前同步取得 operation;账号、项目、dialog、mode 和 `AbortController` 共同隔离迟到响应。优化成功形成一层 canonical Prompt 交换快照,失败清除本次临时快照且不恢复更早快照,预设写入清快照。现有 BGM 跑马灯已抽出无业务语义的音频内核,BGM / SFX 各保留 wrapper;SFX wrapper 展示 T1 冻结的 `40 + 12` 预设。 + +共享音频 composer 的 SFX 分支现已展示 `0 / 2048` 计数、一键优化、单层撤销、自动 / 手动时长、`0.5-30s` 且 `0.1s` 步进的 slider、Loop、固定 `ElevenLabs` 胶囊和新模型前端 `5` 泥点兜底。dialog layout 保存并恢复 `soundDurationMode / soundDurationSeconds / soundLoop`;历史 Vidu dialog 和改造入口统一打开 SFX V2 模型面板。同步提交 claim 冻结 canonical Prompt、时长模式、最近手动值和 Loop,只锁当前 dialog,并在 scope 失效后拒绝旧 UI 写回。 + +T4 没有修改 Worker、provider 调用、正式请求的 nullable duration / Loop 映射、服务端动态定价、计费、OSS、持久化详情、External v1、OpenAPI 或 SpacetimeDB schema;这些继续严格归属 T5。T4 单独合入仍不是可发布切点,也未执行真实 LLM、ElevenLabs 或付费生成。 + +### T5:正式提交、Worker、计费、持久化、详情和 External v1 + +- 前端提交冻结 canonical Prompt、duration 和 Loop,正式 POST 不 unsafe retry。 +- 原地演进 `server-rs/crates/api-server/src/vector_engine_audio_generation/generation.rs` 的现有 handler,在定价 / 预扣 / enqueue 前 canonicalize External model,入队 payload 不含提前翻译的 actualPrompt;保留现有路由注册、queue / inline 分流和计费边界。 +- Worker 执行翻译、单次 ElevenLabs、MP3 时长探测、OSS 和权威 metadata / 画布写回;任一阶段失败进入现有退款链路。 +- 实现新模型定价键、历史 Vidu 只读 / 重绘兼容、中英 Prompt + Loop 详情和真实时长。 +- 同批更新 External v1 Rust DTO / handler / OpenAPI / Idempotency-Key 重放 / compact result;任一字段不一致时 T5 不完成。 + +实施记录(`2026-08-07`):T5 已完成。站内与 External SFX 请求在定价、预扣和 enqueue 前统一 canonicalize 为固定模型、canonical userPrompt、nullable 小数时长与 Loop;正式浏览器 POST 不再配置 unsafe retry,queue payload 不包含提前翻译的 actualPrompt。Worker 在既有冻结计费上下文内执行 Luna 英文化、单次 ElevenLabs POST、MP3 校验与实际时长探测、OSS、项目资源 / 账号素材 / 画布完成态写回,并使用 queue job ID 或 inline 预生成的平台 ID 作为 Task ID。服务端重建 `generation_inputs_json`,客户端自报的实际英文 Prompt、实际时长、模型与 Loop 不进入权威 metadata。 + +新定价键 `eleven_text_to_sound_v2` 已加入默认 JSON、api-server 与 SpacetimeDB 值校验,旧 `audio1.0` 键继续保留;历史 SpacetimeDB 定价快照仅缺新键时由受控本地定价补齐读取,下一次后台保存写回完整矩阵,不修改 schema。详情展示中英 Prompt、实际时长、Loop、生成模型与完整平台 Task ID;SFX V2 重绘恢复 userPrompt、duration mode / requested duration 和 Loop,自动时长不会把实际输出时长误作下一次手动值。 + +External v1 Rust handler、共享 DTO、OpenAPI、compact result 与 Agent Skill 已同步 nullable `0.5-30` 时长、Loop、固定模型和实际 `durationSeconds`;接受的 model 形态生成同一 canonical queue payload,旧 / 未知模型在 enqueue 前返回 `400`。External compact 继续隐藏 provider 与 Prompt,只保留稳定资源引用、实际时长和 Loop。T5 定向 Rust、TypeScript、External/OpenAPI、Agent、定价与 SpacetimeDB WASM build 已通过,未执行真实 LLM、ElevenLabs 或其它付费请求;完整失败矩阵、端到端与发布 smoke 继续归属 T6。 + +### T6:测试、文档、灰度和发布门禁 + +- 汇总 T1–T5 分层测试,增加 mock LLM + mock ElevenLabs + mock OSS 失败矩阵、端到端等值、刷新 / 重绘、计费退款、无重试、External 幂等和 BGM 回归。 +- 更新后端架构、前端专题、开发运维和共享项目记忆。 +- 执行定向 TypeScript / Rust / OpenAPI、`npm run typecheck`、`npm run check:encoding`、`git diff --check`、`npm run check:spacetime-schema`、`npm run dev:api-server` + `/healthz`。 +- 不将 mock 测试写成真实 provider 验收,不执行未授权付费生成。 + +实施记录(`2026-08-07`):T6 已完成工程侧测试缝、组合失败矩阵、跨入口补齐、稳定失败分类和发布 runbook。正式 SFX Worker 现由同一编排函数串联计费、翻译、ElevenLabs、OSS、asset object / bind 候选和原子资源 / 素材 / 画布 / job 提交;生产 adapter 继续调用原实现,测试 adapter 覆盖自动 / 手动时长 × Loop、余额不足零外部副作用、翻译 / provider / MP3 / OSS / asset candidate / 原子项目资源 / 账号素材 / 画布写回失败、一次退款和单 job 最多一次 provider POST。ElevenLabs HTTP / 无效音频 / 时长探测分别稳定归类为 `elevenlabs_http_failed / invalid_audio / duration_probe_failed`,OSS 与后续写回归类为 `oss_failed / writeback_failed`;普通用户继续只看到稳定短文案。 + +T6 盘点发现并修复画布 Agent 遗留的 Vidu 参数边界:`generate-sound-effect` 现与站内和 External v1 共用 canonical Prompt、固定模型、`duration = null | 0.5-30` 和 `loop`,显式 `duration: null` 不再被通用 null-default 兼容层错误恢复为手动 `5s`。完整验证和生产门禁记录见 [`docs/【实施记录】SFX生成优化V2.0T6测试与发布门禁-2026-08-07.md`](../../【实施记录】SFX生成优化V2.0T6测试与发布门禁-2026-08-07.md)。本阶段没有调用真实 LLM / ElevenLabs、没有执行付费生成、没有连接生产 SpacetimeDB,也没有执行发布;因此“工程 T6 完成”不等于“生产门禁已放行”。 + +## 依赖和发布列车 + +```text +T0 -> T1 +T1 -> T2 + T3 + T4 +T2 + T3 + T4 -> T5 +T5 -> T6 +``` + +- T2 / T3 可在 T1 契约稳定后并行;T4 可与两者后半程并行。 +- T4 与 T5 之间不存在可发布切点。 +- 不修改 SpacetimeDB schema;如实际实现发现必须修改,立即停止并按 schema 迁移规则重新评审,不得带入本计划默认实施。 + +## 测试门禁 + +| 层级 | 必要覆盖 | +| --- | --- | +| canonical | 空 / 全 ECMAScript trim 字符(含 U+FEFF)、首尾 U+0085 保留、U+200B、内部 U+FEFF、换行、组合字符、ZWJ emoji、2048 / 2049 | +| 预设 | 40 + 12、ID / label 唯一、文案等值、逗号追加、重复、清快照、超限保文 | +| 优化 | Luna + Medium + completion tokens 总预算 `8192`,Chat wire 只含 `max_completion_tokens=8192`,唯一 JSON、布尔门禁、length 直接失败、content_filter、无 tool call、无候选泄漏、dialog / scope 迟到响应 | +| 翻译 | 每轮 Luna + Low + completion tokens 总预算 `8192`,Chat wire 只含 `max_completion_tokens=8192`,中文 / 英文 / 中英混合输入,日文 / 韩文 / 西里尔候选,isEnglish + Script 门禁,2048 / 2049,首轮 length 唯一重试、第二轮 length 最终失败且 provider 0 次 | +| 跨入口 / External model | 登录态、External v1、画布 Agent 共用 canonical SFX queue payload;omitted / null / 空串 / 纯空白 / 包围空白新模型 / 显式新模型共用幂等 payload;`audio1.0` / 未知值为 400 + 零副作用 | +| ElevenLabs | auto / manual × Loop false / true,固定 model / influence / format,Key 不泄漏,网络 / HTTP / body 失败均只有一次 POST | +| 二进制与时长 | `40 MiB` 接受 / `40 MiB + 1 byte` 拒绝,Content-Length / chunked 超限、空 / HTML / JSON / 损坏 MP3、允许与 fallback MIME;有限正时长、30.5 / 60 / 600s 接受,>600s / NaN / 无穷拒绝 | +| 持久化 | prompt / actual_prompt / model / provider / task / actual duration / Loop 权威等值,客户端伪造值失效 | +| 计费 | 余额不足零 LLM / provider;翻译 / provider / MP3 / OSS / DB 失败一次退款 | +| 回归 | BGM Suno、助手、预设、锁和定价不变;其它 Vidu 调用方仍可编译和测试 | + +## 发布与回滚 + +### 发布前 + +- 不打印值地确认生产 `ELEVENLABS_BASE_URL / ELEVENLABS_API_KEY / ELEVENLABS_REQUEST_TIMEOUT_MS` 均已配置。 +- 确认定价 override 包含 `eleven_text_to_sound_v2` 且价格已批准。 +- 只读查询 `external_generation_job` 中 `job_kind = 'editor_sound_effect_generation'` 且 `status IN ('pending', 'running')` 的旧 Vidu payload。非零时先 drain,不得让新 Worker 按 V2 nullable duration / Loop payload 解析旧任务;命令必须显式指定 `--server` / `--server-url`。 +- 先部署 api-server / worker,再部署 web;两者之间使用维护窗或暂时关闭 SFX 提交入口。 +- External v1 变更提前通知调用方并完成 contract smoke。 + +### 观测 + +- 区分 `translation_invalid / translation_upstream_failed / elevenlabs_http_failed / invalid_audio / duration_probe_failed / oss_failed / writeback_failed`。 +- 只记录 operation ID、阶段、HTTP status、耗时、响应字节数和实际时长;不记录 Key 或完整 provider 错误正文。 +- 对账 job 完成数、退款数、ElevenLabs 调用数和完成资源数,识别重复调用和孤儿资源。 + +### 回滚 + +- 回滚时不自动切回 Vidu;先停止新 SFX 入队。 +- 等待或人工收口 V2 queued / running job,避免旧 Worker 无法解析 V2 payload。 +- 协同回滚 web、api-server、worker 和 External v1 文档,禁止只回滚一层。 +- 新生成的 ElevenLabs 素材继续按通用 audio / model / generation inputs 只读展示,不做数据迁移回滚。 +- 没有 SpacetimeDB schema 变更,回滚不执行表迁移或字段删除。 + +## 完成定义 + +- T0 已通过,T1–T5 按依赖顺序实现并分别完成测试门禁。 +- T1–T5 完成各自分层测试,T6 完成全部发布门禁。 +- 新编辑器 SFX 不调用 Vidu,历史 Vidu 数据仍可读和按新模型重绘。 +- 翻译最终失败时 ElevenLabs 调用为 0;成功 job 最多一次 provider POST。 +- MP3 经过有界读取、验证和实际时长探测,权威 metadata 跨队列、OSS、素材、画布、响应和刷新一致。 +- External v1 Rust、OpenAPI、幂等 payload、副作用和最终响应逐字段一致。 +- 配置、日志、fixture、差异和提交不包含真实账号、Key、Token、Cookie 或其它凭据值。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index a5618b756..623089891 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -6770,6 +6770,60 @@ - 非目标:本次只规划视图归并,不实现 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 独有功能。 +## 2026-08-06 SFX 生成优化 V2.0 T0 设计与迁移口径 + +- 权威入口:SFX V2 的可编码规则已完整融合到 `docs/【编辑器】画板音乐生成入口设计-2026-06-18.md`。实现、审查、测试和发布只以该 tracked 权威设计、本条决策和共享实施计划为依据,不依赖团队通过 Git 无法取得的本地资料。 +- 共享视图边界不变:`audio-sound-effect` 与 `audio-background-music` 继续共用 `ImageCanvasAudioGenerationComposerView`,通过 `isSoundEffect` 分流;SFX 和 BGM 的 Prompt 模型、controller、预设 wrapper、锁和提交契约分别维护,不新建独立页面或第二套音频系统。 +- 后端入口边界:正式生成原地演进 `server-rs/crates/shared-contracts/src/assets.rs` 的现有音频 DTO 与 `server-rs/crates/api-server/src/vector_engine_audio_generation/generation.rs` 的现有 handler,保留既有 `/api/editor/audios/*/generations` 路由、queue / inline 分流、计费和队列边界;不得新建平行 DTO、正式生成 BFF、handler 或第二套路由。只有 SFX Prompt 优化内部路由、Worker 翻译 service 和 ElevenLabs adapter 是新增能力。 +- provider 迁移:新编辑器 SFX 任务固定使用 ElevenLabs `eleven_text_to_sound_v2`,不提供模型选择、Vidu fallback 或 `audio1.0` alias。Vidu builder / 轮询仅保留给历史素材和其它未迁移调用方;历史素材可读,重绘新任务使用 ElevenLabs。 +- Prompt 真相:`prompt` 表示用户可见且确认的 canonical `userPrompt`,`actual_prompt` 表示 Worker 严格验收后实际提交给 ElevenLabs 的英文 `actualPrompt`。两者均以 2048 Unicode code points 为上限,只删除首尾 Unicode `White_Space`,不做其它规范化、默认 Prompt 回退或静默截断。 +- 可见交互:SFX 固定 40 个事件预设 + 12 个补充要求。空 Prompt 直接写入;非空 Prompt 末尾已是 Unicode 标点时直接追加,否则使用中文逗号 `,` 分隔。允许重复,不保留选中态,不去重或截断;点击预设清除旧快照。 +- Prompt 助手:一键优化使用 `gpt-5.6-luna`、`reasoning_effort = medium`;Worker 正式英文化使用同模型、`reasoning_effort = low`。两类候选上限均为 2048 Unicode code points,因此分别固定 completion tokens 总预算 `2048 × 4 = 8192`;该预算由隐藏 reasoning tokens 与可见输出 tokens 共享,不包含输入 Prompt tokens,也不是可见正文保证。当前 VectorEngine OpenAI Chat wire 固定发送 `max_completion_tokens = 8192`,内部历史字段名 `max_output_tokens` 不是业务语义。预算不按实际输入长度动态缩小;两者均不发送 temperature 或 function tools,只接受完整 `response.text` 中唯一 JSON object 的严格 envelope。翻译除要求 `isEnglish = true` 外,程序侧还要求候选至少含一个 Latin alphabetic code point,且所有 alphabetic code point 都属于 Latin Script;日文假名、韩文、西里尔、希腊和阿拉伯等非 Latin 字母均失败。优化只有一个业务语义轮,`finish_reason = length` 直接失败;翻译首轮成功响应但候选不合格或 `length` 时使用同一 `userPrompt`、同一 `8192` 上限唯一重试,第二轮 `length` 最终失败,`content_filter` 和 transport 最终失败不开启第二业务语义轮。翻译最终失败时 ElevenLabs 请求数必须为 0。 +- 撤销与锁:一键优化成功产生一层 canonical Prompt 交换快照;优化失败清除本次临时快照,不恢复更早快照。AI 操作和正式提交使用 dialog ID、账号 + 项目 scope、同步 operation ID 与 `AbortController`;只锁当前 SFX dialog。正式提交在第一个 `await` 前冻结 Prompt / duration / Loop;API 接受后结束 `submitting`、进入现有 `queued/generating` 占位,不把接受任务写成生成已完成。 +- 时长与 Loop:首次打开默认自动时长模式,预置最近手动值 `5s`、Loop false;手动范围 `0.5-30s`,UI 步进 `0.1s`。自动模式发送 `duration_seconds = null`,禁用 slider 但保留最近手动值。Loop 是独立 API 布尔参数;系统不根据 Prompt 推断、同步或校验 Loop,Prompt 文本与 Loop 开关不建立业务一致性门禁。 +- ElevenLabs 契约:`POST /v1/sound-generation`,body 固定 `text / model_id / duration_seconds / loop / prompt_influence=0.3`,query 固定 `output_format=mp3_44100_128`,`xi-api-key` 只在服务端 header 注入。provider POST 不自动重试,浏览器正式 POST 不 unsafe retry,队列 `max_attempts = 1`,一个平台 job 最多一次 ElevenLabs POST。成功响应复用现有 `MAX_GENERATED_AUDIO_BYTES = 40 MiB` 做 Content-Length 预检和 `limit + 1` 流式读取,验证 MIME 与真实 MP3,并探测有限正实际时长;实际时长仅受独立技术异常上限 `600s` 约束,不与请求最大 `30s` 比较,`30.5s-600s` 的合法结果可接受。平台使用 operation / queue job ID 作为 `taskId`,不伪造 provider task ID。 +- 结果真相:服务端重建 SFX V2 `generation_inputs_json`,写入 `userPrompt / actualPrompt / model / durationMode / requestedDurationSeconds / actualDurationSeconds / loop`;实际英文 Prompt、实际时长、model 和 Loop 不信任客户端自报。SFX V2 完成响应的 `durationSeconds` 使用同一 MP3 探测值;本条作为后出的 SFX 专项决策,仅在该完成响应上覆盖 2026-07-28“音频生成响应不得新增 `durationSeconds`”的通用口径,不新增资源 / 素材正式时长列,也不把该值写入 `EditorAsset`、`CanvasLayer`、layout 或图片序列字段。信息弹窗展示用户 Prompt、实际英文 Prompt、模型、实际时长、Loop 和平台 Task ID;历史 Vidu 数据不误标英文 Prompt。 +- 计费、External v1 与 schema:新模型键 `eleven_text_to_sound_v2` 保持 5 泥点 / 次,后端入队时冻结价格为真相。External v1 的 duration 演进为可选 / nullable `0.5-30 number`,Loop 缺省 false;`model` 的 omitted / null / 空串 / 纯空白 / 首尾空白包围的新模型 / 显式新模型统一 canonicalize 为 `eleven_text_to_sound_v2`,并产生相同幂等 payload。显式旧 `audio1.0` 和未知非空值返回 `400 BAD_REQUEST`,且必须为零入队、零预扣、零 LLM、零 provider;Rust、OpenAPI、幂等重放与最终响应必须在 T5 同批变更。本次不修改 SpacetimeDB schema,复用现有 `prompt`、`actual_prompt`、`generation_inputs_json` 和画布 layout。 +- 共享计划:脱敏 T1–T6 任务、当前基线、测试矩阵、旧 Vidu 队列 drain、发布与回滚门禁记录在 `docs/project-memory/plans/【实施计划】SFX生成优化V2.0任务拆解-2026-08-06.md`。T0 只验收设计、决策、共享计划、差距归属和安全记录,不要求当前 Vidu V1 代码、OpenAPI 或实际 API 已与 SFX V2 设计一致;实现差距归入 T1–T5,T6 统一验收。 +- 安全与状态:机器本地未跟踪资料不得进入提交,也不得被仓库文档链接、引用或作为团队证据源;ElevenLabs Key 只允许从服务端私密环境配置读取,不进入浏览器、日志、fixture、共享文档或 Git。T0 已通过,可以进入 T1–T5 实现;后续仍不得把秘密值、个人本地资料或不可审计记录写入仓库。 + +## 2026-08-06 SFX 生成优化 V2.0 T2 助手与 Worker 翻译 service + +- 一键优化:新增登录态内部路由 `POST /api/editor/audios/sound-effects/prompts/optimizations`,复用 T1 的最小请求 / 响应 DTO、现有编辑器 `LlmClient`、标准成功 / 错误 envelope 和 route tracking。请求固定 Luna、OpenAI Chat、Medium、completion tokens 总预算 `8192`,当前 VectorEngine Chat wire 只发送 `max_completion_tokens=8192`,不发送 temperature 或 function tools;路由独立使用 `32 KiB` body limit,不增加功能级限流器,也不进入 External v1。 +- 优化验收:内部五字段 envelope 必须从完整 `response.text` 直接反序列化,允许外围 JSON whitespace,拒绝代码块、前后解释、多个 JSON、额外 / 重复字段、错误类型、tool call 和未完成 finish reason。候选只删除首尾 Unicode `White_Space`,要求 `1-2048` code points、至少一个 Han code point,并严格执行 `true / true / false / false`;失败 HTTP 响应不携带候选、内部 envelope 或上游回显正文。 +- 翻译 service:在现有 `vector_engine_audio_generation` 内新增仅 crate 内部生成流水线可见、没有同步 HTTP 路由的翻译 service。每个业务语义轮固定 Luna、OpenAI Chat、Low、completion tokens 总预算 `8192`,当前 VectorEngine Chat wire 只发送 `max_completion_tokens=8192`,不发送 temperature 或 tools;候选严格执行五字段结构、`true / true / true / false`、`1-2048` code points、至少一个 Latin alphabetic code point,且所有 alphabetic code point 都属于 Latin Script。Han、假名、韩文、西里尔、希腊和阿拉伯字母均失败,Common / Inherited 数字、标点、空白和符号允许。 +- 轮次与错误:首轮成功返回但结构、判断、Script、长度或 `finish_reason=length` 不合格时,只以同一 canonical `userPrompt` 开启唯一第二业务语义轮;不得读取或传递首轮候选。首轮 `content_filter` 和 transport / timeout / 上游最终失败直接结束;`LlmClient` 内部 transport retry 仍属于当前业务轮。第二轮任何失败均最终失败。service 错误展示只提供 `translation_invalid / translation_upstream_failed` 分类和安全中文消息,不保存或输出未通过候选。 +- 阶段边界:T2 没有调用 ElevenLabs、创建额外任务、扣费、修改队列 payload、持久化 `actual_prompt` 或变更 External v1 / OpenAPI / SpacetimeDB schema。T5 接入正式 Worker 时必须移除 T2 的 staged dead-code 豁免,并把翻译结果作为唯一 `actualPrompt` 进入 provider;T2 单独合入仍不是可发布切点。 + +## 2026-08-07 SFX 生成优化 V2.0 T4 前端 controller 与参数 UI + +- 状态模型:SFX 使用独立于 BGM 的纯状态模型和 dialog-scoped controller。优化与提交均在第一个 `await` 前同步 claim operation;账号、项目、dialog、mode 与 `AbortController` 共同判定响应归属。关闭、删除、mode / scope 切换后的旧响应不能写回新面板;同一按钮双击只有第一个 operation 生效。 +- Prompt 与撤销:计数、优化、预设、撤销和提交统一复用 T1 的 Unicode `White_Space` canonicalizer 与 `1-2048` code point 规则。优化开始时以当前 canonical Prompt 替换旧快照,成功转为单层交换快照,失败清除本次临时快照且不恢复更早快照;预设写入清快照,手动编辑优化结果后仍可在两个 canonical 版本间反复交换。 +- 预设视图:BGM 预设跑马灯抽出无业务语义的音频内核,保留单一可访问控件队列、无缝滚动、hover、触摸、页面可见性和 reduced-motion 行为;BGM / SFX 各自保留 wrapper、预设模型和业务 class。SFX wrapper 展示固定 `40 + 12` 预设,不保存展开、滚动或 hover 状态。 +- 参数与布局:SFX 首次打开为自动时长、预置手动值 `5s`、Loop false;手动 slider 为 `0.5-30s`、步进 `0.1s`,自动模式禁用 slider 但保留最近手动值。layout 恢复 `soundDurationMode / soundDurationSeconds / soundLoop`,历史 Vidu dialog 与改造入口统一打开固定 `eleven_text_to_sound_v2` / `ElevenLabs` 面板。前端显示新模型 `5` 泥点兜底,正式价格仍以后端 T5 入队冻结值为真相。 +- 锁与阶段边界:优化、提交或既有生成态只锁当前 SFX dialog 的输入、预设、滚动、参数、撤销和生成。提交 claim 同步冻结 canonical Prompt、时长模式、最近手动值与 Loop;T4 不改变正式请求的 nullable duration / Loop 映射,不接 Worker 翻译或 ElevenLabs,不修改服务端动态定价、计费、OSS、持久化详情、External v1、OpenAPI 或 SpacetimeDB schema。T4 必须与 T5 同一发布列车,不能单独发布。 + +## 2026-08-07 SFX 生成优化 V2.0 T5 正式生成与 External v1 + +- 正式执行链:站内与 External 请求在定价、预扣和 enqueue 前统一收敛为 canonical userPrompt、`model = eleven_text_to_sound_v2`、nullable 小数 duration 与 Loop;队列载荷不包含 actualPrompt。Worker 在既有冻结计费上下文内顺序执行 Luna 英文化、单次 ElevenLabs POST、MP3 校验 / 实际时长探测、OSS 和项目资源 / 账号素材 / 画布完成态写回,任一失败进入既有退款边界。queue 使用 job ID,inline 在 provider 前生成平台 Task ID,不伪造 provider task ID。 +- 权威结果:服务端只保留 Agent 身份关联字段并重建 SFX V2 `generation_inputs_json`,统一写入 userPrompt、actualPrompt、固定模型、duration mode、请求 / 实际时长和 Loop;客户端自报的实际英文 Prompt、实际时长、模型和 Loop 均被覆盖。信息弹窗展示中英 Prompt、实际时长、Loop、模型和完整平台 Task ID;重绘优先恢复 V2 metadata,自动模式恢复默认最近手动值 `5s`,不把实际输出时长当作手动请求值。历史 Vidu 素材仍按旧字段只读,并以新模型重绘。 +- 定价兼容:默认配置、api-server 与 SpacetimeDB 值校验同时要求保留 `audio1.0` 和新增 `eleven_text_to_sound_v2`。已存在的 SpacetimeDB 定价快照仅缺新键时,api-server 从当前受控默认 / override 补入该键后读取;其它缺失模型仍失败。该兼容不修改 schema、不在读取时写库,下一次后台保存自然持久化完整矩阵;队列计费、响应和资产成本继续使用入队冻结价格。 +- External v1:Rust DTO / handler、OpenAPI、幂等 canonical payload、compact result 与仓库 Agent Skill 同批演进。model 的省略 / null / 空串 / 纯 Unicode White_Space / 包围空白新模型 / 显式新模型统一入队;旧模型和未知非空值在 enqueue 前返回 `400`。完成结果增加实际 `durationSeconds` 与 Loop,继续隐藏 provider、userPrompt 和 actualPrompt,只暴露稳定结果引用。 +- 阶段状态:T1–T5 已完成,可以进入 T6;T6 仍需汇总 mock LLM / ElevenLabs / OSS 失败矩阵、计费退款、端到端等值、BGM 回归、API smoke、旧 Vidu 队列 drain 和发布 / 回滚门禁。T5 未执行真实 LLM、ElevenLabs 或其它付费请求,且没有 SpacetimeDB schema、migration 或 bindings 变更。 + +## 2026-08-07 SFX Prompt 边界 canonicalization 改为 ECMAScript trim + +- 决策:SFX 的 `userPrompt` 与 `actualPrompt` 从首尾 Unicode `White_Space` 规则改为 ECMAScript `String.trim()` 语义。TypeScript 直接使用 `String.trim()`;Rust 以等值边界字符集合实现,不能使用语义不同的 Rust `str::trim()`。因此首尾 `U+FEFF` 删除、首尾 `U+0085` 保留,内部空白、内部 `U+FEFF`、`U+200B`、组合字符和 ZWJ emoji 继续保持原样。 +- 范围:只影响 SFX Prompt 的输入、优化候选、Worker 翻译候选、正式请求、metadata 校验与 ElevenLabs body;BGM Prompt 以及 External v1 `model` 的 Unicode `White_Space` canonicalization 不变。 +- 验证:共享 fixture 锁定 ECMAScript 全部首尾删除字符、`U+FEFF` 边界删除与内部保留、`U+0085` 边界保留、内部空白和 `1 / 2048 / 2049` code point;前后端必须共同消费该 fixture。 + +## 2026-08-07 SFX 生成优化 V2.0 T6 Worker 组合门禁 + +- 正式编排:SFX Worker 以同一个内部编排函数串联现有计费、翻译、ElevenLabs、OSS、asset object / bind 候选准备和原子项目资源 / 账号素材 / 画布 / job 提交。生产 adapter 继续调用正式实现,测试 adapter 只替换外部边界;禁止另写与生产分叉的“测试专用业务流程”。 +- 失败与退款:余额不足时 Worker future 不得被 poll,LLM / ElevenLabs / OSS / 写回均为零;预扣后的翻译、provider、MP3、OSS 或写回失败全部一次退款。组合矩阵必须证明每个 job 的 ElevenLabs POST 最多一次、翻译最终失败 provider 为零、成功只扣费一次。 +- 分类:内部稳定 reason code 固定为 `translation_invalid / translation_upstream_failed / translation_budget_exhausted / elevenlabs_http_failed / invalid_audio / duration_probe_failed / oss_failed / writeback_failed`。MIME、空 body 和大小归 `invalid_audio`;MP3 识别、帧读取和时长门禁归 `duration_probe_failed`。普通用户继续只读稳定短文案,不暴露 endpoint、上游正文或凭据。 +- 跨入口:画布 Agent `generate-sound-effect` 与站内 / External v1 共用 canonical Prompt、固定模型、nullable `0.5-30` 小数时长和 Loop;省略 duration 为手动 `5s`,显式 null 为自动。SFX 参数解析必须保留该 null,不能被通用 null-default 兼容层改写。最终仍进入相同 `editor_sound_effect_generation` queue payload,不新增 Agent 专属链路。 +- 发布边界:T6 工程实施和 mock / loopback 门禁不等于真实 provider 或生产验收。发布前关闭 SFX 入队,使用显式 `--server` / `--server-url` 只读查询 `external_generation_job` 中 pending / running 的 `editor_sound_effect_generation`,清零后按 api-server / Worker → Web 顺序部署并灰度;禁止 `--root-dir`、删除任务伪造 drain 或自动回退 Vidu。本次没有 SpacetimeDB schema、migration 或 bindings 变更。 ## 2026-08-06 编辑器生成结果使用 durable receipt 与统一原子提交 - 背景:图片、改图、去背景、图集 / UI 多产物、角色动作、视频、音效和背景音乐在 OSS 结果可用后,仍分段 confirm object、创建 project resource / account asset、保存 canvas 和 complete job。任一中间失败都会留下部分业务事实;只把 `external_generation_job` 当 operation journal 又无法覆盖无 job 的 inline,也无法独立证明某批 resource/asset/canvas 已作为一笔提交完成。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 4ec26f972..c77ee2053 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -4437,6 +4437,12 @@ - 处理:先确定权威组件边界,再按完整调用链解决冲突。图片画布音频入口当前决策是恢复一个共享 `ImageCanvasAudioGenerationComposerView`,由组件内 `isSoundEffect` 分流;BGM/SFX 的 validator、写回、锁和提交契约仍分别保持。不要只补一个常量后继续维持已经废弃的双 composer 边界。 - 验证:同时渲染 `audio-sound-effect` 与 `audio-background-music`,覆盖两个 mode 的正向控件和互斥负向断言、dialog / mode 切换、BGM 稳定 ID 与 controller 缺失的失败关闭,并运行 `ImageCanvasGenerationComposerView.test.tsx` 与 typecheck。 +## SFX Worker 不能只靠分层单测证明退款和零副作用(2026-08-07) + +- 现象:LLM、ElevenLabs adapter、OSS 和 metadata 各自测试都通过,但无法直接证明余额不足时外部调用为零、翻译失败不会调用 provider、OSS / DB 失败只退款一次,或项目资源 / 素材 / 画布使用同一份权威 metadata。 +- 原因:正式 SFX handler 把计费、翻译、provider、持久化和写回内联在一个 future 中;分层测试只能证明单个 helper,不能证明组合顺序和“失败后不继续”。同时若把 mock 流程另写一遍,它本身又可能与生产逻辑漂移。 +- 处理:抽出单一 Worker 编排函数和计费 / stage adapter。生产 adapter 代理现有正式实现:OSS 后只准备 asset object / binding 候选,项目资源、账号素材、画布和 job 终态通过同一原子提交落库;测试 adapter 逐段记录调用与注入失败。组合矩阵同时断言 charge / refund、LLM / provider / OSS / writeback 计数、稳定 reason code 和权威值等值。ElevenLabs 二进制、MIME、大小、timeout 和 MP3 仍由 loopback adapter 测试负责,组合 mock 不替代协议测试。 +- 验证:自动 / 手动时长 × Loop 四组合成功;余额不足;翻译、HTTP、无效音频、时长探测、OSS PUT / HEAD、asset confirm / bind、项目资源、账号素材和画布写回逐点失败;所有 job provider POST `<= 1`,预扣后失败 refund `= 1`。 ## 生成结果的稳定 ID 和 job 终态都不能代替 durable receipt(2026-08-06) - 现象:Provider / OSS 已成功,但项目资源、账号素材、binding、画布和 job 只完成一部分;不确定结果重放时,有时又复制一批素材或重复推进 canvas revision。inline 路径在进程重启后尤其无法判断前一次提交是否整笔完成。 diff --git a/docs/project-memory/todos/【已知问题】AppConfig与AppState调试输出敏感配置泄漏-Master遗留-2026-08-07.md b/docs/project-memory/todos/【已知问题】AppConfig与AppState调试输出敏感配置泄漏-Master遗留-2026-08-07.md new file mode 100644 index 000000000..3b743469f --- /dev/null +++ b/docs/project-memory/todos/【已知问题】AppConfig与AppState调试输出敏感配置泄漏-Master遗留-2026-08-07.md @@ -0,0 +1,50 @@ +# AppConfig 与 AppState 调试输出敏感配置泄漏:Master 遗留问题 + +状态:**主泄漏链已由 master 修复并合入本分支**;第二档的历史 provider 配置类型脱敏仍未完成。 + +## 修复进展 + +- master 提交 `497484409`「修复应用状态调试输出密钥泄漏」(PR #150,Closes #148)落地第一档主泄漏链。 +- 本分支 `feat/sound_opt` 已于合并提交 `38e195060` 合入该修复。 + +已收口的部分(合并后逐条核对属实): + +- `AppConfig`(`server-rs/crates/api-server/src/config.rs:32`)去掉 `derive(Debug)`,改为手写**允许清单** Debug:只输出枚举、数值、布尔等封闭字段,加一个 `credentials: ""` 占位,并以 `finish_non_exhaustive()` 收尾。因此**新增的自由字符串字段默认缺席**,不需要逐个补脱敏标记。 +- `AppState` / `AppStateInner`(`server-rs/crates/api-server/src/state.rs:237`)同样改为手写摘要:内嵌 client 只输出 `*_enabled` 布尔,不递归下钻。 +- `SpacetimeClientConfig`(`server-rs/crates/spacetime-client/src/active.rs:110`)隐藏 `token` 与 `server_url` / `database` 自由字符串。这一条独立于 `AppConfig`——`SpacetimeClient` 的手写 Debug 一直在透传整个 `config`,只修 `AppConfig` 修不掉它。 +- 哨兵测试 `debug_summaries_redact_all_runtime_credentials`(`server-rs/crates/api-server/src/state.rs`)用唯一哨兵值覆盖 `AppConfig` / `SpacetimeClientConfig` / `SpacetimeClient` / `AppStateInner` / `AppState` 五条 Debug 路径,并以精确字符串比对锁定 `AppConfig` 的允许输出字段集合。 +- 本分支在该哨兵测试中补入 `elevenlabs_base_url` 与 `elevenlabs_api_key`,锁定 SFX V2 新增凭据同样默认缺席。 + +## 仍未完成:第二档 provider 配置类型 + +PR #150 明确把这一档排除在外(提交信息原文:「剩余边界:报告第二档中的历史 provider 配置类型独立 Debug 脱敏另行处理,本 PR 聚焦第一档主泄漏链」)。 + +合并后仍为明文 `derive(Debug)` 的类型: + +| 类型 | 位置 | 明文敏感字段 | +|---|---|---| +| `VectorEngineAudioSettings` | `platform-audio/src/types.rs:84` | `api_key` | +| `VectorEngineImageSettings` | `platform-image/src/vector_engine/types.rs:4` | `api_key` | +| `LlmConfig` | `platform-llm/src/lib.rs:64` | `api_key` | +| `OssConfig` | `platform-oss/src/lib.rs:72` | `access_key_id` / `access_key_secret` | +| `WechatPayConfig` | `platform-wechat/src/pay.rs:98` | `private_key_pem` / `api_v3_key` | +| `WechatConfig` | `platform-wechat/src/subscribe_message.rs:27` | `app_secret` | +| `Hyper3dSettings` | `platform-hyper3d/src/types.rs:2` | `api_key` | +| `VolcengineSpeechConfig` | `platform-speech/src/lib.rs:68` | `api_key` / `access_key` | +| `MattingConfig` | `platform-matting/src/lib.rs:49` | `access_key_id` / `access_key_secret` | + +已完成脱敏、可作为施工模板的两个:`ElevenLabsAudioSettings`(`platform-audio/src/elevenlabs.rs:19`,`api_key` → `[redacted]`,配套哨兵测试 `settings_debug_redacts_the_api_key`)与 `OpenAiImageSettings`(`api-server/src/openai_image_generation.rs:38`,`api_key` → ``,内嵌 `Option` 只打 `.is_some()`)。 + +风险评估:这些类型不再经由 `AppConfig` / `AppState` 的 Debug 递归暴露(第一档已阻断),只有在被**单独** Debug 格式化时才泄漏。当前未发现生产代码这样做,风险维持 P2。 + +## 审查归属 + +第一档已修复,后续审查若发现 `AppConfig` / `AppState` / `SpacetimeClientConfig` 的 Debug 再次泄漏,按**回归**处理,哨兵测试应当先红。 + +第二档仍视为已知 master 遗留问题,不作为 `feat/sound_opt` 或 SFX V2 的新增缺陷重复报告。 + +## 后续修复建议 + +按上表逐个补手写脱敏 Debug,每个配一条哨兵测试。更彻底的做法是引入 secret wrapper 类型(仓库当前没有 `secrecy` / `zeroize` 等依赖),让「密钥字段不能被 Debug 打印」成为类型系统保证;代价是要动十几个 crate 的字段类型与取值点,需独立排期。 + +规约现状:AGENTS.md 第 22 行只约束「禁止**提交**密钥到 git」,不覆盖运行时输出。PR #150 已在 `docs/project-memory/shared-memory/team-conventions.md` 补上运行时日志规约,其中对本档的要求是:仍使用派生 `Debug` 的历史 provider 类型**不得新增整对象日志调用**,后续按类型独立脱敏。也就是说第二档在完成前已有明确的止血约束,本文件只跟踪剩余施工项。 diff --git a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md index 74eb79be7..b06bc3a5a 100644 --- a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md +++ b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md @@ -25,7 +25,7 @@ - 编辑器自己生成的标准纯色背景抠图资产在保存源图后统一以 `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)→ 父侧阿里云 → 父侧本地键色` 链路。 - 请求与可见性边界:同源画布 BFF 的角色、图标 spritesheet 和 UI 设计图素材提取 request DTO 保留 `segModel`,前端固定自动填充 `birefnet`,该字段不是用户选择或用户可见生成输入。External OpenAPI 是否接受 `segModel` 是独立契约,不能由站内 BFF 自动外推。`background_mode`、`cross_check` 以及角色动作的 `seg_model` 只由 api-server 决定并通过 loopback RPC 传给 worker。 - 普通用户生成结果不携带生成 provider:图片、图标图集、视频、音频和角色动画完成响应的公开类型均无 `provider`,前端也不得把 provider 写入新建结果图层;项目资源、素材和历史画布快照中的 provider 由后端 User/Public mapper 统一省略。真实 provider 仅留在服务端持久化、tracking / tracing 与后台原始审计。历史派生画布资源的内部处理模型被 User mapper 清空后,图层 hydration 必须继续沿 `sourceResourceId` 回溯第一个正常生成模型,供图片信息和画布 ZIP 展示;来源链缺失或没有正常模型时显示 `-`。 -- 手动去背景与角色动作透明化失败时,前端只消费后端按任务类型返回的稳定业务文案;不得从队列 `lastErrorMessage`、HTTP error details 或图层状态恢复和展示 BgFilter、分割模型、provider 等内部诊断。 +- 手动去背景、角色动作透明化、音效和背景音乐生成失败时,前端只消费后端按任务类型返回的稳定业务文案;不得从队列 `lastErrorMessage`、HTTP error details 或图层状态恢复和展示 BgFilter、分割模型、provider、请求端点、传输错误或上游状态等内部诊断。 - BgFilter 单次真实 provider attempt 不再使用独立固定 timeout,而由父子共同按 `attempt = N × est × 2` 运行时派生;一次逻辑调用的 `callBudgetMs = 2 × attempt + 1s`,从子 worker 取得 provider permit 后才开始计时。当前冻结 `N=16 / est=5000ms` 时为 `160s / 321s`。父侧继续管理父 job / request 总预算,为每个内部 RPC 单独派生 `maxQueueWaitMs`;排队只消耗该字段,不侵蚀 `callBudgetMs`,flat 还需预留阿里云和本地键色 fallback 时间。角色动作不再按本次实际帧数增加 attempt,`32 / 40 / 48` 帧使用同一公式。角色动画继续用 `buffer_unordered(frame_count.max(1))` 同时提交单帧逻辑调用,由唯一子 worker 保证健康进程内实际在飞的 provider 请求不超过 `N`、admission 不超过默认保险丝 `Q=2048`;父流程仍按“对应绿幕源图上传 OSS 并释放原帧字节 → 以 object key 调内部 worker / 按 object key 降级 → 父侧完成透明帧处理并落 OSS”连续组成无序在途流水线,允许响应乱序,并在收口时 collect / drain 全部已提交 frame future、按 `frameIndex` 恢复顺序。任一帧最终失败时仍先排空全部已启动请求,再使整个动作任务失败退款,不发布缺帧动画;最终图片处理、OSS、画布写回和计费始终属于父流程。 - 多产物生成以后端项目快照为唯一画布真相:同一任务实际产生的原始产物、抠图 / 透明化结果和拆分结果都要先登记为 `editor_project_resource`,再通过一次 `canvasCompletion` 原子写入画布。角色形象、图标 spritesheet 和 UI 素材提取的纯色背景原图不能只留在 OSS。透明后处理成功时,处理结果保持主图层和 `generatedLayerId` 锚点,三类任务同时把 provider 原图作为第二个图层放在透明主结果右侧,图标和 UI 的实际拆分素材从 provider 原图右侧开始放置。透明背景处理最终失败时,只把已保存的原图作为唯一主图完成占位,不放透明处理图,图标和 UI 不继续拆分。source-only fallback 的前端只消费后端返回的 `project` / `resource` 快照,不按缺失字段自行构造透明图、切片或图层;任务以 `completed + warning` 收口。该收口只捕获透明背景处理本身的最终失败;phase 上报、provider 原图持久化、透明处理图持久化和 `canvasCompletion` 写回错误仍正常传播,不能被原图降级吞掉。通用 `warning.reason` 是可直接展示的完整原因。它与 `sliceWarning` 只在 `postprocess-failed-source-preserved` 这一条上互斥(透明背景最终失败不会进入拆分);风格归一化和像素规整产生的通用 `warning` 可与 `sliceWarning` 并存,此时 inline 与队列两条链路都必须按“通用在前、拆分在后”拼成同一条提示展示,不得只取其一。既有 `sliceWarning.reason` 只表示透明图成功后的自动拆分失败,保留后端原始诊断,inline 前端仅在展示时补充“图集已生成,但自动拆分未完成:”提示,queue worker 则把归一后的字符串交给 BFF `warning` 由前端直接展示。无项目上下文时不创建项目资源或画布图层。 - 图片快速编辑面板只保留一个提示词输入框和模型选择,不展示额外参考图或比例 / 尺寸控件;原图 / 原素材作为 `/api/editor/images/edits` 的 `sourceImageSrc` 直接提交,不作为 `referenceImageSrcs`。完整图标图集 `icon-spritesheet` 支持快速编辑,拆分后的单个 `icon` 不提供该入口,前后端必须使用同一素材类型规则。打开快速编辑时画布必须自动平移缩放,让原素材完整落在可视区上半部分,底部面板固定出现在素材下方且不遮挡内容,竖屏 UI 素材也必须完整展示。快速编辑右侧显示矩形、椭圆、画笔框选工具,但进入时不默认启用;点击工具后显示选中态,再点同一工具取消启用。完成框选后,画布红色细框显示连续序号,提示词可按这些编号填写每个区域怎么改。点击 `修改` 后仍停留在当前快速编辑面板显示修改中,不创建独立 `Quick Edit Generator` 画布占位;生成成功后直接用结果覆盖原图图层,失败时保留当前面板并在错误红框中显示具体错误文案。 diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index 13c2b5395..cf736d951 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -308,7 +308,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复 - Match3D 1:1 容器 UI:VectorEngine `/v1/images/edits` multipart 参考图。该容器参考图是后端生图协议输入,必须通过 `include_bytes!` 随 `api-server` 编译进二进制,避免 API 单独发布或运行目录缺少 `public/` 时生成失败。 - 敲木鱼敲击物和背景环境图:VectorEngine `/v1/images/edits`,模型固定 `gpt-image-2`。敲击物支持 multipart 多参考图,第一张固定为后端内嵌默认木鱼图,用户上传图只作为新主题参考;prompt 必须要求 `1:1` 单一纯绿色 `#00FF00 / RGB(0,255,0)` 绿幕背景主体图,并禁止黑底、白底、棋盘格和任何实底背景。当前敲击物和返回按钮上传 OSS 前只做服务端绿幕去背后处理,避免泛抠图误伤玉米等主体像素。背景环境图只使用第一步抠图完成后的透明敲击物图作为参考,prompt 必须要求中央主体预留区保持干净,中央 40% 区域禁止出现主题主体、主体局部特写、轮廓影子或重复元素,主题元素只能作为外围氛围,且必须显式声明不继承任何绿色底色、绿幕底色或纯绿色画布。 - Hyper3D / Rodin:只保留后端安全代理和旧数据兼容;Rodin 提交、状态、下载和响应解析归属 `platform-hyper3d`,`api-server/src/hyper3d_generation.rs` 只做路由、配置和错误 envelope 映射;新 Match3D 草稿和批量新增不再生成 GLB。 -- 音频:视觉小说专用音频路由保留;VectorEngine Suno/Vidu provider 协议、任务提交/查询、音频 URL 提取、下载、MIME/extension 归一和 OSS put 请求准备归属 `platform-audio`。`api-server/src/vector_engine_audio_generation.rs` 只做路由、配置、计费、asset object confirm、entity binding 和错误 envelope 映射;拼图、抓大鹅和敲木鱼提示词生成音效入口暂时关闭,通用 `/api/creation/audio/*` 对这些目标返回 `410 Gone`。敲木鱼创作只接收上传 / 录音音频资产;前端选择或录音阶段只在浏览器本地处理待提交音频,统一限制裁切后最长 1 秒、裁掉前后声音过小片段,并用浏览器端近似响度算法平衡到 `-15 LKFS` 后做峰值保护。点击生成时才直传 OSS 并确认 `asset_object`,创作 JSON 只提交轻量 `WoodenFishAudioAsset`,不得继续上传 Data URL 音频;未提供时由 `api-server` 写回内置默认木鱼音 `/wooden-fish/default-hit-sound.mp3`。 +- 音频:视觉小说专用音频路由保留;VectorEngine Suno/Vidu provider 协议、任务提交/查询、音频 URL 提取、下载、MIME/extension 归一,以及 ElevenLabs SFX 单次同步二进制请求、`40 MiB` 有界读取、MP3 验证和 `600s` 技术异常上限内的实际时长探测均归属 `platform-audio`。ElevenLabs 直接 adapter 不进入 Suno/Vidu 的 submit + poll 枚举,OSS put 请求准备以显式 provider / file stem 描述来源。`api-server/src/vector_engine_audio_generation.rs` 只做路由、配置、计费、asset object confirm、entity binding 和错误 envelope 映射;SFX Worker 在该模块内以同一编排函数串联计费、翻译、ElevenLabs、OSS、项目资源 / 账号素材 / 画布写回,生产 adapter 复用正式边界、测试 adapter 只注入 mock。内部失败分类固定为 `translation_invalid / translation_upstream_failed / translation_budget_exhausted / elevenlabs_http_failed / invalid_audio / duration_probe_failed / oss_failed / writeback_failed`,普通用户读取边界继续返回稳定短文案。拼图、抓大鹅和敲木鱼提示词生成音效入口暂时关闭,通用 `/api/creation/audio/*` 对这些目标返回 `410 Gone`。敲木鱼创作只接收上传 / 录音音频资产;前端选择或录音阶段只在浏览器本地处理待提交音频,统一限制裁切后最长 1 秒、裁掉前后声音过小片段,并用浏览器端近似响度算法平衡到 `-15 LKFS` 后做峰值保护。点击生成时才直传 OSS 并确认 `asset_object`,创作 JSON 只提交轻量 `WoodenFishAudioAsset`,不得继续上传 Data URL 音频;未提供时由 `api-server` 写回内置默认木鱼音 `/wooden-fish/default-hit-sound.mp3`。 - OSS:私有 generated path 进入浏览器前必须通过 `/api/assets/read-url` 换签;不要裸请求 `/generated-*`。请求参数的安全语义不能混用:`legacyPublicPath` 是历史公开作品兼容口,只允许 `platform_oss::LEGACY_PUBLIC_PREFIXES` 中的 curated 前缀匿名换签;`objectKey` 是正式对象引用,绝不能复用该前缀旁路,必须查询 `asset_object` 并校验配置 bucket、精确 key、`PublicRead` 或当前 owner。External OpenAPI 的 `/api/external/v1/assets/read-url` 还必须有 `editor:asset` scope,并始终以 API Key 绑定的 `owner_user_id` 执行同一 owner 校验;后台跨账号预览只能走管理员鉴权后的 `/admin/api/assets/read-url`。`/api/assets/read-bytes` 与主站 read-url 共用完全相同的授权,默认仍应由浏览器使用 signed URL 直读,bytes 只作跨域字节读取 fallback。前端如果收到同一 OSS bucket 的完整 `https://*.oss-*.aliyuncs.com/generated-*` 地址,也必须先归一为 legacy path 后走同一换签链路,避免裸连私有 bucket 403 或绕过签名缓存。OSS 签名、读签名、HEAD 和 PUT 的结构化日志由 `platform-oss` 输出,排查资产写入 / 确认失败时优先按 `operation`、`object_key` / `key_prefix`、`status_class`、`error_kind` 和 `elapsed_ms` 下钻。新上传 generated 私有对象默认写入 `Cache-Control: public, max-age=31536000, immutable`;旧对象若缺该头,只能依赖 `ETag` / `Last-Modified` 协商缓存,应通过 OSS 元数据刷新或 CDN 配置补齐,不要恢复 api-server 静态代理。`editor-agent/` 前缀只用于服务端内部读写画布 Agent 会话消息文档,不属于浏览器直传 legacy public prefix;`/api/assets/direct-upload-tickets` 必须拒绝 `legacyPrefix=editor-agent`,内部读取只允许 `editor-agent/{conversationId}.json` 形态。 - 外部 API 失败审计:外部供应商调用未成功时,`api-server` 必须发送 OTLP 失败事件并写入 `tracking_event`。VectorEngine 图片 provider 在 `platform-image` 内输出结构化日志和 `PlatformImageFailureAudit`,覆盖 `request_send`、`response_body`、`upstream_status`、`response_parse`、`missing_image` 和 `image_download` 阶段;编辑器 `screenColor=auto` 的 gpt-5-mini 背景色决策同样必须审计每次已发出的 LLM 调用失败,包括传输 / 超时、上游拒绝、响应体解析、空响应和返回候选外颜色;即使随后降级默认背景色并继续主流程也不得只记 warning。`api-server` 将这些失败映射成 `external_api_call_failure`,`scope_kind = module`、`scope_id = provider`、`module_key = external-api`。metadata 固定包含 provider、endpoint、operation、failureStage、statusCode、statusClass、timeout、retryable、errorMessage、latencyMs、promptChars、referenceImageCount、imageModel、rawExcerpt,以及在调用方可获得上下文时补充的 `userId`(触发者)和 `profileId`(草稿 / 作品 / 场景作用域)。图片生成入口应优先把 owner user id 和 profile id 透传到失败审计,不要只保留 provider 级聚合,否则很难按“谁触发、哪个作品触发”定位问题。普通调用入库优先复用 tracking outbox,outbox 不可写或保护阈值拒绝时回退同步写 SpacetimeDB;不得新增前端兜底或在 SpacetimeDB reducer 内做外部 I/O。`bgfilter-worker` 是受限资源例外:它使用共享 tracking outbox 基础目录下独立的 `bgfilter-worker/` 子目录,provider 失败审计在 spawn 前受进程级 `1024` 硬上限保护并由 shutdown tracker 跟踪;满载、outbox 缺失、保护阈值拒绝或写盘失败时直接丢弃并观测,不回退同步直写 SpacetimeDB。优雅退出先排空已获准任务的 enqueue,再封存并尽力 flush;进程被强杀时只有已 enqueue 记录可在下次启动重放。 - 外部生成运行记录:所有外部生成编排的完成态统一写入 `tracking_event`,`event_key = external_generation_run`,`scope_kind = module`,`scope_id = provider`,`module_key = external-generation`。metadata 固定包含 `runId`、`provider`、`operation`、`requestLabel`、`requestPayload`、`status`、`success`、`failureReason`、`providerRequestId`、`resultPayload`、`startedAtMicros`、`completedAtMicros` 和 `durationMs`。这类记录只用于运行审计和排障,不再走 `ai_task` 旧表。 @@ -439,7 +439,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复 - `asset_kind` 是资源 / 素材唯一权威媒体类别,不新增或返回 `media_type` / `mediaType`。角色动作预览 MP4 使用 `asset_kind = video`,最终透明帧集使用 `asset_kind = character-animation`;前端据此派生具体渲染器。 - `editor_project_resource` 与 `editor_asset` 表尾只追加 `image_sequence_frames_json: Option` 和 `image_sequence_duration_ms: Option`;`editor_showcase_asset` 作为提交时冻结的审核与公开快照,也在表尾追加并从账号素材复制相同两字段。前者保存完整有效帧数组,数组位置是唯一播放顺序,正式帧对象不保存或返回 `frameIndex`;后者只表示该图片序列完整播放一次的毫秒时长。帧数始终取数组长度,FPS 在播放或导出时即时推导,不持久化 `frame_count` 或 `fps`。 -- 图片序列时长不能复用音频 / 视频生成请求的 `durationSeconds`,资源和素材也不保存通用 `duration_seconds`。角色动作与视频生成响应保留各自既有的请求 / 结果级秒数;音频 / 视频的用户可见时长只作为字符串展示项写入 `generation_inputs_json.fields[]`,上传媒体使用本次上传探测值,带时长选项的生成任务使用用户提交值,不再复制到 `EditorAsset`、`CanvasLayer` 或画布 layout,也不在素材放置 / 工程恢复时探测或从 layout、resource 做双来源回退。素材详情和画布 ZIP 用户可见元数据只透传实际存在的 `fields[]` 时长项;缺少该项时省略时长,不生成 `--:--` 等占位值。音频播放控件只信任 `