From c72d295a84ecd933c9f85b8a6648c25a10eeceab Mon Sep 17 00:00:00 2001 From: Linghong Date: Thu, 6 Aug 2026 11:47:04 +0000 Subject: [PATCH] =?UTF-8?q?=E6=96=87=E6=A1=A3=EF=BC=9A=E5=86=BB=E7=BB=93SF?= =?UTF-8?q?X=E7=94=9F=E6=88=90=E4=BC=98=E5=8C=96V2=E5=AE=9E=E6=96=BD?= =?UTF-8?q?=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 完善SFX V2权威设计、LLM参数、二进制与时长安全边界 新增可共享的T0至T6任务拆解、测试矩阵和迁移发布门禁 同步项目决策日志与文档索引 --- docs/README.md | 1 + ...计划】SFX生成优化V2.0任务拆解-2026-08-06.md | 234 ++++++++++++++++++ .../shared-memory/decision-log.md | 17 ++ ...编辑器】画板音乐生成入口设计-2026-06-18.md | 217 +++++++++++++--- 4 files changed, 437 insertions(+), 32 deletions(-) create mode 100644 docs/project-memory/plans/【实施计划】SFX生成优化V2.0任务拆解-2026-08-06.md diff --git a/docs/README.md b/docs/README.md index dd71e8670..ab190cd5e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,6 +20,7 @@ - [图片画布编辑器 MVP 接入方案](./technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md) - [图片画布编辑器前端拆分计划](./technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md) - [画板音乐生成入口设计](./【编辑器】画板音乐生成入口设计-2026-06-18.md) +- [SFX 生成优化 V2.0 任务拆解](./project-memory/plans/【实施计划】SFX生成优化V2.0任务拆解-2026-08-06.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/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..481c5c16d --- /dev/null +++ b/docs/project-memory/plans/【实施计划】SFX生成优化V2.0任务拆解-2026-08-06.md @@ -0,0 +1,234 @@ +# SFX 生成优化 V2.0 任务拆解 + +日期:`2026-08-06` + +状态:`T0 暂未通过` + +开发分支:`feat/sound_opt` + +代码基线:`b79f72391ba95d85c3e1dedb0c2109b28b719b91` + +权威设计:[`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 口径” | +| 一键优化与翻译的 `max_output_tokens` completion 总预算和 `length` 行为已冻结 | 已完成 | 两类请求均为 `2048 × 4 = 8192`,预算包含 reasoning 与可见输出,见本文“Prompt 与 LLM 口径” | +| 旧 Vidu 队列 drain、发布顺序和回滚门禁已冻结 | 已完成 | 本文“发布与回滚” | +| 已识别的旧凭据已轮换失效,且有不含秘密值的审计证据 | 待外部完成 | 填写下方安全审计记录 | +| 业务 owner 与 API owner 已对最终口径签字 | 待外部完成 | 填写下方 owner 签字记录 | + +T0 全部行变为“已完成”前,T1–T5 不进入实现提交。 + +## 安全审计与 owner 签字记录 + +该记录只保存日期、责任人 / 工单标识和布尔结论,禁止写入账号、密码、Key、Token、Cookie 或任何可恢复凭据的值。 + +| 记录 | 日期 | 责任人 / 工单 | 结论 | +| --- | --- | --- | --- | +| 旧凭据轮换 | 待填 | 待填 | 待确认旧凭据已失效 | +| 本地资料隔离 | `2026-08-06` | 本机 Git exclude | 已确认仅本机可见、未被 Git 跟踪且不作为团队证据源 | +| 业务 owner 签字 | 待填 | 待填 | 待确认 | +| API owner 签字 | 待填 | 待填 | 待确认 | + +## 目标和非目标 + +### 目标 + +- 在 `/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。 +- 只删除首尾 Unicode `White_Space`,不做 NFC、内部空白折叠、换行转换、标点替换或静默截断。 +- 一键优化固定 `gpt-5.6-luna`、`reasoning_effort = medium`;Worker 翻译固定同模型、`reasoning_effort = low`。两类请求分别按各自 2048 Unicode code point 候选上限的 4 倍,固定 `max_output_tokens = 8192`;它是隐藏 reasoning token 与可见 JSON 输出 token 共享的 completion 总预算,不是可见正文保证,也不按实际输入长度缩小。两者均不发送 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 / 发布 / 回滚门禁。 +- 安全审计与 owner 签字记录必须真实、脱敏并可审计,未填写时 T0 保持未通过。 + +### T1:Prompt 规则、52 预设、共享契约与 metadata 基础 + +- 实现前后端 Unicode 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。 + +### T2:一键优化 BFF 和 Worker 翻译 service + +- 增加登录态 SFX Prompt 优化 BFF,固定 Luna + Medium + `max_output_tokens = 8192` completion 总预算,32 KiB body limit,严格唯一 JSON envelope,不调用音频 provider 或正式计费。 +- 增加仅 Worker 可调用的 Luna + Low 翻译 service,每次业务尝试固定 `max_output_tokens = 8192` completion 总预算,严格 `isEnglish` + Unicode Script 门禁、保真判断和最多一次业务重试。 +- 测试覆盖中文 / 英文 / 中英混合输入,日文 / 韩文 / 西里尔等非 Latin 字母候选,actualPrompt 2048 / 2049,请求体精确 token 上限,以及优化直接拒绝 `length`、翻译首轮 `length` 重试一次 / 第二轮 `length` 最终失败和 `content_filter / transport` 行为。 + +### 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,不执行真实付费请求。 + +### 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 同一发布列车。 + +### 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 不完成。 + +### 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 验收,不执行未授权付费生成。 + +## 依赖和发布列车 + +```text +T0 -> T1 +T1 -> T2 + T3 + T4 +T2 + T3 + T4 -> T5 +T5 -> T6 +``` + +- T2 / T3 可在 T1 契约稳定后并行;T4 可与两者后半程并行。 +- T4 与 T5 之间不存在可发布切点。 +- 不修改 SpacetimeDB schema;如实际实现发现必须修改,立即停止并按 schema 迁移规则重新评审,不得带入本计划默认实施。 + +## 测试门禁 + +| 层级 | 必要覆盖 | +| --- | --- | +| canonical | 空 / 全 Unicode White_Space、U+0085、U+200B、U+FEFF、换行、组合字符、ZWJ emoji、2048 / 2049 | +| 预设 | 40 + 12、ID / label 唯一、文案等值、逗号追加、重复、清快照、超限保文 | +| 优化 | Luna + Medium + `max_output_tokens=8192` completion 总预算请求体、唯一 JSON、布尔门禁、length 直接失败、content_filter、无 tool call、无候选泄漏、dialog / scope 迟到响应 | +| 翻译 | 每轮 Luna + Low + `max_output_tokens=8192` completion 总预算请求体,中文 / 英文 / 中英混合输入,日文 / 韩文 / 西里尔候选,isEnglish + Script 门禁,2048 / 2049,首轮 length 唯一重试、第二轮 length 最终失败且 provider 0 次 | +| External model | 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` 且价格已批准。 +- 只读查询 `editor_sound_effect_generation` 的 queued / running 旧 Vidu payload。非零时先 drain,不得让新 Worker 按 V2 nullable duration / Loop payload 解析旧任务。 +- 先部署 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 的全部 tracked 文档和可审计记录完整后才允许 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 818c66c18..44efbfb69 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -6710,3 +6710,20 @@ - 业务隔离:共享组件不等于共享规则。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 独有功能。 + +## 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,因此分别固定 `max_output_tokens = 2048 × 4 = 8192`;真实 VectorEngine Chat 探测已确认该字段限制隐藏 reasoning token 与可见输出 token 共享的 completion 总预算,不包含输入 prompt token,也不是可见正文保证。预算不按实际输入长度动态缩小;两者均不发送 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 不信任客户端自报。信息弹窗展示用户 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。旧凭据轮换失效的脱敏审计证据、业务 owner 和 API owner 签字尚未填写,因此 T0 暂未通过,T1–T5 不放行;不得伪造日期、责任人、工单或签字。 diff --git a/docs/【编辑器】画板音乐生成入口设计-2026-06-18.md b/docs/【编辑器】画板音乐生成入口设计-2026-06-18.md index 9800aa1c4..d1c5ce36c 100644 --- a/docs/【编辑器】画板音乐生成入口设计-2026-06-18.md +++ b/docs/【编辑器】画板音乐生成入口设计-2026-06-18.md @@ -8,9 +8,11 @@ 本次只在 `/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-04 起,本文增加 BGM Prompt 优化 V1.0 口径。2026-08-06 起,本文同时作为 SFX 生成优化 V2.0 的权威工程设计;实现、审查、测试和发布只以本文冻结的字段、模型、参数、状态和迁移口径为依据。 -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)。 +音效与背景音乐继续使用同一个音频 composer,并由组件内的 `isSoundEffect = dialog.mode === 'audio-sound-effect'` 隔离行为。共享视图不等于共享业务规则:BGM 继续使用 Suno、200 字 canonical Prompt、30 个预设、AI 补全 / 简化、单层撤销和方案 A 提交锁;SFX V2 固定使用 ElevenLabs `eleven_text_to_sound_v2`、52 个预设、一键优化、自动中译英、自动 / 手动时长和 Loop。两条路径的 Prompt 模型、controller、预设 wrapper、锁和提交契约必须分别维护,不得交叉复用业务状态。 + +SFX V2 当前只完成产品与技术口径冻结,T0 暂未通过,不表示功能已上线。T0 只验收权威设计、tracked 共享计划、决策记录、对当前 V1 代码 / OpenAPI 差距的 T1–T5 归属和可审计安全记录;不要求代码或实际 API 在 T0 与设计一致。T0 通过前不放行 T1–T5。在正式切换完成前,当前运行代码仍是 Vidu SFX V1 行为。历史 Vidu 素材继续只读展示;重绘时使用历史用户 Prompt 打开 SFX V2 面板,新任务统一走 ElevenLabs,不回退 Vidu。 ## 入口与交互 @@ -19,17 +21,19 @@ - `生成游戏音效` - `生成游戏背景音乐` 3. 选择某一项后创建独立 `generation-dialog` 画布生成对象,并通过现有 placement 模型避让已有图层和占位。 -4. 面板 UI 复用 `生成角色形象` 的紧凑结构:上方为字段区,底部为参数 / 模型 / 生成按钮区,不写规则说明类文案。音效参数按钮靠左下角,固定模型胶囊紧贴生成按钮;背景音乐字段区增加字符计数、可展开预设词条、AI 补全、一键简化和单层撤销,底部仍保留现有动态泥点价格、固定 `Suno` 模型胶囊和生成按钮。 +4. 面板 UI 复用 `生成角色形象` 的紧凑结构:上方为字段区,底部为参数 / 模型 / 生成按钮区,不写规则说明类文案。SFX 字段区增加字符计数、可展开的 52 个预设、一键优化和单层撤销,底部增加自动 / 手动时长、Loop、固定 `ElevenLabs` 模型胶囊和动态泥点价格;BGM 字段区保留字符计数、预设、AI 补全、一键简化和单层撤销,底部保留固定 `Suno` 模型胶囊和动态泥点价格。 5. 生成中隐藏设置面板,只保留画布中的音频生成占位;失败后恢复面板并展示短错误。 ## 面板字段 ### 生成游戏音效 -- `prompt`:用户输入的音效提示词。前端与 BFF 对内仍使用 `prompt`,提交到 VectorEngine Vidu 时同时写入 `prompt` 与 `sound` 同值,兼容线上网关对 `sound` 字段的反序列化要求。 -- `model`:固定默认 `audio1.0`,UI 以禁用态模型胶囊显示为 `Vidu`,位置紧贴生成按钮;暂不展示 Suno 文生音效模型入口。 -- `duration`:Vidu 音频时长,UI 以一个无标题参数按钮展示当前秒数,点击后展开与视频时长一致的滑动条,范围 `2-10` 秒,步进 `1` 秒,默认 `5` 秒。 -- 提交到 VectorEngine 时映射为 Vidu 文生音频字段:`model: "audio1.0"`、`prompt`、`sound: prompt`、`duration`、可选 `seed`。当前编辑器音效固定使用 Vidu `audio1.0`,不再走 Suno `task: "sound"` / `metadata_params` 文生音效契约。 +- `prompt`:用户在输入框确认的原始语言音效描述,语义为 `userPrompt`,继续复用对外请求和持久化字段 `prompt`。消费动作前只删除首尾 Unicode `White_Space` code point,不改写内部空白、换行、标点、零宽字符或 Unicode 形式;按 Unicode code point 计数,合法范围为 `1-2048`。空值和全空白不得回退“游戏音效”。 +- `actualPrompt`:Worker 在正式生成时对冻结的 `userPrompt` 进行统一英文化并严格验收后得到的英文 Prompt,继续复用持久化字段 `actual_prompt`。前端不提交 `actualPrompt`,ElevenLabs 只接收验收通过的 `actualPrompt`。 +- `model`:新任务固定 `eleven_text_to_sound_v2`,UI 以禁用态模型胶囊显示 `ElevenLabs`,客户端不决定模型。历史 `audio1.0` 只作旧素材展示和其它未迁移调用方的兼容标识,不是新编辑器 SFX 任务的 alias 或 fallback。 +- `duration`:`null` 表示自动时长,有限数值表示手动时长。首次打开面板默认处于手动模式,手动值默认 `5s`,范围 `0.5-30s`,UI 步进 `0.1s`。切到自动模式后禁用 slider 但保留最近手动值,关闭自动后恢复该值。服务端只校验有限值与范围,不把 UI 步进扩大成 provider 精度限制。 +- `loop`:独立布尔参数,默认 `false`,由页面开关原样冻结并传入 ElevenLabs。Prompt 是自由文本,系统不从 Prompt 推断、同步或校验 Loop;Prompt 文本与 Loop 开关不建立业务一致性门禁。 +- `prompt_influence`:服务端固定 `0.3`,不向前端开放滑杆或请求字段。输出格式固定为 query `output_format=mp3_44100_128`。 ### 生成游戏背景音乐 @@ -39,6 +43,130 @@ - 前端请求继续使用 `gptDescriptionPrompt`,Rust DTO 继续使用 `gpt_description_prompt`;不得因“唯一最终 Prompt”语义把请求字段改名为 `actualPrompt`。响应中的 `actualPrompt` / `actual_prompt` 继续保留既有生成审计语义。 - `gpt_description_prompt` 按 Apifox 契约限制 200 个 Unicode code point。前端、BFF 和 `platform-audio` 允许且必须执行同一套首尾 Unicode 空白清理;除此之外不得执行 Unicode 规范化、内部空白折叠、内部换行转换、标点替换或静默截断。 +## SFX 生成优化 V2.0 + +### Prompt 规范化、计数与字段语义 + +- `canonicalUserPrompt = 删除当前输入首尾属于 Unicode White_Space 属性的 code point`。TypeScript 使用 `\p{White_Space}` 边界判定,Rust 使用 `char::is_whitespace`;不得直接使用会额外删除 `U+FEFF` 的 JavaScript `String.trim()` 代替该契约。 +- 内部空格、CR / LF、标点、`U+200B`、`U+FEFF`、组合字符和 ZWJ emoji 原样保留;不做 NFC、空白折叠、换行转换、标点替换或静默截断。`U+0085` 在首尾属于应删除空白。 +- 字符数按 Unicode code point 计算。canonical Prompt 为空时禁用一键优化和正式生成,BFF 也返回 `400 BAD_REQUEST`;`1-2048` 允许优化和生成;超过 2048 时完整保留文本但禁用并拒绝请求。 +- 输入期间允许暂存边界空白;点击预设、一键优化、撤销或正式生成时才在同步动作内 canonicalize 并写回输入框。 +- 持久化语义固定为 `record.prompt = canonical userPrompt`、`record.actual_prompt = validated actualPrompt`,不新增同义数据库列,不得用英文覆盖 `prompt`。 + +### 52 个预设与追加规则 + +预设分为 40 个事件预设和 12 个补充要求;每项固定 `id / category / group / label / prompt`,只有下表 `Prompt` 可见文本写入输入框。ID、分类、分组、颜色和预设元数据不进入正式 Prompt、生成请求或持久化记录。 + +| 分类 | 分组 | 词条 | Prompt | +| --- | --- | --- | --- | +| 事件预设 | UI 与操作 | 轻触按钮 | 柔和的按钮点击声 | +| 事件预设 | UI 与操作 | 确认操作 | 明亮的确认提示音 | +| 事件预设 | UI 与操作 | 返回取消 | 轻微下降的取消提示音 | +| 事件预设 | UI 与操作 | 页面切换 | 快速掠过的界面切换声 | +| 事件预设 | UI 与操作 | 通知提醒 | 清晰柔和的通知提示音 | +| 事件预设 | UI 与操作 | 操作错误 | 短促克制的错误提示音 | +| 事件预设 | 拾取与奖励 | 金币拾取 | 金币拾取时清脆的金属叮当声 | +| 事件预设 | 拾取与奖励 | 道具拾取 | 拾取道具时轻快的提示音 | +| 事件预设 | 拾取与奖励 | 获得奖励 | 奖励出现时明亮的提示音 | +| 事件预设 | 拾取与奖励 | 宝箱开启 | 金属锁扣弹开,随后响起明亮的奖励提示音 | +| 事件预设 | 拾取与奖励 | 解锁内容 | 锁定状态解除,随后响起解锁提示音 | +| 事件预设 | 拾取与奖励 | 稀有掉落 | 稀有物品出现时闪耀的奖励提示音 | +| 事件预设 | 成长与结果 | 物品合成 | 两件物品融合,随后响起明亮的完成提示音 | +| 事件预设 | 成长与结果 | 角色升级 | 能量快速上升,随后响起明亮的升级提示音 | +| 事件预设 | 成长与结果 | 任务完成 | 任务完成提示音,随后响起简短的奖励音符 | +| 事件预设 | 成长与结果 | 成就达成 | 明亮的成就提示音,随后响起简短的庆祝音符 | +| 事件预设 | 成长与结果 | 挑战胜利 | 明亮的胜利提示音,随后响起短暂的庆祝音符 | +| 事件预设 | 成长与结果 | 挑战失败 | 低沉的失败提示音 | +| 事件预设 | 角色与战斗 | 角色跳跃 | 角色轻盈跳起的声音 | +| 事件预设 | 角色与战斗 | 角色落地 | 角色落地时轻微的撞击声 | +| 事件预设 | 角色与战斗 | 轻度受击 | 轻微撞击的受击声 | +| 事件预设 | 角色与战斗 | 重度受击 | 沉重有力的撞击声 | +| 事件预设 | 角色与战斗 | 攻击挥动 | 武器快速挥过空气的呼啸声 | +| 事件预设 | 角色与战斗 | 攻击命中 | 武器击中目标的清晰撞击声 | +| 事件预设 | 角色与战斗 | 格挡成功 | 武器碰撞,随后被挡开的金属声 | +| 事件预设 | 角色与战斗 | 物体破碎 | 物体撞击地面后快速破碎的声音 | +| 事件预设 | 技能与状态 | 技能蓄力 | 能量逐渐聚集的低沉嗡鸣声 | +| 事件预设 | 技能与状态 | 魔法释放 | 柔和的魔法能量扩散,带有圆润空灵的闪光声 | +| 事件预设 | 技能与状态 | 治疗恢复 | 柔和能量扩散,带有温暖圆润的提示音 | +| 事件预设 | 技能与状态 | 护盾生成 | 能量向外展开,形成稳定的护盾声 | +| 事件预设 | 技能与状态 | 瞬间移动 | 能量快速收缩,随后以短促的空气抽离声消失 | +| 事件预设 | 技能与状态 | 冰冻技能 | 冰霜能量扩散,随后响起清脆的冻结声 | +| 事件预设 | 技能与状态 | 火焰技能 | 火焰迅速喷发,带有短促的燃烧声 | +| 事件预设 | 技能与状态 | 状态强化 | 能量逐渐上升,形成稳定明亮的提示音 | +| 事件预设 | 机关与场景互动 | 门开启 | 门锁解除,随后厚重的木门缓慢打开 | +| 事件预设 | 机关与场景互动 | 机关启动 | 机关解锁,随后齿轮开始转动 | +| 事件预设 | 机关与场景互动 | 拉杆触发 | 拉杆被扳动,随后远处机关启动 | +| 事件预设 | 机关与场景互动 | 石块移动 | 大型石块缓慢移动时低沉的摩擦声 | +| 事件预设 | 机关与场景互动 | 传送门开启 | 能量旋转聚集,随后响起持续、空灵的传送门展开声 | +| 事件预设 | 机关与场景互动 | 倒计时警告 | 逐渐加快的倒计时提示音 | +| 补充要求 | 风格方向 | 休闲可爱 | 轻快可爱的卡通风格 | +| 补充要求 | 风格方向 | 复古街机 | 复古街机风格 | +| 补充要求 | 风格方向 | 科幻电子 | 干净的科幻电子音色 | +| 补充要求 | 风格方向 | 奇幻魔法 | 柔和梦幻的魔法音色 | +| 补充要求 | 风格方向 | 写实自然 | 自然真实的声音质感 | +| 补充要求 | 风格方向 | 卡通夸张 | 夸张鲜明的卡通风格 | +| 补充要求 | 反馈要求 | 轻柔反馈 | 轻柔克制 | +| 补充要求 | 反馈要求 | 有力反馈 | 更有力的撞击感 | +| 补充要求 | 反馈要求 | 短促反馈 | 短促的单次声音 | +| 补充要求 | 反馈要求 | 两段递进 | 由弱到强的两段变化 | +| 补充要求 | 反馈要求 | 干净突出 | 主体声音清晰,减少杂音 | +| 补充要求 | 反馈要求 | 柔和不刺耳 | 圆润柔和,避免尖锐高频 | + +点击预设时: + +1. 先 canonicalize 并写回当前 Prompt。 +2. 当前 Prompt 为空时直接写入预设 `prompt`。 +3. 当前 Prompt 非空时,末尾已是 Unicode 标点类别则直接追加;否则先追加中文逗号 `,`,再追加预设文本。 +4. 允许重复点击同一词条,不保留选中状态,不去重,不截断。 +5. 点击任意预设清除旧撤销快照;追加后超限时保留完整文本,只进入通用过长状态。 +6. 跑马灯、展开状态和滚动位置是临时 UI,不写入画布 layout。 + +### 一键优化、撤销与前端锁 + +- 一键优化只走登录态内部 BFF `POST /api/editor/audios/sound-effects/prompts/optimizations`,请求为 `{ currentPrompt: string }`,成功响应为 `{ prompt: string, charCount: number }`。该路由不向 External v1 开放,不调用 ElevenLabs、不创建正式任务、不触发 SFX 生成扣费。 +- 优化固定使用 `gpt-5.6-luna`、OpenAI Chat、`reasoning_effort = medium`、`max_output_tokens = 8192`,不发送 temperature 或 function tools;`8192` 按候选 Prompt 上限 `2048 Unicode code points × 4` 固定计算,是隐藏 reasoning token 与可见 JSON 输出 token 共享的 completion 总预算,不是 8192 个可见正文 token 的保证,也不按本次输入长度动态缩小。服务端固定 `32 KiB` body limit,canonical 字符范围为 `1-2048`。 +- LLM 必须返回唯一 JSON object,内部 envelope 固定包含 `prompt: string`、`isDirectWritebackFormat: boolean`、`isContentComplete: boolean`、`hasObviousFragment: boolean`、`hasGenerationParameterContent: boolean`。完整 `response.text` 只允许 JSON whitespace 包围的单一 object;拒绝代码块、前后解释、多个 JSON 值、tool call、子串提取和自动修复。 +- 候选 canonicalize 后必须非空、不超过 2048、至少含一个 Han code point,且四个布尔值依次为 `true / true / false / false`。`finish_reason = length` 或 `content_filter` 均失败,不写回候选。失败响应不暴露未通过候选或内部 envelope。 +- AI 开始前 canonicalize 并写回 Prompt,以该值取代旧快照作为本次临时快照。成功后转为单层可撤销快照;失败保留请求前 Prompt、清除临时快照,不恢复更早快照。 +- 手动编辑 AI 结果后撤销仍可用;再次优化以当前 canonical Prompt 取代旧快照;撤销时当前 canonical Prompt 与快照互换,允许在两个版本间反复切换。快照只包含 Prompt,不包含时长、Loop、预设滚动或展开状态。 +- 使用 dialog ID、账号 + 项目 scope、同步 operation ID 和 `AbortController`隔离并发;旧响应迟到、dialog 关闭或 scope 切换后不得写入当前面板。优化中锁定当前 SFX 输入框、预设、滚动、参数、撤销和生成,不锁整个画布。 +- 正式生成在点击事件的第一个 `await` 之前同步取得当前 dialog 提交锁,canonicalize 并写回 Prompt,校验并冻结 Prompt、duration mode、手动时长和 Loop。同一 dialog 重复点击必须忽略。 +- API 拒绝时解锁,原 scope 仍匹配则恢复面板并保留已写回 Prompt 与旧撤销快照;scope 已失效则不写旧 UI。API 接受并创建 job 后 `submitting` 结束,进入现有 `queued/generating` 占位并隐藏 composer;Worker 失败后按现有生成占位失败路径恢复面板并允许基于原冻结参数重试。“提交成功”只表示 API 已创建任务,不表示音频已生成完成。 + +### Worker 翻译、ElevenLabs 和结果真相 + +- API 只把 canonical `userPrompt` 入队,翻译必须在 Worker 内执行,不在入队前同步生成英文。翻译的每次业务尝试固定使用 `gpt-5.6-luna`、OpenAI Chat、`reasoning_effort = low`、`max_output_tokens = 8192`,不发送 temperature 或 function tools;`8192` 同样按 `actualPrompt` 上限 `2048 Unicode code points × 4` 固定计算,是隐藏 reasoning 与可见 JSON 输出共享的 completion 总预算,不因重试或本次输入长度改变,也不表示可见正文一定可使用 8192 tokens。 +- 翻译内部 envelope 固定为 `prompt: string`、`isEnglish: boolean`、`isFaithfulTranslation: boolean`、`isDirectGenerationFormat: boolean`、`hasAddedOrRemovedRequirement: boolean`。每次必须对完整 `response.text` 做单一 JSON object 全量解析;候选 canonicalize 后必须非空、不超过 2048,四个布尔值必须为 `true / true / true / false`。 +- `isEnglish = true` 是 LLM 语义判断,程序侧还必须执行 Unicode Script 门禁:候选至少包含一个 Script=Latin 的 alphabetic code point,且所有 alphabetic code point 的 Script 都是 Latin;Common / Inherited 的数字、标点、空白和声音设计符号允许保留。Han、Hiragana、Katakana、Hangul、Cyrillic、Greek、Arabic 等非 Latin 字母都失败。 +- 第一次成功响应的候选为空、`isEnglish != true`、非英文 Script 门禁失败、结构非法、格式 / 保真判断失败、超过 2048 Unicode code points 或 `finish_reason = length` 时,使用同一份原始 `userPrompt` 进行唯一一次业务重试;不得把第一次候选当作新事实源。第二次仍使用 `max_output_tokens = 8192`,第二次 `finish_reason = length` 按最终翻译失败处理。首轮 `content_filter` 直接失败;transport、timeout 或上游最终失败只使用该轮 `LlmClient` 内部 transport retry,不额外开启第二业务语义轮。 +- 第二次任何失败都是最终翻译失败;不返回候选,不调用 ElevenLabs,job 进入失败 / 退款链路。只有验收通过后才允许构造 `actualPrompt` 并调用 provider。 +- ElevenLabs endpoint 为 `POST /v1/sound-generation`,鉴权 `xi-api-key` 只在服务端 header 注入。body 固定包含 `text = actualPrompt`、`model_id = eleven_text_to_sound_v2`、`duration_seconds = null | frozen manual value`、`loop = frozen boolean`、`prompt_influence = 0.3`;query 固定 `output_format=mp3_44100_128`。调用方不能覆盖 model、influence 或 output format。 +- ElevenLabs 没有本链路可用的幂等键,所以 provider POST 不做自动 retry;浏览器正式生成 POST 也保持 0 次 unsafe retry,队列 `max_attempts = 1`。一个平台 job 最多调用一次 ElevenLabs。 +- 成功响应是 MP3 二进制。复用现有 `MAX_GENERATED_AUDIO_BYTES = 40 * 1024 * 1024`,即 `40 MiB`:`Content-Length` 存在且大于该值时在读取前拒绝;长度头缺失或未超限时仍有界流式读取到 `MAX_GENERATED_AUDIO_BYTES + 1`,实际累计达到 `40 MiB + 1 byte` 时失败,不得先无限读入内存。空 body、超限、HTML / JSON 错误页、损坏 MP3 和无法探测正时长的响应均失败。明确接受 `audio/mpeg` / `audio/mp3`;`application/octet-stream` 或缺失 Content-Type 只有在真实 MP3 探测成功时才可接受,显式非音频类型不能仅靠扩展名回退通过。 +- 使用纯 Rust MP3 探测获得实际时长,持久化和响应的 `durationSeconds` 必须来自 MP3 而不是请求时长。实际时长必须是有限正数且不大于独立技术异常上限 `600s`;`600s` 允许,任何大于 `600s` 的结果拒绝。该上限不由请求最大 `30s` 推导,也不要求实际时长接近请求值;通过 MP3、MIME、字节和时长门禁的 `30.5s-600s` 结果均可接受。 +- ElevenLabs 无 provider task ID。queue 模式使用 `external_generation_job.job_id` 作为平台 operation / `taskId`,inline 兼容模式在 provider 调用前生成平台 task ID,不得伪造 ElevenLabs task ID。provider 固定 `elevenlabs`,model 固定 `eleven_text_to_sound_v2`。 +- 预扣成功后才允许翻译和 provider 调用。翻译、provider、二进制验证、时长探测、OSS、资源 / 素材 / 画布写回任一失败都进入现有失败退款边界。 + +### 权威元数据、详情、计费与外部契约 + +- 成功后由服务端重建 `generation_inputs_json`。通用 `fields` 至少包含“用户描述”、“实际英文提示词”和“Loop”;强类型 `soundEffect` read model 固定包含 `schemaVersion = 2`、`userPrompt`、`actualPrompt`、`model`、`durationMode`、`requestedDurationSeconds`、`actualDurationSeconds`、`loop`。 +- `actualPrompt`、实际时长、model 和 Loop 必须由服务端运行结果构造,不信任客户端自报。项目资源、账号素材和画布 layer 共用同一份权威元数据;不修改 SpacetimeDB schema,继续复用现有 `prompt`、`actual_prompt`、`generation_inputs_json` 和画布 layout。 +- SFX V2 信息弹窗稳定展示用户原始 Prompt、实际英文 Prompt、生成模型 `eleven_text_to_sound_v2`、实际时长、Loop 和平台 Task ID。历史 Vidu 素材只显示旧字段,不得把与 `prompt` 相同的历史 `actual_prompt` 误标成“实际英文提示词”。 +- 新模型按次价格固定保持 `5` 泥点,新定价键为 `eleven_text_to_sound_v2`、单位 `perGeneration`。旧 `audio1.0` 定价键只保留历史配置兼容,新编辑器请求只读新键;正式扣费以后端入队时冻结的价格快照为真相。 +- 站内与 External v1 继续复用同一 SFX 请求契约:`prompt`、固定 `model = eleven_text_to_sound_v2`、可选 / nullable `duration: 0.5-30 number`、`loop: boolean = false`。进入队列前将 duration 省略与 null 统一序列化为 null,将 Loop 缺省统一为 false,再计算幂等 payload。 +- External v1 `model` 先删除首尾 Unicode `White_Space`,再按下表进入大小写敏感的 canonicalization;不做 alias、自动纠错或静默 provider 回退。 + +| External `model` 输入 | 结果 | canonical queue payload | +| --- | --- | --- | +| 省略 / `null` / 空串 / 纯 Unicode 空白 | 接受 | `eleven_text_to_sound_v2` | +| 首尾空白包围的 `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 前收敛为同一 canonical model,因而 omitted / null / 空白 / 显式新模型不得产生不同幂等 payload。`audio1.0` 与未知模型必须在入队前失败,并由测试证明零入队、零预扣、零 LLM 和零 provider。成功响应增加可选 `durationSeconds` 和 `loop`;Rust DTO、`docs/openapi/genarrative-external-v1.openapi.json`、`202 / poll / final response`、Idempotency-Key 重放测试和 compact result 必须同批保持一致。 +- ElevenLabs 配置只允许从服务端 `ELEVENLABS_BASE_URL`、`ELEVENLABS_API_KEY`、`ELEVENLABS_REQUEST_TIMEOUT_MS` 读取,Key 不进入浏览器、请求体、日志、fixture、共享文档或 Git。运行配置缺失时失败关闭,不回退 Vidu。 + ## BGM Prompt 优化 V1.0 ### 唯一可见最终 Prompt、首尾空白与字符口径 @@ -223,7 +351,7 @@ idle - generated 私有音频资源播放前必须通过 `/api/assets/read-url` 换签;画布卡片不得直接把 `/generated-*` 或 generated OSS 私有地址交给 `