Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions packages/stage-pages/src/pages/settings/modules/hearing.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const providersStore = useProvidersStore()
const { configuredTranscriptionProvidersMetadata } = storeToRefs(providersStore)

const { trackProviderClick } = useAnalytics()
const { stopStream, startStream } = useSettingsAudioDevice()
const { stopStream, startStream, askPermission } = useSettingsAudioDevice()
const { audioInputs, selectedAudioInput, stream } = storeToRefs(useSettingsAudioDevice())
const { startRecord, stopRecord, onStopRecord } = useAudioRecorder(stream)
const { startAnalyzer, stopAnalyzer, onAnalyzerUpdate, volumeLevel } = useAudioAnalyzer()
Expand Down Expand Up @@ -470,7 +470,12 @@ watch(activeTranscriptionProvider, async (provider) => {
}, { immediate: true })

onMounted(async () => {
// Audio devices are loaded on demand when user requests them
// Request mic permission and enumerate devices immediately so the audio input dropdown
// is populated when the user opens this page. Without this, the dropdown stays empty
// until the user manually interacts with it, making STT appear broken.
askPermission().catch(() => {
// Permission denied — the dropdown will remain empty and the user will see a warning.
})
Comment on lines +476 to +478
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The askPermission() call catches errors but doesn't surface them to the user, despite the comment stating that the user will see a warning. If microphone permission is denied, the audio input dropdown will remain empty without any feedback, which can be confusing for users. It's better to capture the error and display it using the existing error ref so the user understands why the device list is empty.

  askPermission().catch((err) => {
    // Permission denied — the dropdown will remain empty.
    // We surface the error so the user knows why devices are missing.
    error.value = err instanceof Error ? err.message : String(err)
  })

syncOpenAICompatibleSettings()
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,14 @@ onMounted(async () => {
await providersStore.fetchModelsForProvider(providerId)

const config = providersStore.getProviderConfig(providerId)

// Persist a safe default model if none is saved, or if the saved model is fp32-webgpu which
// is ~700 MB and causes the page to hang indefinitely on first load. Users can switch to
// larger/WebGPU models manually after the initial download succeeds.
if (!config.model || config.model === 'fp32-webgpu') {
config.model = getDefaultKokoroModel(hasWebGPU.value)
Comment on lines +115 to +116
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve user-selected fp32-webgpu model

This condition rewrites config.model from fp32-webgpu to q4f16 on every page mount, so users who explicitly choose the WebGPU model cannot persist that choice across navigation/reload. In practice, the app will silently revert them each time they revisit Kokoro settings, which breaks expected settings persistence and makes manual opt-in effectively non-sticky.

Useful? React with 👍 / 👎.

}

const metadata = providersStore.getProviderMetadata(providerId)
const validationResult = await metadata.validators.validateProviderConfig(config)
if (validationResult.valid) {
Expand Down
4 changes: 4 additions & 0 deletions packages/stage-ui/src/stores/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ const TOOLS_RELATED_ERROR_PATTERNS: RegExp[] = [
/unrecognized request argument.+tools/i, // Azure AI Foundry
/tool use with function calling is unsupported/i, // Google Generative AI
/tool_use_failed/i, // Groq
// NOTICE: Groq rejects OpenAI-specific tool parameters (e.g. capture_tool_errors) that the
// xsai library sends unconditionally. These 400 responses indicate incompatible tool schemas
// rather than a missing feature, so we degrade to tool-less mode and retry.
/property '[^']+' is unsupported/i, // Groq — unsupported OpenAI tool parameters
/does not support function.?calling/i, // Anthropic
/tools?\s+(is|are)\s+not\s+supported/i, // Cloudflare Workers AI
]
Expand Down
10 changes: 10 additions & 0 deletions packages/stage-ui/src/stores/settings/audio-device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,20 @@ export const useSettingsAudioDevice = defineStore('settings-audio-devices', () =
const selectedAudioInputPersist = useLocalStorageManualReset<string>('settings/audio/input', selectedAudioInputNonPersist.value)
const selectedAudioInputEnabledPersist = useLocalStorageManualReset<boolean>('settings/audio/input/enabled', false)

// Persist → composable: keep the composable in sync with what was saved.
watch(selectedAudioInputPersist, (newValue) => {
selectedAudioInputNonPersist.value = newValue
})

// Composable → persist: when the composable auto-selects the default device (e.g. after
// permission is granted and the device list populates for the first time), write it back
// so the dropdown and stream use the same value on next load.
watch(selectedAudioInputNonPersist, (newValue) => {
if (newValue && !selectedAudioInputPersist.value) {
selectedAudioInputPersist.value = newValue
}
})

watch(selectedAudioInputEnabledPersist, (val) => {
if (val) {
startStream()
Expand Down
15 changes: 10 additions & 5 deletions packages/stage-ui/src/workers/kokoro/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,15 @@ export function kokoroModelsToModelInfo(hasWebGPU: boolean, t?: (key: string) =>
}

/**
* Get the default model based on WebGPU availability
* @param hasWebGPU - Whether WebGPU is available
* @returns The default model to use
* Get the default model based on WebGPU availability.
*
* NOTICE: fp32-webgpu is intentionally excluded from the automatic default even when WebGPU is
* available. The full-precision WebGPU model is ~700 MB and causes the settings page to hang on
* first visit while the worker attempts a silent background download. q4f16 (~320 MB, WASM) is
* the best practical default: it works across all browsers, downloads in a reasonable time, and
* produces near-identical quality to the larger variants for conversational output.
* Users who want the WebGPU model can select it manually after the initial load succeeds.
*/
export function getDefaultKokoroModel(hasWebGPU: boolean): KokoroQuantization {
return hasWebGPU ? 'fp32-webgpu' : 'q4f16'
export function getDefaultKokoroModel(_hasWebGPU: boolean): KokoroQuantization {
return 'q4f16'
}