98 lines
3.3 KiB
TypeScript
98 lines
3.3 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import {
|
|
voxCPMVoiceExists,
|
|
registerVoxCPMVoice,
|
|
bootstrapVoxCPMReferenceClip,
|
|
} from '@/lib/audio/voxcpm-registration';
|
|
|
|
const cfg = { baseUrl: 'https://voxcpm.test/v1', apiKey: 'k', model: 'voxcpm2' };
|
|
|
|
afterEach(() => vi.unstubAllGlobals());
|
|
|
|
describe('voxCPMVoiceExists', () => {
|
|
it('lists voices and checks membership', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn(
|
|
async () =>
|
|
new Response(JSON.stringify({ voices: ['default', 'voxcpm:voice:abc'] }), {
|
|
status: 200,
|
|
}),
|
|
),
|
|
);
|
|
expect(await voxCPMVoiceExists(cfg, 'voxcpm:voice:abc')).toBe(true);
|
|
expect(await voxCPMVoiceExists(cfg, 'voxcpm:voice:missing')).toBe(false);
|
|
});
|
|
|
|
it('returns false when the list call fails', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn(async () => new Response('', { status: 500 })),
|
|
);
|
|
expect(await voxCPMVoiceExists(cfg, 'voxcpm:voice:abc')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('registerVoxCPMVoice', () => {
|
|
it('POSTs multipart name + consent + audio_sample with Bearer auth', async () => {
|
|
const f = vi.fn(
|
|
async () =>
|
|
new Response(JSON.stringify({ success: true, voice: { name: 'voxcpm:voice:abc' } }), {
|
|
status: 200,
|
|
}),
|
|
);
|
|
vi.stubGlobal('fetch', f);
|
|
|
|
const id = await registerVoxCPMVoice(cfg, {
|
|
voiceId: 'voxcpm:voice:abc',
|
|
referenceAudioBase64: btoa('RIFFdata'),
|
|
mimeType: 'audio/wav',
|
|
});
|
|
|
|
expect(id).toBe('voxcpm:voice:abc');
|
|
const [url, init] = f.mock.calls[0] as unknown as [string, RequestInit];
|
|
expect(String(url)).toBe('https://voxcpm.test/v1/audio/voices');
|
|
expect(init.method).toBe('POST');
|
|
const form = init.body as FormData;
|
|
expect(form).toBeInstanceOf(FormData);
|
|
expect(form.get('name')).toBe('voxcpm:voice:abc');
|
|
expect(typeof form.get('consent')).toBe('string');
|
|
expect(form.get('audio_sample')).toBeInstanceOf(Blob);
|
|
expect((init.headers as Record<string, string>).Authorization).toBe('Bearer k');
|
|
});
|
|
|
|
it('throws on non-ok response', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn(async () => new Response('nope', { status: 400 })),
|
|
);
|
|
await expect(
|
|
registerVoxCPMVoice(cfg, { voiceId: 'v', referenceAudioBase64: btoa('x') }),
|
|
).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('bootstrapVoxCPMReferenceClip', () => {
|
|
it('synthesizes the descriptor prompt into base64 wav', async () => {
|
|
const wav = new Uint8Array([82, 73, 70, 70]); // "RIFF"
|
|
const f = vi.fn(
|
|
async () => new Response(wav, { status: 200, headers: { 'content-type': 'audio/wav' } }),
|
|
);
|
|
vi.stubGlobal('fetch', f);
|
|
|
|
const out = await bootstrapVoxCPMReferenceClip(cfg, {
|
|
design: { identity: 'male teacher', texture: 'warm', delivery: 'calm' },
|
|
language: 'en',
|
|
});
|
|
|
|
expect(out.mimeType).toContain('wav');
|
|
expect(typeof out.referenceAudioBase64).toBe('string');
|
|
expect(out.referenceAudioBase64.length).toBeGreaterThan(0);
|
|
|
|
const [url, init] = f.mock.calls[0] as unknown as [string, RequestInit];
|
|
expect(String(url)).toBe('https://voxcpm.test/v1/audio/speech');
|
|
const payload = JSON.parse(String(init.body));
|
|
expect(payload.input).toContain('(male teacher, warm, calm)');
|
|
});
|
|
});
|