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 = { [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 = { '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 = new Set( TTS_PROVIDER_DEFINITIONS.map((definition) => definition.id), ); const TTS_PROVIDER_TYPE_SET: ReadonlySet = 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'; }