From 391d21b996bc0a3b664416b9332da3f0f4b26180 Mon Sep 17 00:00:00 2001 From: Neko Date: Thu, 18 Jun 2026 15:10:34 +0800 Subject: [PATCH] refactor(audio-pipelines-transcribe): use `@proj-airi/audio` (#1990) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by-agent: Codex --- package.json | 3 +- .../audio-pipelines-transcribe/package.json | 1 + .../audio-pipelines-transcribe/src/index.ts | 1 + .../src/utils/index.browser.test.ts | 31 +++++++++++++++++++ .../src/utils/index.ts | 16 ++++++++-- .../vitest.config.ts | 2 +- pnpm-lock.yaml | 3 ++ vitest.config.ts | 1 - 8 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 packages/audio-pipelines-transcribe/src/index.ts create mode 100644 packages/audio-pipelines-transcribe/src/utils/index.browser.test.ts diff --git a/package.json b/package.json index d24f487cb..85ca0172b 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "build:packages": "turbo run build -F=\"./packages/*\"", "build:engines": "turbo run build -F=\"./engines/*\"", "test": "vitest --coverage", - "test:run": "vitest run && pnpm run test-vishot:run && pnpm run test-ui:run", + "test:run": "vitest run && pnpm run test-audio-pipelines-transcribe:run && pnpm run test-vishot:run && pnpm run test-ui:run", + "test-audio-pipelines-transcribe:run": "vitest run --config packages/audio-pipelines-transcribe/vitest.config.ts", "test-vishot:run": "vitest run --config packages/vishot-runtime/vitest.config.ts", "test-ui:run": "vitest run --config packages/stage-ui/vitest.config.ts", "lint": "moeru-lint .", diff --git a/packages/audio-pipelines-transcribe/package.json b/packages/audio-pipelines-transcribe/package.json index 6a20821e5..59ba0baca 100644 --- a/packages/audio-pipelines-transcribe/package.json +++ b/packages/audio-pipelines-transcribe/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "@moeru/std": "catalog:", + "@proj-airi/audio": "workspace:^", "uncrypto": "catalog:" } } diff --git a/packages/audio-pipelines-transcribe/src/index.ts b/packages/audio-pipelines-transcribe/src/index.ts new file mode 100644 index 000000000..9c56149ef --- /dev/null +++ b/packages/audio-pipelines-transcribe/src/index.ts @@ -0,0 +1 @@ +export * from './utils' diff --git a/packages/audio-pipelines-transcribe/src/utils/index.browser.test.ts b/packages/audio-pipelines-transcribe/src/utils/index.browser.test.ts new file mode 100644 index 000000000..7f4ef8e84 --- /dev/null +++ b/packages/audio-pipelines-transcribe/src/utils/index.browser.test.ts @@ -0,0 +1,31 @@ +import { toWav } from '@proj-airi/audio/encoding' +import { describe, expect, it } from 'vitest' + +import { mediaStreamFromAudioFile } from '.' + +function createSineWaveFile() { + const sampleRate = 44_100 + const durationSeconds = 0.05 + const sampleCount = Math.floor(sampleRate * durationSeconds) + const samples = new Float32Array(sampleCount) + + for (let index = 0; index < sampleCount; index += 1) { + const phase = (index / sampleRate) * 440 * Math.PI * 2 + samples[index] = Math.sin(phase) + } + + return new File([toWav(samples.buffer, sampleRate)], 'tone.wav', { type: 'audio/wav' }) +} + +describe('mediaStreamFromAudioFile', () => { + it('decodes a browser audio file into a media stream and releases resources', async () => { + const result = await mediaStreamFromAudioFile(createSineWaveFile()) + + expect(result.stream).toBeInstanceOf(MediaStream) + expect(result.stream.getAudioTracks()).toHaveLength(1) + + await result.cleanup() + + expect(result.stream.getAudioTracks()[0]?.readyState).toBe('ended') + }) +}) diff --git a/packages/audio-pipelines-transcribe/src/utils/index.ts b/packages/audio-pipelines-transcribe/src/utils/index.ts index 687658a98..366e9955b 100644 --- a/packages/audio-pipelines-transcribe/src/utils/index.ts +++ b/packages/audio-pipelines-transcribe/src/utils/index.ts @@ -1,5 +1,12 @@ import { tryCatch } from '@moeru/std' +/** + * Decodes an audio file into a playable {@link MediaStream}. + * + * Use this when browser transcription code needs to feed file-backed audio + * into APIs that consume live media streams. The returned cleanup function + * stops the one-shot buffer source and releases the owned {@link AudioContext}. + */ export async function mediaStreamFromAudioFile(file: File): Promise<{ cleanup: () => Promise stream: MediaStream @@ -26,10 +33,15 @@ export async function mediaStreamFromAudioFile(file: File): Promise<{ try { source.stop() } - - catch { /* noop */ } + catch { + // `AudioBufferSourceNode.stop()` throws if playback already ended or + // was stopped by the caller; cleanup should remain idempotent. + } source.disconnect() destination.disconnect() + for (const track of destination.stream.getTracks()) { + track.stop() + } await audioContext.close() }, } diff --git a/packages/audio-pipelines-transcribe/vitest.config.ts b/packages/audio-pipelines-transcribe/vitest.config.ts index 5407dd8ee..e99c86074 100644 --- a/packages/audio-pipelines-transcribe/vitest.config.ts +++ b/packages/audio-pipelines-transcribe/vitest.config.ts @@ -25,7 +25,7 @@ export default defineConfig(({ mode }) => ({ browser: { provider: playwright(), enabled: true, - // at least one instance is required + // Vitest browser mode requires an explicit browser instance list. instances: [ { browser: 'chromium' }, ], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80e3169a9..869295102 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3300,6 +3300,9 @@ importers: '@moeru/std': specifier: 'catalog:' version: 0.1.0-beta.17 + '@proj-airi/audio': + specifier: workspace:^ + version: link:../audio uncrypto: specifier: 'catalog:' version: 0.1.3 diff --git a/vitest.config.ts b/vitest.config.ts index 46fd9ca54..c0bef9272 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -7,7 +7,6 @@ export default defineConfig({ 'apps/ui-server-auth', 'apps/ui-admin', 'apps/stage-tamagotchi', - 'packages/audio-pipelines-transcribe', 'packages/cap-vite', 'packages/core-agent', 'packages/vishot-runner-browser',