From a2bef067be925a34c8ab6ae66904ea970ede1a39 Mon Sep 17 00:00:00 2001 From: CyberSprout Date: Wed, 28 Oct 2026 21:12:16 +0800 Subject: [PATCH] fix(stage-ui): keep wake word preparation when an equal catalog arrives during load (#3104) --- .../src/libs/voice/wake-word-detector.test.ts | 21 +++++++++++++++++++ .../src/libs/voice/wake-word-detector.ts | 5 +++-- packages/testing-audio/src/describe.ts | 7 ------- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/stage-ui/src/libs/voice/wake-word-detector.test.ts b/packages/stage-ui/src/libs/voice/wake-word-detector.test.ts index ad8b59debe..f555ac1f71 100644 --- a/packages/stage-ui/src/libs/voice/wake-word-detector.test.ts +++ b/packages/stage-ui/src/libs/voice/wake-word-detector.test.ts @@ -252,6 +252,27 @@ describe('wakeWordDetector', () => { expect(harness.spotter.setKeywords).not.toHaveBeenCalled() }) + it('becomes ready when the same catalog arrives again while the spotter is loading', async () => { + let finishLoad!: () => void + const loaded = new Promise(resolve => finishLoad = resolve) + const harness = createHarness() + harness.createSpotter.mockImplementationOnce(async () => { + await loaded + return harness.spotter + }) + const catalog = () => pronunciations([{ characterId: 'alice', text: 'Alice', tokens: [['AE1', 'L', 'IH0', 'S']] }]) + + // A reactive catalog can emit an equal list again, for example after the card store hydrates. + const first = harness.detector.setPronunciations(catalog()) + const second = harness.detector.setPronunciations(catalog()) + finishLoad() + await Promise.all([first, second]) + + expect(harness.createSpotter).toHaveBeenCalledTimes(1) + expect(harness.detector.ready).toBe(true) + expect(harness.onPreparationChange).toHaveBeenLastCalledWith('ready') + }) + it('does not load a model for pronunciations of another model', async () => { const harness = createHarness() diff --git a/packages/stage-ui/src/libs/voice/wake-word-detector.ts b/packages/stage-ui/src/libs/voice/wake-word-detector.ts index 218497a3a8..bd42a3e9dd 100644 --- a/packages/stage-ui/src/libs/voice/wake-word-detector.ts +++ b/packages/stage-ui/src/libs/voice/wake-word-detector.ts @@ -152,7 +152,8 @@ export class WakeWordDetector { const keywords = this.keywordsOf([...next.values()]) const update = this.updates.then(async () => { // A newer list or a stop replaced this one while an earlier update ran. - if (generation !== this.generation || next !== this.pronunciations) + // An equal list replaces `pronunciations` without a new generation, so only the generation tells a newer list. + if (generation !== this.generation) return if (keywords.length === 0) { @@ -184,7 +185,7 @@ export class WakeWordDetector { // A keyword update starts a new stream in the Worker. this.streamSampleRate = undefined this.resetStream = false - if (generation !== this.generation || next !== this.pronunciations) + if (generation !== this.generation) return this.keywordsReady = true this.options.onPreparationChange?.('ready') diff --git a/packages/testing-audio/src/describe.ts b/packages/testing-audio/src/describe.ts index 02b28243a2..359f0613f8 100644 --- a/packages/testing-audio/src/describe.ts +++ b/packages/testing-audio/src/describe.ts @@ -37,13 +37,6 @@ const audioTestAPI = createAudioTestAPI< }] }, async execute({ plan, task, invokeHandler, runPreflight }) { - // NOTICE: - // CI wake word cases time out while preparation stays `preparing`. The root cause is not confirmed. - // Context: audio-web wake word cases time out in the CI Unit Test job. - // Remove this skip after the preparation failure has a verified fix on CI. - if (env.CI && plan.definition.pipelines?.includes('kws')) - task.context.skip(true, 'Wake word preparation times out on CI. The root cause is not confirmed.') - // Electron bundles the wake word model. Web downloads it, so the download must finish before the recording starts. if (plan.definition.pipelines?.includes('kws') && inject('fakemicRuntime').kind === 'web') await prepareWakeWordModel()