feat(stage-*): OpenAI speech

This commit is contained in:
Neko Ayaka
2025-07-18 02:45:08 +08:00
parent c684258e70
commit 4c25c85d6f
6 changed files with 211 additions and 161 deletions
@@ -203,7 +203,6 @@ watch(providers, async () => {
</div>
</template>
<!-- Replace the default playground with our standalone component -->
<template #playground>
<SpeechPlayground
:available-voices="availableVoices"
@@ -1,106 +1,97 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import type { SpeechProvider } from '@xsai-ext/shared-providers'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
ProviderBaseUrlInput,
ProviderBasicSettings,
ProviderSettingsContainer,
ProviderSettingsLayout,
SpeechPlayground,
SpeechProviderSettings,
} from '@proj-airi/stage-ui/components'
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
import { FieldRange } from '@proj-airi/ui'
import { storeToRefs } from 'pinia'
import { computed, onMounted, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
const { t } = useI18n()
const router = useRouter()
const speechStore = useSpeechStore()
const providersStore = useProvidersStore()
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
const { providers } = storeToRefs(providersStore)
const { t } = useI18n()
const defaultVoiceSettings = {
speed: 1.0,
}
// Get provider metadata
const providerId = 'openai'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
const providerId = 'openai-audio-speech'
const defaultModel = 'gpt-4o-mini-tts'
// Use computed properties for settings
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
const speed = ref<number>(1.0)
providers.value[providerId].apiKey = value
},
// Check if API key is configured
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
const availableVoices = computed(() => {
return speechStore.availableVoices[providerId] || []
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || 'https://api.openai.com/v1/',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
onMounted(() => {
// Initialize provider if it doesn't exist
if (!providers.value[providerId]) {
providers.value[providerId] = {
baseUrl: 'https://api.openai.com/v1/',
}
// Generate speech with ElevenLabs-specific parameters
async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean) {
const provider = providersStore.getProviderInstance(providerId) as SpeechProvider<string>
if (!provider) {
throw new Error('Failed to initialize speech provider')
}
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
baseUrl.value = providers.value[providerId]?.baseUrl || 'https://api.openai.com/v1/'
})
// Get provider configuration
const providerConfig = providersStore.getProviderConfig(providerId)
// Watch settings and update the provider configuration
watch([apiKey, baseUrl], () => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
baseUrl: baseUrl.value || 'https://api.openai.com/v1/',
}
})
// Get model from configuration or use default
const model = providerConfig.model as string | undefined || defaultModel
function handleResetSettings() {
providers.value[providerId] = {
baseUrl: 'https://api.openai.com/v1/',
}
// ElevenLabs doesn't need SSML conversion, but if SSML is provided, use it directly
return await speechStore.speech(
provider,
model,
input,
voiceId,
{
...providerConfig,
...defaultVoiceSettings,
},
)
}
watch(speed, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.speed = speed.value
})
</script>
<template>
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName || 'OpenAI'"
:provider-icon="providerMetadata?.icon"
:on-back="() => router.back()"
<SpeechProviderSettings
:provider-id="providerId"
:default-model="defaultModel"
:additional-settings="defaultVoiceSettings"
>
<ProviderSettingsContainer>
<ProviderBasicSettings
:title="t('settings.pages.providers.common.section.basic.title')"
:description="t('settings.pages.providers.common.section.basic.description')"
:on-reset="handleResetSettings"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName || 'OpenAI'"
placeholder="sk-..."
/>
</ProviderBasicSettings>
<!-- Voice settings specific to ElevenLabs -->
<template #voice-settings>
<!-- Speed control - common to most providers -->
<FieldRange
v-model="speed"
:label="t('settings.pages.providers.provider.common.fields.field.speed.label')"
:description="t('settings.pages.providers.provider.common.fields.field.speed.description')"
:min="0.5"
:max="2.0" :step="0.01"
/>
</template>
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
<ProviderBaseUrlInput
v-model="baseUrl"
placeholder="https://api.openai.com/v1/"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
<template #playground>
<SpeechPlayground
:available-voices="availableVoices"
:generate-speech="handleGenerateSpeech"
:api-key-configured="apiKeyConfigured"
default-text="Hello! This is a test of the OpenAI Speech."
/>
</template>
</SpeechProviderSettings>
</template>
<route lang="yaml">
@@ -203,7 +203,6 @@ watch(providers, async () => {
</div>
</template>
<!-- Replace the default playground with our standalone component -->
<template #playground>
<SpeechPlayground
:available-voices="availableVoices"
@@ -1,106 +1,97 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import type { SpeechProvider } from '@xsai-ext/shared-providers'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
ProviderBaseUrlInput,
ProviderBasicSettings,
ProviderSettingsContainer,
ProviderSettingsLayout,
SpeechPlayground,
SpeechProviderSettings,
} from '@proj-airi/stage-ui/components'
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
import { FieldRange } from '@proj-airi/ui'
import { storeToRefs } from 'pinia'
import { computed, onMounted, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
const { t } = useI18n()
const router = useRouter()
const speechStore = useSpeechStore()
const providersStore = useProvidersStore()
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
const { providers } = storeToRefs(providersStore)
const { t } = useI18n()
const defaultVoiceSettings = {
speed: 1.0,
}
// Get provider metadata
const providerId = 'openai'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
const providerId = 'openai-audio-speech'
const defaultModel = 'gpt-4o-mini-tts'
// Use computed properties for settings
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
const speed = ref<number>(1.0)
providers.value[providerId].apiKey = value
},
// Check if API key is configured
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
const availableVoices = computed(() => {
return speechStore.availableVoices[providerId] || []
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || 'https://api.openai.com/v1/',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
onMounted(() => {
// Initialize provider if it doesn't exist
if (!providers.value[providerId]) {
providers.value[providerId] = {
baseUrl: 'https://api.openai.com/v1/',
}
// Generate speech with ElevenLabs-specific parameters
async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean) {
const provider = providersStore.getProviderInstance(providerId) as SpeechProvider<string>
if (!provider) {
throw new Error('Failed to initialize speech provider')
}
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
baseUrl.value = providers.value[providerId]?.baseUrl || 'https://api.openai.com/v1/'
})
// Get provider configuration
const providerConfig = providersStore.getProviderConfig(providerId)
// Watch settings and update the provider configuration
watch([apiKey, baseUrl], () => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
baseUrl: baseUrl.value || 'https://api.openai.com/v1/',
}
})
// Get model from configuration or use default
const model = providerConfig.model as string | undefined || defaultModel
function handleResetSettings() {
providers.value[providerId] = {
baseUrl: 'https://api.openai.com/v1/',
}
// ElevenLabs doesn't need SSML conversion, but if SSML is provided, use it directly
return await speechStore.speech(
provider,
model,
input,
voiceId,
{
...providerConfig,
...defaultVoiceSettings,
},
)
}
watch(speed, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.speed = speed.value
})
</script>
<template>
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName || 'OpenAI'"
:provider-icon="providerMetadata?.icon"
:on-back="() => router.back()"
<SpeechProviderSettings
:provider-id="providerId"
:default-model="defaultModel"
:additional-settings="defaultVoiceSettings"
>
<ProviderSettingsContainer>
<ProviderBasicSettings
:title="t('settings.pages.providers.common.section.basic.title')"
:description="t('settings.pages.providers.common.section.basic.description')"
:on-reset="handleResetSettings"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName || 'OpenAI'"
placeholder="sk-..."
/>
</ProviderBasicSettings>
<!-- Voice settings specific to ElevenLabs -->
<template #voice-settings>
<!-- Speed control - common to most providers -->
<FieldRange
v-model="speed"
:label="t('settings.pages.providers.provider.common.fields.field.speed.label')"
:description="t('settings.pages.providers.provider.common.fields.field.speed.description')"
:min="0.5"
:max="2.0" :step="0.01"
/>
</template>
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
<ProviderBaseUrlInput
v-model="baseUrl"
placeholder="https://api.openai.com/v1/"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
<template #playground>
<SpeechPlayground
:available-voices="availableVoices"
:generate-speech="handleGenerateSpeech"
:api-key-configured="apiKeyConfigured"
default-text="Hello! This is a test of the OpenAI Speech."
/>
</template>
</SpeechProviderSettings>
</template>
<route lang="yaml">
@@ -74,7 +74,7 @@ export const useSpeechStore = defineStore('speech', () => {
// Helper function to determine if a provider is a speech provider
function isSpeechProvider(providerId: string): boolean {
return ['elevenlabs', 'microsoft-speech', 'azure-speech', 'google', 'amazon', 'alibaba-cloud-model-studio', 'volcengine', 'player2-speech'].includes(providerId)
return ['openai-audio-speech', 'elevenlabs', 'microsoft-speech', 'azure-speech', 'google', 'amazon', 'alibaba-cloud-model-studio', 'volcengine', 'player2-speech'].includes(providerId)
}
async function loadVoicesForProvider(provider: string) {
+70
View File
@@ -606,6 +606,76 @@ export const useProvidersStore = defineStore('providers', () => {
} satisfies ModelInfo
})
},
listVoices: async () => {
return [
{
id: 'alloy',
name: 'Alloy',
provider: 'openai-audio-speech',
languages: [],
},
{
id: 'ash',
name: 'Ash',
provider: 'openai-audio-speech',
languages: [],
},
{
id: 'ballad',
name: 'Ballad',
provider: 'openai-audio-speech',
languages: [],
},
{
id: 'coral',
name: 'Coral',
provider: 'openai-audio-speech',
languages: [],
},
{
id: 'echo',
name: 'Echo',
provider: 'openai-audio-speech',
languages: [],
},
{
id: 'fable',
name: 'Fable',
provider: 'openai-audio-speech',
languages: [],
},
{
id: 'onyx',
name: 'Onyx',
provider: 'openai-audio-speech',
languages: [],
},
{
id: 'nova',
name: 'Nova',
provider: 'openai-audio-speech',
languages: [],
},
{
id: 'sage',
name: 'Sage',
provider: 'openai-audio-speech',
languages: [],
},
{
id: 'shimmer',
name: 'Shimmer',
provider: 'openai-audio-speech',
languages: [],
},
{
id: 'verse',
name: 'Verse',
provider: 'openai-audio-speech',
languages: [],
},
] satisfies VoiceInfo[]
},
},
validators: {
validateProviderConfig: (config) => {