Files
openmaic/OpenMAIC/tests/audio/lemonade-tts.test.ts
2026-08-16 14:58:47 +08:00

122 lines
3.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
import { generateTTS } from '@/lib/audio/tts-providers';
const mockFetch = vi.fn() as Mock;
vi.stubGlobal('fetch', mockFetch);
function wavBytes(): ArrayBuffer {
const data = new Uint8Array(16);
data[0] = 0x52; // 'R'
data[1] = 0x49; // 'I'
data[2] = 0x46; // 'F'
data[3] = 0x46; // 'F'
data[8] = 0x57; // 'W'
data[9] = 0x41; // 'A'
data[10] = 0x56; // 'V'
data[11] = 0x45; // 'E'
return data.buffer;
}
describe('Lemonade TTS', () => {
beforeEach(() => {
mockFetch.mockReset();
});
it('posts to /audio/speech with kokoro-v1 + wav and bubble-up audio bytes', async () => {
const buffer = wavBytes();
mockFetch.mockResolvedValueOnce({
ok: true,
arrayBuffer: async () => buffer,
headers: { get: () => 'audio/wav' },
});
const result = await generateTTS(
{
providerId: 'lemonade-tts',
baseUrl: 'http://localhost:13305/v1/',
voice: 'af_heart',
},
'hello world',
);
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:13305/v1/audio/speech',
expect.objectContaining({ method: 'POST' }),
);
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
expect(body).toEqual({
model: 'kokoro-v1',
input: 'hello world',
voice: 'af_heart',
speed: 1.0,
response_format: 'wav',
});
expect(result.audio).toBeInstanceOf(Uint8Array);
expect(result.audio.byteLength).toBe(16);
expect(result.format).toBe('wav');
});
it('falls back to af_heart when no voice is provided', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
arrayBuffer: async () => wavBytes(),
headers: { get: () => 'audio/wav' },
});
await generateTTS({ providerId: 'lemonade-tts', voice: '' }, 'hi');
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
expect(body.voice).toBe('af_heart');
});
it('uses the selected voice consistently regardless of text language', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
arrayBuffer: async () => wavBytes(),
headers: { get: () => 'audio/wav' },
});
await generateTTS({ providerId: 'lemonade-tts', voice: 'af_heart' }, '给我讲讲 Python');
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
expect(body.voice).toBe('af_heart');
});
it('does not require an API key (keyless provider)', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
arrayBuffer: async () => wavBytes(),
headers: { get: () => 'audio/wav' },
});
await generateTTS({ providerId: 'lemonade-tts', voice: 'af_heart' }, 'hi');
expect(mockFetch.mock.calls[0][1].headers.Authorization).toBeUndefined();
});
it('attaches Bearer auth when apiKey is provided', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
arrayBuffer: async () => wavBytes(),
headers: { get: () => 'audio/wav' },
});
await generateTTS({ providerId: 'lemonade-tts', apiKey: 'sk-lm', voice: 'af_heart' }, 'hi');
expect(mockFetch.mock.calls[0][1].headers.Authorization).toBe('Bearer sk-lm');
});
it('throws on non-OK responses', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 400,
text: async () => 'bad voice',
statusText: 'Bad Request',
});
await expect(generateTTS({ providerId: 'lemonade-tts', voice: 'foo' }, 'hi')).rejects.toThrow(
/Lemonade TTS API error/,
);
});
});