From 46147b0f8b90fd4eff23080b9aa36bfb2184a5e7 Mon Sep 17 00:00:00 2001 From: Neko Ayaka Date: Thu, 26 Mar 2026 19:03:21 +0800 Subject: [PATCH] fix(stage-*): incorrect use of useAudioDevices hook --- apps/stage-pocket/src/App.vue | 4 +- apps/stage-tamagotchi/src/renderer/App.vue | 4 +- apps/stage-web/src/App.vue | 4 +- .../providers/transcription-playground.vue | 2 +- .../audio/{device.ts => audio-device.ts} | 26 ++++++------ .../stage-ui/src/composables/audio/index.ts | 2 +- packages/stage-ui/src/stores/audio.ts | 42 +------------------ .../src/stores/settings/audio-device.ts | 10 +++-- 8 files changed, 32 insertions(+), 62 deletions(-) rename packages/stage-ui/src/composables/audio/{device.ts => audio-device.ts} (67%) diff --git a/apps/stage-pocket/src/App.vue b/apps/stage-pocket/src/App.vue index ec5ddee4f..d62556bbc 100644 --- a/apps/stage-pocket/src/App.vue +++ b/apps/stage-pocket/src/App.vue @@ -7,7 +7,7 @@ import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/c import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/context-bridge' import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card' import { useOnboardingStore } from '@proj-airi/stage-ui/stores/onboarding' -import { useSettings } from '@proj-airi/stage-ui/stores/settings' +import { useSettings, useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings' import { useTheme } from '@proj-airi/ui' import { StageTransitionGroup } from '@proj-airi/ui-transitions' import { storeToRefs } from 'pinia' @@ -26,6 +26,7 @@ const settings = storeToRefs(settingsStore) const onboardingStore = useOnboardingStore() const serverChannelStore = useModsServerChannelStore() const characterOrchestratorStore = useCharacterOrchestratorStore() +const settingsAudioDeviceStore = useSettingsAudioDevice() const { showingSetup } = storeToRefs(onboardingStore) const { isDark } = useTheme() const cardStore = useAiriCardStore() @@ -80,6 +81,7 @@ onMounted(async () => { await displayModelsStore.loadDisplayModelsFromIndexedDB() await settingsStore.initializeStageModel() + await settingsAudioDeviceStore.initialize() }) onUnmounted(() => { diff --git a/apps/stage-tamagotchi/src/renderer/App.vue b/apps/stage-tamagotchi/src/renderer/App.vue index 58103880d..e8f332c46 100644 --- a/apps/stage-tamagotchi/src/renderer/App.vue +++ b/apps/stage-tamagotchi/src/renderer/App.vue @@ -14,7 +14,7 @@ import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/conte import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card' import { usePerfTracerBridgeStore } from '@proj-airi/stage-ui/stores/perf-tracer-bridge' import { listProvidersForPluginHost, shouldPublishPluginHostCapabilities } from '@proj-airi/stage-ui/stores/plugin-host-capabilities' -import { useSettings } from '@proj-airi/stage-ui/stores/settings' +import { useSettings, useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings' import { useTheme } from '@proj-airi/ui' import { storeToRefs } from 'pinia' import { onMounted, onUnmounted, watch } from 'vue' @@ -61,6 +61,7 @@ const characterOrchestratorStore = useCharacterOrchestratorStore() const analyticsStore = useSharedAnalyticsStore() const pluginHostInspectorStore = usePluginHostInspectorStore() const stageWindowLifecycleStore = useStageWindowLifecycleStore() +const settingsAudioDeviceStore = useSettingsAudioDevice() const context = useElectronEventaContext() usePerfTracerBridgeStore() initializeStageThreeRuntimeTraceBridge() @@ -123,6 +124,7 @@ onMounted(async () => { await chatSessionStore.initialize() await displayModelsStore.loadDisplayModelsFromIndexedDB() await settingsStore.initializeStageModel() + await settingsAudioDeviceStore.initialize() const serverChannelConfig = await getServerChannelConfig() serverChannelSettingsStore.websocketTlsConfig = serverChannelConfig.tlsConfig diff --git a/apps/stage-web/src/App.vue b/apps/stage-web/src/App.vue index f6ce15602..15e16ec01 100644 --- a/apps/stage-web/src/App.vue +++ b/apps/stage-web/src/App.vue @@ -8,7 +8,7 @@ import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/c import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/context-bridge' import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card' import { useOnboardingStore } from '@proj-airi/stage-ui/stores/onboarding' -import { useSettings } from '@proj-airi/stage-ui/stores/settings' +import { useSettings, useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings' import { useTheme } from '@proj-airi/ui' import { StageTransitionGroup } from '@proj-airi/ui-transitions' import { storeToRefs } from 'pinia' @@ -32,6 +32,7 @@ const onboardingStore = useOnboardingStore() const chatSessionStore = useChatSessionStore() const serverChannelStore = useModsServerChannelStore() const characterOrchestratorStore = useCharacterOrchestratorStore() +const settingsAudioDeviceStore = useSettingsAudioDevice() const { showingSetup } = storeToRefs(onboardingStore) const { isDark } = useTheme() const cardStore = useAiriCardStore() @@ -93,6 +94,7 @@ onMounted(async () => { await displayModelsStore.loadDisplayModelsFromIndexedDB() await settingsStore.initializeStageModel() + await settingsAudioDeviceStore.initialize() }) onUnmounted(() => { diff --git a/packages/stage-ui/src/components/scenarios/providers/transcription-playground.vue b/packages/stage-ui/src/components/scenarios/providers/transcription-playground.vue index ac0c4dd12..8405d2365 100644 --- a/packages/stage-ui/src/components/scenarios/providers/transcription-playground.vue +++ b/packages/stage-ui/src/components/scenarios/providers/transcription-playground.vue @@ -6,9 +6,9 @@ import { until } from '@vueuse/core' import { computed, onUnmounted, ref, shallowRef, watch } from 'vue' import { useI18n } from 'vue-i18n' +import { useAudioDevice } from '../../../composables/audio' import { useAudioAnalyzer } from '../../../composables/audio/audio-analyzer' import { useAudioRecorder } from '../../../composables/audio/audio-recorder' -import { useAudioDevice } from '../../../composables/audio/device' import { LevelMeter, TestDummyMarker, ThresholdMeter } from '../../gadgets' const props = defineProps<{ diff --git a/packages/stage-ui/src/composables/audio/device.ts b/packages/stage-ui/src/composables/audio/audio-device.ts similarity index 67% rename from packages/stage-ui/src/composables/audio/device.ts rename to packages/stage-ui/src/composables/audio/audio-device.ts index 1e0716291..d7674a704 100644 --- a/packages/stage-ui/src/composables/audio/device.ts +++ b/packages/stage-ui/src/composables/audio/audio-device.ts @@ -1,13 +1,13 @@ import { useDevicesList, useUserMedia } from '@vueuse/core' -import { computed, nextTick, onMounted, ref, watch } from 'vue' +import { computed, nextTick, ref, watch } from 'vue' -export function useAudioDevice() { - const devices = useDevicesList({ constraints: { audio: true }, requestPermissions: true }) +export function useAudioDevice(requestPermission: boolean = false) { + const devices = useDevicesList({ constraints: { audio: true }, requestPermissions: requestPermission }) const audioInputs = computed(() => devices.audioInputs.value) - const selectedAudioInput = ref(devices.audioInputs.value[0]?.deviceId || '') + const selectedAudioInput = ref(devices.audioInputs.value.find(device => device.deviceId === 'default')?.deviceId || '') const deviceConstraints = computed(() => ({ audio: { - ...(selectedAudioInput.value ? { deviceId: { exact: selectedAudioInput.value } } : {}), + deviceId: { exact: selectedAudioInput.value }, autoGainControl: true, echoCancellation: true, noiseSuppression: true, @@ -17,13 +17,12 @@ export function useAudioDevice() { watch(audioInputs, () => { if (!selectedAudioInput.value && audioInputs.value.length > 0) { - selectedAudioInput.value = audioInputs.value[0]?.deviceId + selectedAudioInput.value = audioInputs.value.find(input => input.deviceId === 'default')?.deviceId || audioInputs.value[0].deviceId } }) - // Lifecycle - onMounted(() => { - devices.ensurePermissions() + function askPermission() { + return devices.ensurePermissions() .then(() => nextTick()) .then(() => { if (audioInputs.value.length > 0 && !selectedAudioInput.value) { @@ -32,15 +31,18 @@ export function useAudioDevice() { }) .catch((error) => { console.error('Error ensuring permissions:', error) + throw error // Re-throw so callers can handle the error }) - }) + } return { audioInputs, selectedAudioInput, stream, - stopStream, - startStream, deviceConstraints, + + askPermission, + startStream, + stopStream, } } diff --git a/packages/stage-ui/src/composables/audio/index.ts b/packages/stage-ui/src/composables/audio/index.ts index 903edee13..b56ed309b 100644 --- a/packages/stage-ui/src/composables/audio/index.ts +++ b/packages/stage-ui/src/composables/audio/index.ts @@ -1,4 +1,4 @@ export * from './audio-analyzer' export * from './audio-context' +export * from './audio-device' export * from './audio-recorder' -export * from './device' diff --git a/packages/stage-ui/src/stores/audio.ts b/packages/stage-ui/src/stores/audio.ts index 61d3b3e79..72870dcba 100644 --- a/packages/stage-ui/src/stores/audio.ts +++ b/packages/stage-ui/src/stores/audio.ts @@ -1,6 +1,5 @@ -import { useDevicesList, useUserMedia } from '@vueuse/core' import { defineStore } from 'pinia' -import { computed, nextTick, ref, shallowRef, watch } from 'vue' +import { computed, ref, shallowRef } from 'vue' function calculateVolumeWithLinearNormalize(analyser: AnalyserNode) { const dataBuffer = new Uint8Array(analyser.frequencyBinCount) @@ -74,45 +73,6 @@ export const useAudioContext = defineStore('audio-context', () => { } }) -export function useAudioDevice(requestPermission: boolean = false) { - const devices = useDevicesList({ constraints: { audio: true }, requestPermissions: requestPermission }) - const audioInputs = computed(() => devices.audioInputs.value) - const selectedAudioInput = ref(devices.audioInputs.value.find(device => device.deviceId === 'default')?.deviceId || '') - const deviceConstraints = computed(() => ({ audio: { deviceId: { exact: selectedAudioInput.value }, autoGainControl: true, echoCancellation: true, noiseSuppression: true } })) - const { stream, stop: stopStream, start: startStream } = useUserMedia({ constraints: deviceConstraints, enabled: false, autoSwitch: true }) - - watch(audioInputs, () => { - if (!selectedAudioInput.value && audioInputs.value.length > 0) { - selectedAudioInput.value = audioInputs.value.find(input => input.deviceId === 'default')?.deviceId || audioInputs.value[0].deviceId - } - }) - - function askPermission() { - return devices.ensurePermissions() - .then(() => nextTick()) - .then(() => { - if (audioInputs.value.length > 0 && !selectedAudioInput.value) { - selectedAudioInput.value = audioInputs.value.find(input => input.deviceId === 'default')?.deviceId || audioInputs.value[0].deviceId - } - }) - .catch((error) => { - console.error('Error ensuring permissions:', error) - throw error // Re-throw so callers can handle the error - }) - } - - return { - audioInputs, - selectedAudioInput, - stream, - deviceConstraints, - - askPermission, - startStream, - stopStream, - } -} - export const useSpeakingStore = defineStore('character-speaking', () => { const nowSpeakingAvatarBorderOpacityMin = 30 const nowSpeakingAvatarBorderOpacityMax = 100 diff --git a/packages/stage-ui/src/stores/settings/audio-device.ts b/packages/stage-ui/src/stores/settings/audio-device.ts index 19a931c59..31d6ec566 100644 --- a/packages/stage-ui/src/stores/settings/audio-device.ts +++ b/packages/stage-ui/src/stores/settings/audio-device.ts @@ -1,8 +1,8 @@ import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables' import { defineStore } from 'pinia' -import { onMounted, watch } from 'vue' +import { watch } from 'vue' -import { useAudioDevice } from '../audio' +import { useAudioDevice } from '../../composables/audio' export const useSettingsAudioDevice = defineStore('settings-audio-devices', () => { const { audioInputs, deviceConstraints, selectedAudioInput: selectedAudioInputNonPersist, startStream, stopStream, stream, askPermission } = useAudioDevice() @@ -23,7 +23,7 @@ export const useSettingsAudioDevice = defineStore('settings-audio-devices', () = } }) - onMounted(() => { + function initialize() { const hasSelectedInput = selectedAudioInputPersist.value && audioInputs.value.some(device => device.deviceId === selectedAudioInputPersist.value) @@ -33,7 +33,7 @@ export const useSettingsAudioDevice = defineStore('settings-audio-devices', () = if (selectedAudioInputNonPersist.value && !selectedAudioInputEnabledPersist.value) { selectedAudioInputPersist.value = selectedAudioInputNonPersist.value } - }) + } function resetState() { selectedAudioInputPersist.reset() @@ -50,6 +50,8 @@ export const useSettingsAudioDevice = defineStore('settings-audio-devices', () = stream, + initialize, + askPermission, startStream, stopStream,