mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
134 lines
4.6 KiB
TypeScript
134 lines
4.6 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import {
|
|
ASSEMBLYAI_TRANSCRIPT_URL,
|
|
ASSEMBLYAI_UPLOAD_URL,
|
|
assemblyAiDiarize,
|
|
} from '@/utilities/ai/assemblyAiDiarize'
|
|
|
|
type FetchCall = { url: string; init: RequestInit | undefined }
|
|
|
|
const jsonResponse = (body: unknown, status = 200): Response =>
|
|
new Response(JSON.stringify(body), { status })
|
|
|
|
const audioBlob = (): Blob => new Blob([new Uint8Array([1, 2, 3])], { type: 'audio/mpeg' })
|
|
|
|
const installFetch = (
|
|
handler: (url: string, init: RequestInit | undefined, call: number) => Response,
|
|
): FetchCall[] => {
|
|
const calls: FetchCall[] = []
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
const url = String(input)
|
|
calls.push({ url, init })
|
|
return handler(url, init, calls.length)
|
|
}),
|
|
)
|
|
return calls
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
vi.unstubAllEnvs()
|
|
})
|
|
|
|
describe('AssemblyAI diarization provider (C200)', () => {
|
|
it('uploads, submits with speaker labels and normalizes ms to seconds', async () => {
|
|
vi.stubEnv('ASSEMBLYAI_API_KEY', 'test-key')
|
|
const calls = installFetch((url, init) => {
|
|
if (url === ASSEMBLYAI_UPLOAD_URL)
|
|
return jsonResponse({ upload_url: 'https://cdn/audio.mp3' })
|
|
if (url === ASSEMBLYAI_TRANSCRIPT_URL && init?.method === 'POST') {
|
|
return jsonResponse({ id: 'tr_1', status: 'queued' })
|
|
}
|
|
return jsonResponse({
|
|
status: 'completed',
|
|
utterances: [
|
|
{ speaker: 'A', start: 1000, end: 2500 },
|
|
{ speaker: 'B', start: 2500, end: 4000 },
|
|
{ speaker: 'C', start: 'x', end: 10 },
|
|
],
|
|
})
|
|
})
|
|
|
|
const progress = vi.fn()
|
|
const result = await assemblyAiDiarize(audioBlob(), {
|
|
pollIntervalMs: 1,
|
|
onProgress: progress,
|
|
})
|
|
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
turns: [
|
|
{ speaker: 'A', startSeconds: 1, endSeconds: 2.5 },
|
|
{ speaker: 'B', startSeconds: 2.5, endSeconds: 4 },
|
|
],
|
|
})
|
|
const create = calls.find(
|
|
(call) => call.url === ASSEMBLYAI_TRANSCRIPT_URL && call.init?.method === 'POST',
|
|
)
|
|
expect(create?.init?.body).toContain('"speaker_labels":true')
|
|
expect(create?.init?.body).toContain('"speech_models":["universal-2"]')
|
|
expect(create?.init?.body).toContain('"language_code":"pt"')
|
|
expect(new Headers(calls[0]?.init?.headers).get('authorization')).toBe('test-key')
|
|
expect(calls.some((call) => call.init?.method === 'DELETE')).toBe(true)
|
|
expect(progress).toHaveBeenCalled()
|
|
})
|
|
|
|
it('answers 503 without a key and never calls the provider', async () => {
|
|
vi.stubEnv('ASSEMBLYAI_API_KEY', '')
|
|
const calls = installFetch(() => jsonResponse({}))
|
|
|
|
expect(await assemblyAiDiarize(audioBlob())).toEqual({
|
|
ok: false,
|
|
error: 'Separação de falantes está indisponível no momento.',
|
|
status: 503,
|
|
})
|
|
expect(calls).toHaveLength(0)
|
|
})
|
|
|
|
it('degrades to an error when the upload fails', async () => {
|
|
vi.stubEnv('ASSEMBLYAI_API_KEY', 'test-key')
|
|
const calls = installFetch(() => jsonResponse({}, 500))
|
|
|
|
const result = await assemblyAiDiarize(audioBlob())
|
|
expect(result.ok).toBe(false)
|
|
expect(calls).toHaveLength(1)
|
|
})
|
|
|
|
it('degrades to an error and deletes the remote transcript on provider error', async () => {
|
|
vi.stubEnv('ASSEMBLYAI_API_KEY', 'test-key')
|
|
const calls = installFetch((url, init) => {
|
|
if (url === ASSEMBLYAI_UPLOAD_URL)
|
|
return jsonResponse({ upload_url: 'https://cdn/audio.mp3' })
|
|
if (url === ASSEMBLYAI_TRANSCRIPT_URL && init?.method === 'POST') {
|
|
return jsonResponse({ id: 'tr_9', status: 'queued' })
|
|
}
|
|
if (init?.method === 'DELETE') return jsonResponse({}, 200)
|
|
return jsonResponse({ status: 'error', error: 'provider exploded' })
|
|
})
|
|
|
|
expect(await assemblyAiDiarize(audioBlob(), { pollIntervalMs: 1 })).toMatchObject({ ok: false })
|
|
expect(calls.some((call) => call.init?.method === 'DELETE')).toBe(true)
|
|
})
|
|
|
|
it('accepts a completed transcript without utterances (honest empty turns)', async () => {
|
|
vi.stubEnv('ASSEMBLYAI_API_KEY', 'test-key')
|
|
installFetch((url, init) => {
|
|
if (url === ASSEMBLYAI_UPLOAD_URL)
|
|
return jsonResponse({ upload_url: 'https://cdn/audio.mp3' })
|
|
if (url === ASSEMBLYAI_TRANSCRIPT_URL && init?.method === 'POST') {
|
|
return jsonResponse({ id: 'tr_2', status: 'queued' })
|
|
}
|
|
return jsonResponse({ status: 'completed', utterances: [] })
|
|
})
|
|
|
|
expect(await assemblyAiDiarize(audioBlob(), { pollIntervalMs: 1 })).toEqual({
|
|
ok: true,
|
|
turns: [],
|
|
})
|
|
})
|
|
})
|