Eliminate the showAllDeepInfraModels runtime/admin config and all code paths that allowed restricting Deepinfra's model catalog. Deepinfra now always shows the full model list regardless of API key or environment variable. Update documentation, environment examples, admin panels, runtime config, provider catalog logic, and tests to reflect this change.
286 lines
9.9 KiB
TypeScript
286 lines
9.9 KiB
TypeScript
import { isKokoroModel } from '@/lib/shared/kokoro';
|
|
|
|
export type TtsProviderId = 'custom-openai' | 'replicate' | 'deepinfra' | 'openai';
|
|
export type TtsProviderType = TtsProviderId | 'unknown';
|
|
export type TtsVoiceSource = 'static' | 'deepinfra-api' | 'custom-openai-api' | 'replicate-api';
|
|
export type ReplicateVoiceInputKey = 'voice' | 'voice_id' | 'speaker';
|
|
|
|
export interface SharedProviderTypeResolverEntry {
|
|
slug: string;
|
|
providerType: TtsProviderId;
|
|
defaultModel?: string | null;
|
|
defaultInstructions?: string | null;
|
|
}
|
|
|
|
export interface TtsModelDefinition {
|
|
id: string;
|
|
name: string;
|
|
}
|
|
|
|
export interface TtsProviderDefinition {
|
|
id: TtsProviderId;
|
|
name: string;
|
|
supportsCustomModel: boolean;
|
|
models: (context?: ResolveProviderModelsContext) => TtsModelDefinition[];
|
|
}
|
|
|
|
export interface ResolveProviderModelsContext {
|
|
apiKey?: string;
|
|
}
|
|
|
|
export interface ResolveVoicesOptions {
|
|
provider: TtsProviderId;
|
|
model: string;
|
|
apiKey?: string;
|
|
baseUrl?: string;
|
|
}
|
|
|
|
const OPENAI_MODELS: TtsModelDefinition[] = [
|
|
{ id: 'tts-1', name: 'TTS-1' },
|
|
{ id: 'tts-1-hd', name: 'TTS-1 HD' },
|
|
{ id: 'gpt-4o-mini-tts', name: 'GPT-4o Mini TTS' },
|
|
];
|
|
|
|
const CUSTOM_OPENAI_MODELS: TtsModelDefinition[] = [
|
|
{ id: 'kokoro', name: 'Kokoro' },
|
|
{ id: 'kitten-tts', name: 'KittenTTS' },
|
|
{ id: 'orpheus', name: 'Orpheus' },
|
|
{ id: 'custom', name: 'Other' },
|
|
];
|
|
|
|
const DEEPINFRA_MODELS_FULL: TtsModelDefinition[] = [
|
|
{ id: 'hexgrad/Kokoro-82M', name: 'hexgrad/Kokoro-82M' },
|
|
{ id: 'canopylabs/orpheus-3b-0.1-ft', name: 'canopylabs/orpheus-3b-0.1-ft' },
|
|
{ id: 'sesame/csm-1b', name: 'sesame/csm-1b' },
|
|
{ id: 'ResembleAI/chatterbox', name: 'ResembleAI/chatterbox' },
|
|
{ id: 'Zyphra/Zonos-v0.1-hybrid', name: 'Zyphra/Zonos-v0.1-hybrid' },
|
|
{ id: 'Zyphra/Zonos-v0.1-transformer', name: 'Zyphra/Zonos-v0.1-transformer' },
|
|
{ id: 'custom', name: 'Other' },
|
|
];
|
|
|
|
export const REPLICATE_KOKORO_82M_VERSIONED_MODEL =
|
|
'alphanumericuser/kokoro-82m:89b6fa84e4fa2dd6bd3a96be3e1f12827a3516c9fda8fddbac7a0be131c9a6f5' as const;
|
|
|
|
const REPLICATE_MODELS: TtsModelDefinition[] = [
|
|
{
|
|
id: REPLICATE_KOKORO_82M_VERSIONED_MODEL,
|
|
name: 'alphanumericuser/kokoro-82m',
|
|
},
|
|
{ id: 'google/gemini-3.1-flash-tts', name: 'google/gemini-3.1-flash-tts' },
|
|
{ id: 'minimax/speech-2.8-turbo', name: 'minimax/speech-2.8-turbo' },
|
|
{ id: 'qwen/qwen3-tts', name: 'qwen/qwen3-tts' },
|
|
{ id: 'inworld/tts-1.5-mini', name: 'inworld/tts-1.5-mini' },
|
|
{ id: 'custom', name: 'Other' },
|
|
];
|
|
const DEEPINFRA_API_VOICE_MODELS = new Set([
|
|
'ResembleAI/chatterbox',
|
|
'Zyphra/Zonos-v0.1-hybrid',
|
|
'Zyphra/Zonos-v0.1-transformer',
|
|
]);
|
|
|
|
const DEFAULT_MODELS: TtsModelDefinition[] = [{ id: 'tts-1', name: 'TTS-1' }];
|
|
|
|
export const OPENAI_DEFAULT_VOICES = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'] as const;
|
|
export const GPT4O_MINI_DEFAULT_VOICES = ['alloy', 'ash', 'coral', 'echo', 'fable', 'onyx', 'nova', 'sage', 'shimmer'] as const;
|
|
export const CUSTOM_OPENAI_DEFAULT_VOICES = ['af_sarah', 'af_bella', 'af_nicole', 'am_adam', 'am_michael', 'bf_emma', 'bf_isabella', 'bm_george', 'bm_lewis'] as const;
|
|
export const KOKORO_DEFAULT_VOICES = [
|
|
'af_alloy', 'af_aoede', 'af_bella', 'af_heart', 'af_jessica', 'af_kore', 'af_nicole', 'af_nova',
|
|
'af_river', 'af_sarah', 'af_sky', 'am_adam', 'am_echo', 'am_eric', 'am_fenrir', 'am_liam',
|
|
'am_michael', 'am_onyx', 'am_puck', 'am_santa', 'bf_alice', 'bf_emma', 'bf_isabella', 'bf_lily',
|
|
'bm_daniel', 'bm_fable', 'bm_george', 'bm_lewis', 'ef_dora', 'em_alex', 'em_santa', 'ff_siwis',
|
|
'hf_alpha', 'hf_beta', 'hm_omega', 'hm_psi', 'if_sara', 'im_nicola', 'jf_alpha', 'jf_gongitsune',
|
|
'jf_nezumi', 'jf_tebukuro', 'jm_kumo', 'pf_dora', 'pm_alex', 'pm_santa', 'zf_xiaobei', 'zf_xiaoni',
|
|
'zf_xiaoxiao', 'zf_xiaoyi', 'zm_yunjian', 'zm_yunxi', 'zm_yunxia', 'zm_yunyang',
|
|
] as const;
|
|
export const ORPHEUS_DEFAULT_VOICES = ['tara', 'leah', 'jess', 'leo', 'dan', 'mia', 'zac'] as const;
|
|
export const SESAME_DEFAULT_VOICES = ['conversational_a', 'conversational_b', 'read_speech_a', 'read_speech_b', 'read_speech_c', 'read_speech_d', 'none'] as const;
|
|
|
|
// Replicate model voices
|
|
export const GEMINI_FLASH_TTS_VOICES = [
|
|
'Zephyr', 'Puck', 'Charon', 'Kore', 'Fenrir', 'Leda', 'Orus', 'Aoede',
|
|
'Callirrhoe', 'Autonoe', 'Enceladus', 'Iapetus', 'Umbriel', 'Algenib',
|
|
'Despina', 'Erinome', 'Laomedeia', 'Achernar', 'Algieba', 'Schedar',
|
|
'Gacrux', 'Pulcherrima', 'Achird', 'Zubenelgenubi', 'Vindemiatrix',
|
|
'Sadachbia', 'Sadaltager', 'Sulafat', 'Alnilam', 'Rasalgethi',
|
|
] as const;
|
|
export const MINIMAX_SPEECH_VOICES = [
|
|
'Deep_Voice_Man', 'Imposing_Manner', 'Elegant_Man', 'Casual_Guy',
|
|
'Friendly_Person', 'Decent_Boy', 'Lively_Girl', 'Exuberant_Girl',
|
|
'Inspirational_girl', 'Young_Knight', 'Abbess', 'Wise_Woman',
|
|
] as const;
|
|
export const QWEN3_TTS_VOICES = ['Aiden', 'Dylan'] as const;
|
|
export const INWORLD_TTS_VOICES = ['Ashley', 'Dennis', 'Alex', 'Darlene'] as const;
|
|
const REPLICATE_DEFAULT_VOICES_BY_MODEL: Record<string, readonly string[]> = {
|
|
[REPLICATE_KOKORO_82M_VERSIONED_MODEL]: KOKORO_DEFAULT_VOICES,
|
|
'google/gemini-3.1-flash-tts': GEMINI_FLASH_TTS_VOICES,
|
|
'minimax/speech-2.8-turbo': MINIMAX_SPEECH_VOICES,
|
|
'qwen/qwen3-tts': QWEN3_TTS_VOICES,
|
|
'inworld/tts-1.5-mini': INWORLD_TTS_VOICES,
|
|
};
|
|
const DEEPINFRA_DEFAULT_VOICES_BY_MODEL: Record<string, readonly string[]> = {
|
|
'hexgrad/Kokoro-82M': KOKORO_DEFAULT_VOICES,
|
|
'canopylabs/orpheus-3b-0.1-ft': ORPHEUS_DEFAULT_VOICES,
|
|
'sesame/csm-1b': SESAME_DEFAULT_VOICES,
|
|
'ResembleAI/chatterbox': ['None'],
|
|
'Zyphra/Zonos-v0.1-hybrid': ['random'],
|
|
'Zyphra/Zonos-v0.1-transformer': ['random'],
|
|
};
|
|
|
|
export const TTS_PROVIDER_DEFINITIONS: TtsProviderDefinition[] = [
|
|
{
|
|
id: 'custom-openai',
|
|
name: 'Custom OpenAI-Like',
|
|
supportsCustomModel: true,
|
|
models: () => CUSTOM_OPENAI_MODELS,
|
|
},
|
|
{
|
|
id: 'replicate',
|
|
name: 'Replicate',
|
|
supportsCustomModel: true,
|
|
models: () => REPLICATE_MODELS,
|
|
},
|
|
{
|
|
id: 'deepinfra',
|
|
name: 'Deepinfra',
|
|
supportsCustomModel: true,
|
|
models: () => DEEPINFRA_MODELS_FULL,
|
|
},
|
|
{
|
|
id: 'openai',
|
|
name: 'OpenAI',
|
|
supportsCustomModel: false,
|
|
models: () => OPENAI_MODELS,
|
|
},
|
|
];
|
|
|
|
const BUILT_IN_PROVIDER_ID_SET: ReadonlySet<TtsProviderId> = new Set(
|
|
TTS_PROVIDER_DEFINITIONS.map((definition) => definition.id),
|
|
);
|
|
const TTS_PROVIDER_TYPE_SET: ReadonlySet<TtsProviderType> = new Set([
|
|
...TTS_PROVIDER_DEFINITIONS.map((definition) => definition.id),
|
|
'unknown',
|
|
]);
|
|
|
|
const MODELS_WITH_INSTRUCTIONS = new Set([
|
|
'gpt-4o-mini-tts',
|
|
'google/gemini-3.1-flash-tts',
|
|
'qwen/qwen3-tts',
|
|
]);
|
|
|
|
const REPLICATE_MODELS_WITHOUT_NATIVE_SPEED = new Set([
|
|
'google/gemini-3.1-flash-tts',
|
|
'qwen/qwen3-tts',
|
|
]);
|
|
|
|
export function supportsTtsInstructions(model: string | null | undefined): boolean {
|
|
return !!model && MODELS_WITH_INSTRUCTIONS.has(model);
|
|
}
|
|
|
|
export function isBuiltInTtsProviderId(value: string | null | undefined): value is TtsProviderId {
|
|
return typeof value === 'string' && BUILT_IN_PROVIDER_ID_SET.has(value as TtsProviderId);
|
|
}
|
|
|
|
export function isTtsProviderType(value: unknown): value is TtsProviderType {
|
|
return typeof value === 'string' && TTS_PROVIDER_TYPE_SET.has(value as TtsProviderType);
|
|
}
|
|
|
|
export function resolveProviderType(
|
|
providerRef: string | null | undefined,
|
|
sharedProviders: readonly SharedProviderTypeResolverEntry[] = [],
|
|
): TtsProviderType {
|
|
if (isBuiltInTtsProviderId(providerRef)) {
|
|
return providerRef;
|
|
}
|
|
|
|
if (!providerRef) {
|
|
return 'unknown';
|
|
}
|
|
|
|
const shared = sharedProviders.find((entry) => entry.slug === providerRef);
|
|
return shared ? shared.providerType : 'unknown';
|
|
}
|
|
|
|
export function supportsNativeModelSpeed(provider: TtsProviderId, model: string | null | undefined): boolean {
|
|
if (!model) {
|
|
return true;
|
|
}
|
|
|
|
if (provider === 'replicate') {
|
|
return !REPLICATE_MODELS_WITHOUT_NATIVE_SPEED.has(model);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
export function getProviderDefinition(provider: string | null | undefined): TtsProviderDefinition | undefined {
|
|
return isBuiltInTtsProviderId(provider)
|
|
? TTS_PROVIDER_DEFINITIONS.find((definition) => definition.id === provider)
|
|
: undefined;
|
|
}
|
|
|
|
export function resolveProviderModels(provider: TtsProviderId, context?: ResolveProviderModelsContext): TtsModelDefinition[] {
|
|
return getProviderDefinition(provider)?.models(context) ?? DEFAULT_MODELS;
|
|
}
|
|
|
|
export function providerSupportsCustomModel(provider: TtsProviderId): boolean {
|
|
return getProviderDefinition(provider)?.supportsCustomModel ?? false;
|
|
}
|
|
|
|
export function getDefaultVoices(provider: TtsProviderId, model: string): string[] {
|
|
if (provider === 'openai') {
|
|
return supportsTtsInstructions(model) ? [...GPT4O_MINI_DEFAULT_VOICES] : [...OPENAI_DEFAULT_VOICES];
|
|
}
|
|
|
|
if (provider === 'custom-openai') {
|
|
return isKokoroModel(model) ? [...KOKORO_DEFAULT_VOICES] : [...CUSTOM_OPENAI_DEFAULT_VOICES];
|
|
}
|
|
|
|
if (provider === 'replicate') {
|
|
return REPLICATE_DEFAULT_VOICES_BY_MODEL[model] ? [...REPLICATE_DEFAULT_VOICES_BY_MODEL[model]] : ['default'];
|
|
}
|
|
|
|
if (provider === 'deepinfra') {
|
|
return DEEPINFRA_DEFAULT_VOICES_BY_MODEL[model]
|
|
? [...DEEPINFRA_DEFAULT_VOICES_BY_MODEL[model]]
|
|
: [...CUSTOM_OPENAI_DEFAULT_VOICES];
|
|
}
|
|
|
|
return [...OPENAI_DEFAULT_VOICES];
|
|
}
|
|
|
|
function parseReplicateModelIdentifier(model: string): {
|
|
owner: string;
|
|
name: string;
|
|
version?: string;
|
|
} | null {
|
|
const [ref, version] = model.split(':', 2);
|
|
const segments = ref.split('/');
|
|
if (segments.length !== 2 || !segments[0] || !segments[1]) {
|
|
return null;
|
|
}
|
|
|
|
const parsed = {
|
|
owner: segments[0],
|
|
name: segments[1],
|
|
};
|
|
|
|
return version
|
|
? { ...parsed, version }
|
|
: parsed;
|
|
}
|
|
|
|
export function resolveVoiceSource(provider: TtsProviderId, model: string): TtsVoiceSource {
|
|
if (provider === 'deepinfra' && DEEPINFRA_API_VOICE_MODELS.has(model)) {
|
|
return 'deepinfra-api';
|
|
}
|
|
|
|
if (provider === 'replicate' && parseReplicateModelIdentifier(model) !== null) {
|
|
return 'replicate-api';
|
|
}
|
|
|
|
if (provider === 'custom-openai') {
|
|
return 'custom-openai-api';
|
|
}
|
|
|
|
return 'static';
|
|
}
|