voice-resolver.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import type { TTSProviderId } from '@/lib/audio/types';
  2. import type { AgentConfig } from '@/lib/orchestration/registry/types';
  3. import { TTS_PROVIDERS } from '@/lib/audio/constants';
  4. export interface ResolvedVoice {
  5. providerId: TTSProviderId;
  6. modelId?: string;
  7. voiceId: string;
  8. }
  9. /**
  10. * Resolve the TTS provider + voice for an agent.
  11. * 1. If agent has voiceConfig and the voice is still valid, use it
  12. * 2. Otherwise, use the first available provider + deterministic voice by index
  13. */
  14. export function resolveAgentVoice(
  15. agent: AgentConfig,
  16. agentIndex: number,
  17. availableProviders: ProviderWithVoices[],
  18. ): ResolvedVoice {
  19. // Agent-specific config
  20. if (agent.voiceConfig) {
  21. // Browser-native voices are dynamic (not in static registry), so skip validation
  22. if (agent.voiceConfig.providerId === 'browser-native-tts') {
  23. return {
  24. providerId: agent.voiceConfig.providerId,
  25. modelId: agent.voiceConfig.modelId,
  26. voiceId: agent.voiceConfig.voiceId,
  27. };
  28. }
  29. const list = getServerVoiceList(agent.voiceConfig.providerId);
  30. if (list.includes(agent.voiceConfig.voiceId)) {
  31. return {
  32. providerId: agent.voiceConfig.providerId,
  33. modelId: agent.voiceConfig.modelId,
  34. voiceId: agent.voiceConfig.voiceId,
  35. };
  36. }
  37. }
  38. // Fallback: first available provider, deterministic voice
  39. if (availableProviders.length > 0) {
  40. const first = availableProviders[0];
  41. return {
  42. providerId: first.providerId,
  43. voiceId: first.voices[agentIndex % first.voices.length].id,
  44. };
  45. }
  46. return { providerId: 'browser-native-tts', voiceId: 'default' };
  47. }
  48. /**
  49. * Get the list of voice IDs for a TTS provider.
  50. * For browser-native-tts, returns empty (browser voices are dynamic).
  51. */
  52. export function getServerVoiceList(providerId: TTSProviderId): string[] {
  53. if (providerId === 'browser-native-tts') return [];
  54. const provider = TTS_PROVIDERS[providerId];
  55. if (!provider) return [];
  56. return provider.voices.map((v) => v.id);
  57. }
  58. export interface ModelVoiceGroup {
  59. modelId: string;
  60. modelName: string;
  61. voices: Array<{ id: string; name: string }>;
  62. }
  63. export interface ProviderWithVoices {
  64. providerId: TTSProviderId;
  65. providerName: string;
  66. voices: Array<{ id: string; name: string }>; // keep for backward compat
  67. modelGroups: ModelVoiceGroup[]; // voices grouped by model
  68. }
  69. /**
  70. * Get all available providers and their voices for the voice picker UI.
  71. * A provider is available if it has an API key or is server-configured.
  72. * Browser-native-tts is excluded (no static voice list).
  73. */
  74. export function getAvailableProvidersWithVoices(
  75. ttsProvidersConfig: Record<
  76. string,
  77. { apiKey?: string; enabled?: boolean; isServerConfigured?: boolean }
  78. >,
  79. ): ProviderWithVoices[] {
  80. const result: ProviderWithVoices[] = [];
  81. for (const [id, config] of Object.entries(TTS_PROVIDERS)) {
  82. const providerId = id as TTSProviderId;
  83. if (providerId === 'browser-native-tts') continue;
  84. if (config.voices.length === 0) continue;
  85. const providerConfig = ttsProvidersConfig[providerId];
  86. const hasApiKey = providerConfig?.apiKey && providerConfig.apiKey.trim().length > 0;
  87. const isServerConfigured = providerConfig?.isServerConfigured === true;
  88. if (hasApiKey || isServerConfigured) {
  89. const allVoices = config.voices.map((v) => ({ id: v.id, name: v.name }));
  90. // Build model groups
  91. const modelGroups: ModelVoiceGroup[] = [];
  92. if (config.models.length > 0) {
  93. for (const model of config.models) {
  94. const compatibleVoices = config.voices
  95. .filter((v) => !v.compatibleModels || v.compatibleModels.includes(model.id))
  96. .map((v) => ({ id: v.id, name: v.name }));
  97. modelGroups.push({
  98. modelId: model.id,
  99. modelName: model.name,
  100. voices: compatibleVoices,
  101. });
  102. }
  103. } else {
  104. // Provider has no model concept (Azure, Browser Native, Doubao)
  105. modelGroups.push({
  106. modelId: '',
  107. modelName: config.name,
  108. voices: allVoices,
  109. });
  110. }
  111. result.push({
  112. providerId,
  113. providerName: config.name,
  114. voices: allVoices,
  115. modelGroups,
  116. });
  117. }
  118. }
  119. return result;
  120. }
  121. /**
  122. * Find a voice display name across all providers.
  123. */
  124. export function findVoiceDisplayName(providerId: TTSProviderId, voiceId: string): string {
  125. const provider = TTS_PROVIDERS[providerId];
  126. if (!provider) return voiceId;
  127. const voice = provider.voices.find((v) => v.id === voiceId);
  128. return voice?.name ?? voiceId;
  129. }