diff --git a/apps/stage-tamagotchi/src/pages/settings/modules/speech.vue b/apps/stage-tamagotchi/src/pages/settings/modules/speech.vue index 843ae10f4..508f10a2b 100644 --- a/apps/stage-tamagotchi/src/pages/settings/modules/speech.vue +++ b/apps/stage-tamagotchi/src/pages/settings/modules/speech.vue @@ -18,6 +18,7 @@ import { FieldRange, Textarea, } from '@proj-airi/ui' +import { watchDebounced } from '@vueuse/core' import { generateSpeech } from '@xsai/generate-speech' import { storeToRefs } from 'pinia' import { onMounted, onUnmounted, ref, watch } from 'vue' @@ -59,9 +60,19 @@ onMounted(async () => { await speechStore.loadVoicesForProvider(activeSpeechProvider.value) }) -watch(activeSpeechProvider, async () => { +watchDebounced(activeSpeechProvider, async () => { await providersStore.loadModelsForConfiguredProviders() await speechStore.loadVoicesForProvider(activeSpeechProvider.value) +}, { debounce: 100 }) + +watch(activeSpeechVoiceId, (newId) => { + if (newId) { + const voices = availableVoices.value[activeSpeechProvider.value] || [] + const existingVoice = voices.find(voice => voice.id === newId) + if (!existingVoice) { + updateCustomVoiceName(newId) + } + } }) // Function to generate speech diff --git a/apps/stage-tamagotchi/src/pages/settings/providers/openai-compatible-audio-speech.vue b/apps/stage-tamagotchi/src/pages/settings/providers/openai-compatible-audio-speech.vue new file mode 100644 index 000000000..0a7bf7c7b --- /dev/null +++ b/apps/stage-tamagotchi/src/pages/settings/providers/openai-compatible-audio-speech.vue @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + meta: + layout: settings + stageTransition: + name: slide + diff --git a/apps/stage-tamagotchi/src/pages/settings/providers/openai-compatible-audio-transcription.vue b/apps/stage-tamagotchi/src/pages/settings/providers/openai-compatible-audio-transcription.vue new file mode 100644 index 000000000..8a431ddee --- /dev/null +++ b/apps/stage-tamagotchi/src/pages/settings/providers/openai-compatible-audio-transcription.vue @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + meta: + layout: settings + stageTransition: + name: slide + diff --git a/apps/stage-tamagotchi/src/pages/settings/providers/openai-compatible.vue b/apps/stage-tamagotchi/src/pages/settings/providers/openai-compatible.vue new file mode 100644 index 000000000..44b91ea40 --- /dev/null +++ b/apps/stage-tamagotchi/src/pages/settings/providers/openai-compatible.vue @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + meta: + layout: settings + stageTransition: + name: slide + diff --git a/packages/i18n/src/locales/en/settings.yaml b/packages/i18n/src/locales/en/settings.yaml index bdf011d48..23fe6ea9c 100644 --- a/packages/i18n/src/locales/en/settings.yaml +++ b/packages/i18n/src/locales/en/settings.yaml @@ -423,6 +423,9 @@ pages: openai: description: openai.com title: OpenAI + openai-compatible: + description: OpenAI Compatible + title: OpenAI Compatible openrouter: description: openrouter.ai title: OpenRouter diff --git a/packages/i18n/src/locales/es/settings.yaml b/packages/i18n/src/locales/es/settings.yaml index 5e3bb5536..5149cef08 100644 --- a/packages/i18n/src/locales/es/settings.yaml +++ b/packages/i18n/src/locales/es/settings.yaml @@ -422,6 +422,9 @@ pages: openai: description: openai.com title: OpenAI + openai-compatible: + description: OpenAI Compatible + title: OpenAI Compatible openrouter: description: openrouter.ai title: OpenRouter diff --git a/packages/i18n/src/locales/zh-Hans/settings.yaml b/packages/i18n/src/locales/zh-Hans/settings.yaml index ba0dd48b4..730b64ee7 100644 --- a/packages/i18n/src/locales/zh-Hans/settings.yaml +++ b/packages/i18n/src/locales/zh-Hans/settings.yaml @@ -392,6 +392,9 @@ pages: openai: description: OpenAi.com title: OpenAI + openai-compatible: + description: OpenAI Compatible + title: OpenAI Compatible openrouter: description: OpenRouter.ai title: OpenRouter diff --git a/packages/stage-ui/src/components/Scenarios/Providers/OpenAICompatibleSpeechPlayground.vue b/packages/stage-ui/src/components/Scenarios/Providers/OpenAICompatibleSpeechPlayground.vue new file mode 100644 index 000000000..f4878abdb --- /dev/null +++ b/packages/stage-ui/src/components/Scenarios/Providers/OpenAICompatibleSpeechPlayground.vue @@ -0,0 +1,200 @@ + + + + + + + + + {{ t('settings.pages.providers.provider.elevenlabs.playground.title') }} + + + + + + + + + + + + + + + + + + + + + {{ isGenerating ? t('settings.pages.providers.provider.elevenlabs.playground.buttons.button.test-voice.generating') : t('settings.pages.providers.provider.elevenlabs.playground.buttons.button.test-voice.label') }} + + + + + + {{ t('settings.pages.modules.speech.sections.section.playground.buttons.stop.label') }} + + + + + + {{ t('settings.pages.providers.provider.elevenlabs.playground.validation.error-missing-api-key') }} + + + {{ errorMessage }} + + + + + + + diff --git a/packages/stage-ui/src/stores/onboarding.ts b/packages/stage-ui/src/stores/onboarding.ts index a53bb889f..dd7cdb046 100644 --- a/packages/stage-ui/src/stores/onboarding.ts +++ b/packages/stage-ui/src/stores/onboarding.ts @@ -16,7 +16,7 @@ export const useOnboardingStore = defineStore('onboarding', () => { // Check if any essential provider is configured const hasEssentialProviderConfigured = computed(() => { - const essentialProviders = ['openai', 'anthropic', 'google-generative-ai', 'openrouter-ai', 'ollama', 'deepseek'] + const essentialProviders = ['openai', 'anthropic', 'google-generative-ai', 'openrouter-ai', 'ollama', 'deepseek', 'openai-compatible'] return essentialProviders.some(providerId => providersStore.configuredProviders[providerId]) }) diff --git a/packages/stage-ui/src/stores/providers.ts b/packages/stage-ui/src/stores/providers.ts index 36e96d354..c666326ec 100644 --- a/packages/stage-ui/src/stores/providers.ts +++ b/packages/stage-ui/src/stores/providers.ts @@ -812,6 +812,54 @@ export const useProvidersStore = defineStore('providers', () => { }, }, }, + 'openai-compatible': { + id: 'openai-compatible', + category: 'chat', + tasks: ['text-generation'], + nameKey: 'settings.pages.providers.provider.openai-compatible.title', + name: 'OpenAI Compatible', + descriptionKey: 'settings.pages.providers.provider.openai-compatible.description', + description: 'Connect to any API that follows the OpenAI specification.', + icon: 'i-lobe-icons:openai', + defaultOptions: () => ({ + baseUrl: '', + }), + createProvider: async config => createOpenAI((config.apiKey as string).trim(), (config.baseUrl as string).trim()), + capabilities: { + listModels: async (config) => { + return (await listModels({ + ...createOpenAI((config.apiKey as string).trim(), (config.baseUrl as string).trim()).model(), + })).map((model) => { + return { + id: model.id, + name: model.id, + provider: 'openai-compatible', + description: '', + contextLength: 0, + deprecated: false, + } satisfies ModelInfo + }) + }, + }, + validators: { + validateProviderConfig: (config) => { + const errors = [ + !config.apiKey && new Error('API key is required'), + !config.baseUrl && new Error('Base URL is required'), + ].filter(Boolean) + + if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) { + return notBaseUrlError.value + } + + return { + errors, + reason: errors.filter(e => e).map(e => String(e)).join(', ') || '', + valid: !!config.apiKey && !!config.baseUrl, + } + }, + }, + }, 'openai-audio-speech': { id: 'openai-audio-speech', category: 'speech', @@ -929,6 +977,57 @@ export const useProvidersStore = defineStore('providers', () => { }, }, }, + 'openai-compatible-audio-speech': { + id: 'openai-compatible-audio-speech', + category: 'speech', + tasks: ['text-to-speech'], + nameKey: 'settings.pages.providers.provider.openai-compatible.title', + name: 'OpenAI Compatible', + descriptionKey: 'settings.pages.providers.provider.openai-compatible.description', + description: 'Connect to any API that follows the OpenAI specification.', + icon: 'i-lobe-icons:openai', + defaultOptions: () => ({ + baseUrl: '', + }), + createProvider: async config => createOpenAI((config.apiKey as string).trim(), (config.baseUrl as string).trim()), + capabilities: { + listModels: async (config) => { + return (await listModels({ + ...createOpenAI((config.apiKey as string).trim(), (config.baseUrl as string).trim()).model(), + })).map((model) => { + return { + id: model.id, + name: model.id, + provider: 'openai-compatible-audio-speech', + description: '', + contextLength: 0, + deprecated: false, + } satisfies ModelInfo + }) + }, + listVoices: async () => { + return [] + }, + }, + validators: { + validateProviderConfig: (config) => { + const errors = [ + !config.apiKey && new Error('API key is required'), + !config.baseUrl && new Error('Base URL is required'), + ].filter(Boolean) + + if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) { + return notBaseUrlError.value + } + + return { + errors, + reason: errors.filter(e => e).map(e => String(e)).join(', ') || '', + valid: !!config.apiKey && !!config.baseUrl, + } + }, + }, + }, 'openai-audio-transcription': { id: 'openai-audio-transcription', category: 'transcription', @@ -976,6 +1075,54 @@ export const useProvidersStore = defineStore('providers', () => { }, }, }, + 'openai-compatible-audio-transcription': { + id: 'openai-compatible-audio-transcription', + category: 'transcription', + tasks: ['speech-to-text', 'automatic-speech-recognition', 'asr', 'stt'], + nameKey: 'settings.pages.providers.provider.openai-compatible.title', + name: 'OpenAI Compatible', + descriptionKey: 'settings.pages.providers.provider.openai-compatible.description', + description: 'Connect to any API that follows the OpenAI specification.', + icon: 'i-lobe-icons:openai', + defaultOptions: () => ({ + baseUrl: '', + }), + createProvider: async config => createOpenAI((config.apiKey as string).trim(), (config.baseUrl as string).trim()), + capabilities: { + listModels: async (config) => { + return (await listModels({ + ...createOpenAI((config.apiKey as string).trim(), (config.baseUrl as string).trim()).model(), + })).map((model) => { + return { + id: model.id, + name: model.id, + provider: 'openai-compatible-audio-transcription', + description: '', + contextLength: 0, + deprecated: false, + } satisfies ModelInfo + }) + }, + }, + validators: { + validateProviderConfig: (config) => { + const errors = [ + !config.apiKey && new Error('API key is required'), + !config.baseUrl && new Error('Base URL is required'), + ].filter(Boolean) + + if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) { + return notBaseUrlError.value + } + + return { + errors, + reason: errors.filter(e => e).map(e => String(e)).join(', ') || '', + valid: !!config.apiKey && !!config.baseUrl, + } + }, + }, + }, 'azure-ai-foundry': { id: 'azure-ai-foundry', category: 'chat',