实现 SFX V2 T4 生成面板交互

新增独立 SFX Prompt 助手状态机与请求隔离、单层撤销。

抽取通用音频预设跑马灯并接入 SFX 预设、计数和优化入口。

实现自动/手动时长、Loop、模型展示及同步提交参数冻结。

恢复项目布局状态,补齐回归测试并更新 T4 权威文档与共享决策。
This commit is contained in:
2026-08-07 03:49:04 +00:00
parent 9d1f4c1975
commit e1e4f487f5
27 changed files with 2895 additions and 562 deletions
@@ -4,6 +4,7 @@ import soundEffectPromptCanonicalizationCases from '../../../packages/shared/tes
import {
canonicalizeSoundEffectPrompt,
countSoundEffectPromptCodePoints,
createSoundEffectPromptStateModel,
SOUND_EFFECT_PROMPT_MAX_CODE_POINTS,
validateSoundEffectPrompt,
} from './ImageCanvasSoundEffectPromptModel';
@@ -67,4 +68,94 @@ describe('ImageCanvasSoundEffectPromptModel', () => {
});
}
});
it('creates one dialog-scoped optimization operation and swaps one undo snapshot', () => {
const model = createSoundEffectPromptStateModel();
const started = model.beginOperation({
dialogId: 'dialog-a',
status: 'optimizing',
prompt: '\u0085金币落地声\u3000',
});
expect(started.started).toBe(true);
expect(started.canonicalPrompt).toBe('金币落地声');
expect(started.state).toMatchObject({
status: 'optimizing',
undoPromptSnapshot: null,
temporaryPromptSnapshot: '金币落地声',
});
expect(
model.beginOperation({
dialogId: 'dialog-a',
status: 'optimizing',
prompt: '另一个输入',
}).reason,
).toBe('duplicate');
const resolved = model.resolveAiOperation(
started.operation!,
'清脆明亮的金币落地声',
);
expect(resolved).toMatchObject({
applied: true,
prompt: '清脆明亮的金币落地声',
state: {
status: 'idle',
undoPromptSnapshot: '金币落地声',
temporaryPromptSnapshot: null,
},
});
expect(
model.swapUndoSnapshot('dialog-a', '清脆明亮的金币落地声'),
).toMatchObject({
applied: true,
prompt: '金币落地声',
state: { undoPromptSnapshot: '清脆明亮的金币落地声' },
});
});
it('clears the replaced snapshot when a later optimization fails', () => {
const model = createSoundEffectPromptStateModel();
const first = model.beginOperation({
dialogId: 'dialog-a',
status: 'optimizing',
prompt: '原始描述',
});
model.resolveAiOperation(first.operation!, '第一次优化');
const second = model.beginOperation({
dialogId: 'dialog-a',
status: 'optimizing',
prompt: '手动修改后的描述',
});
expect(second.state.undoPromptSnapshot).toBeNull();
expect(model.rejectOperation(second.operation!)).toMatchObject({
applied: true,
prompt: '手动修改后的描述',
state: {
status: 'idle',
undoPromptSnapshot: null,
temporaryPromptSnapshot: null,
},
});
});
it('keeps submission and preset state isolated by dialog ID', () => {
const model = createSoundEffectPromptStateModel();
const submission = model.beginOperation({
dialogId: 'dialog-a',
status: 'submitting',
prompt: ' 提交描述 ',
});
expect(submission.started).toBe(true);
expect(model.preparePreset('dialog-a', '不应写入').applied).toBe(false);
expect(model.preparePreset('dialog-b', ' 另一面板 ').prompt).toBe(
'另一面板',
);
expect(
model.completeSubmittingOperation(submission.operation!),
).toMatchObject({
applied: true,
prompt: '提交描述',
state: { status: 'idle' },
});
});
});