实现 SFX V2 T1 契约与提示词基础

统一前后端 SFX Prompt Unicode canonicalization、字符计数和 2048 边界
固化 52 个音效预设、追加规则及跨语言测试向量
演进共享音频请求响应、duration、Loop 和 V2 metadata 契约
增加 External model 输入矩阵纯函数并保持正式接线归属 T5
补齐前端提交、读取、深拷贝与请求边界回归测试
更新共享计划并标记 T1 完成且当前不可发布
This commit is contained in:
2026-08-06 13:05:48 +00:00
parent 6fd731f56c
commit cd80c085ff
24 changed files with 1559 additions and 97 deletions
@@ -1,3 +1,8 @@
import {
EDITOR_SOUND_EFFECT_MODEL,
SOUND_EFFECT_DURATION_MAX_SECONDS,
SOUND_EFFECT_DURATION_MIN_SECONDS,
} from '../../../packages/shared/src/contracts/editorAudio';
import type {
EditorAssetGenerationInputs,
EditorAssetLibrarySnapshot,
@@ -21,6 +26,7 @@ import type {
PerfectPixelOperationSnapshot,
SnapCandidate,
} from './ImageCanvasEditorTypes';
import { validateSoundEffectPrompt } from './ImageCanvasSoundEffectPromptModel';
export const EDITOR_ASSET_FOLDERS: EditorAssetFolder[] = [
{
@@ -2170,6 +2176,7 @@ export function generationInputsOrNull(
const snapshot = value as {
fields?: unknown;
references?: unknown;
soundEffect?: unknown;
};
const fields = Array.isArray(snapshot.fields)
? snapshot.fields.flatMap((field) => {
@@ -2206,7 +2213,81 @@ export function generationInputsOrNull(
})
: [];
return fields.length || references.length ? { fields, references } : null;
const hasSoundEffect = Object.prototype.hasOwnProperty.call(
snapshot,
'soundEffect',
);
const soundEffect = hasSoundEffect
? soundEffectGenerationMetadataOrNull(snapshot.soundEffect)
: null;
if (hasSoundEffect && !soundEffect) {
return null;
}
return fields.length || references.length || soundEffect
? {
fields,
references,
...(soundEffect ? { soundEffect } : {}),
}
: null;
}
function soundEffectGenerationMetadataOrNull(
value: unknown,
): NonNullable<CanvasGenerationInputs['soundEffect']> | null {
if (!isSnapshotRecord(value)) {
return null;
}
const userPrompt =
typeof value.userPrompt === 'string' ? value.userPrompt : '';
const actualPrompt =
typeof value.actualPrompt === 'string' ? value.actualPrompt : '';
const userPromptValidation = validateSoundEffectPrompt(userPrompt);
const actualPromptValidation = validateSoundEffectPrompt(actualPrompt);
if (
value.schemaVersion !== 2 ||
value.model !== EDITOR_SOUND_EFFECT_MODEL ||
(value.durationMode !== 'auto' && value.durationMode !== 'manual') ||
typeof value.loop !== 'boolean' ||
!userPromptValidation.ok ||
userPromptValidation.prompt !== userPrompt ||
!actualPromptValidation.ok ||
actualPromptValidation.prompt !== actualPrompt ||
typeof value.actualDurationSeconds !== 'number' ||
!Number.isFinite(value.actualDurationSeconds) ||
value.actualDurationSeconds <= 0 ||
value.actualDurationSeconds > 600
) {
return null;
}
let requestedDurationSeconds: number | null;
if (value.durationMode === 'auto') {
if (value.requestedDurationSeconds !== null) {
return null;
}
requestedDurationSeconds = null;
} else {
if (
typeof value.requestedDurationSeconds !== 'number' ||
!Number.isFinite(value.requestedDurationSeconds) ||
value.requestedDurationSeconds < SOUND_EFFECT_DURATION_MIN_SECONDS ||
value.requestedDurationSeconds > SOUND_EFFECT_DURATION_MAX_SECONDS
) {
return null;
}
requestedDurationSeconds = value.requestedDurationSeconds;
}
return {
schemaVersion: 2,
userPrompt,
actualPrompt,
model: EDITOR_SOUND_EFFECT_MODEL,
durationMode: value.durationMode,
requestedDurationSeconds,
actualDurationSeconds: value.actualDurationSeconds,
loop: value.loop,
};
}
export function canvasAssetKindOrNull(value: unknown): CanvasAssetKind | null {