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
This commit is contained in:
+2
-1
@@ -34,7 +34,8 @@
|
|||||||
"build:packages": "turbo run build -F=\"./packages/*\"",
|
"build:packages": "turbo run build -F=\"./packages/*\"",
|
||||||
"build:engines": "turbo run build -F=\"./engines/*\"",
|
"build:engines": "turbo run build -F=\"./engines/*\"",
|
||||||
"test": "vitest --coverage",
|
"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-vishot:run": "vitest run --config packages/vishot-runtime/vitest.config.ts",
|
||||||
"test-ui:run": "vitest run --config packages/stage-ui/vitest.config.ts",
|
"test-ui:run": "vitest run --config packages/stage-ui/vitest.config.ts",
|
||||||
"lint": "moeru-lint .",
|
"lint": "moeru-lint .",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@moeru/std": "catalog:",
|
"@moeru/std": "catalog:",
|
||||||
|
"@proj-airi/audio": "workspace:^",
|
||||||
"uncrypto": "catalog:"
|
"uncrypto": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './utils'
|
||||||
@@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
import { tryCatch } from '@moeru/std'
|
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<{
|
export async function mediaStreamFromAudioFile(file: File): Promise<{
|
||||||
cleanup: () => Promise<void>
|
cleanup: () => Promise<void>
|
||||||
stream: MediaStream
|
stream: MediaStream
|
||||||
@@ -26,10 +33,15 @@ export async function mediaStreamFromAudioFile(file: File): Promise<{
|
|||||||
try {
|
try {
|
||||||
source.stop()
|
source.stop()
|
||||||
}
|
}
|
||||||
|
catch {
|
||||||
catch { /* noop */ }
|
// `AudioBufferSourceNode.stop()` throws if playback already ended or
|
||||||
|
// was stopped by the caller; cleanup should remain idempotent.
|
||||||
|
}
|
||||||
source.disconnect()
|
source.disconnect()
|
||||||
destination.disconnect()
|
destination.disconnect()
|
||||||
|
for (const track of destination.stream.getTracks()) {
|
||||||
|
track.stop()
|
||||||
|
}
|
||||||
await audioContext.close()
|
await audioContext.close()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default defineConfig(({ mode }) => ({
|
|||||||
browser: {
|
browser: {
|
||||||
provider: playwright(),
|
provider: playwright(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
// at least one instance is required
|
// Vitest browser mode requires an explicit browser instance list.
|
||||||
instances: [
|
instances: [
|
||||||
{ browser: 'chromium' },
|
{ browser: 'chromium' },
|
||||||
],
|
],
|
||||||
|
|||||||
Generated
+3
@@ -3300,6 +3300,9 @@ importers:
|
|||||||
'@moeru/std':
|
'@moeru/std':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.1.0-beta.17
|
version: 0.1.0-beta.17
|
||||||
|
'@proj-airi/audio':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../audio
|
||||||
uncrypto:
|
uncrypto:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.1.3
|
version: 0.1.3
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export default defineConfig({
|
|||||||
'apps/ui-server-auth',
|
'apps/ui-server-auth',
|
||||||
'apps/ui-admin',
|
'apps/ui-admin',
|
||||||
'apps/stage-tamagotchi',
|
'apps/stage-tamagotchi',
|
||||||
'packages/audio-pipelines-transcribe',
|
|
||||||
'packages/cap-vite',
|
'packages/cap-vite',
|
||||||
'packages/core-agent',
|
'packages/core-agent',
|
||||||
'packages/vishot-runner-browser',
|
'packages/vishot-runner-browser',
|
||||||
|
|||||||
Reference in New Issue
Block a user