纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口 完成项目 TypeScript/Prettier 与 Rust 全量格式化 修复 Pingora expected executable 门禁的空白敏感误报 同步开发运维文档与 AGC skill pack 格式化忽略规则
76 KiB
画板音乐生成入口设计
日期:2026-06-18
更新时间:2026-08-07
范围
本次只在 /editor/canvas 图片画布编辑器内新增底部 生成音乐 入口,用于生成完整游戏音效或游戏背景音乐。该入口属于画板生成类工具,不新增平台玩法入口、不进入作品发布链路,也不修改现有视觉小说音频生成开关。
2026-08-04 起,本文增加 BGM Prompt 优化 V1.0 口径。2026-08-06 起,本文同时作为 SFX 生成优化 V2.0 的权威工程设计;实现、审查、测试和发布只以本文冻结的字段、模型、参数、状态和迁移口径为依据。
音效与背景音乐继续使用同一个音频 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–T6 工程实施;这不表示功能已上线。登录态一键优化、Worker 翻译、ElevenLabs 直接二进制 adapter、dialog-scoped 前端交互、正式提交 / Worker / 计费 / OSS / 权威 metadata / External v1、组合失败矩阵和发布 runbook 已完成。历史 Vidu 素材继续只读展示;重绘时使用历史用户 Prompt 打开 SFX V2 面板,新任务统一走 ElevenLabs,不回退 Vidu。生产配置确认、旧 Vidu 队列 drain、灰度和实际发布仍须按 T6 门禁人工执行。
入口与交互
- 底部 AI 画布工具栏新增
生成音乐。 - 点击
生成音乐后先弹出页面级 fixed 选项框,选项为:生成游戏音效生成游戏背景音乐
- 选择某一项后创建独立
generation-dialog画布生成对象,并通过现有 placement 模型避让已有图层和占位。 - 面板 UI 复用
生成角色形象的紧凑结构:上方为字段区,底部为参数 / 模型 / 生成按钮区,不写规则说明类文案。SFX 字段区增加字符计数、可展开的 52 个预设、一键优化和单层撤销,底部增加自动 / 手动时长、Loop、固定ElevenLabs模型胶囊和动态泥点价格;BGM 字段区保留字符计数、预设、AI 补全、一键简化和单层撤销,底部保留固定Suno模型胶囊和动态泥点价格。 - 生成中隐藏设置面板,只保留画布中的音频生成占位;失败后恢复面板并展示短错误。
面板字段
生成游戏音效
prompt:用户在输入框确认的原始语言音效描述,语义为userPrompt,继续复用对外请求和持久化字段prompt。消费动作前按 ECMAScriptString.trim()语义只删除首尾空白和行终止符,包含U+FEFF;不改写内部空白、换行、标点、零宽字符或 Unicode 形式。首尾U+0085不属于该删除集合。按 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,不向前端开放滑杆或请求字段。输出格式固定为 queryoutput_format=mp3_44100_128。
生成游戏背景音乐
gpt_description_prompt:用户输入框当前文本按本文规则清理首尾 Unicode 空白后形成的背景音乐提示词,是正式 BGM 生成的唯一最终 Prompt。make_instrumental:固定传true,不在 UI 中展示为可改字段。- 提交到 VectorEngine 时映射为 Suno 纯音乐模式字段:
mv、gpt_description_prompt、make_instrumental: true。mv后端固定使用默认 Suno 模型,UI 以禁用态模型胶囊显示Suno。 - 前端请求继续使用
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 = 对当前输入执行 ECMAScript String.trim()。TypeScript 直接使用String.trim();Rust 使用等值的 ECMAScript 边界字符集合,不能误用会保留U+FEFF、删除U+0085的 Ruststr::trim()。- 内部空格、CR / LF、标点、
U+200B、内部U+FEFF、组合字符和 ZWJ emoji 原样保留;首尾U+FEFF删除,首尾U+0085保留。不做 NFC、空白折叠、换行转换、标点替换或静默截断。 - 字符数按 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 与操作 | 操作错误 | 短促克制的错误提示音 |
| 事件预设 | 拾取与奖励 | 金币拾取 | 金币拾取时清脆的金属叮当声 |
| 事件预设 | 拾取与奖励 | 道具拾取 | 拾取道具时轻快的提示音 |
| 事件预设 | 拾取与奖励 | 获得奖励 | 奖励出现时明亮的提示音 |
| 事件预设 | 拾取与奖励 | 宝箱开启 | 金属锁扣弹开,随后响起明亮的奖励提示音 |
| 事件预设 | 拾取与奖励 | 解锁内容 | 锁定状态解除,随后响起解锁提示音 |
| 事件预设 | 拾取与奖励 | 稀有掉落 | 稀有物品出现时闪耀的奖励提示音 |
| 事件预设 | 成长与结果 | 物品合成 | 两件物品融合,随后响起明亮的完成提示音 |
| 事件预设 | 成长与结果 | 角色升级 | 能量快速上升,随后响起明亮的升级提示音 |
| 事件预设 | 成长与结果 | 任务完成 | 任务完成提示音,随后响起简短的奖励音符 |
| 事件预设 | 成长与结果 | 成就达成 | 明亮的成就提示音,随后响起简短的庆祝音符 |
| 事件预设 | 成长与结果 | 挑战胜利 | 明亮的胜利提示音,随后响起短暂的庆祝音符 |
| 事件预设 | 成长与结果 | 挑战失败 | 低沉的失败提示音 |
| 事件预设 | 角色与战斗 | 角色跳跃 | 角色轻盈跳起的声音 |
| 事件预设 | 角色与战斗 | 角色落地 | 角色落地时轻微的撞击声 |
| 事件预设 | 角色与战斗 | 轻度受击 | 轻微撞击的受击声 |
| 事件预设 | 角色与战斗 | 重度受击 | 沉重有力的撞击声 |
| 事件预设 | 角色与战斗 | 攻击挥动 | 武器快速挥过空气的呼啸声 |
| 事件预设 | 角色与战斗 | 攻击命中 | 武器击中目标的清晰撞击声 |
| 事件预设 | 角色与战斗 | 格挡成功 | 武器碰撞,随后被挡开的金属声 |
| 事件预设 | 角色与战斗 | 物体破碎 | 物体撞击地面后快速破碎的声音 |
| 事件预设 | 技能与状态 | 技能蓄力 | 能量逐渐聚集的低沉嗡鸣声 |
| 事件预设 | 技能与状态 | 魔法释放 | 柔和的魔法能量扩散,带有圆润空灵的闪光声 |
| 事件预设 | 技能与状态 | 治疗恢复 | 柔和能量扩散,带有温暖圆润的提示音 |
| 事件预设 | 技能与状态 | 护盾生成 | 能量向外展开,形成稳定的护盾声 |
| 事件预设 | 技能与状态 | 瞬间移动 | 能量快速收缩,随后以短促的空气抽离声消失 |
| 事件预设 | 技能与状态 | 冰冻技能 | 冰霜能量扩散,随后响起清脆的冻结声 |
| 事件预设 | 技能与状态 | 火焰技能 | 火焰迅速喷发,带有短促的燃烧声 |
| 事件预设 | 技能与状态 | 状态强化 | 能量逐渐上升,形成稳定明亮的提示音 |
| 事件预设 | 机关与场景互动 | 门开启 | 门锁解除,随后厚重的木门缓慢打开 |
| 事件预设 | 机关与场景互动 | 机关启动 | 机关解锁,随后齿轮开始转动 |
| 事件预设 | 机关与场景互动 | 拉杆触发 | 拉杆被扳动,随后远处机关启动 |
| 事件预设 | 机关与场景互动 | 石块移动 | 大型石块缓慢移动时低沉的摩擦声 |
| 事件预设 | 机关与场景互动 | 传送门开启 | 能量旋转聚集,随后响起持续、空灵的传送门展开声 |
| 事件预设 | 机关与场景互动 | 倒计时警告 | 逐渐加快的倒计时提示音 |
| 补充要求 | 风格方向 | 休闲可爱 | 轻快可爱的卡通风格 |
| 补充要求 | 风格方向 | 复古街机 | 复古街机风格 |
| 补充要求 | 风格方向 | 科幻电子 | 干净的科幻电子音色 |
| 补充要求 | 风格方向 | 奇幻魔法 | 柔和梦幻的魔法音色 |
| 补充要求 | 风格方向 | 写实自然 | 自然真实的声音质感 |
| 补充要求 | 风格方向 | 卡通夸张 | 夸张鲜明的卡通风格 |
| 补充要求 | 反馈要求 | 轻柔反馈 | 轻柔克制 |
| 补充要求 | 反馈要求 | 有力反馈 | 更有力的撞击感 |
| 补充要求 | 反馈要求 | 短促反馈 | 短促的单次声音 |
| 补充要求 | 反馈要求 | 两段递进 | 由弱到强的两段变化 |
| 补充要求 | 反馈要求 | 干净突出 | 主体声音清晰,减少杂音 |
| 补充要求 | 反馈要求 | 柔和不刺耳 | 圆润柔和,避免尖锐高频 |
点击预设时:
- 先 canonicalize 并写回当前 Prompt。
- 当前 Prompt 为空时直接写入预设
prompt。 - 当前 Prompt 非空时,末尾已是 Unicode 标点类别则直接追加;否则先追加中文逗号
,,再追加预设文本。 - 允许重复点击同一词条,不保留选中状态,不去重,不截断。
- 点击任意预设清除旧撤销快照;追加后超限时保留完整文本,只进入通用过长状态。
- 跑马灯、展开状态和滚动位置是临时 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,completion tokens 总预算固定为8192,不发送 temperature 或 function tools;8192按候选 Prompt 上限2048 Unicode code points × 4固定计算,由隐藏 reasoning tokens 与可见 JSON 输出 tokens 共享,不包含输入 Prompt tokens,不是 8192 个可见正文 token 的保证,也不按本次输入长度动态缩小。当前 VectorEngine OpenAI Chat wire 固定发送max_completion_tokens = 8192;内部历史字段名max_output_tokens不是业务语义。服务端固定32 KiBbody 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,completion tokens 总预算固定为8192,不发送 temperature 或 function tools;8192同样按actualPrompt上限2048 Unicode code points × 4固定计算,由隐藏 reasoning tokens 与可见 JSON 输出 tokens 共享,不包含输入 Prompt tokens,不因重试或本次输入长度改变,也不表示可见正文一定可使用 8192 tokens。当前 Chat wire 同样只发送max_completion_tokens = 8192。 - 翻译内部 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进行唯一一次业务重试;不得把第一次候选当作新事实源。第二次仍使用8192completion tokens 总预算,第二次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;mp3_44100_128是请求格式,不把响应必须精确为128 kbps扩展成硬校验,合法结果不得只因探测码率不同被拒绝。 - 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、资源 / 素材 / 画布写回任一失败都进入现有失败退款边界。
- 正式 Worker 使用同一内部编排函数串联计费、翻译、ElevenLabs、OSS 和项目资源 / 账号素材 / 画布写回。生产 adapter 必须继续调用现有正式实现;测试 adapter 只替换外部边界,用于证明余额不足零外部副作用、失败一次退款、单 job 最多一次 provider POST 和权威 metadata 等值,不得复制第二套业务流程或把 mock 结果表述成真实 provider 验收。
- Worker 内部失败分类固定为
translation_invalid / translation_upstream_failed / translation_budget_exhausted / elevenlabs_http_failed / invalid_audio / duration_probe_failed / oss_failed / writeback_failed。这些 code 只用于任务记录、日志、指标和后台排障;普通用户失败文案保持稳定短文案,不透出 endpoint、provider 原始正文或凭据。
权威元数据、详情、计费与外部契约
- 成功后由服务端重建
generation_inputs_json。通用fields至少包含“用户描述”、“实际英文提示词”和“Loop”;强类型soundEffectread 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、可选 / nullableduration: 0.5-30 number、loop: boolean = false。进入队列前将 duration 省略与 null 统一序列化为 null,将 Loop 缺省统一为 false,再计算幂等 payload。 - External v1
model先删除首尾 UnicodeWhite_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读取,request timeout 默认180000ms;Key 不进入浏览器、请求体、日志、fixture、共享文档或 Git。base URL 或 Key 缺失时失败关闭,不回退 Vidu。 - 画布 Agent
generate-sound-effect不是独立契约:其确认后 payload 同样固定 canonical Prompt、eleven_text_to_sound_v2、duration = null | 0.5-30和loop,缺省仍为手动5s/ Loop false。Agent 的显式duration:null表示自动时长,不能被通用 null-default 兼容层改写;最终继续进入相同editor_sound_effect_generation队列与 Worker。
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不属于 UnicodeWhite_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 个或更多 UnicodeWhite_Space组成,canonicalization 后仍为空字符串,AI 补全、一键简化和正式生成全部禁止;不得按规范化前的长度把全空白输入归入“可简化”状态。 - 一键简化最大输入固定为正式生成合法上限的 10 倍,即
200 × 10 = 2000个 canonical Unicode code point。超过 2000 时必须完整保留当前文本供用户手动编辑,不得截断或丢失,但 AI 补全、一键简化和正式生成都不可用。
预设库与追加规则
预设分为三组,仅用于颜色和滚动组织;分组、ID、颜色等隐藏元数据不进入 Prompt、Suno 请求或生成记录。写入输入框的是下表右列完整可见文本。
| 分组 | 预设 | 写入输入框的文本 |
|---|---|---|
| 用途 | 菜单待机 | 低干扰、适合菜单待机的背景音乐 |
| 用途 | 休闲消除 | 轻快可爱的休闲消除背景音乐 |
| 用途 | 解谜思考 | 安静专注的解谜思考背景音乐 |
| 用途 | 探索冒险 | 温和推进的探索冒险背景音乐 |
| 用途 | 对白场景 | 克制柔和、留出对白空间的背景音乐 |
| 用途 | 战斗前 | 蓄势待发的战斗前背景音乐 |
| 用途 | 胜利结算 | 明亮满足的胜利结算背景音乐 |
| 用途 | 失败结算 | 克制低落的失败结算背景音乐 |
| 用途 | 日常经营 | 轻松有序的日常经营背景音乐 |
| 用途 | 农场经营 | 自然温暖的农场经营背景音乐 |
| 用途 | 校园日常 | 青春轻松的校园日常背景音乐 |
| 用途 | 美食厨房 | 温暖活泼的美食厨房背景音乐 |
| 氛围 | 温暖治愈 | 柔和明亮的治愈背景音乐 |
| 氛围 | 神秘悬疑 | 克制神秘的悬疑背景音乐 |
| 氛围 | 紧张推进 | 稳定推进、逐渐紧张的背景音乐 |
| 场景 | 森林自然 | 清新自然的森林背景音乐 |
| 场景 | 雨夜静谧 | 雨夜静谧、略带神秘感的背景音乐 |
| 场景 | 海洋漂流 | 开阔舒缓的海洋漂流背景音乐 |
| 场景 | 山野远行 | 自由舒展的山野远行背景音乐 |
| 场景 | 糖果乐园 | 甜美活泼的糖果乐园背景音乐 |
| 场景 | 温馨小屋 | 温暖安静的温馨小屋背景音乐 |
| 场景 | 城市夜晚 | 克制迷人的城市夜晚背景音乐 |
| 场景 | 太空科幻 | 空灵未来感的太空科幻背景音乐 |
| 场景 | 赛博街区 | 冷静律动的赛博街区背景音乐 |
| 场景 | 古风幻想 | 空灵雅致的古风幻想背景音乐 |
| 场景 | 童话花园 | 梦幻轻盈的童话花园背景音乐 |
| 场景 | 海底遗迹 | 深邃神秘的海底遗迹背景音乐 |
| 场景 | 沙漠遗迹 | 苍茫神秘的沙漠遗迹背景音乐 |
| 场景 | 熔岩洞穴 | 炽热压迫的熔岩洞穴背景音乐 |
| 场景 | 奇妙博物馆 | 好奇灵动的奇妙博物馆背景音乐 |
点击预设时:
- 先按统一规则删除输入框文本两端的 Unicode
White_Spacecode point,并把结果写回输入框。 - 规范化后的字符串为空,直接写入该预设的完整可见文本。
- 规范化后的字符串非空,检查其最后一个 Unicode code point。
- 最后一个字符属于 Unicode 标点类别时,直接追加预设文本;否则先追加中文句号
。,再追加预设文本。 - 首尾空白在追加前已经删除;内部空格和内部换行保持原位,不得继续清理。写入输入框的规范化文本、句号和预设可见文本共同构成新的唯一最终 Prompt。
- 允许重复点击同一个预设,每次都按同一规则追加。
- 点击任意预设后清除旧撤销快照并隐藏撤销按钮。
- 追加后允许超过 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 补全固定 completion tokens 总预算为
2048;当前 VectorEngine OpenAI Chat wire 发送max_completion_tokens = 2048。该预算包含模型可能消耗的隐藏 reasoning tokens 与可见输出 tokens,不含输入 Prompt tokens,也不保证可见正文长度。 - 服务端模板必须要求:保留用户明确的主题、场景、风格、情绪、乐器、能量、韵律、时长、循环和避免项;按场景选择性补足场景、氛围、能量、韵律、乐器、旋律、声音设计、循环和避免项,不为凑全方向堆砌形容词。
- 用户描述已足够完整时,只补充一至两个与主题匹配的具体声音细节。发现冲突时,优先级为“明确避免项和限制 > 明确玩法用途与场景 > 风格、情绪、能量与韵律 > 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 Chatfinish_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 处理中保持输入框不变。
- 一键简化的每个业务语义轮(包括 180 字首轮和 170 字第二轮)固定 completion tokens 总预算为
8192;当前 VectorEngine OpenAI Chat wire 发送max_completion_tokens = 8192。该预算包含模型可能消耗的隐藏 reasoning tokens 与可见输出 tokens,不含输入 Prompt tokens,也不保证可见正文长度。 - 服务端在本次简化中冻结
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 中做子串提取。若可提取的字符串promptcanonicalize 后包含有效字符,第二次使用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 字内容修复轮;这类失败只应用该轮内部既有的
LlmClienttransport 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 助手状态为:
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 终态响应作为客户端可观察结算点,不为此新增协议。
画布数据
- 生成器继续保存到画布 layout JSON 的
itemType: "generation-dialog",不新增表。 - 新增生成器模式:
audio-sound-effectaudio-background-music
- 新增结果图层媒体类型
mediaType="audio"。 - 新增素材类型:
assetKind="sound-effect"assetKind="background-music"
- 音频结果以小型音频卡加入画布,卡片底部使用融入卡片的自定义播放器组件承载进度、时间和音量,播放 / 暂停只收口到卡片中央图标按钮;生成成功后同时保存为 OSS 私有对象、画布资源和账号级素材库素材,响应携带
objectKey/assetObjectId供后续换签和复用。 - 从素材库拖放音效或背景音乐时,卡片中心必须落在鼠标松手对应的画布位置,不叠加点击添加素材时使用的级联错位量;音频卡除中央播放按钮、底部进度 / 音量控件、标签和信息按钮外,其余卡片区域都作为选择与拖拽热区。
- 普通素材上传入口首版支持图片、MP3 和 MP4;MP3 / MP4 先走 OSS 直传和 asset object confirm,再以素材库素材保存,其他音视频格式暂不开放。
- 音频结果卡片底部显示播放器辅助控件,不在卡片左下角或悬停左上角展示时长;提示词固定显示在卡片左上角。
- 音频结果卡片底部播放器辅助控件仅在鼠标悬停音频卡片时从下方滑入显示,移出后向下滑出收起。
- 音频卡片中央播放区按状态切换:未悬停且未播放时显示音效 / 背景音乐图标,悬停且未播放时显示播放按钮,播放中始终显示暂停按钮。
- generated 私有音频资源播放前必须通过
/api/assets/read-url换签;画布卡片不得直接把/generated-*或 generated OSS 私有地址交给<audio>裸请求。 - 音频元数据弹窗使用
时长,不使用图片 / 视频的分辨率语义;音频生成占位不显示分辨率或时长角标。 - 音频图层右上角标签显示在信息按钮左侧,和其他素材卡右上角信息区保持一致。
- 元数据弹窗按音频显示
音频信息/音频类型/时长,生成输入快照只展示用户面板字段。BGM 快照保存输入框已经写回的 canonical Prompt,不保存助手系统模板、内部引导或预设元数据;SFX V2 快照由服务端权威构造用户 Prompt、实际英文 Prompt、请求参数、实际时长和 Loop,不保存预设 ID、助手 envelope、撤销快照或未验收翻译候选。 - 音频图层上方浮动工具栏只保留
改造和下载按钮。只有当前 owner 的可执行生成输入存在时才显示改造;点击后按action恢复对应的音效或背景音乐生成面板,SFX V2 优先按服务端权威 metadata 恢复模型、自动 / 手动时长与 Loop,稳定字段 ID 作为兼容回退;面板不展示参考图组件,允许修改后生成新音频,新结果落在原音频旁边。
前端与 BFF 契约
前端新增两个 BFF client:
POST /api/editor/audios/sound-effects/generations
{
prompt: string,
model: "eleven_text_to_sound_v2",
duration?: number | null,
loop?: boolean
}
POST /api/editor/audios/background-music/generations
{
gptDescriptionPrompt: string,
makeInstrumental: true
}
SFX Prompt 助手新增一个登录态内部 BFF:
POST / api / editor / audios / sound - effects / prompts / optimizations;
{
currentPrompt: string;
}
SFX 优化成功响应复用 { prompt: string, charCount: number };该路由不进入 External v1。
BGM Prompt 助手保留两个登录态内部 BFF:
POST / api / editor / audios / background - music / prompts / completions;
{
currentPrompt: string;
}
POST / api / editor / audios / background - music / prompts / simplifications;
{
currentPrompt: string;
}
统一 Prompt 助手响应:
{
prompt: string,
charCount: number
}
- 客户端不得提交
maxChars、targetChars、模型名、预设 ID、分组、颜色、内部模板或格式 / 完整性 / 残句判断;这些由服务端固定或由内部 LLM 结果产生。 - 三个 Prompt 助手 BFF 都不调用 Suno 或 ElevenLabs、钱包扣费、正式 generation queue、OSS、素材库,也不在 handler 中同步执行 SpacetimeDB 业务写入;成功路由的通用 tracking 仍由现有本机 outbox 异步承接。
- 三个助手请求中的
currentPrompt必须是前端已经写回输入框的 canonical Prompt;响应prompt也必须先执行各自已冻结的 canonicalization,charCount是响应 canonical Prompt 的 Unicode code point 数。 - 补全与简化成功响应都只包含上面的
prompt/charCount业务字段;内部四字段 envelope、originalPrompt、目标字数、业务语义轮信息和未通过候选均不得出现在成功或失败响应中。失败继续使用现有 API 错误 envelope。 - 简化 BFF 只接受总字符数为 201–2000 且至少含 1 个有效字符的 canonical
currentPrompt;超过 2000 返回现有400 BAD_REQUEST错误 envelope,并标记字段currentPrompt。 - 三个助手路由都设置
32 KiBHTTP 请求体上限。请求体超过该上限时保留 Axum413 PAYLOAD_TOO_LARGE,不得被通用 JSON rejection 降为400;字符上限与原始 body 上限分别校验,不能互相替代。 - 不增加 Prompt 助手专属的用户级、IP 级、时间窗口或令牌桶限流,也不新增本功能主动产生的
429/Retry-After。现有 api-server 全局并发背压、前端防重复操作、Nginx 保护和上游真实429的安全映射保持不变。 - 三个成功路由必须进入
tracking.rs显式静态映射:SFX 优化使用event_key = editor_sound_effect_prompt_optimization,BGM 补全使用event_key = editor_background_music_prompt_completion,BGM 简化使用event_key = editor_background_music_prompt_simplification;三者都使用module_key = editor、User scope。普通 route tracking 继续只记录成功响应,不在助手 handler 中新增同步埋点副作用。 - BGM 正式提交必须使用输入框已经写回的 canonical Prompt,不得额外拼接用户不可见内容,也不得把空 Prompt 回退为“游戏背景音乐”。
- BGM 正式 POST 不使用现有允许 unsafe method 的自动重试,保证一次点击不会由 client 内部重发。本文不新增 Idempotency-Key、持久提交账本或服务端 dedupe;其它生成 mode 的 retry 行为保持不变。
- SFX 正式 POST 同样不允许 client 内部 unsafe retry;队列稳定 request identity 与 External v1
Idempotency-Key仍按现有 namespace 分别维护。
统一响应:
{
ok: true,
audioSrc: string,
objectKey?: string | null,
assetObjectId?: string | null,
width: 420,
height: 120,
sourceType: "generated",
prompt: string,
actualPrompt?: string | null,
model: string,
taskId: string,
priceMudPoints: number,
audioKind: "sound-effect" | "background-music",
durationSeconds?: number | null,
loop?: boolean | null
}
- 成功响应不携带
provider;该信息只保留在服务端持久化、任务追踪、日志和后台排障中。 - BGM 成功响应必须同时填充
prompt和actualPrompt,两者都与本次 canonical Prompt 逐 code point 等值,不得携带助手模板、内部引导或隐藏前后缀;共享响应类型为兼容 SFX 与历史数据仍可保留actualPrompt可选。 - SFX V2 成功响应的
prompt必须等于 canonicaluserPrompt,actualPrompt必须等于验收通过且实际提交给 ElevenLabs 的英文 Prompt,durationSeconds必须来自 MP3 探测,loop必须等于本次冻结并发送的布尔值。 - 默认 queue 模式的首次生成响应继续只携带现有
queueState;上面的完整音频响应是 inline 或 worker 内部完成边界,不要求给 queue 首次响应或普通 metadata-only job result 新增 Prompt 字段。
后端实现
本节是对现有编辑器音频链路的原地演进说明,不是新建入口清单。正式生成继续复用 server-rs/crates/shared-contracts/src/assets.rs 中现有编辑器音频 DTO,以及 server-rs/crates/api-server/src/vector_engine_audio_generation/generation.rs 中现有 generation handler、queue / inline 分流和既有路由注册;禁止为 SFX V2 新建平行 DTO、平行正式生成 handler、第二组 /api/editor/audios/*/generations 路由或绕过现有计费 / 队列的入口。只有 SFX Prompt 优化内部路由、Worker 翻译 service 和 ElevenLabs adapter 是本切片新增的独立能力。
- 在
shared-contracts/src/assets.rs原地演进现有编辑器音频请求 / 响应 DTO,增加 fixed model、nullable 小数 duration、Loop、实际时长和 V2 metadata;不得创建同义 DTO 或第二套请求 / 响应契约。 - 保留并复用
shared-contracts现有最小 BGM Prompt 助手请求 / 响应 DTO;前后端继续共享currentPrompt、prompt和charCount字段,不把内部 LLM 判断暴露给客户端。SFX 优化只增加其自身最小助手 DTO,不复制正式生成 DTO。 - 前端、
api-server与platform-audio必须实现同一 BGM canonicalization 语义并复用同一组跨语言测试向量:只删除首尾 UnicodeWhite_Space,保留内部空白和全部其它 code point。该操作必须幂等,不得直接混用语义不同的 TypeScript / Rust 原生trim。 - 在
platform-audio现有编辑器音频 adapter 边界内原地演进 body builder / submit 能力:- 背景音乐 body 使用
mv、gpt_description_prompt、make_instrumental。 - 背景音乐在 body builder 边界防御性执行幂等 canonicalization,再按 canonical Prompt 检查至少一个有效字符和最多 200 个 Unicode code point;校验通过后用 canonical Prompt 构造 Suno body。不得复用语义不同的
normalize_limited_text,不得提供默认 Prompt。 - Suno 音乐接口路径固定为
/suno/submit/music;VECTOR_ENGINE_BASE_URL即使配置为带/v1的图片接口根,也要在platform-audio中归一为根路径后再拼接,避免误请求/v1/suno/submit/music。 - 新编辑器音效使用独立 ElevenLabs 直接二进制 adapter,不伪装成 Vidu / Suno 的 submit + poll 任务。adapter 负责 endpoint 归一、
xi-api-keyheader、固定 query / body、单次 POST、有界二进制读取、MP3 验证和时长探测。 - Vidu
audio1.0的 body builder、轮询和下载能力仅保留给历史展示和其它未迁移调用方;新audio-sound-effect任务不进入/ent/v2/text2audio或/ent/v2/tasks/{taskId}/creations,也不使用 Sunotask: "sound"。 - Suno 提交成功后的任务 ID 兼容与 wav clip 轮询逻辑只保留给背景音乐链路;
/suno/fetch/{taskId}返回audiopipe.suno.ai/?item_id=...时,该地址只作为 clip id 来源,不作为最终下载文件,后端继续调用/suno/act/wav/{clipId}获取稳定 wav URL,避免 worker 在不完整 chunked body 上卡满超时。 - VectorEngine 音频响应的
code需要兼容"success"、"ok"、"0"、"200"以及数字0/200;HTTP 非 2xx 时后端错误信息应透出安全的上游状态和短响应摘要,避免前端只显示笼统提交失败。
- 背景音乐 body 使用
- 在
api-server/src/vector_engine_audio_generation/generation.rs原地演进现有编辑器音频 generation handler;以下正式路由保持原路径和现有注册,不新增平行 BFF:/api/editor/audios/sound-effects/generations/api/editor/audios/background-music/generations
- 保留并复用
api-server现有登录态内部 BGM Prompt 助手 BFF:POST /api/editor/audios/background-music/prompts/completionsPOST /api/editor/audios/background-music/prompts/simplifications
- 在
api-server增加登录态内部 SFX Prompt 优化 BFFPOST /api/editor/audios/sound-effects/prompts/optimizations,并增加仅 Worker 可调用的 SFX 翻译 service;不暴露同步翻译 HTTP 路由。 - Prompt 助手 BFF 在入站和 LLM 候选出站边界执行 BGM canonicalization;服务端字符数、0 / 1 / 2 个有效字符规则、正式生成 200 字限制和简化 201–2000 字资格都基于 canonical Prompt。助手使用现有编辑器专用 LLM client、
gpt-5.6-luna请求模型、固定reasoning_effort=medium和显式 OpenAI Chat 协议;画布 Agent 其它调用仍使用gpt-5.4-mini。补全固定执行一个业务语义轮,简化按180 -> 170最多两个业务语义轮,并按“一键简化”章节冻结originalPrompt、派生每轮currentPrompt。LlmClient在单轮内部执行的 transport retry 不计入业务语义轮数,简化第一轮 transport、超时或上游失败不进入 170 字轮。服务端负责模板组装、在正文解析或候选提取前检查finish_reason、canonical 字符校验、对完整response.text中单个 JSON object 的serde_json全量解析、补全与简化共用的内部 envelope 校验、执行格式 / 完整性 / 残句三个布尔判断和现有 API 错误 envelope;不自行猜测三个语义判断,也不向客户端返回未通过候选。助手不发送 function tools,不接受 tool call,不从代码块或解释中截取 JSON,不自动修复,也不做运行时双协议 fallback。 finish_reason检查复用并公开platform-llm现有 API-kind-aware 未完成原因 predicate;不得在LlmClient全局拒绝普通纯文本响应,也不得改变其它调用方既有的长文本降级行为。- 三个助手路由使用各自的
32 KiBbody limit,并在tracking.rs中注册上述 User-scope 成功事件;不增加助手专属限流器、本地额度计数或功能级429。 - Prompt 助手继续复用
LlmClient现有失败原文日志行为。本需求不增加请求级日志开关、脱敏、metadata-only 模式或相关上线门禁。 - 现有 generation handler 的 BGM 分支在入站时防御性执行同一幂等 canonicalization,规范化后校验有效字符和 200 字限制。登录态站内 handler 在 queue / inline 分流前把 canonical Prompt 和固定
make_instrumental:true写入本次请求值,确保正式 generation queue 请求载荷、持久化记录和内部完成响应等值;删除空 Prompt 默认回退。该站内收口不改变 External v1 的路由、OpenAPI、Idempotency-Key 或 payload 等值语义。助手模板只用于生成输入框可见候选,不得进入正式队列或 Suno 请求。 - 同一现有 generation handler 的 SFX 分支入站后执行 SFX canonicalization、
1-2048code point、固定模型、nullable duration 和 Loop 校验,再构造 canonical queue payload 与冻结价格。Worker 负责翻译、单次 ElevenLabs、MP3 验证 / 时长探测、OSS 与权威写回,并复用现有预扣、退款、lease 和 fencing 边界。 - 客户端不提交音频
priceMudPoints;服务端按现役动态定价配置解析并在入队时冻结本次价格,后续计费、资产成本和内部完成响应复用该冻结值。BGM 价格不变;SFX V2 以新模型键保持 5 泥点 / 次。 - 生成音频持久化后返回 OSS
objectKey与assetObjectId;前端保存素材库时继续使用audioSrc作为兼容路径,并把 OSS 身份写入素材记录。 - SFX V2 不修改 SpacetimeDB schema,不新增 Prompt 助手持久化表,不向
/api/external/v1暴露助手,也不修改platform-llm日志策略;正式 External v1 SFX 生成请求 / 响应的 OpenAPI 必须在 T5 与 Rust DTO 同批更新。
验收
- 底部工具栏显示
生成音乐。 - 点击
生成音乐只出现选项框,不立刻创建占位。 - 点击
生成游戏音效后出现 SFX V2 面板:文本字段为prompt,显示0 / 2048Unicode code point 计数、52 个固定预设、一键优化、单层撤销、自动 / 手动时长和 Loop,右侧固定显示ElevenLabs模型胶囊和生成按钮。 - SFX 首次打开自动时长开启、预置手动时长为
5s、Loop 为 false;手动 slider 范围0.5-30s、步进0.1s,自动模式禁用 slider 并保留最近手动值。 - SFX 面板提交
model: "eleven_text_to_sound_v2"、canonicalprompt、duration: null | number和loop: boolean;服务端只向 ElevenLabs 发送 Worker 验收后的英文actualPrompt,不再调用 Vidu 或 Suno 音效契约。 - SFX canonical 边界测试覆盖首尾
U+FEFF删除、内部U+FEFF保留、首尾U+0085保留、内部空白不折叠,以及1 / 2048 / 2049Unicode code point;TypeScript 与 Rust 结果必须等值。 - SFX 一键优化的预设写入、中文逗号追加、重复点击、清快照、超限保文、AI 成功 / 失败 / 迟到响应、交换撤销和当前 dialog 全控件锁定都按 SFX V2 章节验收。
- 点击
生成游戏背景音乐后出现背景音乐面板,字段为gpt_description_prompt,右侧固定显示Suno模型胶囊,不展示make_instrumental。 - 音效提交到
/api/editor/audios/sound-effects/generations,背景音乐提交到/api/editor/audios/background-music/generations。 - 成功后画布新增音频卡,能通过卡片中央播放按钮播放,底部进度、时间和音量控件可操作。
- 从素材库拖放音效或背景音乐后,音频卡中心位于鼠标松手位置;拖动卡片任意非播放 / 播放器控件区域都能移动图层,点击中央播放按钮只播放或暂停,不启动拖拽。
- 成功后音频素材自动出现在账号级素材库;从素材库再次添加到画布时仍恢复为
mediaType="audio"音频图层。 - 普通素材上传 MP3 / MP4 会写入 OSS 并进入素材库;MP3 添加到画布为音频图层,MP4 添加到画布为视频图层。
- 成功后的音频卡展示提示词;卡片中央按未悬停图标、悬停播放、播放中暂停切换,不在底部重复显示播放 / 暂停按钮,播放器辅助控件直接融入卡片底部。
- 鼠标未悬停音频卡时底部播放器辅助控件不可见,悬停时从底部滑动显现;提示词位于卡片左上角。
- 音频卡片、悬停角标和待生成占位不显示时长;信息弹窗显示时长。
- 音频素材浮动工具栏在存在可执行生成输入时显示
改造与下载按钮,否则只显示下载;改造恢复对应生成面板且没有参考图组件。 - 私有 generated 音频能先换签再预览播放,不出现播放条一直为
0:00的裸路径失败状态。 - 刷新后 layout 能恢复音频生成器和音频图层。
- BGM 输入框按 canonical Prompt 的 Unicode code point 显示
0 / 200计数和动作状态,但编辑期间不因计数而改写输入框;canonical Prompt 为空时三个动作全部禁止,1 个有效字符且不超限时可以生成但不能 AI 补全,至少 2 个有效字符且不超限时可以 AI 补全,至少含 1 个有效字符且为 201–2000 个 code point 时只能一键简化,超过 2000 时三个动作全部禁止且完整保留文本。200 / 201 个纯 UnicodeWhite_Space原始输入均先归一为空,不能简化。 - 三组 30 个 BGM 预设按本文固定文案写入或追加;点击前先删除首尾 Unicode
White_Space并写回,再基于规范化结果判断空值和末尾标点。重复点击和追加后超限不得丢失 canonical Prompt,内部空格和内部换行保持原位。 - 预设展开后默认无缝慢速循环,桌面端左 / 中 / 右区域分别加速向左、暂停、加速向右,左右箭头同步加速;组件在触摸环境被渲染时可横向滚动并选择词条,本需求不恢复移动端图片画布入口。
- AI 补全的输入和候选都先 canonicalize;补全只执行一个业务语义轮,候选必须通过共用四字段 envelope、有效字符、200 字和
true / true / false三个判断后,才写回一条中文 canonical Prompt。失败、空值、结构或判断错误、格式错误或超限结果保留请求前已经写回的 canonical Prompt,错误响应不暴露候选,也不调用 Suno 或扣除正式音乐生成泥点。 - 一键简化资格、180 / 170 目标后的实际字符数和候选校验都基于 canonical Prompt;第一次使用冻结的
originalPrompt,第二次优先处理第一次成功响应中可提取的非空 canonical 候选,否则回退处理originalPrompt,且两个业务语义轮都以同一originalPrompt作保真参照。第一轮 transport、超时或上游失败直接失败,不进入第二轮;每轮内部的LlmClienttransport retry 不增加业务语义轮数。候选必须同时通过结构、有效字符、200 字、格式、完整性和残句校验;第二次仍失败时保留请求前已经写回的 canonical Prompt,不暴露任一未通过候选,程序不得截断。 - OpenAI Chat
finish_reason = length / content_filter的正文即使形成合法四字段 JSON 也不能通过或被提取;补全遇到两者均直接失败,简化第一轮length只以冻结的originalPrompt进入 170 字轮、第一轮content_filter直接失败,第二轮遇到任一未完成 reason 都最终失败。缺失、空值和未知自定义 reason 继续执行其余门禁。 - Prompt 助手协议测试必须证明请求显式使用 OpenAI Chat 且不发送 function tools;只接受完整
response.text全量解析所得的单个 JSON object。外围 JSON whitespace 可以通过,代码块、前后解释、多个 JSON 值、畸形 JSON、仅能子串提取的 object 和任意 tool call 均失败,不触发自动修复或运行时协议 fallback。 - Prompt 助手轮次测试必须区分业务语义轮和单轮内部 transport retry:补全始终只有一个业务语义轮;简化只有第一轮成功返回但候选不合格时才进入 170 字轮,第一轮 transport、超时或上游失败不进入第二轮。
- AI 补全和简化成功均产生一层 canonical Prompt 交换式撤销快照;手动编辑后仍可撤销,点击预设清除快照,点击撤销前先规范化当前输入并可在两个 canonical 版本间反复互换。
- 撤销按钮按“单层撤销”一节的矩阵逐行验收:初始与无快照时隐藏;AI 处理期间显示并禁用,且仍在可访问树中,不得用隐藏或视觉伪装代替禁用;成功后启用,失败后隐藏,手动编辑后仍启用,点击预设后隐藏;
submitting期间有快照显示并禁用、无快照隐藏,解除锁定后按快照恢复启用或隐藏;连续点击撤销在两个版本间互换且保持启用。 - AI 操作的旧响应、关闭 dialog 后的响应或其它 dialog 的响应不得覆盖当前 Prompt;同一按钮双击只产生一个有效助手请求。
- BGM 点击生成后在首个
await前同步锁定当前 dialog;同一 dialog 快速重复点击只产生一次正式请求、一个生成任务和一次扣费,retryable HTTP 状态或 transport error 也不由 client 自动重发,不锁整个画布或其它 dialog。 - BGM 提交期间归档或切走面板不会取消已发出的正式请求;失败时旧面板不抢回焦点。删除 dialog 或切换账号 / 项目后,后端已经创建的正式任务继续处理;钱包回调只允许作用于原账号仍为当前账号的页面,任务列表回调只允许作用于原账号与原项目仍为当前账号与项目的页面,dialog / canvas / asset / layer 写回还必须匹配原 scope version 与原 BGM dialog。本需求不新增跨 scope 的恢复或刷新机制。
- BGM 正式提交会删除首尾 Unicode
White_Space并同步写回输入框,不回退默认 Prompt;首尾 U+0085 等White_Space被删除,内部空格和 LF / CRLF 原样保留,U+200B、U+FEFF、组合字符和 ZWJ emoji 不被误删。canonical Prompt 在输入框、BFF、队列载荷、Suno body、生成记录和结果响应中完全一致,且没有用户不可见的前缀、后缀或模板。 - BGM 边界测试覆盖 200 / 201 个纯 Unicode
White_Space均归一为空并禁止三动作、大量边界空白包围A后只允许生成、A加 199 个内部空格再加B后只允许简化、201 个 U+200B 或 U+FEFF 只允许简化、边界空白包围 200 个A后允许补全和生成,以及 TypeScript 与 Rust 对 U+0085、U+200B 和 U+FEFF 的一致行为。 - BGM 助手入口测试覆盖 canonical 2000 字允许简化、2001 字返回
400且不调用 LLM;两个助手路由 body 超过32 KiB时返回413;连续合法请求不因本功能新增限流器返回429。 - 两个助手成功路由分别产生
editor_background_music_prompt_completion/editor_background_music_prompt_simplificationtracking event,均为module_key = editor、User scope;失败响应沿用普通 route tracking 只记录成功的现状。 - BGM Suno body 仍只包含
mv、gpt_description_prompt、make_instrumental,固定Suno胶囊和动态泥点价格不变;历史和其它未迁移 Vidu 调用方的 builder / 轮询能力保持可用,但新编辑器 SFX 任务只调用 ElevenLabs。 audio-sound-effect与audio-background-music必须由同一个音频 composer 渲染,并在组件内通过isSoundEffect分支。SFX 不得渲染 BGM 的补全 / 简化、Suno 模型和 BGM 字符规则;BGM 不得渲染 SFX 的一键优化、Loop、ElevenLabs 模型和 SFX 时长控件。两个 mode 相互切换时,菜单、预设滚动、锁、快照和助手状态不得跨分支泄漏。- SFX V2 翻译失败时 ElevenLabs 请求数为 0;成功时每个平台 job 最多一次 ElevenLabs POST,
prompt / actual_prompt、实际时长、Loop、model、provider 和 Task ID 在队列、素材、画布、响应、刷新和重绘后保持权威一致。 - SFX 两类 LLM 请求契约测试分别断言:一键优化每次请求为 Luna + Medium +
max_completion_tokens = 8192,翻译每次业务尝试为 Luna + Low +max_completion_tokens = 8192,两者都不含max_tokens、max_output_tokens或 temperature;测试命名和说明必须把8192解释为包含 reasoning 的 completion tokens 总预算。优化遇到finish_reason = length直接失败且不写回;翻译首轮length只重试一次,第二轮length最终失败且 ElevenLabs 请求数为 0。 - SFX V2 翻译测试覆盖中文、英文和中英混合 userPrompt 统一英文化,覆盖日文假名、韩文和西里尔字母候选被 Script 门禁拒绝,并锁定 actualPrompt 2048 / 2049 Unicode code point 边界。
- SFX V2 二进制测试锁定
40 MiB/40 MiB + 1 byte、有 / 无 / 伪造 Content-Length、chunked 超限、允许 / fallback / 显式错误 MIME 和真实 MP3 探测;时长测试锁定有限正数、30.5s、60s、600s均接受,>600s、NaN 和无穷值拒绝,不把请求30s当作响应硬上限。 - External v1 的 nullable duration、Loop、固定模型、幂等重放和最终响应必须与 Rust 实现及 OpenAPI 逐字段一致。
model的 omitted / null / 空串 / 纯空白 / 包围空白新模型 / 显式新模型必须收敛为同一幂等 payload;audio1.0与未知值必须返回400、零入队、零预扣、零 LLM 和零 provider。 - 本切片的定向前端、shared-contracts、
api-server、platform-audio和端到端 Prompt 等值测试通过,并执行npm run typecheck、对应 Rust 定向测试、npm run check:encoding与git diff --check。