统一SFX提示词 trim 规范化
将 SFX Prompt 首尾规范化改为 ECMAScript String.trim 语义并同步 Rust 等值实现 更新共享边界 fixture、前后端与 Worker 回归测试 同步权威设计、实施计划、决策日志和 External OpenAPI 描述
This commit is contained in:
@@ -4456,7 +4456,7 @@
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 2048,
|
||||
"description": "用户原始语言音效描述。服务端只删除首尾 Unicode White_Space,并按 Unicode code point 校验 1-2048。"
|
||||
"description": "用户原始语言音效描述。服务端按 ECMAScript String.trim() 语义删除首尾空白和行终止符(包括 U+FEFF,保留首尾 U+0085),再按 Unicode code point 校验 1-2048。"
|
||||
},
|
||||
"model": {
|
||||
"type": [
|
||||
|
||||
@@ -87,7 +87,7 @@ T0 已通过,T1–T5 可以按本文依赖顺序进入实现;T0 通过不表
|
||||
### Prompt 与 LLM 口径
|
||||
|
||||
- `userPrompt` 和 `actualPrompt` 上限均为 2048 Unicode code points。
|
||||
- 只删除首尾 Unicode `White_Space`,不做 NFC、内部空白折叠、换行转换、标点替换或静默截断。
|
||||
- 只按 ECMAScript `String.trim()` 删除首尾空白和行终止符,包含 `U+FEFF`、保留首尾 `U+0085`;不做 NFC、内部空白折叠、换行转换、标点替换或静默截断。
|
||||
- 一键优化固定 `gpt-5.6-luna`、`reasoning_effort = medium`;Worker 翻译固定同模型、`reasoning_effort = low`。两类请求分别按各自 2048 Unicode code point 候选上限的 4 倍,固定 `max_output_tokens = 8192`;它是隐藏 reasoning token 与可见 JSON 输出 token 共享的 completion 总预算,不是可见正文保证,也不按实际输入长度缩小。两者均不发送 temperature 或 function tools。
|
||||
- 翻译 envelope 固定 `prompt / isEnglish / isFaithfulTranslation / isDirectGenerationFormat / hasAddedOrRemovedRequirement`,只接受完整 `response.text` 中的唯一 JSON object。
|
||||
- `isEnglish = true` 作为 LLM 语义判断,程序侧另外要求:候选至少含一个 Script=Latin 的 alphabetic code point,且所有 alphabetic code point 的 Script 均为 Latin;Common / Inherited 数字、标点、空白和符号允许。
|
||||
@@ -130,7 +130,7 @@ External v1 `model` 先删除首尾 Unicode `White_Space`,再按大小写敏
|
||||
|
||||
### T1:Prompt 规则、52 预设、共享契约与 metadata 基础
|
||||
|
||||
- 实现前后端 Unicode canonicalization、code point 计数、2048 边界和无默认 Prompt 回退。
|
||||
- 实现前后端 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。
|
||||
@@ -139,6 +139,8 @@ External v1 `model` 先删除首尾 Unicode `White_Space`,再按大小写敏
|
||||
|
||||
补充验收(`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 + `max_output_tokens = 8192` completion 总预算,32 KiB body limit,严格唯一 JSON envelope,不调用音频 provider 或正式计费。
|
||||
@@ -207,7 +209,7 @@ T5 -> T6
|
||||
|
||||
| 层级 | 必要覆盖 |
|
||||
| --- | --- |
|
||||
| canonical | 空 / 全 Unicode White_Space、U+0085、U+200B、U+FEFF、换行、组合字符、ZWJ emoji、2048 / 2049 |
|
||||
| canonical | 空 / 全 ECMAScript trim 字符(含 U+FEFF)、首尾 U+0085 保留、U+200B、内部 U+FEFF、换行、组合字符、ZWJ emoji、2048 / 2049 |
|
||||
| 预设 | 40 + 12、ID / label 唯一、文案等值、逗号追加、重复、清快照、超限保文 |
|
||||
| 优化 | Luna + Medium + `max_output_tokens=8192` completion 总预算请求体、唯一 JSON、布尔门禁、length 直接失败、content_filter、无 tool call、无候选泄漏、dialog / scope 迟到响应 |
|
||||
| 翻译 | 每轮 Luna + Low + `max_output_tokens=8192` completion 总预算请求体,中文 / 英文 / 中英混合输入,日文 / 韩文 / 西里尔候选,isEnglish + Script 门禁,2048 / 2049,首轮 length 唯一重试、第二轮 length 最终失败且 provider 0 次 |
|
||||
|
||||
@@ -6774,3 +6774,9 @@
|
||||
- 定价兼容:默认配置、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。
|
||||
|
||||
@@ -28,7 +28,7 @@ SFX V2 已完成产品与技术口径冻结,T0 已通过,T1–T5 已完成
|
||||
|
||||
### 生成游戏音效
|
||||
|
||||
- `prompt`:用户在输入框确认的原始语言音效描述,语义为 `userPrompt`,继续复用对外请求和持久化字段 `prompt`。消费动作前只删除首尾 Unicode `White_Space` code point,不改写内部空白、换行、标点、零宽字符或 Unicode 形式;按 Unicode code point 计数,合法范围为 `1-2048`。空值和全空白不得回退“游戏音效”。
|
||||
- `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 精度限制。
|
||||
@@ -47,8 +47,8 @@ SFX V2 已完成产品与技术口径冻结,T0 已通过,T1–T5 已完成
|
||||
|
||||
### Prompt 规范化、计数与字段语义
|
||||
|
||||
- `canonicalUserPrompt = 删除当前输入首尾属于 Unicode White_Space 属性的 code point`。TypeScript 使用 `\p{White_Space}` 边界判定,Rust 使用 `char::is_whitespace`;不得直接使用会额外删除 `U+FEFF` 的 JavaScript `String.trim()` 代替该契约。
|
||||
- 内部空格、CR / LF、标点、`U+200B`、`U+FEFF`、组合字符和 ZWJ emoji 原样保留;不做 NFC、空白折叠、换行转换、标点替换或静默截断。`U+0085` 在首尾属于应删除空白。
|
||||
- `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`。
|
||||
@@ -490,6 +490,7 @@ POST /api/editor/audios/background-music/prompts/simplifications
|
||||
- 点击 `生成游戏音效` 后出现 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`。
|
||||
|
||||
@@ -6,22 +6,22 @@
|
||||
"charCount": 13
|
||||
},
|
||||
{
|
||||
"name": "unicode-white-space-boundary",
|
||||
"name": "ecmascript-trim-keeps-u0085-boundary",
|
||||
"input": "\u0085\u2003金币叮当\u00a0\r\n",
|
||||
"prompt": "金币叮当",
|
||||
"charCount": 4
|
||||
"prompt": "\u0085\u2003金币叮当",
|
||||
"charCount": 6
|
||||
},
|
||||
{
|
||||
"name": "internal-white-space-preserved",
|
||||
"name": "internal-white-space-and-u0085-preserved",
|
||||
"input": "\u2003A \n B\u0085",
|
||||
"prompt": "A \n B",
|
||||
"charCount": 5
|
||||
"prompt": "A \n B\u0085",
|
||||
"charCount": 6
|
||||
},
|
||||
{
|
||||
"name": "zero-width-and-bom-preserved",
|
||||
"input": "\u0085\u200b\ufeff\u0085",
|
||||
"prompt": "\u200b\ufeff",
|
||||
"charCount": 2
|
||||
"name": "bom-is-trimmed-at-boundaries-and-preserved-internally",
|
||||
"input": "\ufeff\u200b音\ufeff效\ufeff",
|
||||
"prompt": "\u200b音\ufeff效",
|
||||
"charCount": 4
|
||||
},
|
||||
{
|
||||
"name": "combining-mark-preserved",
|
||||
@@ -36,8 +36,8 @@
|
||||
"charCount": 3
|
||||
},
|
||||
{
|
||||
"name": "unicode-white-space-only",
|
||||
"input": "\u0085\u2003\u00a0\r\n",
|
||||
"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
|
||||
},
|
||||
@@ -54,9 +54,16 @@
|
||||
"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": "\u0085",
|
||||
"prefix": "\ufeff",
|
||||
"input": "声",
|
||||
"suffix": "\u2003",
|
||||
"prompt": "声",
|
||||
|
||||
@@ -228,11 +228,11 @@ mod tests {
|
||||
#[test]
|
||||
fn optimization_input_uses_sfx_canonical_boundaries() {
|
||||
assert_eq!(
|
||||
validate_sound_effect_optimization_input("\u{0085}金币落地\u{2003}")
|
||||
validate_sound_effect_optimization_input("\u{feff}金币落地\u{2003}")
|
||||
.expect("canonical prompt should pass"),
|
||||
"金币落地"
|
||||
);
|
||||
assert!(validate_sound_effect_optimization_input(" \n\u{0085}").is_err());
|
||||
assert!(validate_sound_effect_optimization_input(" \n\u{feff}").is_err());
|
||||
assert!(
|
||||
validate_sound_effect_optimization_input(
|
||||
&"声".repeat(platform_audio::SOUND_EFFECT_PROMPT_MAX_CODE_POINTS)
|
||||
@@ -270,7 +270,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn optimization_candidate_accepts_only_full_strict_json_with_han() {
|
||||
let valid = valid_optimization_candidate("\u{0085}清脆的金币落地声\u{2003}");
|
||||
let valid = valid_optimization_candidate("\u{feff}清脆的金币落地声\u{2003}");
|
||||
assert_eq!(
|
||||
inspect_sound_effect_optimization_candidate_text(&format!("\n{valid}\t"), false),
|
||||
Some(SoundEffectPromptOptimizeResponse {
|
||||
@@ -346,7 +346,7 @@ mod tests {
|
||||
"hasGenerationParameterContent": false,
|
||||
}),
|
||||
json!({
|
||||
"prompt": " \u{0085} ",
|
||||
"prompt": " \u{feff} ",
|
||||
"isDirectWritebackFormat": true,
|
||||
"isContentComplete": true,
|
||||
"hasObviousFragment": false,
|
||||
@@ -394,7 +394,7 @@ mod tests {
|
||||
let (status, _) = post_raw_prompt_request(app.clone(), Some(&token), body, true).await;
|
||||
assert_eq!(status, StatusCode::BAD_REQUEST);
|
||||
}
|
||||
let (status, _) = post_prompt_request(app.clone(), Some(&token), " \u{0085}", true).await;
|
||||
let (status, _) = post_prompt_request(app.clone(), Some(&token), " \u{feff}", true).await;
|
||||
assert_eq!(status, StatusCode::BAD_REQUEST);
|
||||
let (status, _) = post_prompt_request(app, Some(&token), "金币落地", true).await;
|
||||
assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);
|
||||
@@ -426,7 +426,7 @@ mod tests {
|
||||
let mock = spawn_mock_llm_server(vec![MockLlmResponse {
|
||||
status_line: "200 OK",
|
||||
body: chat_text_response(
|
||||
valid_optimization_candidate("\u{0085}清脆明亮的金币落地声\u{2003}"),
|
||||
valid_optimization_candidate("\u{feff}清脆明亮的金币落地声\u{2003}"),
|
||||
Some("stop"),
|
||||
),
|
||||
}]);
|
||||
@@ -438,7 +438,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let (status, payload) =
|
||||
post_prompt_request(app, Some(&token), "\u{0085}金币落地\u{2003}", true).await;
|
||||
post_prompt_request(app, Some(&token), "\u{feff}金币落地\u{2003}", true).await;
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
assert_eq!(payload["ok"], true);
|
||||
assert_eq!(payload["data"]["prompt"], "清脆明亮的金币落地声");
|
||||
|
||||
@@ -1160,7 +1160,7 @@ mod tests {
|
||||
let mut payloads = Vec::new();
|
||||
for model in accepted_models {
|
||||
let prepared = prepare_editor_sound_effect_queue_job(
|
||||
sound_effect_payload("\u{0085}按钮点击声\u{2003}", model, None, false),
|
||||
sound_effect_payload("\u{feff}按钮点击声\u{2003}", model, None, false),
|
||||
&pricing,
|
||||
)
|
||||
.expect("accepted model form should prepare");
|
||||
|
||||
+2
-2
@@ -184,7 +184,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn translation_candidate_accepts_only_full_strict_latin_json() {
|
||||
let valid = valid_translation_candidate("\u{0085}Clean metallic coin pickup.\u{2003}");
|
||||
let valid = valid_translation_candidate("\u{feff}Clean metallic coin pickup.\u{2003}");
|
||||
assert_eq!(
|
||||
inspect_sound_effect_translation_candidate_text(&format!("\n{valid}\t"), false)
|
||||
.as_deref(),
|
||||
@@ -370,7 +370,7 @@ mod tests {
|
||||
state
|
||||
.editor_agent_llm_client()
|
||||
.expect("client should exist"),
|
||||
"\u{0085}金币拾取\u{2003}",
|
||||
"\u{feff}金币拾取\u{2003}",
|
||||
)
|
||||
.await
|
||||
.expect("second business round should pass");
|
||||
|
||||
@@ -289,10 +289,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_prompt_helper_still_preserves_non_whitespace_invisibles() {
|
||||
fn canonical_prompt_helper_uses_ecmascript_trim_boundaries() {
|
||||
assert_eq!(
|
||||
crate::canonicalize_sound_effect_prompt("\u{0085}\u{feff}sound\u{200b}\u{0085}"),
|
||||
"\u{feff}sound\u{200b}"
|
||||
crate::canonicalize_sound_effect_prompt("\u{feff}sound\u{200b}\u{feff}"),
|
||||
"sound\u{200b}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,29 @@ pub struct ValidatedSoundEffectPrompt<'a> {
|
||||
pub char_count: usize,
|
||||
}
|
||||
|
||||
/// 只删除首尾 Unicode `White_Space`,与前端 SFX canonicalizer 保持一致。
|
||||
/// 按 ECMAScript `String.trim()` 语义删除首尾空白和行终止符,与前端 SFX
|
||||
/// canonicalizer 保持一致。内部空白和其它 Unicode scalar value 均保持原样。
|
||||
///
|
||||
/// Rust `char::is_whitespace` 覆盖 U+0085 等 White_Space,同时不会误删
|
||||
/// U+200B 与 U+FEFF。内部空白和其它 Unicode scalar value 均保持原样。
|
||||
/// Rust 原生 `str::trim()` 与 JavaScript 的集合不同:前者会删除 U+0085、保留
|
||||
/// U+FEFF;这里必须反过来,删除首尾 U+FEFF、保留首尾 U+0085。
|
||||
pub fn canonicalize_sound_effect_prompt(prompt: &str) -> &str {
|
||||
prompt.trim_matches(char::is_whitespace)
|
||||
prompt.trim_matches(is_ecmascript_trim_boundary)
|
||||
}
|
||||
|
||||
fn is_ecmascript_trim_boundary(character: char) -> bool {
|
||||
matches!(
|
||||
character,
|
||||
'\u{0009}'..='\u{000D}'
|
||||
| '\u{0020}'
|
||||
| '\u{00A0}'
|
||||
| '\u{1680}'
|
||||
| '\u{2000}'..='\u{200A}'
|
||||
| '\u{2028}'..='\u{2029}'
|
||||
| '\u{202F}'
|
||||
| '\u{205F}'
|
||||
| '\u{3000}'
|
||||
| '\u{FEFF}'
|
||||
)
|
||||
}
|
||||
|
||||
pub fn sound_effect_prompt_code_point_count(prompt: &str) -> usize {
|
||||
|
||||
@@ -15,7 +15,7 @@ fn shared_canonicalization_fixture() -> Value {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sound_effect_prompt_matches_shared_unicode_canonicalization_fixtures() {
|
||||
fn sound_effect_prompt_matches_shared_ecmascript_trim_fixtures() {
|
||||
for case in shared_canonicalization_fixture()
|
||||
.as_array()
|
||||
.expect("fixture should be an array")
|
||||
@@ -71,7 +71,7 @@ fn sound_effect_prompt_matches_shared_unicode_canonicalization_fixtures() {
|
||||
#[test]
|
||||
fn sound_effect_prompt_uses_the_2048_code_point_boundary_without_truncation() {
|
||||
let maximum = format!(
|
||||
"\u{0085}{}\u{2003}",
|
||||
"\u{feff}{}\u{2003}",
|
||||
"😀".repeat(SOUND_EFFECT_PROMPT_MAX_CODE_POINTS)
|
||||
);
|
||||
let accepted = validate_sound_effect_prompt(&maximum)
|
||||
@@ -94,7 +94,7 @@ fn sound_effect_prompt_uses_the_2048_code_point_boundary_without_truncation() {
|
||||
|
||||
#[test]
|
||||
fn sound_effect_prompt_rejects_empty_canonical_values_without_a_default() {
|
||||
for prompt in ["", " \t\r\n", "\u{0085}\u{2003}\u{00a0}"] {
|
||||
for prompt in ["", " \t\r\n", "\u{feff}\u{2003}\u{00a0}"] {
|
||||
let error = validate_sound_effect_prompt(prompt)
|
||||
.expect_err("empty canonical SFX prompt should be rejected");
|
||||
assert_eq!(error.message(), "prompt 不能为空");
|
||||
|
||||
@@ -991,6 +991,7 @@ describe('ImageCanvasEditorModel', () => {
|
||||
{ ...soundEffect, schemaVersion: 1 },
|
||||
{ ...soundEffect, model: 'audio1.0' },
|
||||
{ ...soundEffect, userPrompt: ' 金币落地' },
|
||||
{ ...soundEffect, userPrompt: '\uFEFF金币落地,轻快可爱' },
|
||||
{ ...soundEffect, actualDurationSeconds: 600.1 },
|
||||
{
|
||||
...soundEffect,
|
||||
|
||||
@@ -248,15 +248,15 @@ describe('ImageCanvasSoundEffectPresetModel', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('canonicalizes and appends with the frozen Unicode punctuation rule', () => {
|
||||
it('canonicalizes and appends with the frozen ECMAScript trim rule', () => {
|
||||
const preset = SOUND_EFFECT_PROMPT_PRESETS[40];
|
||||
expect(appendSoundEffectPromptPreset('\u0085\u2003', preset)).toBe(
|
||||
expect(appendSoundEffectPromptPreset('\uFEFF\u2003', preset)).toBe(
|
||||
preset.prompt,
|
||||
);
|
||||
expect(appendSoundEffectPromptPreset('金币落地', preset)).toBe(
|
||||
`金币落地,${preset.prompt}`,
|
||||
);
|
||||
expect(appendSoundEffectPromptPreset('金币落地!\u0085', preset)).toBe(
|
||||
expect(appendSoundEffectPromptPreset('金币落地!\uFEFF', preset)).toBe(
|
||||
`金币落地!${preset.prompt}`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('ImageCanvasSoundEffectPromptModel', () => {
|
||||
});
|
||||
|
||||
it('accepts 2048 code points and rejects 2049 without truncating', () => {
|
||||
const maximum = `\u0085${'😀'.repeat(
|
||||
const maximum = `\uFEFF${'😀'.repeat(
|
||||
SOUND_EFFECT_PROMPT_MAX_CODE_POINTS,
|
||||
)}\u2003`;
|
||||
const accepted = validateSoundEffectPrompt(maximum);
|
||||
@@ -59,7 +59,7 @@ describe('ImageCanvasSoundEffectPromptModel', () => {
|
||||
});
|
||||
|
||||
it('rejects empty canonical prompts without adding a default description', () => {
|
||||
for (const prompt of ['', ' \t\r\n', '\u0085\u2003\u00a0']) {
|
||||
for (const prompt of ['', ' \t\r\n', '\uFEFF\u2003\u00a0']) {
|
||||
expect(validateSoundEffectPrompt(prompt)).toEqual({
|
||||
ok: false,
|
||||
prompt: '',
|
||||
@@ -74,7 +74,7 @@ describe('ImageCanvasSoundEffectPromptModel', () => {
|
||||
const started = model.beginOperation({
|
||||
dialogId: 'dialog-a',
|
||||
status: 'optimizing',
|
||||
prompt: '\u0085金币落地声\u3000',
|
||||
prompt: '\uFEFF金币落地声\u3000',
|
||||
});
|
||||
expect(started.started).toBe(true);
|
||||
expect(started.canonicalPrompt).toBe('金币落地声');
|
||||
|
||||
@@ -6,9 +6,6 @@ import type {
|
||||
export const SOUND_EFFECT_PROMPT_MAX_CODE_POINTS = 2048;
|
||||
const SOUND_EFFECT_PROMPT_OPERATION_ID_PREFIX = 'sound-effect-prompt-operation';
|
||||
|
||||
const LEADING_UNICODE_WHITE_SPACE = /^\p{White_Space}+/u;
|
||||
const TRAILING_UNICODE_WHITE_SPACE = /\p{White_Space}+$/u;
|
||||
|
||||
export type SoundEffectPromptValidationResult =
|
||||
| {
|
||||
ok: true;
|
||||
@@ -57,13 +54,11 @@ export function toSoundEffectGenerationDialog(
|
||||
}
|
||||
|
||||
/**
|
||||
* 只删除首尾 Unicode `White_Space`。内部空白、组合字符、ZWJ、U+200B、U+FEFF
|
||||
* 及其它 code point 均原样保留;不能替换为语义不同的原生 `trim()`。
|
||||
* 按 JavaScript `String.trim()` 删除首尾 ECMAScript 空白和行终止符。内部空白、组合字符、
|
||||
* ZWJ、U+200B 及其它 code point 均原样保留;首尾 U+FEFF 会被删除,U+0085 则保留。
|
||||
*/
|
||||
export function canonicalizeSoundEffectPrompt(prompt: string) {
|
||||
return prompt
|
||||
.replace(LEADING_UNICODE_WHITE_SPACE, '')
|
||||
.replace(TRAILING_UNICODE_WHITE_SPACE, '');
|
||||
return prompt.trim();
|
||||
}
|
||||
|
||||
export function countSoundEffectPromptCodePoints(prompt: string) {
|
||||
|
||||
@@ -1938,7 +1938,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
initialDialog={{
|
||||
id: 'dialog-sound',
|
||||
mode: 'audio-sound-effect',
|
||||
prompt: '\u0085\uFEFF金币掉落叮当声\uFEFF\u0085',
|
||||
prompt: '\uFEFF金币掉落叮当声\uFEFF',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
soundDurationSeconds: 7,
|
||||
@@ -1960,7 +1960,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
await waitFor(() => {
|
||||
expect(generateEditorSoundEffectMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: '\uFEFF金币掉落叮当声\uFEFF',
|
||||
prompt: '金币掉落叮当声',
|
||||
duration: 7,
|
||||
projectId: 'editor-project-audio',
|
||||
assetFolderId: 'project',
|
||||
@@ -1999,7 +1999,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
initialDialog={{
|
||||
id: 'dialog-sound-empty',
|
||||
mode: 'audio-sound-effect',
|
||||
prompt: '\u0085 \n\t\u0085',
|
||||
prompt: '\uFEFF \n\t\uFEFF',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
}}
|
||||
@@ -2031,7 +2031,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
initialDialog={{
|
||||
id: 'dialog-sfx-claim',
|
||||
mode: 'audio-sound-effect',
|
||||
prompt: '\u0085循环环境声\u3000',
|
||||
prompt: '\uFEFF循环环境声\u3000',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
soundModel: 'eleven_text_to_sound_v2',
|
||||
|
||||
@@ -148,7 +148,7 @@ describe('useImageCanvasSoundEffectPromptAssist', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useHarness({
|
||||
initialDialogs: [
|
||||
createDialog('dialog-a', '\u0085金币落地声\u3000', {
|
||||
createDialog('dialog-a', '\uFEFF金币落地声\u3000', {
|
||||
status: 'failed',
|
||||
errorMessage: '旧错误',
|
||||
}),
|
||||
@@ -319,7 +319,7 @@ describe('useImageCanvasSoundEffectPromptAssist', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useHarness({
|
||||
initialDialogs: [
|
||||
createDialog('dialog-a', ' \u0085循环环境声\u3000', {
|
||||
createDialog('dialog-a', ' \uFEFF循环环境声\u3000', {
|
||||
soundDurationMode: 'auto',
|
||||
soundDurationSeconds: 7.3,
|
||||
soundLoop: true,
|
||||
|
||||
Reference in New Issue
Block a user