90 lines
3.3 KiB
TypeScript
90 lines
3.3 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
buildVoiceDesignPrompt,
|
|
normalizeVoiceDesign,
|
|
getDeterministicVoiceId,
|
|
type VoiceDesign,
|
|
} from '@/lib/audio/voice-design';
|
|
|
|
const design: VoiceDesign = {
|
|
identity: 'middle-aged male teacher',
|
|
texture: 'warm low-pitched resonant',
|
|
delivery: 'calm measured encouraging',
|
|
};
|
|
|
|
describe('buildVoiceDesignPrompt', () => {
|
|
it('composes the three layers into one comma-joined prompt', () => {
|
|
expect(buildVoiceDesignPrompt(design)).toBe(
|
|
'middle-aged male teacher, warm low-pitched resonant, calm measured encouraging',
|
|
);
|
|
});
|
|
it('drops blank layers and collapses whitespace', () => {
|
|
expect(
|
|
buildVoiceDesignPrompt({ identity: ' male teacher ', texture: '', delivery: 'slow' }),
|
|
).toBe('male teacher, slow');
|
|
});
|
|
it('strips parentheses so they cannot break the (prompt)text delimiter', () => {
|
|
expect(
|
|
buildVoiceDesignPrompt({
|
|
identity: 'male teacher (deep)',
|
|
texture: '英文(带口音)',
|
|
delivery: 'calm',
|
|
}),
|
|
).toBe('male teacher deep, 英文 带口音, calm');
|
|
});
|
|
});
|
|
|
|
describe('normalizeVoiceDesign', () => {
|
|
it('returns a clean design from a well-formed object', () => {
|
|
expect(normalizeVoiceDesign({ identity: 'a', texture: 'b', delivery: 'c' })).toEqual({
|
|
identity: 'a',
|
|
texture: 'b',
|
|
delivery: 'c',
|
|
});
|
|
});
|
|
it('returns undefined when all layers are empty/missing', () => {
|
|
expect(normalizeVoiceDesign({})).toBeUndefined();
|
|
expect(normalizeVoiceDesign(null)).toBeUndefined();
|
|
expect(normalizeVoiceDesign('nope')).toBeUndefined();
|
|
});
|
|
it('keeps a partial design (some layers present)', () => {
|
|
expect(normalizeVoiceDesign({ identity: 'a' })).toEqual({
|
|
identity: 'a',
|
|
texture: '',
|
|
delivery: '',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getDeterministicVoiceId', () => {
|
|
it('is stable for the same descriptor+provider+model, with the neutral prefix', async () => {
|
|
const opts = { providerId: 'voxcpm-tts', model: 'VoxCPM2' };
|
|
const a = await getDeterministicVoiceId(design, opts);
|
|
const b = await getDeterministicVoiceId(design, opts);
|
|
expect(a).toBe(b);
|
|
expect(a).toMatch(/^auto-[0-9a-f]{16}$/);
|
|
});
|
|
it('changes when descriptor, model, or provider changes', async () => {
|
|
const base = await getDeterministicVoiceId(design, { providerId: 'voxcpm-tts', model: 'm' });
|
|
const tex = await getDeterministicVoiceId(
|
|
{ ...design, texture: 'bright' },
|
|
{ providerId: 'voxcpm-tts', model: 'm' },
|
|
);
|
|
const model = await getDeterministicVoiceId(design, { providerId: 'voxcpm-tts', model: 'm2' });
|
|
const prov = await getDeterministicVoiceId(design, {
|
|
providerId: 'elevenlabs-tts',
|
|
model: 'm',
|
|
});
|
|
expect(tex).not.toBe(base);
|
|
expect(model).not.toBe(base);
|
|
expect(prov).not.toBe(base);
|
|
});
|
|
it('is independent of language (descriptor already encodes it)', async () => {
|
|
// language is not a parameter of the id — same descriptor → same id regardless
|
|
// of which TTS path (narration directive vs discussion locale) resolves it.
|
|
const a = await getDeterministicVoiceId(design, { providerId: 'voxcpm-tts', model: 'm' });
|
|
const b = await getDeterministicVoiceId(design, { providerId: 'voxcpm-tts', model: 'm' });
|
|
expect(a).toBe(b);
|
|
});
|
|
});
|