Implement logic to resolve and inject Replicate Kokoro model-specific language codes based on language tag and voice, using new mapping utilities. Update `buildReplicateInput` to set `language_code` for the built-in Kokoro model. Expand language utilities with code mapping and expose `resolveReplicateKokoroLanguageCode`. Enhance tests to cover language code resolution and input mapping for Replicate Kokoro, and verify language input key discovery for built-in models.
132 lines
5.1 KiB
TypeScript
132 lines
5.1 KiB
TypeScript
import { describe, expect, test } from 'vitest';
|
|
import { readFileSync } from 'fs';
|
|
import { resolve } from 'path';
|
|
|
|
import {
|
|
getTtsLanguageCompatibilityWarnings,
|
|
inferKokoroLanguageFromVoice,
|
|
keepKokoroVoicesInOneLanguage,
|
|
normalizeOptionalLanguageTag,
|
|
normalizeUnicodeToken,
|
|
resolveReplicateKokoroLanguageCode,
|
|
resolveTtsLanguage,
|
|
segmentSentences,
|
|
segmentWords,
|
|
toBaseLanguageCode,
|
|
} from '../../src/lib/shared/language';
|
|
|
|
describe('multilingual language utilities', () => {
|
|
test('keeps the multilingual document fixture readable as UTF-8', () => {
|
|
const fixture = readFileSync(resolve(process.cwd(), 'tests/files/multilingual-sample.txt'), 'utf8');
|
|
expect(fixture).toContain('これは二番目の文です。');
|
|
expect(fixture).toContain('这是第二句话。');
|
|
expect(fixture).toContain('هذه هي الجملة الثانية.');
|
|
});
|
|
|
|
test('infers Kokoro language from a single-language voice selection', () => {
|
|
expect(inferKokoroLanguageFromVoice('jf_alpha')).toBe('ja');
|
|
expect(inferKokoroLanguageFromVoice('zf_xiaobei(0.5)+zm_yunxi(0.5)')).toBe('zh-CN');
|
|
expect(inferKokoroLanguageFromVoice('ff_siwis+jf_alpha')).toBeNull();
|
|
});
|
|
|
|
test('keeps Kokoro multi-voice selections within the newly selected language', () => {
|
|
expect(keepKokoroVoicesInOneLanguage(
|
|
['af_sarah', 'bf_emma', 'jf_alpha'],
|
|
'jf_alpha',
|
|
)).toEqual(['jf_alpha']);
|
|
|
|
expect(keepKokoroVoicesInOneLanguage(
|
|
['zf_xiaobei', 'zm_yunxi', 'af_sarah'],
|
|
'zm_yunxi',
|
|
)).toEqual(['zf_xiaobei', 'zm_yunxi']);
|
|
|
|
expect(keepKokoroVoicesInOneLanguage(
|
|
['af_sarah', 'bf_emma'],
|
|
'bf_emma',
|
|
)).toEqual(['af_sarah', 'bf_emma']);
|
|
});
|
|
|
|
test('normalizes valid EPUB metadata language tags and rejects invalid metadata', () => {
|
|
expect(normalizeOptionalLanguageTag(' ja-jp ')).toBe('ja-JP');
|
|
expect(normalizeOptionalLanguageTag('fr, en')).toBe('fr');
|
|
expect(normalizeOptionalLanguageTag('not_a_language')).toBeNull();
|
|
expect(normalizeOptionalLanguageTag(undefined)).toBeNull();
|
|
});
|
|
|
|
test('warns only for provable Kokoro language compatibility problems', () => {
|
|
expect(getTtsLanguageCompatibilityWarnings({
|
|
model: 'kokoro',
|
|
voice: 'jf_alpha',
|
|
documentLanguage: 'ja',
|
|
})).toEqual([]);
|
|
|
|
expect(getTtsLanguageCompatibilityWarnings({
|
|
model: 'kokoro',
|
|
voice: 'af_sarah',
|
|
documentLanguage: 'ja',
|
|
})).toEqual([
|
|
'Selected Kokoro voice is American English, but the document is Japanese.',
|
|
]);
|
|
|
|
expect(getTtsLanguageCompatibilityWarnings({
|
|
model: 'kokoro',
|
|
voice: 'ff_siwis+jf_alpha',
|
|
documentLanguage: 'fr',
|
|
})).toEqual([]);
|
|
|
|
expect(getTtsLanguageCompatibilityWarnings({
|
|
model: 'kokoro',
|
|
voice: 'af_sarah',
|
|
documentLanguage: 'ar',
|
|
})).toEqual([
|
|
"Kokoro's built-in voice catalog does not include Arabic.",
|
|
'Selected Kokoro voice is American English, but the document is Arabic.',
|
|
]);
|
|
|
|
expect(getTtsLanguageCompatibilityWarnings({
|
|
model: 'custom-unknown-model',
|
|
voice: 'af_sarah',
|
|
documentLanguage: 'ja',
|
|
})).toEqual([]);
|
|
});
|
|
|
|
test('prefers an explicit language and resolves regional tags for Whisper', () => {
|
|
expect(resolveTtsLanguage({ configuredLanguage: 'pt-BR', voice: 'jf_alpha' })).toBe('pt-BR');
|
|
expect(resolveTtsLanguage({ configuredLanguage: 'auto', voice: 'jf_alpha' })).toBe('ja');
|
|
expect(toBaseLanguageCode('zh-CN')).toBe('zh');
|
|
});
|
|
|
|
test('maps Kokoro voices and normalized language tags to Replicate language codes', () => {
|
|
expect(resolveReplicateKokoroLanguageCode({ language: 'en', voice: 'af_sarah' })).toBe('a');
|
|
expect(resolveReplicateKokoroLanguageCode({ language: 'en', voice: 'bf_emma' })).toBe('b');
|
|
expect(resolveReplicateKokoroLanguageCode({ language: 'ja-JP', voice: 'jf_alpha' })).toBe('j');
|
|
expect(resolveReplicateKokoroLanguageCode({ language: 'zh-TW', voice: 'zf_xiaobei' })).toBe('z');
|
|
});
|
|
|
|
test('normalizes Unicode tokens without dropping non-Latin scripts', () => {
|
|
expect(normalizeUnicodeToken('École!')).toBe('école');
|
|
expect(normalizeUnicodeToken('日本語。')).toBe('日本語');
|
|
expect(normalizeUnicodeToken('العربية؟')).toBe('العربية');
|
|
expect(normalizeUnicodeToken('हिन्दी।')).toBe('हिन्दी');
|
|
});
|
|
|
|
test('segments CJK sentences and no-space language words', () => {
|
|
expect(segmentSentences('これは最初の文です。これは二番目の文です。', 'ja')).toEqual([
|
|
'これは最初の文です。',
|
|
'これは二番目の文です。',
|
|
]);
|
|
|
|
const chineseWords = segmentWords('这是第一句话。', 'zh').map((token) => token.text);
|
|
expect(chineseWords.length).toBeGreaterThan(1);
|
|
expect(chineseWords.join('')).toBe('这是第一句话');
|
|
});
|
|
|
|
test('preserves source offsets for Thai word segmentation', () => {
|
|
const text = 'นี่คือประโยคแรก';
|
|
const words = segmentWords(text, 'th');
|
|
expect(words.length).toBeGreaterThan(1);
|
|
for (const word of words) {
|
|
expect(text.slice(word.start, word.end)).toBe(word.text);
|
|
}
|
|
});
|
|
});
|