diff --git a/.github/actions/setup-swiftlint/action.yml b/.github/actions/setup-swiftlint/action.yml
index 55f9f606e..355cb8f83 100644
--- a/.github/actions/setup-swiftlint/action.yml
+++ b/.github/actions/setup-swiftlint/action.yml
@@ -44,17 +44,28 @@ runs:
shell: bash
run: |
if [[ "${{ inputs.version }}" == "latest" ]]; then
- VERSION=$(curl -s https://api.github.com/repos/realm/SwiftLint/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
- if [[ -z "$VERSION" ]]; then
- echo "Error: Failed to get latest version"
+ # Fetch the latest release tag using a more robust method
+ API_RESPONSE=$(curl -s https://api.github.com/repos/realm/SwiftLint/releases/latest)
+ # Try using jq if available (most GitHub Actions runners have it)
+ if command -v jq >/dev/null 2>&1; then
+ VERSION=$(echo "$API_RESPONSE" | jq -r '.tag_name // empty')
+ else
+ # Fallback: parse JSON manually with sed, being very specific about the tag_name field
+ VERSION=$(echo "$API_RESPONSE" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
+ fi
+
+ # Validate the version looks correct (should contain version numbers)
+ if [[ -z "$VERSION" ]] || [[ "$VERSION" == "null" ]] || [[ ! "$VERSION" =~ ^[0-9] ]]; then
+ echo "Error: Failed to get latest version or got invalid version: '$VERSION'"
+ echo "API Response (first 50 lines):"
+ echo "$API_RESPONSE" | head -50
exit 1
fi
echo "Latest version: $VERSION"
else
VERSION="${{ inputs.version }}"
- if [[ ! "$VERSION" =~ ^v ]]; then
- VERSION="v$VERSION"
- fi
+ # Remove 'v' prefix if user provided it, since SwiftLint tags don't use it
+ VERSION="${VERSION#v}"
echo "Using specified version: $VERSION"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
diff --git a/.gitignore b/.gitignore
index 026d97c30..644073761 100644
--- a/.gitignore
+++ b/.gitignore
@@ -121,4 +121,4 @@ plugins-local
plugins-development
#ia
-GEMINI.md
\ No newline at end of file
+GEMINI.md
diff --git a/packages/stage-pages/src/pages/settings/modules/hearing.vue b/packages/stage-pages/src/pages/settings/modules/hearing.vue
index af66d5fb7..41f1e94ba 100644
--- a/packages/stage-pages/src/pages/settings/modules/hearing.vue
+++ b/packages/stage-pages/src/pages/settings/modules/hearing.vue
@@ -8,10 +8,10 @@ import { useAudioContext } from '@proj-airi/stage-ui/stores/audio'
import { useHearingSpeechInputPipeline, useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
import { useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
-import { Button, FieldCheckbox, FieldRange, FieldSelect } from '@proj-airi/ui'
+import { Button, FieldCheckbox, FieldInput, FieldRange, FieldSelect } from '@proj-airi/ui'
import { until } from '@vueuse/core'
import { storeToRefs } from 'pinia'
-import { computed, onUnmounted, ref, watch } from 'vue'
+import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
@@ -222,8 +222,29 @@ const speakingIndicatorClass = computed(() => {
}
})
-function updateCustomModelName(value: string) {
- activeCustomModelName.value = value
+function updateCustomModelName(value: string | undefined) {
+ const modelValue = value || ''
+ activeCustomModelName.value = modelValue
+ activeTranscriptionModel.value = modelValue
+}
+
+// Sync OpenAI Compatible model from provider config
+function syncOpenAICompatibleSettings() {
+ if (activeTranscriptionProvider.value !== 'openai-compatible-audio-transcription')
+ return
+
+ const providerConfig = providersStore.getProviderConfig(activeTranscriptionProvider.value)
+ // Always sync model from provider config (override any existing value from previous provider)
+ if (providerConfig?.model) {
+ activeTranscriptionModel.value = providerConfig.model as string
+ updateCustomModelName(providerConfig.model as string)
+ }
+ else {
+ // If no model in provider config, use default
+ const defaultModel = 'whisper-1'
+ activeTranscriptionModel.value = defaultModel
+ updateCustomModelName(defaultModel)
+ }
}
onStopRecord(async (recording) => {
@@ -427,6 +448,7 @@ watch(activeTranscriptionProvider, async (provider) => {
return
await hearingStore.loadModelsForProvider(provider)
+ syncOpenAICompatibleSettings()
// Auto-select first model for Web Speech API if no model is selected
if (provider === 'browser-web-speech-api' && !activeTranscriptionModel.value) {
@@ -438,6 +460,11 @@ watch(activeTranscriptionProvider, async (provider) => {
}
}, { immediate: true })
+onMounted(async () => {
+ // Audio devices are loaded on demand when user requests them
+ syncOpenAICompatibleSettings()
+})
+
onUnmounted(() => {
stopSTTTest()
stopAudioMonitoring()
@@ -537,19 +564,25 @@ onUnmounted(() => {
-
+
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.title') }}
- {{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.subtitle') }}
+
+
+ {{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.subtitle') }}
+
+
+ Enter the transcription model to use (e.g., 'whisper-1', 'gpt-4o-transcribe')
+
-
+
@@ -558,14 +591,26 @@ onUnmounted(() => {
-
+
+
+
+
+
+
@@ -576,8 +621,8 @@ onUnmounted(() => {
-
-
+
+
(null)
const errorMessage = ref('')
+// Sync OpenAI Compatible model and voice from provider config
+function syncOpenAICompatibleSettings() {
+ if (activeSpeechProvider.value !== 'openai-compatible-audio-speech')
+ return
+
+ const providerConfig = providersStore.getProviderConfig(activeSpeechProvider.value)
+ // Sync model from provider config (override any existing value from previous provider)
+ if (providerConfig?.model) {
+ activeSpeechModel.value = providerConfig.model as string
+ }
+ else {
+ // If no model in provider config, use default
+ activeSpeechModel.value = 'tts-1'
+ }
+ // Sync voice from provider config (override any existing value from previous provider)
+ // Use updateCustomVoiceName to ensure proper reactivity
+ if (providerConfig?.voice) {
+ activeSpeechVoiceId.value = providerConfig.voice as string
+ updateCustomVoiceName(providerConfig.voice as string)
+ }
+ else {
+ // If no voice in provider config, use default
+ activeSpeechVoiceId.value = 'alloy'
+ updateCustomVoiceName('alloy')
+ }
+}
+
onMounted(async () => {
await providersStore.loadModelsForConfiguredProviders()
await speechStore.loadVoicesForProvider(activeSpeechProvider.value)
+ syncOpenAICompatibleSettings()
})
-watch(activeSpeechProvider, async () => {
+watch(activeSpeechProvider, async (newProvider) => {
await providersStore.loadModelsForConfiguredProviders()
- await speechStore.loadVoicesForProvider(activeSpeechProvider.value)
+ await speechStore.loadVoicesForProvider(newProvider)
+ syncOpenAICompatibleSettings()
})
// Function to generate speech
@@ -75,16 +104,6 @@ async function generateTestSpeech() {
if (useSSML.value && !ssmlText.value.trim())
return
- if (!activeSpeechModel.value) {
- console.error('No model selected')
- return
- }
-
- if (!activeSpeechVoice.value) {
- console.error('No voice selected')
- return
- }
-
const provider = await providersStore.getProviderInstance(activeSpeechProvider.value) as SpeechProviderWithExtraOptions
if (!provider) {
console.error('Failed to initialize speech provider')
@@ -93,6 +112,37 @@ async function generateTestSpeech() {
const providerConfig = providersStore.getProviderConfig(activeSpeechProvider.value)
+ // For OpenAI Compatible providers, fall back to provider config for model and voice
+ let model = activeSpeechModel.value
+ let voice = activeSpeechVoice.value
+
+ if (activeSpeechProvider.value === 'openai-compatible-audio-speech') {
+ if (!model && providerConfig?.model) {
+ model = providerConfig.model as string
+ }
+ if (!voice && providerConfig?.voice) {
+ voice = {
+ id: providerConfig.voice as string,
+ name: providerConfig.voice as string,
+ description: providerConfig.voice as string,
+ previewURL: '',
+ languages: [{ code: 'en', title: 'English' }],
+ provider: activeSpeechProvider.value,
+ gender: 'neutral',
+ }
+ }
+ }
+
+ if (!model) {
+ console.error('No model selected')
+ return
+ }
+
+ if (!voice) {
+ console.error('No voice selected')
+ return
+ }
+
isGenerating.value = true
errorMessage.value = ''
@@ -104,12 +154,12 @@ async function generateTestSpeech() {
const input = useSSML.value
? ssmlText.value
- : speechStore.supportsSSML ? speechStore.generateSSML(testText.value, activeSpeechVoice.value, { ...providerConfig, pitch: pitch.value }) : testText.value
+ : speechStore.supportsSSML ? speechStore.generateSSML(testText.value, voice, { ...providerConfig, pitch: pitch.value }) : testText.value
const response = await generateSpeech({
- ...provider.speech(activeSpeechModel.value, providerConfig),
+ ...provider.speech(model, providerConfig),
input,
- voice: activeSpeechVoice.value.id,
+ voice: voice.id,
})
// Convert the response to a blob and create an object URL
@@ -169,8 +219,8 @@ function updateCustomVoiceName(value: string | undefined) {
}
}
-function updateCustomModelName(value: string) {
- activeSpeechModel.value = value
+function updateCustomModelName(value: string | undefined) {
+ activeSpeechModel.value = value || ''
}
@@ -237,7 +287,7 @@ function updateCustomModelName(value: string) {
-
+
@@ -248,48 +298,62 @@ function updateCustomModelName(value: string) {
-
-
-
-
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.loading') }}
+
+
+
-
-
+
+
+
+
+
+
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.loading') }}
+
-
-
-
- {{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_models') }}
-
-
- {{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_models_description') }}
-
-
-
-
-
-
+
-
+
+
+
+
+ {{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_models') }}
+
+
+ {{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_models_description') }}
+
+
+
+
+
+
+
+
@@ -331,15 +395,20 @@ function updateCustomModelName(value: string) {
-
+
diff --git a/packages/stage-pages/src/pages/settings/providers/speech/openai-audio-speech.vue b/packages/stage-pages/src/pages/settings/providers/speech/openai-audio-speech.vue
index 465f80433..7d7740378 100644
--- a/packages/stage-pages/src/pages/settings/providers/speech/openai-audio-speech.vue
+++ b/packages/stage-pages/src/pages/settings/providers/speech/openai-audio-speech.vue
@@ -7,9 +7,9 @@ import {
} from '@proj-airi/stage-ui/components'
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
-import { FieldRange } from '@proj-airi/ui'
+import { FieldRange, FieldSelect } from '@proj-airi/ui'
import { storeToRefs } from 'pinia'
-import { computed, ref, watch } from 'vue'
+import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const speechStore = useSpeechStore()
@@ -27,14 +27,55 @@ const defaultModel = 'gpt-4o-mini-tts'
const speed = ref(1.0)
+// Model selection
+const model = computed({
+ get: () => providers.value[providerId]?.model as string | undefined || defaultModel,
+ set: (value) => {
+ if (!providers.value[providerId])
+ providers.value[providerId] = {}
+ providers.value[providerId].model = value
+ },
+})
+
+// Load models and voices
+const providerModels = computed(() => {
+ return providersStore.getModelsForProvider(providerId)
+})
+
+const isLoadingModels = computed(() => {
+ return providersStore.isLoadingModels[providerId] || false
+})
+
// Check if API key is configured
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
+// Filter voices based on the selected model's compatibility
const availableVoices = computed(() => {
- return speechStore.availableVoices[providerId] || []
+ const allVoices = speechStore.availableVoices[providerId] || []
+ const selectedModel = model.value || defaultModel
+
+ // Filter voices to only show those compatible with the selected model
+ return allVoices.filter((voice) => {
+ // If voice has no compatibleModels array, include it (backward compatibility)
+ if (!voice.compatibleModels || voice.compatibleModels.length === 0) {
+ return true
+ }
+ // Check if the selected model is in the voice's compatibleModels array
+ return voice.compatibleModels.includes(selectedModel)
+ })
})
-// Generate speech with ElevenLabs-specific parameters
+// Load models and voices on mount
+onMounted(async () => {
+ await providersStore.loadModelsForConfiguredProviders()
+ await providersStore.fetchModelsForProvider(providerId)
+ // Load voices
+ // NOTE: OpenAI does not provide an API endpoint to retrieve available voices.
+ // Voices are hardcoded in provider metadata - this is a provider limitation, not an application limitation.
+ await speechStore.loadVoicesForProvider(providerId)
+})
+
+// Generate speech with OpenAI-specific parameters
async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean) {
const provider = await providersStore.getProviderInstance>(providerId)
if (!provider) {
@@ -44,13 +85,12 @@ async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: bo
// Get provider configuration
const providerConfig = providersStore.getProviderConfig(providerId)
- // Get model from configuration or use default
- const model = providerConfig.model as string | undefined || defaultModel
+ // Use the reactive model computed property (not a local variable)
+ const modelToUse = model.value || defaultModel
- // ElevenLabs doesn't need SSML conversion, but if SSML is provided, use it directly
return await speechStore.speech(
provider,
- model,
+ modelToUse,
input,
voiceId,
{
@@ -64,6 +104,14 @@ watch(speed, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.speed = speed.value
})
+
+watch(model, async () => {
+ const providerConfig = providersStore.getProviderConfig(providerId)
+ providerConfig.model = model.value
+ // Reload voices when model changes to ensure compatibility filtering is applied
+ // Note: Voice compatibility varies by model - some voices (ballad, verse, marin, cedar) are only compatible with gpt-4o-mini-tts models
+ await speechStore.loadVoicesForProvider(providerId)
+})
@@ -72,8 +120,17 @@ watch(speed, async () => {
:default-model="defaultModel"
:additional-settings="defaultVoiceSettings"
>
-
+
+
+
-import type { RemovableRef } from '@vueuse/core'
import type { SpeechProvider } from '@xsai-ext/providers/utils'
import {
Alert,
- ProviderAdvancedSettings,
- ProviderApiKeyInput,
- ProviderBaseUrlInput,
- ProviderBasicSettings,
- ProviderSettingsContainer,
- ProviderSettingsLayout,
SpeechPlaygroundOpenAICompatible,
+ SpeechProviderSettings,
} from '@proj-airi/stage-ui/components'
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
-import { FieldRange } from '@proj-airi/ui'
+import { FieldInput, FieldRange } from '@proj-airi/ui'
import { storeToRefs } from 'pinia'
-import { computed, ref } from 'vue'
+import { computed, onMounted, ref, watch } from 'vue'
+import { useI18n } from 'vue-i18n'
const speechStore = useSpeechStore()
const providersStore = useProvidersStore()
-const { providers } = storeToRefs(providersStore) as { providers: RemovableRef> }
+const { providers } = storeToRefs(providersStore)
+const { t } = useI18n()
const defaultVoiceSettings = {
speed: 1.0,
@@ -29,58 +25,99 @@ const defaultVoiceSettings = {
// Get provider metadata
const providerId = 'openai-compatible-audio-speech'
+const defaultModel = 'tts-1'
-// Settings refs
-const apiKey = computed({
- get: () => providers.value[providerId]?.apiKey || '',
- set: (value) => {
- if (providers.value[providerId])
- providers.value[providerId].apiKey = value
- },
-})
-
-const baseUrl = computed({
- get: () => providers.value[providerId]?.baseUrl || '',
- set: (value) => {
- if (providers.value[providerId])
- providers.value[providerId].baseUrl = value
- },
-})
+// Initialize speed from provider config or default
+const speed = ref(
+ (providers.value[providerId] as any)?.voiceSettings?.speed
+ || (providers.value[providerId] as any)?.speed
+ || defaultVoiceSettings.speed,
+)
+// Model selection
const model = computed({
- get: () => providers.value[providerId]?.model || 'tts-1',
+ get: () => providers.value[providerId]?.model as string | undefined || defaultModel,
set: (value) => {
- if (providers.value[providerId])
- providers.value[providerId].model = value
+ if (!providers.value[providerId])
+ providers.value[providerId] = {}
+ providers.value[providerId].model = value
},
})
const voice = computed({
get: () => providers.value[providerId]?.voice || 'alloy',
set: (value) => {
- if (providers.value[providerId])
- providers.value[providerId].voice = value
+ if (!providers.value[providerId])
+ providers.value[providerId] = {}
+ providers.value[providerId].voice = value
},
})
-const speed = ref(1.0)
+// TODO: use `useRefHistory` for this
+// Watch provider config changes to sync local refs (for reset functionality)
+watch(
+ () => providers.value[providerId],
+ (newConfig) => {
+ if (newConfig) {
+ // Sync speed from voiceSettings or direct speed property
+ const config = newConfig as any
+ const newSpeed = config.voiceSettings?.speed || config.speed || defaultVoiceSettings.speed
+ if (Math.abs(speed.value - newSpeed) > 0.001) // Use small epsilon for float comparison
+ speed.value = newSpeed
+
+ // Sync model if it was reset
+ if (!config.model && model.value !== defaultModel)
+ model.value = defaultModel
+
+ // Sync voice if it was reset
+ if (!config.voice && voice.value !== 'alloy')
+ voice.value = 'alloy'
+ }
+ else {
+ // Provider config was reset, reset our local refs to defaults
+ speed.value = defaultVoiceSettings.speed
+ model.value = defaultModel
+ voice.value = 'alloy'
+ }
+ },
+ { deep: true, immediate: true },
+)
// Check if API key is configured
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
-// Generate speech with specific parameters
+// Ensure provider config is initialized on mount
+onMounted(() => {
+ if (!providers.value[providerId]) {
+ providers.value[providerId] = {}
+ }
+ // Initialize model and voice if they don't exist
+ if (!providers.value[providerId].model) {
+ providers.value[providerId].model = defaultModel
+ }
+ if (!providers.value[providerId].voice) {
+ providers.value[providerId].voice = 'alloy'
+ }
+})
+
+// Generate speech with OpenAI-compatible parameters
async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean, modelId?: string) {
const provider = await providersStore.getProviderInstance>(providerId)
- if (!provider)
+ if (!provider) {
throw new Error('Failed to initialize speech provider')
+ }
+ // Get provider configuration
const providerConfig = providersStore.getProviderConfig(providerId)
+ // Use the reactive model computed property (not a local variable)
+ const modelToUse = modelId || model.value || defaultModel
+
return await speechStore.speech(
provider,
- modelId || model.value,
+ modelToUse,
input,
- voiceId || voice.value,
+ voiceId || (voice.value as string),
{
...providerConfig,
...defaultVoiceSettings,
@@ -89,54 +126,75 @@ async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: bo
)
}
+watch(speed, async () => {
+ if (!providers.value[providerId])
+ providers.value[providerId] = {}
+ providers.value[providerId].speed = speed.value
+})
+
+watch(model, () => {
+ // Ensure provider config exists
+ if (!providers.value[providerId])
+ providers.value[providerId] = {}
+ // Save model to provider config (this persists to localStorage automatically)
+ providers.value[providerId].model = model.value
+})
+
+watch(voice, () => {
+ // Ensure provider config exists
+ if (!providers.value[providerId])
+ providers.value[providerId] = {}
+ // Save voice to provider config (this persists to localStorage automatically)
+ providers.value[providerId].voice = voice.value
+})
+
// Use the composable to get validation logic and state
const {
- t,
- router,
- providerMetadata,
isValidating,
isValid,
validationMessage,
- handleResetSettings,
forceValid,
} = useProviderValidation(providerId)
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
+
+
+
-
+
+
@@ -161,16 +219,8 @@ const {
{{ t('settings.dialogs.onboarding.validationSuccess') }}
-
-
-
-
+
+
diff --git a/packages/stage-pages/src/pages/settings/providers/transcription/browser-web-speech-api.vue b/packages/stage-pages/src/pages/settings/providers/transcription/browser-web-speech-api.vue
index 6fa3e2545..60f27a94e 100644
--- a/packages/stage-pages/src/pages/settings/providers/transcription/browser-web-speech-api.vue
+++ b/packages/stage-pages/src/pages/settings/providers/transcription/browser-web-speech-api.vue
@@ -104,8 +104,9 @@ const isWebSpeechAPIAvailable = computed(() => {
&& ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window)
})
-onMounted(() => {
+onMounted(async () => {
ensureProviderSettings()
+ // Audio devices are loaded on demand when user requests them
})
// Speech-to-Text test state (always uses Web Speech API)
@@ -388,14 +389,8 @@ onUnmounted(() => {
-
-
-
-
Please select an audio input device to test
-
-
-
+
{
/>
+
+
+
+
+
Please select an audio input device to test
+
+
+
diff --git a/packages/stage-pages/src/pages/settings/providers/transcription/openai-audio-transcription.vue b/packages/stage-pages/src/pages/settings/providers/transcription/openai-audio-transcription.vue
index 31e22262a..ffa046b71 100644
--- a/packages/stage-pages/src/pages/settings/providers/transcription/openai-audio-transcription.vue
+++ b/packages/stage-pages/src/pages/settings/providers/transcription/openai-audio-transcription.vue
@@ -7,8 +7,9 @@ import {
} from '@proj-airi/stage-ui/components'
import { useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
+import { FieldSelect } from '@proj-airi/ui'
import { storeToRefs } from 'pinia'
-import { computed } from 'vue'
+import { computed, onMounted, watch } from 'vue'
const hearingStore = useHearingStore()
const providersStore = useProvidersStore()
@@ -18,31 +19,60 @@ const { providers } = storeToRefs(providersStore)
const providerId = 'openai-audio-transcription'
const defaultModel = 'whisper-1'
+// Model selection
+const model = computed({
+ get: () => providers.value[providerId]?.model as string | undefined || defaultModel,
+ set: (value) => {
+ if (!providers.value[providerId])
+ providers.value[providerId] = {}
+ providers.value[providerId].model = value
+ },
+})
+
+// Load models
+const providerModels = computed(() => {
+ return providersStore.getModelsForProvider(providerId)
+})
+
+const isLoadingModels = computed(() => {
+ return providersStore.isLoadingModels[providerId] || false
+})
+
// Check if API key is configured
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
-// Generate speech with ElevenLabs-specific parameters
+// Load models on mount
+onMounted(async () => {
+ await providersStore.loadModelsForConfiguredProviders()
+ await providersStore.fetchModelsForProvider(providerId)
+})
+
+// Generate transcription
async function handleGenerateTranscription(file: File) {
const provider = await providersStore.getProviderInstance>(providerId)
if (!provider) {
- throw new Error('Failed to initialize speech provider')
+ throw new Error('Failed to initialize transcription provider')
}
// Get provider configuration
const providerConfig = providersStore.getProviderConfig(providerId)
// Get model from configuration or use default
- const model = providerConfig.model as string | undefined || defaultModel
+ const modelToUse = providerConfig.model as string | undefined || defaultModel
- // ElevenLabs doesn't need SSML conversion, but if SSML is provided, use it directly
return await hearingStore.transcription(
providerId,
provider,
- model,
+ modelToUse,
file,
'json',
)
}
+
+watch(model, async () => {
+ const providerConfig = providersStore.getProviderConfig(providerId)
+ providerConfig.model = model.value
+})
@@ -50,6 +80,17 @@ async function handleGenerateTranscription(file: File) {
:provider-id="providerId"
:default-model="defaultModel"
>
+
+
+
+
providers.value[providerId]?.baseUrl || '',
+ get: () => {
+ const stored = providers.value[providerId]?.baseUrl
+ if (stored)
+ return stored
+ // Use default from provider metadata if available
+ const metadata = providersStore.getProviderMetadata(providerId)
+ return metadata?.defaultOptions?.().baseUrl as string | undefined || ''
+ },
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
@@ -52,6 +59,15 @@ const model = computed({
},
})
+// Load models
+const providerModels = computed(() => {
+ return providersStore.getModelsForProvider(providerId)
+})
+
+const isLoadingModels = computed(() => {
+ return providersStore.isLoadingModels[providerId] || false
+})
+
// Check if API key is configured
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
@@ -61,10 +77,21 @@ async function handleGenerateTranscription(file: File) {
if (!provider)
throw new Error('Failed to initialize transcription provider')
+ // Get provider configuration
+ const providerConfig = providersStore.getProviderConfig(providerId)
+
+ // Get model from configuration or use the reactive model value
+ const modelToUse = providerConfig.model as string | undefined || model.value
+
+ // Validate model - throw error if no valid model configured
+ if (!modelToUse || !isValidTranscriptionModel(modelToUse)) {
+ throw new Error(`Invalid or missing transcription model. Please configure a valid model in the provider settings.`)
+ }
+
return await hearingStore.transcription(
providerId,
provider,
- model.value,
+ modelToUse,
file,
'json',
)
@@ -81,6 +108,75 @@ const {
handleResetSettings,
forceValid,
} = useProviderValidation(providerId)
+
+// Expand Advanced section if there's a base URL validation error
+const shouldExpandAdvanced = computed(() => {
+ if (!validationMessage.value)
+ return false
+ // Check if validation message mentions base URL
+ const message = validationMessage.value.toLowerCase()
+ return message.includes('base url') || message.includes('baseurl')
+})
+
+// Valid transcription models (OpenAI doesn't provide an API to list these)
+const VALID_TRANSCRIPTION_MODELS = [
+ 'whisper-1',
+ 'gpt-4o-transcribe',
+ 'gpt-4o-mini-transcribe',
+ 'gpt-4o-mini-transcribe-2025-12-15',
+ 'gpt-4o-transcribe-diarize',
+]
+
+// Check if a model is a valid transcription model
+function isValidTranscriptionModel(modelName: string | undefined | null): boolean {
+ if (!modelName)
+ return false
+ // Check if it's a known transcription model
+ if (VALID_TRANSCRIPTION_MODELS.includes(modelName))
+ return true
+ // Allow custom models that might be transcription-compatible
+ // But reject obvious chat models
+ if (modelName.includes('gpt-4') && !modelName.includes('transcribe') && !modelName.includes('whisper'))
+ return false
+ return true
+}
+
+// Initialize provider settings on mount
+onMounted(async () => {
+ providersStore.initializeProvider(providerId)
+ // Initialize baseUrl with default if not set
+ if (!providers.value[providerId]?.baseUrl) {
+ const metadata = providersStore.getProviderMetadata(providerId)
+ const defaultBaseUrl = metadata?.defaultOptions?.().baseUrl as string | undefined
+ if (defaultBaseUrl) {
+ baseUrl.value = defaultBaseUrl
+ }
+ }
+ // Validate and reset model if it's invalid (e.g., a chat model)
+ const currentModel = model.value
+ if (currentModel && !isValidTranscriptionModel(currentModel)) {
+ console.warn(`Invalid transcription model "${currentModel}" detected. Resetting to default "whisper-1".`)
+ model.value = 'whisper-1'
+ }
+ // Load models if API key and base URL are configured
+ if (apiKey.value && baseUrl.value) {
+ await providersStore.loadModelsForConfiguredProviders()
+ await providersStore.fetchModelsForProvider(providerId)
+ }
+})
+
+// Watch for API key and base URL changes to reload models
+watch([apiKey, baseUrl], async ([newApiKey, newBaseUrl]) => {
+ if (newApiKey && newBaseUrl) {
+ await providersStore.fetchModelsForProvider(providerId)
+ }
+})
+
+// Watch model changes to save to provider config
+watch(model, () => {
+ const providerConfig = providersStore.getProviderConfig(providerId)
+ providerConfig.model = model.value
+})
@@ -89,62 +185,85 @@ const {
:provider-icon-color="providerMetadata?.iconColor"
:on-back="() => router.back()"
>
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
-
- {{ t('settings.dialogs.onboarding.validationFailed') }}
-
- {{ t('settings.pages.providers.common.continueAnyway') }}
-
-
-
-
-
- {{ validationMessage }}
-
-
-
-
-
- {{ t('settings.dialogs.onboarding.validationSuccess') }}
-
-
-
+
+
+
+
+ {{ t('settings.dialogs.onboarding.validationFailed') }}
+
+ {{ t('settings.pages.providers.common.continueAnyway') }}
+
+
+
+
+
+ {{ validationMessage }}
+
+
+
+
+
+ {{ t('settings.dialogs.onboarding.validationSuccess') }}
+
+
+
-
+
+
+
diff --git a/packages/stage-ui/src/components/menu/radio-card-many-select.vue b/packages/stage-ui/src/components/menu/radio-card-many-select.vue
index d41868d6f..af7989696 100644
--- a/packages/stage-ui/src/components/menu/radio-card-many-select.vue
+++ b/packages/stage-ui/src/components/menu/radio-card-many-select.vue
@@ -135,21 +135,24 @@ function updateCustomValue(value: string) {