合并远端主分支更新
同步主分支音效生成与External v1契约更新 保留图标规范与图集生成链路 适配VectorEngine LLM客户端命名并解决前端提交语义冲突
This commit is contained in:
@@ -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:
|
||||
@@ -105,6 +105,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.
|
||||
|
||||
@@ -197,6 +197,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.
|
||||
|
||||
@@ -652,13 +652,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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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-*` 旧路径习惯。
|
||||
|
||||
Vendored
+3
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4564,25 +4564,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": [
|
||||
@@ -4744,6 +4760,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": [
|
||||
{
|
||||
@@ -4789,6 +4821,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": [
|
||||
@@ -4873,9 +5045,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",
|
||||
|
||||
@@ -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 或其它凭据值。
|
||||
@@ -6785,6 +6785,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 已作为一笔提交完成。
|
||||
|
||||
@@ -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 路径在进程重启后尤其无法判断前一次提交是否整笔完成。
|
||||
|
||||
@@ -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: "<redacted>"` 占位,并以 `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` → `<redacted>`,内嵌 `Option<AppState>` 只打 `.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 类型**不得新增整对象日志调用**,后续按类型独立脱敏。也就是说第二档在完成前已有明确的止血约束,本文件只跟踪剩余施工项。
|
||||
@@ -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` 由前端直接展示。无项目上下文时不创建项目资源或画布图层。
|
||||
- 单产物画布生成同样以后端项目布局为唯一真相:普通图片、图标规范、视频和音频在项目画布生成器中提交 `canvasCompletion` 后,前端只应用即时响应携带的项目快照,或在队列完成后重新加载项目。即时响应没有携带项目快照时,前端把对应生成器恢复为可继续操作状态,但不得再用响应媒体本地补建结果图层;上述分支只有在没有项目上下文的临时画布生成中才允许走本地落图回退。
|
||||
|
||||
@@ -309,7 +309,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` 旧表。
|
||||
@@ -440,7 +440,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<String>` 和 `image_sequence_duration_ms: Option<u64>`;`editor_showcase_asset` 作为提交时冻结的审核与公开快照,也在表尾追加并从账号素材复制相同两字段。前者保存完整有效帧数组,数组位置是唯一播放顺序,正式帧对象不保存或返回 `frameIndex`;后者只表示该图片序列完整播放一次的毫秒时长。帧数始终取数组长度,FPS 在播放或导出时即时推导,不持久化 `frame_count` 或 `fps`。
|
||||
- 图片序列时长不能复用音频 / 视频生成请求的 `durationSeconds`,资源和素材也不保存通用 `duration_seconds`。角色动作与视频生成响应保留各自既有的请求 / 结果级秒数;音频 / 视频的用户可见时长只作为字符串展示项写入 `generation_inputs_json.fields[]`,上传媒体使用本次上传探测值,带时长选项的生成任务使用用户提交值,不再复制到 `EditorAsset`、`CanvasLayer` 或画布 layout,也不在素材放置 / 工程恢复时探测或从 layout、resource 做双来源回退。素材详情和画布 ZIP 用户可见元数据只透传实际存在的 `fields[]` 时长项;缺少该项时省略时长,不生成 `--:--` 等占位值。音频播放控件只信任 `<audio>` 的 `loadedmetadata.duration`,展示字段不得参与播放、裁切或变速。音频生成响应不得新增 `durationSeconds`,这些展示值也不得映射到资源、素材正式列或图片序列字段。
|
||||
- 图片序列时长不能复用音频 / 视频生成请求的 `durationSeconds`,资源和素材也不保存通用 `duration_seconds`。角色动作与视频生成响应保留各自既有的请求 / 结果级秒数;普通音频 / 视频的用户可见时长只作为字符串展示项写入 `generation_inputs_json.fields[]`,上传媒体使用本次上传探测值,带时长选项的生成任务使用用户提交值,不再复制到 `EditorAsset`、`CanvasLayer` 或画布 layout,也不在素材放置 / 工程恢复时探测或从 layout、resource 做双来源回退。素材详情和画布 ZIP 用户可见元数据只透传实际存在的 `fields[]` 时长项;缺少该项时省略时长,不生成 `--:--` 等占位值。音频播放控件只信任 `<audio>` 的 `loadedmetadata.duration`,展示字段不得参与播放、裁切或变速。SFX V2 是唯一例外:完成响应的 `durationSeconds` 必须来自 MP3 探测,且同一实际值写入 `generation_inputs_json.soundEffect.actualDurationSeconds`;该例外仍不得新增资源 / 素材正式时长列、写入 `EditorAsset` / `CanvasLayer` / layout,或复用图片序列字段。
|
||||
- 角色动作 worker 在透明帧全部持久化后创建最终项目资源与账号素材,并写入帧数组与 `image_sequence_duration_ms`;生成响应可继续返回请求 / 结果层面的 `frameCount`、`fps`、`durationSeconds`,但它们不是持久化真相。预览视频仍作为独立 `asset_kind = video` 中间素材保留并承担生成成本,最终派生素材成本为 0。
|
||||
- `create_editor_project_resource`、`create_editor_asset` 和同源资源回填必须在 procedure/storage 边界验证最终状态:`asset_kind = character-animation` 必须同时包含至少两帧的有效数组和大于 0 的 `image_sequence_duration_ms`;其他类别不得携带任一图片序列字段。同源资源只允许从 `None` 单调补齐字段,非空冲突失败关闭,补写后的 `updated_at` 不得早于既有时间。
|
||||
- `generation_inputs_json` 只保存用户生成 / 重放输入,例如 `fields`、`references`、`artSpec`;动作输出和内部背景决策色不得再写入该 JSON。背景决策审计继续走独立审计链路。
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
# SFX 生成优化 V2.0 T6 测试与发布门禁实施记录
|
||||
|
||||
日期:`2026-08-07`
|
||||
|
||||
状态:`工程实施完成;生产配置、队列 drain、灰度和实际发布待人工执行`
|
||||
|
||||
## 文档定位
|
||||
|
||||
本文记录 SFX V2 T6 的实际工程范围、组合测试证据和发布 / 回滚门禁。产品与技术规则以[画板音乐生成入口设计](./【编辑器】画板音乐生成入口设计-2026-06-18.md)为准,分阶段依赖以[SFX V2 任务拆解](./project-memory/plans/【实施计划】SFX生成优化V2.0任务拆解-2026-08-06.md)为准。
|
||||
|
||||
本文不是生产部署授权。所有自动测试都使用内存 mock、loopback HTTP server 或仓库 fixture;没有调用真实 LLM、ElevenLabs、OSS 或其它付费 provider,不能表述为真实 provider 验收。
|
||||
|
||||
## 实施基线与边界
|
||||
|
||||
- 隔离分支:`codex/sfx-v2-t6`。
|
||||
- 基线:`feat/sound_opt@69f850ff827fe64ec1c7f7a325e34ecf9c338252`,已包含 master 合并、Chat completion tokens 契约修复、未知请求字段拒绝、稳定用户失败文案和 Worker provider budget。
|
||||
- 不修改 SpacetimeDB schema、migration、bindings 或表目录。
|
||||
- 不修改 BGM Suno、BGM Prompt 助手、BGM 预设、提交锁和定价语义。
|
||||
- 不执行生产配置写入、数据库查询、旧任务 drain、服务部署、流量切换或真实生成。
|
||||
|
||||
## 正式 Worker 组合测试缝
|
||||
|
||||
正式 SFX Worker 现在通过同一个内部编排函数按顺序执行:
|
||||
|
||||
```text
|
||||
计费预扣
|
||||
-> Luna 英文化
|
||||
-> 单次 ElevenLabs POST
|
||||
-> MP3 / 实际时长门禁
|
||||
-> OSS PUT / HEAD
|
||||
-> asset object / bind 候选准备
|
||||
-> 原子提交项目资源 / 账号素材 / 画布完成态 / queue job
|
||||
```
|
||||
|
||||
生产 adapter 仍调用原有统一计费、`LlmClient`、`platform-audio` ElevenLabs adapter、OSS 和编辑器原子持久化实现,没有复制第二套业务流程。测试 adapter 只替换外部边界,以相同编排函数覆盖:
|
||||
|
||||
- 自动 / 手动时长 × Loop false / true 四种成功组合;
|
||||
- 余额不足时 LLM、ElevenLabs、OSS 和全部写回均为零;
|
||||
- `translation_invalid / translation_upstream_failed` 时 ElevenLabs 为零;
|
||||
- ElevenLabs HTTP、无效 body / MIME / 大小、损坏 MP3 / 时长探测失败;
|
||||
- OSS PUT、OSS HEAD、asset object / bind 候选准备、原子项目资源 / 账号素材 / 画布写回失败;
|
||||
- 所有预扣后的失败恰好退款一次,成功只扣费一次;
|
||||
- 每个 job 的 ElevenLabs POST 最多一次;
|
||||
- `prompt / actualPrompt / model / provider / taskId / actual duration / loop / generationInputs.soundEffect` 在响应、项目资源、账号素材和画布 layer 等值。
|
||||
|
||||
`platform-audio` 的 loopback HTTP 测试继续负责真实 adapter 形态:固定 endpoint / query / header / body、自动 / 手动时长、Loop、MIME fallback、`40 MiB` 与 chunked 上限、timeout / 429 / 5xx / body 失败无 retry、MP3 probe 和独立 `600s` 上限。组合 mock 不替代这些 adapter 测试。
|
||||
|
||||
## 稳定失败分类
|
||||
|
||||
Worker 任务记录和内部观测使用以下稳定 reason code:
|
||||
|
||||
| 阶段 | reason code |
|
||||
| --- | --- |
|
||||
| 翻译候选不合格 | `translation_invalid` |
|
||||
| 翻译 transport / upstream | `translation_upstream_failed` |
|
||||
| 翻译 / provider 预算耗尽 | `translation_budget_exhausted` / `elevenlabs_http_failed` |
|
||||
| ElevenLabs HTTP / timeout / body 读取 | `elevenlabs_http_failed` |
|
||||
| MIME、空 body、大小等音频门禁 | `invalid_audio` |
|
||||
| MP3 识别、帧读取、有限正时长或 600 秒门禁 | `duration_probe_failed` |
|
||||
| OSS PUT / HEAD | `oss_failed` |
|
||||
| asset object / bind 候选、原子项目资源 / 账号素材 / 画布写回 | `writeback_failed` |
|
||||
|
||||
reason code 不包含 endpoint、provider 原始正文或凭据。普通用户读取失败任务时仍只看到“音效生成失败,请稍后重试。”;原始诊断继续留在受控 Worker / tracing / 后台边界。
|
||||
|
||||
## 跨入口补齐
|
||||
|
||||
T6 盘点发现画布 Agent 的 `generate-sound-effect` 虽已使用 ElevenLabs 模型,但仍保留旧 Vidu 的 `2–10` 整数时长且固定 `loop=false`。本阶段修复为:
|
||||
|
||||
- Prompt 使用与站内请求相同的 ECMAScript `String.trim()` 等值 canonicalization 和 `1–2048` code point 门禁;
|
||||
- model 固定 `eleven_text_to_sound_v2`;
|
||||
- `duration` 接受 `null` 或有限 `0.5–30` 小数,缺省仍为手动 `5s`;
|
||||
- `loop` 为独立布尔值,缺省 false;
|
||||
- 显式 `duration: null` 在 Agent 参数解析中保留为自动时长,不经过通用“顶层 null 当缺省”兼容层;
|
||||
- Agent 最终生成与登录态、External v1 进入同一个 `editor_sound_effect_generation` canonical queue payload。
|
||||
|
||||
External v1 仍要求 `Idempotency-Key`。接受的 model 形态在 enqueue 前收敛为同一 payload;`audio1.0` 和未知模型在计费、LLM 与 provider 前返回 `400`。compact result 继续只暴露稳定资源引用、实际时长和 Loop,不暴露 Prompt 或 provider。
|
||||
|
||||
## 验证结果
|
||||
|
||||
| 门禁 | 结果 |
|
||||
| --- | --- |
|
||||
| `platform-audio` 分层与 ElevenLabs loopback | `60/60` |
|
||||
| `platform-editor-agent` | `24/24` |
|
||||
| api-server SFX Prompt / 翻译 / Worker 组合矩阵 | `32/32` |
|
||||
| External v1 / OpenAPI / 幂等 | `13/13` |
|
||||
| external-generation Worker / compact / deadline | `31/31` |
|
||||
| api-server BGM 回归 | `35/35` |
|
||||
| SFX / BGM 前端提交、刷新、重绘、metadata | `14` 个文件、`451/451` |
|
||||
| `cargo check -p api-server --all-targets` | 通过;仅既有 dead-code warning |
|
||||
| `npm run typecheck` | 通过 |
|
||||
| `npm run check:spacetime-schema` | `137` 张表通过,确认无 schema diff |
|
||||
| `npm run check:encoding` | `5248` 个文件通过 |
|
||||
| `cargo fmt --all -- --check` | 通过 |
|
||||
| `git diff --check` | 通过 |
|
||||
|
||||
本地运行态 smoke 使用独立临时数据库、临时 data dir 和 `18000–18004` 端口,未复用或修改原工作树正在运行的 `3000 / 3101 / 8082 / 8083` 服务。结果如下:
|
||||
|
||||
| 服务 | 地址 | 门禁 | 结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| SpacetimeDB | `http://127.0.0.1:18002` | `GET /v1/ping` | HTTP 200 |
|
||||
| BgFilter worker | `http://127.0.0.1:18004` | `GET /readyz` | HTTP 200,`ready=true` |
|
||||
| api-server | `http://127.0.0.1:18001` | `GET /healthz` | HTTP 200,`service=genarrative-api-server` |
|
||||
| Web | `http://127.0.0.1:18000` | `GET /` | HTTP 200 |
|
||||
| Admin Web | `http://127.0.0.1:18003/admin/` | `GET /admin/` | HTTP 200 |
|
||||
|
||||
临时进程树停止后,`18000–18004` 五个端口均已释放。该 smoke 只证明本地进程、路由和临时 SpacetimeDB 模块可以启动,不包含真实 ElevenLabs / LLM / OSS 调用,也不代表生产配置或生产队列已验收。
|
||||
|
||||
## 生产发布门禁
|
||||
|
||||
以下步骤未在 T6 工程实施中执行。发布人员必须在维护窗内逐项记录时间、目标环境和结果,但不得把 Key、Token、Cookie、完整 provider 错误正文或宿主私密路径写入仓库。
|
||||
|
||||
### 1. 配置与价格
|
||||
|
||||
在目标 host 的受控 secret 环境中只检查“是否存在”,不输出值:
|
||||
|
||||
```bash
|
||||
for name in ELEVENLABS_BASE_URL ELEVENLABS_API_KEY ELEVENLABS_REQUEST_TIMEOUT_MS; do
|
||||
test -n "$(printenv "$name")" || { echo "missing required setting: $name" >&2; exit 1; }
|
||||
done
|
||||
```
|
||||
|
||||
确认生产定价配置包含 `eleven_text_to_sound_v2`,单位 `perGeneration`,批准价格为 `5` 泥点;禁止只依赖前端兜底价格。
|
||||
|
||||
### 2. 关闭入队并排空旧任务
|
||||
|
||||
先进入维护窗或关闭 SFX 提交入口,再对显式目标 server 做只读查询。`<database>` 和 `<server-url>` 必须由发布环境明确提供;禁止依赖默认 server,禁止使用 `--root-dir`:
|
||||
|
||||
```bash
|
||||
spacetime sql <database> \
|
||||
--server <server-url> \
|
||||
--format json \
|
||||
"SELECT job_id, status, request_payload_json FROM external_generation_job WHERE job_kind = 'editor_sound_effect_generation' AND (status = 'pending' OR status = 'running')"
|
||||
```
|
||||
|
||||
门禁要求结果为零行。非零时保持 SFX 入队关闭,让当前旧 Worker drain;不得用新 Worker 解析存量 Vidu payload,也不得删除或改写任务来伪造清零。该查询只读,不修改 schema 或数据。
|
||||
|
||||
### 3. 部署顺序与灰度
|
||||
|
||||
1. 保持 SFX 入队关闭。
|
||||
2. 部署共享 env 已对齐的 api-server / external-generation worker。
|
||||
3. 检查 `/healthz`,确认 Worker 可启动且没有配置失败。
|
||||
4. 部署 Web。
|
||||
5. 先放开小比例 SFX 入队,观察失败分类、退款、provider POST 和完成资源对账,再逐步放量。
|
||||
6. External v1 调用方完成 contract smoke 后再结束维护窗。
|
||||
|
||||
灰度必须对账:job 完成数、退款数、ElevenLabs POST 数、完成资源数和孤儿资源数。单 job provider POST 大于 1、翻译失败仍出现 provider POST、成功 job 出现退款或失败 job 无退款时立即停止放量。
|
||||
|
||||
## 回滚门禁
|
||||
|
||||
- 不自动切回 Vidu,也不在失败时静默 fallback。
|
||||
- 先停止新 SFX 入队,再等待或人工收口 V2 pending / running job。
|
||||
- Web、api-server、Worker 和 External v1 契约协同回滚,禁止只回滚一层。
|
||||
- 已成功生成的 ElevenLabs 素材继续按通用音频资产和现有 metadata 只读展示,不做数据迁移回滚。
|
||||
- 本阶段没有 SpacetimeDB schema 变更,回滚不得执行表迁移、字段删除或数据重建。
|
||||
|
||||
## 发布判定
|
||||
|
||||
T6 工程代码与本地确定性门禁完成后,只能判定“具备进入生产维护窗验证的条件”。只有配置存在、价格批准、旧 SFX 队列为零、部署健康检查通过、灰度对账无异常且 External contract smoke 通过后,生产发布才可放行。
|
||||
@@ -183,6 +183,10 @@ spacetime sql <database> "SELECT * FROM runtime_setting LIMIT 1" --server http:/
|
||||
|
||||
本地 `.env`、`.env.local` 或 `.env.secrets.local` 修改后必须重启 `api-server` 才会生效;若已经通过 `npm run dev` 启动完整联调,可在该终端输入 `rs api-server`。排查图片编辑器 VectorEngine 生成链路时,确认 `VECTOR_ENGINE_BASE_URL`、`VECTOR_ENGINE_API_KEY` 和 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 只在本地或服务器密钥文件中配置,不能写入 Git。`VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 是单次 attempt 的配置上限,默认 `1000000`;配置加载层允许显式值低于该默认值,不再在读取环境变量时强制抬高。业务模型和 VectorEngine provider 首选请求都使用 `gpt-image-2`,符合条件时才回退到兜底模型 `gpt-image-2-c`;图片协议、URL / base64 响应解析、远端图片下载和 provider 侧结构化日志在 `server-rs/crates/platform-image`,`api-server` 只做编辑器请求编排、OSS / asset 持久化、计费和失败审计落库。`platform-image` 会在 JSON 生成和 multipart 编辑请求发送前按同一 GPT-image-2 family 规则归一显式像素尺寸;若请求发送失败,先按同一 `request_id` 查看 provider 日志与 `external_api_call_failure.metadata_json.errorSource`,当前 multipart `/v1/images/edits` 单独强制 HTTP/1.1。
|
||||
|
||||
编辑器 ElevenLabs 音效生成只从服务端读取 `ELEVENLABS_BASE_URL`、`ELEVENLABS_API_KEY` 和 `ELEVENLABS_REQUEST_TIMEOUT_MS`,timeout 默认 `180000ms`;base URL 或 Key 缺失时失败关闭,不回退 Vidu。生产 API 与 external-generation worker 通过共享 API env 取得同一配置,模板见 `deploy/env/api-server.env.example`;Key 不得进入 Web/Vite 环境、命令参数、日志、fixture 或仓库。普通测试只使用 loopback mock,禁止把真实付费请求作为 T3 自动验收。
|
||||
|
||||
SFX V2 发布必须使用维护窗:先关闭 SFX 入队,再对显式目标执行只读 `spacetime sql <database> --server <server-url> --format json "SELECT job_id, status, request_payload_json FROM external_generation_job WHERE job_kind = 'editor_sound_effect_generation' AND (status = 'pending' OR status = 'running')"`;结果非零时保持旧 Worker drain,不得删除任务或让新 Worker 解析旧 Vidu payload。禁止依赖默认 server,禁止使用 `--root-dir`。清零后先部署共享 env 已对齐的 api-server / external-generation worker,检查 `/healthz` 和 Worker 启动,再部署 Web 并小流量开放 SFX。灰度对账 job 完成数、退款数、ElevenLabs POST 数、完成资源数和孤儿资源;翻译失败仍调用 provider、单 job provider POST 大于一次、成功退款或失败未退款均应立即停止放量。回滚先停止入队并收口 V2 pending / running job,不自动切回 Vidu,不执行 SpacetimeDB schema 或数据回滚。完整清单见 `docs/【实施记录】SFX生成优化V2.0T6测试与发布门禁-2026-08-07.md`。
|
||||
|
||||
VectorEngine 图片生成 / 编辑在 `request_send` 阶段出现 `timeout`、`connect`、libcurl 35 SSL connect reset、libcurl 56 receive error / `unexpected eof while reading`、recv failure 等临时传输错误,或在 `upstream_status` 阶段收到 408 / 429 / 5xx(例如 Nginx HTML `502 Bad Gateway`)时,`platform-image` 会在一次业务请求总上限 5 次内处理;multipart 图片编辑每次重试都会重新构造 form,避免复用已消费的 body。首个 provider attempt 使用 `gpt-image-2`;明确模型不可用、408 / 非拒绝类 429 / 5xx、响应解析失败或非拒绝类缺图时,下一 attempt 直接切兜底模型 `gpt-image-2-c`,之后只在剩余次数内重试兜底模型。发送 / 连接错误无法确认上游是否已受理,只重试同一首选模型,不切模型;认证、普通参数、安全拒绝、图片下载和 budget 错误同样不切。worker 从 job 开始的同一时钟起点计算绝对 deadline,常规保留最后 `60` 秒给审计、OSS 和终态写回;job 预算小于 `120` 秒时保留一半。VectorEngine 单次 attempt timeout 取配置值和剩余 provider 预算的较小值;退避或模型切换后已没有下一次 attempt 的预算时立即停止。该 deadline 覆盖参考图、provider 请求 / 响应和响应图片下载的整次 provider future,但只在 worker 进程内通过 `RequestContext` 传递;普通 HTTP / `inline` 没有该 deadline,继续保持原有 timeout 和重试行为。日志中 `VectorEngine 首选图片模型失败,切换兼容模型` 会携带 `fallback_from_model` / `fallback_to_model`;即使回退成功,首选模型错误仍写入 `external_api_call_failure`,成功运行摘要的 `recoveredFailureCount` 同时递增。排查生产失败时应同时统计 fallback / retry 日志和最终 audit,避免把一次用户请求内的多次发送误判成多个用户请求。这项收口不修改 lease 续租 / fencing、迟到写回仲裁、attempt 耗尽与原子退款语义。
|
||||
|
||||
图片编辑器生成属于持久队列长任务:提交接口返回 job 后,前端通过 `/api/runtime/external-generation/jobs/{jobId}` 与编辑器项目资源状态收敛。生产排查小程序或 WebView `Failed to fetch` 时,若 Nginx access log 为 `499`、`upstream_status=-`,先按提交请求的 `request_id`、job id、worker 日志和 `external_api_call_failure` 对齐真实任务,不把客户端断开直接判定为 provider 失败。
|
||||
@@ -701,6 +705,7 @@ OpenTelemetry 现阶段默认开启 OTLP traces / metrics / logs,但本地日
|
||||
- `GENARRATIVE_DATABASE_BACKUP_*`
|
||||
- `GENARRATIVE_LLM_*`
|
||||
- `VECTOR_ENGINE_*`
|
||||
- `ELEVENLABS_*`
|
||||
- ~~`APIMART_*`~~(已弃用,LLM 文本调用统一迁移到 VectorEngine)
|
||||
- `APIMART_*`(历史残留,创意 Agent LLM 已迁移到 VectorEngine)
|
||||
- `HYPER3D_*`
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"prices": { "480p": 10, "720p": 20, "1080p": 40 }
|
||||
},
|
||||
"audio1.0": { "unit": "perGeneration", "price": 5 },
|
||||
"eleven_text_to_sound_v2": { "unit": "perGeneration", "price": 5 },
|
||||
"chirp-v5": { "unit": "perGeneration", "price": 12 }
|
||||
}
|
||||
}
|
||||
@@ -46,6 +47,7 @@
|
||||
- `price`:单一价格,适合音效、背景音乐等单次生成模型。
|
||||
- `prices`:档位价格,图片模型按尺寸档位配置,视频模型按分辨率配置。
|
||||
- 生图模型必须补齐支持尺寸:`gemini-3.1-flash-image-preview` 配 `0.5K / 1K / 2K`,`gpt-image-2` 配 `1K / 2K`。
|
||||
- 新编辑器 SFX 只读取 `eleven_text_to_sound_v2`;`audio1.0` 继续保留为历史 Vidu 配置兼容键,两者均按次独立配置。
|
||||
|
||||
后端保存前校验当前正式模型、必要尺寸和必要分辨率都存在且大于 0。
|
||||
|
||||
@@ -61,6 +63,8 @@ SpacetimeDB 模块会在事务内重复执行同等强度的校验,并拒绝
|
||||
|
||||
所有会调用外部生成 provider 的编辑器生成请求都必须由后端计算价格,前端请求不提交价格字段;同步执行按当前运行时配置进入 `execute_billable_asset_operation_with_cost` 预扣泥点,预扣失败不得继续调用上游。外部生成队列在入队时把价格写入 `external_generation_job.price_mud_points`,worker 必须用该冻结价格完成扣费、退款、响应和资产成本持久化,配置更新不得改变已入队任务金额。普通图片、规范、角色、UI 设计、宣发素材、快速编辑 / 图片修改、图标 spritesheet、UI 设计图提取素材、视频、角色动作、音效和背景音乐均遵循该规则。背景色决策(gpt-5-mini)本身也是一次上游调用,同样必须在预扣泥点之后发起:预扣前只做颜色无关的算价 / 校验(动画用默认色占位算价),决策放进 billable 闭包,余额不足则决策不跑、决策失败走失败退款。需要向前端展示实际扣费时,由后端在响应中返回 `priceMudPoints`。
|
||||
|
||||
SFX V2 上线前已经存在的 SpacetimeDB 定价快照可能只有 `audio1.0`。读取这类历史快照时,`api-server` 只允许从当前受控默认配置或本地 override 补入缺失的 `eleven_text_to_sound_v2` 条目,使旧快照可继续读取;其它必需模型缺失仍失败。该兼容不修改 schema,也不在读取时写数据库;下一次后台保存完整定价矩阵时自然持久化新键。发布前仍应确认运行时配置中的新键和价格已经批准。
|
||||
|
||||
## 运行时身份首次授权
|
||||
|
||||
模型定价 writer、外部生成队列和钱包调用都以真实 SpacetimeDB `ctx.sender()` 校验运行时服务 identity。原始 bootstrap secret 固定为 64 位十六进制;首次授权使用与当前 `spacetime_module.wasm` 构建时注入 SHA-256 摘要对应的原始值,模块收到原始值后重新计算 SHA-256 并做常量时间比较,WASM 只嵌入摘要、不嵌入原文。bootstrap secret 只能在配置表为空时建立首个受信身份,表存在后不能重复使用。queue 和钱包 runtime guard 只接受精确 `writer_identity`,迁移操作员身份不自动获得在线生成或钱包权限;因此当前生产 API、worker 和 controller 必须继承同一份 runtime token。非 HTTP 角色只做 queue procedure 鉴权预检,不具备 seed 或轮换身份的职责。migration operator 与 runtime writer 必须互斥:任何已登记 operator 都不能成为 writer,当前 writer 也不能被授权为 operator;一旦已有 operator,bootstrap secret 不得再新增或接管 operator。
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
- 用户要求“角色规范图”且语义是角色的规范展板、风格展板或设定板时,仍走 `generate_image`,不要误分流到 `generate_character`;只有实际生成角色立绘、角色主形象或角色视觉资产时才走 `generate_character`。用户要求多个图标素材、图集或 spritesheet 时才走 `generate_icon_spritesheet`。
|
||||
- 所有生成必须走 `execute_billable_asset_operation_with_cost` 与模型定价配置,禁止绕过定价收口。
|
||||
- function-calling 的 JSON Schema 必须与参数默认值和运行时校验保持一致,不能只在 description 中提示会被运行时拒绝的组合。`generate-ui-design` 固定 `gpt-image-2`,因此 `image_size` 只暴露 `1K / 2K`;其它可切换图片模型的工具通过共享条件 schema 在显式选择 `gpt-image-2` 时同样把 `image_size` 限制为 `1K / 2K`,省略模型时仍按默认 nanobanana2 允许 `0.5K`。`generate-video` 省略 `model` 时按默认 `seedance2.0-fast` 约束 `resolution` 为 `480p / 720p`,显式选择其它模型时仍使用其现有分辨率范围。运行时强类型校验继续作为最终防线。
|
||||
- `generate-sound-effect` 与站内 / External v1 的 SFX V2 契约一致:Prompt 使用 ECMAScript `String.trim()` 等值 canonicalization 且限制 `1–2048` Unicode code points,model 固定 `eleven_text_to_sound_v2`,`duration` 缺省为手动 `5s`、显式 `null` 为自动时长、数值范围为有限 `0.5–30` 小数,`loop` 缺省 false。显式 `duration:null` 必须绕过通用“顶层 null 当缺省”兼容层,不能在 job payload 中变回 `5s`;确认后的 canonical payload 继续进入现有 `editor_sound_effect_generation` Worker,不新增 Agent 专属音频链路。
|
||||
- 图层操作及其他未注册的画板功能第一期不进入对话工具面,仍走现有面板。
|
||||
|
||||
## 当前分支落地状态
|
||||
@@ -96,7 +97,7 @@
|
||||
- `EditorAgentToolCall.args` 的正式持久化契约是**校验后的规范参数 JSON**,不是 LLM 返回的原始 JSON。api-server 收到工具调用后,必须先按已注册的 ToolArgs 反序列化、补齐字段默认值、删除未进入 ToolArgs 的未知 / 退役字段、执行工具参数校验,再重新序列化并写入 `args`;校验失败的调用不得持久化为待确认消息。所有有明确默认值的工具标量参数在强类型 ToolArgs 中必须使用非 `Option` 字段:调用方省略字段或把顶层字段显式传为 `null` 时,统一在 ToolArgs 反序列化前视为未提供,由 Serde 补齐默认值,并把具体默认值写入规范 `args`;没有默认值的必填字段显式传为 `null` 时同样按缺失处理.(for compatibility) 后续计价、确认展示和 job payload 不得再次使用 `unwrap_or` 补同一默认值。LLM 原始参数只作为本次规范化的瞬时输入,不作为执行或审计真相;确认、取消、任务回填与后续上下文统一读取同一条消息中的规范 `args`。图片参数继续只保存由真实 data key 计算出的 opaque SHA-256 `imageId`;不得为了前端预览把 `args` 中的图片 ID 改写成 `objectKey`、URL 或展示对象,也不得由前端重组或回传一份新的执行参数。
|
||||
- api-server 内画布 Agent 工具统一实现 object-safe `EditorAgentTool: ToolDyn`。`validate_args`、计价、确认展示、worker job 构建、`format_execute_message` 和结果媒体投影都使用统一 JSON 边界;每个具体工具实现负责把 JSON 反序列化为自己的强类型 Args / 结果,并把校验与完成消息格式化转发到 `platform-editor-agent` 中既有的 typed `validate_args` / `format_execute_message`,不得在调用方复制工具规则。`editor_agent_tool(toolName, context)` 是唯一按工具名分派的位置,规划、确认和任务回填只调用返回的 dyn tool;新增工具必须补齐同一个 trait 实现和该工厂分支。LLM builder 的 `.tool(...)` 注册列表仍是独立显式清单,不属于本次动态分派。framework runner 必须在 `ToolCallOutput` 中保留工具返回的结构化 output;runner 写入 LLM memory 与 api-server 使用规范参数持久化 system text 时统一调用公开的 `format_tool_call_message`,不得丢弃 `TOOL_CALL_PENDING_MESSAGE` 后自行拼另一套“等待确认”输出。
|
||||
- `EditorAgentToolCall.displayArgs` 是必填、只读的用户确认展示投影,与 `args` 分离:
|
||||
- `stringArgs` 保存提示词、比例、清晰度、模型、时长等可展示参数的稳定名称、用户可见标题和值;前端渲染模型字段时复用图片编辑器公共展示名映射,`gemini-3.1-flash-image-preview` 显示为 `nanobanana2`、`audio1.0` 显示为 `Vidu`、`chirp-v5` 显示为 `Suno`,视频模型显示现有产品标签,不得改写后端参数真相;
|
||||
- `stringArgs` 保存提示词、比例、清晰度、模型、时长等可展示参数的稳定名称、用户可见标题和值;前端渲染模型字段时复用图片编辑器公共展示名映射,`gemini-3.1-flash-image-preview` 显示为 `nanobanana2`、`eleven_text_to_sound_v2` 显示为 `ElevenLabs`、历史 `audio1.0` 显示为 `Vidu`、`chirp-v5` 显示为 `Suno`,视频模型显示现有产品标签,不得改写后端参数真相;
|
||||
- `imageArgs` 按“目标图片 / 参考图片”等参数分组,每个 `refs` 项包含与规范参数对应的 `imageId`,以及后端从已校验会话上下文解析出的 `objectKey`、`imageSrc`、可选 `thumbnailSrc` / `label` / `width` / `height`。
|
||||
- `extras.priceMudPoints` 保存创建待确认消息时按后端运行时模型定价快照计算的预计泥点消耗;前端统一展示为“预计消耗 N泥点”,不自行计算价格。
|
||||
- `displayArgs` 只能由 api-server 按已注册 tool 白名单,基于已经通过 ToolArgs 校验的 `args` 和当前请求开始时从 OSS 会话文档一次性构建的 `EditorToolContext` 生成;该 context 必须按 opaque `ImageId` 同时保存执行所需的 `dataKey` 与展示所需的图片地址、Object Key、缩略图、label、宽高,参数校验、确认展示和 job payload 统一查同一份 context。不能信任 LLM 自报的展示地址、标题或素材元数据。展示投影不参与确认执行,确认接口仍只读取同一条持久化 tool call 的 `args`,避免“看到的素材”和“实际执行的素材”分叉。
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
|
||||
日期:`2026-06-18`
|
||||
|
||||
更新时间:`2026-08-06`
|
||||
更新时间:`2026-08-07`
|
||||
|
||||
## 范围
|
||||
|
||||
本次只在 `/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–T6 工程实施;这不表示功能已上线。登录态一键优化、Worker 翻译、ElevenLabs 直接二进制 adapter、dialog-scoped 前端交互、正式提交 / Worker / 计费 / OSS / 权威 metadata / External v1、组合失败矩阵和发布 runbook 已完成。历史 Vidu 素材继续只读展示;重绘时使用历史用户 Prompt 打开 SFX V2 面板,新任务统一走 ElevenLabs,不回退 Vidu。生产配置确认、旧 Vidu 队列 drain、灰度和实际发布仍须按 T6 门禁人工执行。
|
||||
|
||||
## 入口与交互
|
||||
|
||||
@@ -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`。消费动作前按 ECMAScript `String.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`,不向前端开放滑杆或请求字段。输出格式固定为 query `output_format=mp3_44100_128`。
|
||||
|
||||
### 生成游戏背景音乐
|
||||
|
||||
@@ -39,6 +43,133 @@
|
||||
- 前端请求继续使用 `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` 的 Rust `str::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 与操作 | 操作错误 | 短促克制的错误提示音 |
|
||||
| 事件预设 | 拾取与奖励 | 金币拾取 | 金币拾取时清脆的金属叮当声 |
|
||||
| 事件预设 | 拾取与奖励 | 道具拾取 | 拾取道具时轻快的提示音 |
|
||||
| 事件预设 | 拾取与奖励 | 获得奖励 | 奖励出现时明亮的提示音 |
|
||||
| 事件预设 | 拾取与奖励 | 宝箱开启 | 金属锁扣弹开,随后响起明亮的奖励提示音 |
|
||||
| 事件预设 | 拾取与奖励 | 解锁内容 | 锁定状态解除,随后响起解锁提示音 |
|
||||
| 事件预设 | 拾取与奖励 | 稀有掉落 | 稀有物品出现时闪耀的奖励提示音 |
|
||||
| 事件预设 | 成长与结果 | 物品合成 | 两件物品融合,随后响起明亮的完成提示音 |
|
||||
| 事件预设 | 成长与结果 | 角色升级 | 能量快速上升,随后响起明亮的升级提示音 |
|
||||
| 事件预设 | 成长与结果 | 任务完成 | 任务完成提示音,随后响起简短的奖励音符 |
|
||||
| 事件预设 | 成长与结果 | 成就达成 | 明亮的成就提示音,随后响起简短的庆祝音符 |
|
||||
| 事件预设 | 成长与结果 | 挑战胜利 | 明亮的胜利提示音,随后响起短暂的庆祝音符 |
|
||||
| 事件预设 | 成长与结果 | 挑战失败 | 低沉的失败提示音 |
|
||||
| 事件预设 | 角色与战斗 | 角色跳跃 | 角色轻盈跳起的声音 |
|
||||
| 事件预设 | 角色与战斗 | 角色落地 | 角色落地时轻微的撞击声 |
|
||||
| 事件预设 | 角色与战斗 | 轻度受击 | 轻微撞击的受击声 |
|
||||
| 事件预设 | 角色与战斗 | 重度受击 | 沉重有力的撞击声 |
|
||||
| 事件预设 | 角色与战斗 | 攻击挥动 | 武器快速挥过空气的呼啸声 |
|
||||
| 事件预设 | 角色与战斗 | 攻击命中 | 武器击中目标的清晰撞击声 |
|
||||
| 事件预设 | 角色与战斗 | 格挡成功 | 武器碰撞,随后被挡开的金属声 |
|
||||
| 事件预设 | 角色与战斗 | 物体破碎 | 物体撞击地面后快速破碎的声音 |
|
||||
| 事件预设 | 技能与状态 | 技能蓄力 | 能量逐渐聚集的低沉嗡鸣声 |
|
||||
| 事件预设 | 技能与状态 | 魔法释放 | 柔和的魔法能量扩散,带有圆润空灵的闪光声 |
|
||||
| 事件预设 | 技能与状态 | 治疗恢复 | 柔和能量扩散,带有温暖圆润的提示音 |
|
||||
| 事件预设 | 技能与状态 | 护盾生成 | 能量向外展开,形成稳定的护盾声 |
|
||||
| 事件预设 | 技能与状态 | 瞬间移动 | 能量快速收缩,随后以短促的空气抽离声消失 |
|
||||
| 事件预设 | 技能与状态 | 冰冻技能 | 冰霜能量扩散,随后响起清脆的冻结声 |
|
||||
| 事件预设 | 技能与状态 | 火焰技能 | 火焰迅速喷发,带有短促的燃烧声 |
|
||||
| 事件预设 | 技能与状态 | 状态强化 | 能量逐渐上升,形成稳定明亮的提示音 |
|
||||
| 事件预设 | 机关与场景互动 | 门开启 | 门锁解除,随后厚重的木门缓慢打开 |
|
||||
| 事件预设 | 机关与场景互动 | 机关启动 | 机关解锁,随后齿轮开始转动 |
|
||||
| 事件预设 | 机关与场景互动 | 拉杆触发 | 拉杆被扳动,随后远处机关启动 |
|
||||
| 事件预设 | 机关与场景互动 | 石块移动 | 大型石块缓慢移动时低沉的摩擦声 |
|
||||
| 事件预设 | 机关与场景互动 | 传送门开启 | 能量旋转聚集,随后响起持续、空灵的传送门展开声 |
|
||||
| 事件预设 | 机关与场景互动 | 倒计时警告 | 逐渐加快的倒计时提示音 |
|
||||
| 补充要求 | 风格方向 | 休闲可爱 | 轻快可爱的卡通风格 |
|
||||
| 补充要求 | 风格方向 | 复古街机 | 复古街机风格 |
|
||||
| 补充要求 | 风格方向 | 科幻电子 | 干净的科幻电子音色 |
|
||||
| 补充要求 | 风格方向 | 奇幻魔法 | 柔和梦幻的魔法音色 |
|
||||
| 补充要求 | 风格方向 | 写实自然 | 自然真实的声音质感 |
|
||||
| 补充要求 | 风格方向 | 卡通夸张 | 夸张鲜明的卡通风格 |
|
||||
| 补充要求 | 反馈要求 | 轻柔反馈 | 轻柔克制 |
|
||||
| 补充要求 | 反馈要求 | 有力反馈 | 更有力的撞击感 |
|
||||
| 补充要求 | 反馈要求 | 短促反馈 | 短促的单次声音 |
|
||||
| 补充要求 | 反馈要求 | 两段递进 | 由弱到强的两段变化 |
|
||||
| 补充要求 | 反馈要求 | 干净突出 | 主体声音清晰,减少杂音 |
|
||||
| 补充要求 | 反馈要求 | 柔和不刺耳 | 圆润柔和,避免尖锐高频 |
|
||||
|
||||
点击预设时:
|
||||
|
||||
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`,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 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`,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` 进行唯一一次业务重试;不得把第一次候选当作新事实源。第二次仍使用 `8192` completion 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”;强类型 `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` 读取,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、首尾空白与字符口径
|
||||
@@ -125,6 +256,7 @@
|
||||
|
||||
- 点击 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、代码块、具体艺人或歌曲模仿要求。
|
||||
@@ -139,6 +271,7 @@
|
||||
### 一键简化
|
||||
|
||||
- 点击一键简化时先按统一规则规范化输入框首尾空白并同步写回;只在规范化后的当前 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、代码块、字数报告、处理过程或删改说明。它与“内部结构可解析”是两个独立校验项;程序不得另用关键词或未定义的正则推断标题、解释等语义格式。
|
||||
@@ -223,7 +356,7 @@ idle
|
||||
- generated 私有音频资源播放前必须通过 `/api/assets/read-url` 换签;画布卡片不得直接把 `/generated-*` 或 generated OSS 私有地址交给 `<audio>` 裸请求。
|
||||
- 音频元数据弹窗使用 `时长`,不使用图片 / 视频的分辨率语义;音频生成占位不显示分辨率或时长角标。
|
||||
- 音频图层右上角标签显示在信息按钮左侧,和其他素材卡右上角信息区保持一致。
|
||||
- 元数据弹窗按音频显示 `音频信息` / `音频类型` / `时长`,生成输入快照只展示用户面板字段;BGM 快照保存输入框已经写回的 canonical Prompt,不保存助手系统模板、内部引导或预设元数据。
|
||||
- 元数据弹窗按音频显示 `音频信息` / `音频类型` / `时长`。BGM 生成输入快照保存输入框已经写回的 canonical Prompt,不保存助手系统模板、内部引导或预设元数据;SFX V2 快照由服务端权威构造用户 Prompt、实际英文 Prompt、请求参数、实际时长和 Loop,不保存预设 ID、助手 envelope、撤销快照或未验收翻译候选。
|
||||
- 音频图层上方浮动工具栏只保留 `改造` 和 `下载按钮`。点击 `改造` 后打开对应的音效或背景音乐生成面板,不展示参考图组件;面板底部模型与参数位置和原生成入口一致,并允许继续修改后再次生成,新结果落在原音频旁边。
|
||||
|
||||
## 前端与 BFF 契约
|
||||
@@ -234,8 +367,9 @@ idle
|
||||
POST /api/editor/audios/sound-effects/generations
|
||||
{
|
||||
prompt: string,
|
||||
model: "audio1.0",
|
||||
duration: number
|
||||
model: "eleven_text_to_sound_v2",
|
||||
duration?: number | null,
|
||||
loop?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
@@ -247,7 +381,18 @@ POST /api/editor/audios/background-music/generations
|
||||
}
|
||||
```
|
||||
|
||||
BGM Prompt 助手新增两个登录态内部 BFF:
|
||||
SFX Prompt 助手新增一个登录态内部 BFF:
|
||||
|
||||
```ts
|
||||
POST /api/editor/audios/sound-effects/prompts/optimizations
|
||||
{
|
||||
currentPrompt: string
|
||||
}
|
||||
```
|
||||
|
||||
SFX 优化成功响应复用 `{ prompt: string, charCount: number }`;该路由不进入 External v1。
|
||||
|
||||
BGM Prompt 助手保留两个登录态内部 BFF:
|
||||
|
||||
```ts
|
||||
POST /api/editor/audios/background-music/prompts/completions
|
||||
@@ -273,15 +418,16 @@ POST /api/editor/audios/background-music/prompts/simplifications
|
||||
```
|
||||
|
||||
- 客户端不得提交 `maxChars`、`targetChars`、模型名、预设 ID、分组、颜色、内部模板或格式 / 完整性 / 残句判断;这些由服务端固定或由内部 LLM 结果产生。
|
||||
- 两个助手 BFF 不调用 Suno、钱包扣费、正式 generation queue、OSS、素材库,也不在 handler 中同步执行 SpacetimeDB 业务写入;成功路由的通用 tracking 仍由现有本机 outbox 异步承接。
|
||||
- 两个助手请求中的 `currentPrompt` 必须是前端已经写回输入框的 canonical Prompt;响应 `prompt` 也必须先执行同一 canonicalization,`charCount` 是响应 canonical Prompt 的 Unicode code point 数。
|
||||
- 三个 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 KiB` HTTP 请求体上限。请求体超过该上限时保留 Axum `413 PAYLOAD_TOO_LARGE`,不得被通用 JSON rejection 降为 `400`;字符上限与原始 body 上限分别校验,不能互相替代。
|
||||
- 三个助手路由都设置 `32 KiB` HTTP 请求体上限。请求体超过该上限时保留 Axum `413 PAYLOAD_TOO_LARGE`,不得被通用 JSON rejection 降为 `400`;字符上限与原始 body 上限分别校验,不能互相替代。
|
||||
- 不增加 Prompt 助手专属的用户级、IP 级、时间窗口或令牌桶限流,也不新增本功能主动产生的 `429` / `Retry-After`。现有 api-server 全局并发背压、前端防重复操作、Nginx 保护和上游真实 `429` 的安全映射保持不变。
|
||||
- 两个成功路由必须进入 `tracking.rs` 显式静态映射:补全使用 `event_key = editor_background_music_prompt_completion`,简化使用 `event_key = editor_background_music_prompt_simplification`;两者都使用 `module_key = editor`、User scope。普通 route tracking 继续只记录成功响应,不在助手 handler 中新增同步埋点副作用。
|
||||
- 三个成功路由必须进入 `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 分别维护。
|
||||
|
||||
统一响应:
|
||||
|
||||
@@ -297,52 +443,60 @@ POST /api/editor/audios/background-music/prompts/simplifications
|
||||
prompt: string,
|
||||
actualPrompt?: string | null,
|
||||
model: string,
|
||||
provider: string,
|
||||
taskId: string,
|
||||
priceMudPoints: number,
|
||||
audioKind: "sound-effect" | "background-music",
|
||||
durationSeconds?: number | null
|
||||
durationSeconds?: number | null,
|
||||
loop?: boolean | null
|
||||
}
|
||||
```
|
||||
|
||||
- 成功响应不携带 `provider`;该信息只保留在服务端持久化、任务追踪、日志和后台排障中。
|
||||
- BGM 成功响应必须同时填充 `prompt` 和 `actualPrompt`,两者都与本次 canonical Prompt 逐 code point 等值,不得携带助手模板、内部引导或隐藏前后缀;共享响应类型为兼容 SFX 与历史数据仍可保留 `actualPrompt` 可选。
|
||||
- SFX V2 成功响应的 `prompt` 必须等于 canonical `userPrompt`,`actualPrompt` 必须等于验收通过且实际提交给 ElevenLabs 的英文 Prompt,`durationSeconds` 必须来自 MP3 探测,`loop` 必须等于本次冻结并发送的布尔值。
|
||||
- 默认 queue 模式的首次生成响应继续只携带现有 `queueState`;上面的完整音频响应是 inline 或 worker 内部完成边界,不要求给 queue 首次响应或普通 metadata-only job result 新增 Prompt 字段。
|
||||
|
||||
## 后端实现
|
||||
|
||||
- 在 `shared-contracts/src/assets.rs` 增加编辑器音频请求 / 响应 DTO。
|
||||
- 在 `shared-contracts` 增加最小 BGM Prompt 助手请求 / 响应 DTO;前后端共享 `currentPrompt`、`prompt` 和 `charCount` 字段,不把内部 LLM 判断暴露给客户端。
|
||||
本节是对现有编辑器音频链路的原地演进说明,不是新建入口清单。正式生成继续复用 `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 语义并复用同一组跨语言测试向量:只删除首尾 Unicode `White_Space`,保留内部空白和全部其它 code point。该操作必须幂等,不得直接混用语义不同的 TypeScript / Rust 原生 `trim`。
|
||||
- 在 `platform-audio` 增加编辑器专用 body builder 和 submit 函数:
|
||||
- 在 `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`。
|
||||
- 音效 body 使用 Vidu 文生音频契约:提交 `/ent/v2/text2audio`,请求体包含 `model: "audio1.0"`、`prompt`、`sound: prompt`、`duration` 和可选 `seed`;`model` 和 `prompt` 为文档必填,`sound` 用于兼容线上网关实际校验,`prompt` 最长 1500 字符,`duration` 按 Vidu 文档限制在 `2-10` 秒。
|
||||
- 编辑器音效轮询使用 Vidu 路径 `/ent/v2/tasks/{taskId}/creations`,不再使用 Suno `/suno/fetch/{taskId}`;Suno 文生音效 `task: "sound"` 暂不从编辑器入口暴露。
|
||||
- 新编辑器音效使用独立 ElevenLabs 直接二进制 adapter,不伪装成 Vidu / Suno 的 submit + poll 任务。adapter 负责 endpoint 归一、`xi-api-key` header、固定 query / body、单次 POST、有界二进制读取、MP3 验证和时长探测。
|
||||
- Vidu `audio1.0` 的 body builder、轮询和下载能力仅保留给历史展示和其它未迁移调用方;新 `audio-sound-effect` 任务不进入 `/ent/v2/text2audio` 或 `/ent/v2/tasks/{taskId}/creations`,也不使用 Suno `task: "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 时后端错误信息应透出安全的上游状态和短响应摘要,避免前端只显示笼统提交失败。
|
||||
- 在 `api-server` 增加编辑器音频 BFF:
|
||||
- 在 `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:
|
||||
- 保留并复用 `api-server` 现有登录态内部 BGM Prompt 助手 BFF:
|
||||
- `POST /api/editor/audios/background-music/prompts/completions`
|
||||
- `POST /api/editor/audios/background-music/prompts/simplifications`
|
||||
- 在 `api-server` 增加登录态内部 SFX Prompt 优化 BFF `POST /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 KiB` body limit,并在 `tracking.rs` 中注册上述 User-scope 成功事件;不增加助手专属限流器、本地额度计数或功能级 `429`。
|
||||
- 三个助手路由使用各自的 `32 KiB` body limit,并在 `tracking.rs` 中注册上述 User-scope 成功事件;不增加助手专属限流器、本地额度计数或功能级 `429`。
|
||||
- Prompt 助手继续复用 `LlmClient` 现有失败原文日志行为。本需求不增加请求级日志开关、脱敏、metadata-only 模式或相关上线门禁。
|
||||
- BGM generation BFF 在入站时防御性执行同一幂等 canonicalization,规范化后校验有效字符和 200 字限制。登录态站内 handler 在 queue / inline 分流前把 canonical Prompt 和固定 `make_instrumental:true` 写入本次请求值,确保正式 generation queue 请求载荷、持久化记录和内部完成响应等值;删除空 Prompt 默认回退。该站内收口不改变 External v1 的路由、OpenAPI、Idempotency-Key 或 payload 等值语义。助手模板只用于生成输入框可见候选,不得进入正式队列或 Suno 请求。SFX 继续使用现有规范化、回退、Vidu body 和 1500 字限制。
|
||||
- BFF 复用现有 `vector_engine_audio_generation` 的任务轮询、下载、OSS 持久化和计费包装。客户端不提交 BGM `priceMudPoints`;服务端按现役动态定价配置解析并在入队时冻结本次价格,后续计费、资产成本和内部完成响应复用该冻结值。T5 不修改价格或计费规则。
|
||||
- 现有 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-2048` code point、固定模型、nullable duration 和 Loop 校验,再构造 canonical queue payload 与冻结价格。Worker 负责翻译、单次 ElevenLabs、MP3 验证 / 时长探测、OSS 与权威写回,并复用现有预扣、退款、lease 和 fencing 边界。
|
||||
- 客户端不提交音频 `priceMudPoints`;服务端按现役动态定价配置解析并在入队时冻结本次价格,后续计费、资产成本和内部完成响应复用该冻结值。BGM 价格不变;SFX V2 以新模型键保持 5 泥点 / 次。
|
||||
- 生成音频持久化后返回 OSS `objectKey` 与 `assetObjectId`;前端保存素材库时继续使用 `audioSrc` 作为兼容路径,并把 OSS 身份写入素材记录。
|
||||
- 本切片不修改 SpacetimeDB schema,不新增 Prompt 助手持久化表,不向 `/api/external/v1` 暴露助手,也不修改 External v1 OpenAPI 或 `platform-llm` 日志策略。
|
||||
- SFX V2 不修改 SpacetimeDB schema,不新增 Prompt 助手持久化表,不向 `/api/external/v1` 暴露助手,也不修改 `platform-llm` 日志策略;正式 External v1 SFX 生成请求 / 响应的 OpenAPI 必须在 T5 与 Rust DTO 同批更新。
|
||||
|
||||
## 验收
|
||||
|
||||
- 底部工具栏显示 `生成音乐`。
|
||||
- 点击 `生成音乐` 只出现选项框,不立刻创建占位。
|
||||
- 点击 `生成游戏音效` 后出现音效面板,文本字段为 `prompt`;底部左侧只有一个无标题时长参数按钮,右侧为固定 `Vidu` 模型胶囊和生成按钮。
|
||||
- 音效时长使用滑动条选择 `2-10` 秒,步进 `1` 秒,默认 `5` 秒,提交到 BFF 的字段为 `duration`。
|
||||
- 音效面板模型显示 `Vidu`,提交 `model: "audio1.0"`;后端转发到 Vidu 时同时携带 `prompt` 和 `sound`;不显示 Suno 文生音效模型或 Suno 音效入口。
|
||||
- 点击 `生成游戏音效` 后出现 SFX V2 面板:文本字段为 `prompt`,显示 `0 / 2048` Unicode code point 计数、52 个固定预设、一键优化、单层撤销、自动 / 手动时长和 Loop,右侧固定显示 `ElevenLabs` 模型胶囊和生成按钮。
|
||||
- SFX 首次打开自动时长开启、预置手动时长为 `5s`、Loop 为 false;手动 slider 范围 `0.5-30s`、步进 `0.1s`,自动模式禁用 slider 并保留最近手动值。
|
||||
- SFX 面板提交 `model: "eleven_text_to_sound_v2"`、canonical `prompt`、`duration: null | number` 和 `loop: boolean`;服务端只向 ElevenLabs 发送 Worker 验收后的英文 `actualPrompt`,不再调用 Vidu 或 Suno 音效契约。
|
||||
- SFX canonical 边界测试覆盖首尾 `U+FEFF` 删除、内部 `U+FEFF` 保留、首尾 `U+0085` 保留、内部空白不折叠,以及 `1 / 2048 / 2049` Unicode 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`。
|
||||
- 成功后画布新增音频卡,能通过卡片中央播放按钮播放,底部进度、时间和音量控件可操作。
|
||||
@@ -372,6 +526,11 @@ POST /api/editor/audios/background-music/prompts/simplifications
|
||||
- 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_simplification` tracking event,均为 `module_key = editor`、User scope;失败响应沿用普通 route tracking 只记录成功的现状。
|
||||
- BGM Suno body 仍只包含 `mv`、`gpt_description_prompt`、`make_instrumental`,固定 `Suno` 胶囊和动态泥点价格不变;SFX 的 Vidu body、默认 Prompt、时长与 1500 字限制无回归。
|
||||
- `audio-sound-effect` 与 `audio-background-music` 必须由同一个音频 composer 渲染,并在组件内通过 `isSoundEffect` 分支。SFX 不得渲染 BGM 控件或尚未实施的 SFX V2 控件,BGM 不得渲染 Vidu 与音效时长控件;两个 mode 相互切换时,菜单、预设滚动、锁和助手状态不得跨分支泄漏。
|
||||
- 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`。
|
||||
|
||||
@@ -6,3 +6,38 @@ export type BackgroundMusicPromptAssistResponse = {
|
||||
prompt: string;
|
||||
charCount: number;
|
||||
};
|
||||
|
||||
export type SoundEffectPromptOptimizeRequest = {
|
||||
currentPrompt: string;
|
||||
};
|
||||
|
||||
export type SoundEffectPromptOptimizeResponse = {
|
||||
prompt: string;
|
||||
charCount: number;
|
||||
};
|
||||
|
||||
export const EDITOR_SOUND_EFFECT_MODEL = 'eleven_text_to_sound_v2' as const;
|
||||
export const SOUND_EFFECT_DURATION_MIN_SECONDS = 0.5;
|
||||
export const SOUND_EFFECT_DURATION_MAX_SECONDS = 30;
|
||||
|
||||
export type EditorSoundEffectModel = typeof EDITOR_SOUND_EFFECT_MODEL;
|
||||
|
||||
export type EditorSoundEffectDurationMode = 'auto' | 'manual';
|
||||
|
||||
export type EditorSoundEffectGenerationRequest = {
|
||||
prompt: string;
|
||||
model: EditorSoundEffectModel;
|
||||
duration?: number | null;
|
||||
loop?: boolean;
|
||||
};
|
||||
|
||||
export type EditorSoundEffectGenerationMetadataV2 = {
|
||||
schemaVersion: 2;
|
||||
userPrompt: string;
|
||||
actualPrompt: string;
|
||||
model: EditorSoundEffectModel;
|
||||
durationMode: EditorSoundEffectDurationMode;
|
||||
requestedDurationSeconds: number | null;
|
||||
actualDurationSeconds: number;
|
||||
loop: boolean;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
[
|
||||
{
|
||||
"name": "plain-ascii-boundary-space",
|
||||
"input": " thunder crack ",
|
||||
"prompt": "thunder crack",
|
||||
"charCount": 13
|
||||
},
|
||||
{
|
||||
"name": "ecmascript-trim-keeps-u0085-boundary",
|
||||
"input": "\u0085\u2003金币叮当\u00a0\r\n",
|
||||
"prompt": "\u0085\u2003金币叮当",
|
||||
"charCount": 6
|
||||
},
|
||||
{
|
||||
"name": "internal-white-space-and-u0085-preserved",
|
||||
"input": "\u2003A \n B\u0085",
|
||||
"prompt": "A \n B\u0085",
|
||||
"charCount": 6
|
||||
},
|
||||
{
|
||||
"name": "bom-is-trimmed-at-boundaries-and-preserved-internally",
|
||||
"input": "\ufeff\u200b音\ufeff效\ufeff",
|
||||
"prompt": "\u200b音\ufeff效",
|
||||
"charCount": 4
|
||||
},
|
||||
{
|
||||
"name": "combining-mark-preserved",
|
||||
"input": "\u2003e\u0301\u00a0",
|
||||
"prompt": "e\u0301",
|
||||
"charCount": 2
|
||||
},
|
||||
{
|
||||
"name": "zwj-emoji-counts-code-points",
|
||||
"input": "\n👩💻\r",
|
||||
"prompt": "👩💻",
|
||||
"charCount": 3
|
||||
},
|
||||
{
|
||||
"name": "ecmascript-trim-only",
|
||||
"input": "\u0009\u000a\u000b\u000c\u000d\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff",
|
||||
"prompt": "",
|
||||
"charCount": 0
|
||||
},
|
||||
{
|
||||
"name": "empty",
|
||||
"input": "",
|
||||
"prompt": "",
|
||||
"charCount": 0
|
||||
},
|
||||
{
|
||||
"name": "one-code-point-boundary",
|
||||
"input": "声",
|
||||
"prompt": "声",
|
||||
"charCount": 1,
|
||||
"validation": "valid"
|
||||
},
|
||||
{
|
||||
"name": "u0085-is-a-valid-code-point",
|
||||
"input": "\u0085",
|
||||
"prompt": "\u0085",
|
||||
"charCount": 1,
|
||||
"validation": "valid"
|
||||
},
|
||||
{
|
||||
"name": "exactly-2048-code-points",
|
||||
"prefix": "\ufeff",
|
||||
"input": "声",
|
||||
"suffix": "\u2003",
|
||||
"prompt": "声",
|
||||
"repeat": 2048,
|
||||
"charCount": 2048,
|
||||
"validation": "valid"
|
||||
},
|
||||
{
|
||||
"name": "over-limit-2049-code-points",
|
||||
"input": "声",
|
||||
"prompt": "声",
|
||||
"repeat": 2049,
|
||||
"charCount": 2049,
|
||||
"validation": "too-long"
|
||||
}
|
||||
]
|
||||
Generated
+62
@@ -1588,6 +1588,15 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-as-inner"
|
||||
version = "0.6.1"
|
||||
@@ -4065,9 +4074,13 @@ dependencies = [
|
||||
name = "platform-audio"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"platform-oss",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde_json",
|
||||
"symphonia",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"urlencoding",
|
||||
@@ -5851,6 +5864,55 @@ version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "symphonia"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"symphonia-bundle-mp3",
|
||||
"symphonia-core",
|
||||
"symphonia-metadata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-bundle-mp3"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"symphonia-core",
|
||||
"symphonia-metadata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-core"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bitflags 1.3.2",
|
||||
"bytemuck",
|
||||
"lazy_static",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-metadata"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"symphonia-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
|
||||
@@ -112,6 +112,7 @@ pingora-http = { version = "0.8.1", default-features = false }
|
||||
pingora-proxy = { version = "0.8.1", default-features = false }
|
||||
rand_core = "0.6"
|
||||
reqwest = { version = "0.12", default-features = false }
|
||||
regex = "1"
|
||||
rmcp = { version = "=2.2.0", default-features = false }
|
||||
ring = "0.17"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
@@ -120,6 +121,7 @@ serde_urlencoded = "0.7"
|
||||
sha1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
socket2 = "0.6"
|
||||
symphonia = { version = "0.5", default-features = false, features = ["mp3"] }
|
||||
spacetimedb = "=2.7.0"
|
||||
spacetimedb-sdk = "=2.7.0"
|
||||
spacetimedb-lib = { version = "=2.7.0", default-features = false }
|
||||
|
||||
@@ -67,6 +67,10 @@
|
||||
"unit": "perGeneration",
|
||||
"price": 5
|
||||
},
|
||||
"eleven_text_to_sound_v2": {
|
||||
"unit": "perGeneration",
|
||||
"price": 5
|
||||
},
|
||||
"chirp-v5": {
|
||||
"unit": "perGeneration",
|
||||
"price": 12
|
||||
|
||||
@@ -5394,6 +5394,10 @@ mod tests {
|
||||
payload["models"]["audio1.0"]["price"],
|
||||
Value::Number(5.into())
|
||||
);
|
||||
assert_eq!(
|
||||
payload["models"]["eleven_text_to_sound_v2"]["price"],
|
||||
Value::Number(5.into())
|
||||
);
|
||||
assert_eq!(
|
||||
payload["models"]["chirp-v5"]["price"],
|
||||
Value::Number(12.into())
|
||||
@@ -5460,6 +5464,7 @@ mod tests {
|
||||
"prices": { "480p": 11, "720p": 22, "1080p": 44 }
|
||||
},
|
||||
"audio1.0": { "unit": "perGeneration", "price": 15 },
|
||||
"eleven_text_to_sound_v2": { "unit": "perGeneration", "price": 16 },
|
||||
"chirp-v5": { "unit": "perGeneration", "price": 9 }
|
||||
}
|
||||
})
|
||||
@@ -5498,6 +5503,10 @@ mod tests {
|
||||
payload["models"]["audio1.0"]["unit"],
|
||||
Value::String("perGeneration".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
payload["models"]["eleven_text_to_sound_v2"]["price"],
|
||||
Value::Number(16.into())
|
||||
);
|
||||
assert_eq!(
|
||||
payload["models"]["seedance2.0"]["prices"]["720p"],
|
||||
Value::Number(26.into())
|
||||
|
||||
@@ -17,6 +17,7 @@ const DEFAULT_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS: u64 = 600;
|
||||
const DEFAULT_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS: u64 = 900;
|
||||
const DEFAULT_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS: u64 = 1_800;
|
||||
pub(crate) const DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS: u64 = 1_000_000;
|
||||
pub(crate) const DEFAULT_ELEVENLABS_REQUEST_TIMEOUT_MS: u64 = 180_000;
|
||||
const DEFAULT_EDITOR_BGFILTER_BASE_URL: &str = "http://58.87.105.82/bgfilter";
|
||||
const DEFAULT_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS: u64 = 5_000;
|
||||
const BGFILTER_ATTEMPT_SAFETY_FACTOR: u64 = 2;
|
||||
@@ -189,6 +190,9 @@ pub struct AppConfig {
|
||||
pub vector_engine_api_key: Option<String>,
|
||||
pub vector_engine_image_request_timeout_ms: u64,
|
||||
pub vector_engine_audio_request_timeout_ms: u64,
|
||||
pub elevenlabs_base_url: String,
|
||||
pub elevenlabs_api_key: Option<String>,
|
||||
pub elevenlabs_request_timeout_ms: u64,
|
||||
pub hyper3d_base_url: String,
|
||||
pub hyper3d_api_key: Option<String>,
|
||||
pub hyper3d_model_request_timeout_ms: u64,
|
||||
@@ -486,6 +490,9 @@ impl Default for AppConfig {
|
||||
vector_engine_api_key: None,
|
||||
vector_engine_image_request_timeout_ms: DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS,
|
||||
vector_engine_audio_request_timeout_ms: 180_000,
|
||||
elevenlabs_base_url: String::new(),
|
||||
elevenlabs_api_key: None,
|
||||
elevenlabs_request_timeout_ms: DEFAULT_ELEVENLABS_REQUEST_TIMEOUT_MS,
|
||||
hyper3d_base_url: "https://api.hyper3d.com/api/v2".to_string(),
|
||||
hyper3d_api_key: None,
|
||||
hyper3d_model_request_timeout_ms: 180_000,
|
||||
@@ -1204,6 +1211,16 @@ impl AppConfig {
|
||||
config.vector_engine_audio_request_timeout_ms = vector_engine_audio_request_timeout_ms;
|
||||
}
|
||||
|
||||
if let Some(elevenlabs_base_url) = read_first_non_empty_env(&["ELEVENLABS_BASE_URL"]) {
|
||||
config.elevenlabs_base_url = elevenlabs_base_url;
|
||||
}
|
||||
config.elevenlabs_api_key = read_first_non_empty_env(&["ELEVENLABS_API_KEY"]);
|
||||
if let Some(elevenlabs_request_timeout_ms) =
|
||||
read_first_positive_u64_env(&["ELEVENLABS_REQUEST_TIMEOUT_MS"])
|
||||
{
|
||||
config.elevenlabs_request_timeout_ms = elevenlabs_request_timeout_ms;
|
||||
}
|
||||
|
||||
if let Some(hyper3d_base_url) =
|
||||
read_first_non_empty_env(&["HYPER3D_BASE_URL", "RODIN_BASE_URL"])
|
||||
{
|
||||
@@ -1631,7 +1648,7 @@ mod tests {
|
||||
AppConfig, DEFAULT_EDITOR_BGFILTER_BASE_URL,
|
||||
DEFAULT_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS,
|
||||
DEFAULT_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD,
|
||||
DEFAULT_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS,
|
||||
DEFAULT_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS, DEFAULT_ELEVENLABS_REQUEST_TIMEOUT_MS,
|
||||
DEFAULT_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS,
|
||||
DEFAULT_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS,
|
||||
DEFAULT_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS, ExternalGenerationMode,
|
||||
@@ -1841,6 +1858,55 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_elevenlabs_settings_fail_closed_with_a_bounded_timeout() {
|
||||
let config = AppConfig::default();
|
||||
|
||||
assert!(config.elevenlabs_base_url.is_empty());
|
||||
assert!(config.elevenlabs_api_key.is_none());
|
||||
assert_eq!(
|
||||
config.elevenlabs_request_timeout_ms,
|
||||
DEFAULT_ELEVENLABS_REQUEST_TIMEOUT_MS
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_env_reads_elevenlabs_settings() {
|
||||
let _guard = ENV_LOCK
|
||||
.get_or_init(|| Mutex::new(()))
|
||||
.lock()
|
||||
.expect("env lock should not poison");
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("ELEVENLABS_BASE_URL");
|
||||
std::env::remove_var("ELEVENLABS_API_KEY");
|
||||
std::env::remove_var("ELEVENLABS_REQUEST_TIMEOUT_MS");
|
||||
std::env::set_var(
|
||||
"ELEVENLABS_BASE_URL",
|
||||
"https://elevenlabs.internal.example/v1",
|
||||
);
|
||||
std::env::set_var("ELEVENLABS_API_KEY", "elevenlabs-test-key");
|
||||
std::env::set_var("ELEVENLABS_REQUEST_TIMEOUT_MS", "190000");
|
||||
}
|
||||
|
||||
let config = AppConfig::from_env();
|
||||
assert_eq!(
|
||||
config.elevenlabs_base_url,
|
||||
"https://elevenlabs.internal.example/v1"
|
||||
);
|
||||
assert_eq!(
|
||||
config.elevenlabs_api_key.as_deref(),
|
||||
Some("elevenlabs-test-key")
|
||||
);
|
||||
assert_eq!(config.elevenlabs_request_timeout_ms, 190_000);
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("ELEVENLABS_BASE_URL");
|
||||
std::env::remove_var("ELEVENLABS_API_KEY");
|
||||
std::env::remove_var("ELEVENLABS_REQUEST_TIMEOUT_MS");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_env_reads_non_public_models_and_urls() {
|
||||
let _guard = ENV_LOCK
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user