fix(stage-*): async audio implementation
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import type { ChatProvider } from '@xsai-ext/shared-providers'
|
||||
|
||||
import { useMicVAD } from '@proj-airi/stage-ui/composables'
|
||||
import { useChatStore, useConsciousnessStore, useProvidersStore, useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { useChatStore, useConsciousnessStore, useProvidersStore, useSettingsAudioDevice } from '@proj-airi/stage-ui/stores'
|
||||
import { BasicTextarea } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
@@ -13,9 +13,8 @@ import TamagotchiChatHistory from './ChatHistory.vue'
|
||||
const messageInput = ref('')
|
||||
const listening = ref(false)
|
||||
|
||||
// const { audioInputs } = useDevicesList({ constraints: { audio: true }, requestPermissions: true })
|
||||
// const { selectedAudioDevice, isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings())
|
||||
const { isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings())
|
||||
// const { askPermission } = useSettingsAudioDevice()
|
||||
const { enabled, selectedAudioInput } = storeToRefs(useSettingsAudioDevice())
|
||||
const { send, onAfterSend, discoverToolsCompatibility } = useChatStore()
|
||||
const { messages } = storeToRefs(useChatStore())
|
||||
const { t } = useI18n()
|
||||
@@ -44,7 +43,7 @@ async function handleSend() {
|
||||
}
|
||||
}
|
||||
|
||||
const { destroy, start } = useMicVAD(selectedAudioDeviceId, {
|
||||
const { destroy, start } = useMicVAD(selectedAudioInput, {
|
||||
onSpeechStart: () => {
|
||||
// TODO: interrupt the playback
|
||||
// TODO: interrupt any of the ongoing TTS
|
||||
@@ -86,8 +85,8 @@ function handleTranscription(_buffer: Float32Array) {
|
||||
// selectedAudioDevice.value = found
|
||||
// }
|
||||
|
||||
watch(isAudioInputOn, async (value) => {
|
||||
if (value === 'false') {
|
||||
watch(enabled, async (value) => {
|
||||
if (value === false) {
|
||||
destroy()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,21 +20,19 @@ import { startClickThrough, stopClickThrough } from '../utils/windows'
|
||||
|
||||
useTauriGlobalShortcuts()
|
||||
const windowStore = useWindowControlStore()
|
||||
const { width, height } = useWindowSize()
|
||||
|
||||
const centerPos = computed(() => ({ x: width.value / 2, y: height.value / 2 }))
|
||||
const live2dFocusAt = ref<Point>(centerPos.value)
|
||||
const resourcesStore = useResourcesStore()
|
||||
const mcpStore = useMcpStore()
|
||||
|
||||
const { listen } = useTauriEvent<AiriTamagotchiEvents>()
|
||||
const { invoke } = useTauriCore()
|
||||
const { width, height } = useWindowSize()
|
||||
const { connected, serverCmd, serverArgs } = storeToRefs(mcpStore)
|
||||
const { scale, positionInPercentageString } = storeToRefs(useLive2d())
|
||||
|
||||
const centerPos = computed(() => ({ x: width.value / 2, y: height.value / 2 }))
|
||||
const { live2dLookAtX, live2dLookAtY, isCursorInside } = useTauriWindowClickThrough(centerPos)
|
||||
const shouldHideView = computed(() => isCursorInside.value && !windowStore.isControlActive && windowStore.isIgnoringMouseEvent)
|
||||
|
||||
const resourcesStore = useResourcesStore()
|
||||
const mcpStore = useMcpStore()
|
||||
const { connected, serverCmd, serverArgs } = storeToRefs(mcpStore)
|
||||
|
||||
const { scale, positionInPercentageString } = storeToRefs(useLive2d())
|
||||
const live2dFocusAt = ref<Point>(centerPos.value)
|
||||
|
||||
watch([live2dLookAtX, live2dLookAtY], ([x, y]) => live2dFocusAt.value = { x, y }, { immediate: true })
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { TranscriptionProvider } from '@xsai-ext/shared-providers'
|
||||
|
||||
import { Button, ErrorContainer, LevelMeter, RadioCardSimple, TestDummyMarker, ThresholdMeter, TimeSeriesChart } from '@proj-airi/stage-ui/components'
|
||||
import { useAudioAnalyzer, useAudioRecorder } from '@proj-airi/stage-ui/composables'
|
||||
import { Alert, Button, ErrorContainer, LevelMeter, RadioCardManySelect, RadioCardSimple, TestDummyMarker, ThresholdMeter, TimeSeriesChart } from '@proj-airi/stage-ui/components'
|
||||
import { useAudioAnalyzer, useAudioDevice, useAudioRecorder } from '@proj-airi/stage-ui/composables'
|
||||
import { useAudioContext, useHearingStore, useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||
import { FieldCheckbox, FieldRange, FieldSelect } from '@proj-airi/ui'
|
||||
import { useDevicesList, useUserMedia } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import workletUrl from '../../../tauri/vad/process.worklet?worker&url'
|
||||
@@ -17,15 +16,20 @@ import { createVAD, VADAudioManager } from '../../../tauri/vad'
|
||||
const { t } = useI18n()
|
||||
|
||||
const hearingStore = useHearingStore()
|
||||
const { activeTranscriptionProvider, activeTranscriptionModel } = storeToRefs(hearingStore)
|
||||
const {
|
||||
activeTranscriptionProvider,
|
||||
activeTranscriptionModel,
|
||||
providerModels,
|
||||
activeProviderModelError,
|
||||
isLoadingActiveProviderModels,
|
||||
supportsModelListing,
|
||||
transcriptionModelSearchQuery,
|
||||
activeCustomModelName,
|
||||
} = storeToRefs(hearingStore)
|
||||
const providersStore = useProvidersStore()
|
||||
const { configuredTranscriptionProvidersMetadata } = storeToRefs(providersStore)
|
||||
|
||||
const devices = useDevicesList({ constraints: { audio: true } })
|
||||
const audioInputs = computed(() => devices.audioInputs.value)
|
||||
const selectedAudioInput = ref<string>(devices.audioInputs.value[0]?.deviceId || '')
|
||||
const deviceConstraints = computed<MediaStreamConstraints>(() => ({ audio: { deviceId: { exact: selectedAudioInput.value }, autoGainControl: true, echoCancellation: true, noiseSuppression: true } }))
|
||||
const { stream, stop: streamStop, start: streamStart } = useUserMedia({ constraints: deviceConstraints, enabled: false, autoSwitch: false })
|
||||
const { audioInputs, selectedAudioInput, stream, stopStream, startStream } = useAudioDevice()
|
||||
const { startRecord, stopRecord, onStopRecord } = useAudioRecorder(stream)
|
||||
const { startAnalyzer, stopAnalyzer, onAnalyzerUpdate, volumeLevel } = useAudioAnalyzer()
|
||||
const { audioContext } = storeToRefs(useAudioContext())
|
||||
@@ -149,7 +153,7 @@ async function setupAudioMonitoring() {
|
||||
// Clean up existing connections
|
||||
await stopAudioMonitoring()
|
||||
|
||||
await streamStart()
|
||||
await startStream()
|
||||
if (!stream.value) {
|
||||
console.warn('No audio stream available')
|
||||
return
|
||||
@@ -160,7 +164,7 @@ async function setupAudioMonitoring() {
|
||||
return
|
||||
|
||||
try {
|
||||
if (recording) {
|
||||
if (recording && recording.size > 0) {
|
||||
audios.value.push(recording)
|
||||
|
||||
const provider = await providersStore.getProviderInstance<TranscriptionProvider<string>>(activeTranscriptionProvider.value)
|
||||
@@ -170,7 +174,8 @@ async function setupAudioMonitoring() {
|
||||
|
||||
// Get model from configuration or use default
|
||||
const model = activeTranscriptionModel.value
|
||||
const res = await hearingStore.transcription(provider, model, new File([recording], 'recording.wav'), 'json')
|
||||
const res = await hearingStore.transcription(provider, model, new File([recording], 'recording.wav'))
|
||||
|
||||
transcriptions.value.push(res.text)
|
||||
}
|
||||
}
|
||||
@@ -226,7 +231,7 @@ async function stopAudioMonitoring() {
|
||||
await vadManager.value.stop()
|
||||
}
|
||||
if (stream.value) { // Stop media stream
|
||||
streamStop()
|
||||
stopStream()
|
||||
}
|
||||
|
||||
stopAnalyzer()
|
||||
@@ -266,12 +271,6 @@ watch(monitorVolume, () => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(audioInputs, () => {
|
||||
if (!selectedAudioInput.value && audioInputs.value.length > 0) {
|
||||
selectedAudioInput.value = audioInputs.value[0]?.deviceId
|
||||
}
|
||||
})
|
||||
|
||||
watch(vadThreshold, () => {
|
||||
// Update VAD threshold if model is loaded
|
||||
if (vadManager.value && isVADModelLoaded.value) {
|
||||
@@ -318,13 +317,12 @@ const speakingIndicatorClass = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
devices.ensurePermissions().then(() => nextTick()).then(() => {
|
||||
if (audioInputs.value.length > 0 && !selectedAudioInput.value) {
|
||||
selectedAudioInput.value = audioInputs.value[0]?.deviceId
|
||||
}
|
||||
})
|
||||
function updateCustomModelName(value: string) {
|
||||
activeCustomModelName.value = value
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await hearingStore.loadModelsForProvider(activeTranscriptionProvider.value)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -352,6 +350,7 @@ onUnmounted(() => {
|
||||
value: input.deviceId,
|
||||
}))"
|
||||
placeholder="Select an audio input device"
|
||||
layout="vertical"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -406,6 +405,66 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model selection section -->
|
||||
<div v-if="activeTranscriptionProvider && supportsModelListing">
|
||||
<div flex="~ col gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg md:text-2xl">
|
||||
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.title') }}
|
||||
</h2>
|
||||
<div text="neutral-400 dark:neutral-400">
|
||||
<span>{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.subtitle') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoadingActiveProviderModels" class="flex items-center justify-center py-4">
|
||||
<div class="mr-2 animate-spin">
|
||||
<div i-solar:spinner-line-duotone text-xl />
|
||||
</div>
|
||||
<span>{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.loading') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<ErrorContainer
|
||||
v-else-if="activeProviderModelError"
|
||||
:title="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.error')"
|
||||
:error="activeProviderModelError"
|
||||
/>
|
||||
|
||||
<!-- No models available -->
|
||||
<Alert
|
||||
v-else-if="providerModels.length === 0 && !isLoadingActiveProviderModels"
|
||||
type="warning"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_models') }}
|
||||
</template>
|
||||
<template #content>
|
||||
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_models_description') }}
|
||||
</template>
|
||||
</Alert>
|
||||
|
||||
<!-- Using the new RadioCardManySelect component -->
|
||||
<template v-else-if="providerModels.length > 0">
|
||||
<RadioCardManySelect
|
||||
v-model="activeTranscriptionModel"
|
||||
v-model:search-query="transcriptionModelSearchQuery"
|
||||
:items="providerModels.sort((a, b) => a.id === activeTranscriptionModel ? -1 : b.id === activeTranscriptionModel ? 1 : 0)"
|
||||
:searchable="true"
|
||||
:search-placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.search_placeholder')"
|
||||
:search-no-results-title="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_search_results')"
|
||||
:search-no-results-description="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_search_results_description', { query: transcriptionModelSearchQuery })"
|
||||
:search-results-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.search_results', { count: '{count}', total: '{total}' })"
|
||||
:custom-input-placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.custom_model_placeholder')"
|
||||
:expand-button-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.expand')"
|
||||
:collapse-button-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.collapse')"
|
||||
@update:custom-value="updateCustomModelName"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -420,6 +479,8 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<ErrorContainer v-if="error" title="Error occurred" :error="error" mb-4 />
|
||||
|
||||
<Button class="mb-4" w-full @click="toggleMonitoring">
|
||||
{{ isMonitoring ? 'Stop Monitoring' : 'Start Monitoring' }}
|
||||
</Button>
|
||||
|
||||
@@ -5,9 +5,8 @@ import WhisperWorker from '@proj-airi/stage-ui/libs/workers/worker?worker&url'
|
||||
|
||||
import { toWAVBase64 } from '@proj-airi/audio'
|
||||
import { useMicVAD, useWhisper } from '@proj-airi/stage-ui/composables'
|
||||
import { useAudioContext, useChatStore, useConsciousnessStore, useProvidersStore, useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { useAudioContext, useChatStore, useConsciousnessStore, useProvidersStore, useSettings, useSettingsAudioDevice } from '@proj-airi/stage-ui/stores'
|
||||
import { BasicTextarea } from '@proj-airi/ui'
|
||||
import { useDevicesList } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -24,8 +23,8 @@ const providersStore = useProvidersStore()
|
||||
const { activeProvider, activeModel } = storeToRefs(useConsciousnessStore())
|
||||
const { themeColorsHueDynamic } = storeToRefs(useSettings())
|
||||
|
||||
const { ensurePermissions } = useDevicesList({ constraints: { audio: true } })
|
||||
const { isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings())
|
||||
const { askPermission } = useSettingsAudioDevice()
|
||||
const { enabled, selectedAudioInput } = storeToRefs(useSettingsAudioDevice())
|
||||
const { send, onAfterSend, discoverToolsCompatibility } = useChatStore()
|
||||
const { messages } = storeToRefs(useChatStore())
|
||||
const { audioContext } = useAudioContext()
|
||||
@@ -70,7 +69,7 @@ async function handleSend() {
|
||||
}
|
||||
}
|
||||
|
||||
const { destroy, start } = useMicVAD(selectedAudioDeviceId, {
|
||||
const { destroy, start } = useMicVAD(selectedAudioInput, {
|
||||
onSpeechStart: () => {
|
||||
// TODO: interrupt the playback
|
||||
// TODO: interrupt any of the ongoing TTS
|
||||
@@ -104,8 +103,8 @@ async function handleTranscription(buffer: ArrayBufferLike) {
|
||||
generate({ type: 'generate', data: { audio: audioBase64, language: 'en' } })
|
||||
}
|
||||
|
||||
watch(isAudioInputOn, async (value) => {
|
||||
if (value === 'false') {
|
||||
watch(enabled, async (value) => {
|
||||
if (value === false) {
|
||||
destroy()
|
||||
terminate()
|
||||
}
|
||||
@@ -113,7 +112,7 @@ watch(isAudioInputOn, async (value) => {
|
||||
|
||||
watch(showMicrophoneSelect, async (value) => {
|
||||
if (value) {
|
||||
await ensurePermissions()
|
||||
await askPermission()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { ChatProvider } from '@xsai-ext/shared-providers'
|
||||
|
||||
import { useMicVAD } from '@proj-airi/stage-ui/composables'
|
||||
import { useChatStore, useConsciousnessStore, useProvidersStore, useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { useChatStore, useConsciousnessStore, useProvidersStore, useSettings, useSettingsAudioDevice } from '@proj-airi/stage-ui/stores'
|
||||
import { BasicTextarea } from '@proj-airi/ui'
|
||||
import { useDark, useResizeObserver, useScreenSafeArea } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
@@ -30,9 +30,9 @@ const { activeProvider, activeModel } = storeToRefs(useConsciousnessStore())
|
||||
|
||||
useResizeObserver(document.documentElement, () => screenSafeArea.update())
|
||||
|
||||
// const { audioInputs } = useDevicesList({ constraints: { audio: true }, requestPermissions: true })
|
||||
// const { selectedAudioDevice, isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings())
|
||||
const { isAudioInputOn, selectedAudioDeviceId, themeColorsHueDynamic, stageView, stageViewControlsEnabled } = storeToRefs(useSettings())
|
||||
// const { askPermission } = useSettingsAudioDevice()
|
||||
const { themeColorsHueDynamic, stageView, stageViewControlsEnabled } = storeToRefs(useSettings())
|
||||
const { enabled, selectedAudioInput } = storeToRefs(useSettingsAudioDevice())
|
||||
const { send, onAfterSend, discoverToolsCompatibility } = useChatStore()
|
||||
const { messages } = storeToRefs(useChatStore())
|
||||
const { t } = useI18n()
|
||||
@@ -60,7 +60,7 @@ async function handleSend() {
|
||||
}
|
||||
}
|
||||
|
||||
const { destroy, start } = useMicVAD(selectedAudioDeviceId, {
|
||||
const { destroy, start } = useMicVAD(selectedAudioInput, {
|
||||
onSpeechStart: () => {
|
||||
// TODO: interrupt the playback
|
||||
// TODO: interrupt any of the ongoing TTS
|
||||
@@ -91,8 +91,8 @@ function handleTranscription(_buffer: Float32Array<ArrayBufferLike>) {
|
||||
alert('Transcription is not implemented yet')
|
||||
}
|
||||
|
||||
watch(isAudioInputOn, async (value) => {
|
||||
if (value === 'false') {
|
||||
watch(enabled, async (value) => {
|
||||
if (value === false) {
|
||||
destroy()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { TranscriptionProvider } from '@xsai-ext/shared-providers'
|
||||
|
||||
import { Button, ErrorContainer, LevelMeter, RadioCardSimple, TestDummyMarker, ThresholdMeter, TimeSeriesChart } from '@proj-airi/stage-ui/components'
|
||||
import { useAudioAnalyzer, useAudioRecorder } from '@proj-airi/stage-ui/composables'
|
||||
import { Alert, Button, ErrorContainer, LevelMeter, RadioCardManySelect, RadioCardSimple, TestDummyMarker, ThresholdMeter, TimeSeriesChart } from '@proj-airi/stage-ui/components'
|
||||
import { useAudioAnalyzer, useAudioDevice, useAudioRecorder } from '@proj-airi/stage-ui/composables'
|
||||
import { useAudioContext, useHearingStore, useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||
import { FieldCheckbox, FieldRange, FieldSelect } from '@proj-airi/ui'
|
||||
import { useDevicesList, useUserMedia } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import workletUrl from '../../../workers/vad/process.worklet?worker&url'
|
||||
@@ -17,15 +16,20 @@ import { createVAD, VADAudioManager } from '../../../workers/vad'
|
||||
const { t } = useI18n()
|
||||
|
||||
const hearingStore = useHearingStore()
|
||||
const { activeTranscriptionProvider, activeTranscriptionModel } = storeToRefs(hearingStore)
|
||||
const {
|
||||
activeTranscriptionProvider,
|
||||
activeTranscriptionModel,
|
||||
providerModels,
|
||||
activeProviderModelError,
|
||||
isLoadingActiveProviderModels,
|
||||
supportsModelListing,
|
||||
transcriptionModelSearchQuery,
|
||||
activeCustomModelName,
|
||||
} = storeToRefs(hearingStore)
|
||||
const providersStore = useProvidersStore()
|
||||
const { configuredTranscriptionProvidersMetadata } = storeToRefs(providersStore)
|
||||
|
||||
const devices = useDevicesList({ constraints: { audio: true } })
|
||||
const audioInputs = computed(() => devices.audioInputs.value)
|
||||
const selectedAudioInput = ref<string>(devices.audioInputs.value[0]?.deviceId || '')
|
||||
const deviceConstraints = computed<MediaStreamConstraints>(() => ({ audio: { deviceId: { exact: selectedAudioInput.value }, autoGainControl: true, echoCancellation: true, noiseSuppression: true } }))
|
||||
const { stream, stop: streamStop, start: streamStart } = useUserMedia({ constraints: deviceConstraints, enabled: false, autoSwitch: false })
|
||||
const { audioInputs, selectedAudioInput, stream, stopStream, startStream } = useAudioDevice()
|
||||
const { startRecord, stopRecord, onStopRecord } = useAudioRecorder(stream)
|
||||
const { startAnalyzer, stopAnalyzer, onAnalyzerUpdate, volumeLevel } = useAudioAnalyzer()
|
||||
const { audioContext } = storeToRefs(useAudioContext())
|
||||
@@ -149,7 +153,7 @@ async function setupAudioMonitoring() {
|
||||
// Clean up existing connections
|
||||
await stopAudioMonitoring()
|
||||
|
||||
await streamStart()
|
||||
await startStream()
|
||||
if (!stream.value) {
|
||||
console.warn('No audio stream available')
|
||||
return
|
||||
@@ -160,7 +164,7 @@ async function setupAudioMonitoring() {
|
||||
return
|
||||
|
||||
try {
|
||||
if (recording) {
|
||||
if (recording && recording.size > 0) {
|
||||
audios.value.push(recording)
|
||||
|
||||
const provider = await providersStore.getProviderInstance<TranscriptionProvider<string>>(activeTranscriptionProvider.value)
|
||||
@@ -170,7 +174,8 @@ async function setupAudioMonitoring() {
|
||||
|
||||
// Get model from configuration or use default
|
||||
const model = activeTranscriptionModel.value
|
||||
const res = await hearingStore.transcription(provider, model, new File([recording], 'recording.wav'), 'json')
|
||||
const res = await hearingStore.transcription(provider, model, new File([recording], 'recording.wav'))
|
||||
|
||||
transcriptions.value.push(res.text)
|
||||
}
|
||||
}
|
||||
@@ -226,7 +231,7 @@ async function stopAudioMonitoring() {
|
||||
await vadManager.value.stop()
|
||||
}
|
||||
if (stream.value) { // Stop media stream
|
||||
streamStop()
|
||||
stopStream()
|
||||
}
|
||||
|
||||
stopAnalyzer()
|
||||
@@ -266,12 +271,6 @@ watch(monitorVolume, () => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(audioInputs, () => {
|
||||
if (!selectedAudioInput.value && audioInputs.value.length > 0) {
|
||||
selectedAudioInput.value = audioInputs.value[0]?.deviceId
|
||||
}
|
||||
})
|
||||
|
||||
watch(vadThreshold, () => {
|
||||
// Update VAD threshold if model is loaded
|
||||
if (vadManager.value && isVADModelLoaded.value) {
|
||||
@@ -318,13 +317,12 @@ const speakingIndicatorClass = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
devices.ensurePermissions().then(() => nextTick()).then(() => {
|
||||
if (audioInputs.value.length > 0 && !selectedAudioInput.value) {
|
||||
selectedAudioInput.value = audioInputs.value[0]?.deviceId
|
||||
}
|
||||
})
|
||||
function updateCustomModelName(value: string) {
|
||||
activeCustomModelName.value = value
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await hearingStore.loadModelsForProvider(activeTranscriptionProvider.value)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -352,6 +350,7 @@ onUnmounted(() => {
|
||||
value: input.deviceId,
|
||||
}))"
|
||||
placeholder="Select an audio input device"
|
||||
layout="vertical"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -406,6 +405,66 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model selection section -->
|
||||
<div v-if="activeTranscriptionProvider && supportsModelListing">
|
||||
<div flex="~ col gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg md:text-2xl">
|
||||
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.title') }}
|
||||
</h2>
|
||||
<div text="neutral-400 dark:neutral-400">
|
||||
<span>{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.subtitle') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoadingActiveProviderModels" class="flex items-center justify-center py-4">
|
||||
<div class="mr-2 animate-spin">
|
||||
<div i-solar:spinner-line-duotone text-xl />
|
||||
</div>
|
||||
<span>{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.loading') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<ErrorContainer
|
||||
v-else-if="activeProviderModelError"
|
||||
:title="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.error')"
|
||||
:error="activeProviderModelError"
|
||||
/>
|
||||
|
||||
<!-- No models available -->
|
||||
<Alert
|
||||
v-else-if="providerModels.length === 0 && !isLoadingActiveProviderModels"
|
||||
type="warning"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_models') }}
|
||||
</template>
|
||||
<template #content>
|
||||
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_models_description') }}
|
||||
</template>
|
||||
</Alert>
|
||||
|
||||
<!-- Using the new RadioCardManySelect component -->
|
||||
<template v-else-if="providerModels.length > 0">
|
||||
<RadioCardManySelect
|
||||
v-model="activeTranscriptionModel"
|
||||
v-model:search-query="transcriptionModelSearchQuery"
|
||||
:items="providerModels.sort((a, b) => a.id === activeTranscriptionModel ? -1 : b.id === activeTranscriptionModel ? 1 : 0)"
|
||||
:searchable="true"
|
||||
:search-placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.search_placeholder')"
|
||||
:search-no-results-title="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_search_results')"
|
||||
:search-no-results-description="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_search_results_description', { query: transcriptionModelSearchQuery })"
|
||||
:search-results-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.search_results', { count: '{count}', total: '{total}' })"
|
||||
:custom-input-placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.custom_model_placeholder')"
|
||||
:expand-button-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.expand')"
|
||||
:collapse-button-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.collapse')"
|
||||
@update:custom-value="updateCustomModelName"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -420,6 +479,8 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<ErrorContainer v-if="error" title="Error occurred" :error="error" mb-4 />
|
||||
|
||||
<Button class="mb-4" w-full @click="toggleMonitoring">
|
||||
{{ isMonitoring ? 'Stop Monitoring' : 'Start Monitoring' }}
|
||||
</Button>
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
import type { GenerateTranscriptionResult } from '@xsai/generate-transcription'
|
||||
|
||||
import { FieldRange, FieldSelect } from '@proj-airi/ui'
|
||||
import { until, useDevicesList, useUserMedia } from '@vueuse/core'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { until } from '@vueuse/core'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useAudioRecorder } from '../../../composables/audio/audio-recorder'
|
||||
import { useAudioDevice } from '../../../composables/audio/device'
|
||||
import { LevelMeter, TestDummyMarker, ThresholdMeter } from '../../Gadgets'
|
||||
import { Button } from '../../Misc'
|
||||
|
||||
@@ -18,11 +19,7 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const devices = useDevicesList({ constraints: { audio: true }, requestPermissions: true })
|
||||
const selectedAudioInput = ref<string>(devices.audioInputs.value[0]?.deviceId || '')
|
||||
const deviceConstraints = computed<MediaStreamConstraints>(() => ({ audio: { deviceId: { exact: selectedAudioInput.value } } }))
|
||||
const { stream, stop, start, enabled } = useUserMedia({ constraints: deviceConstraints, enabled: false, autoSwitch: false })
|
||||
const audioInputs = computed(() => devices.audioInputs.value)
|
||||
const { audioInputs, selectedAudioInput, stream, stopStream, startStream } = useAudioDevice()
|
||||
|
||||
const speakingThreshold = ref(25) // 0-100 (for volume-based fallback)
|
||||
const isMonitoring = ref(false)
|
||||
@@ -55,8 +52,7 @@ async function setupAudioMonitoring() {
|
||||
// Clean up existing connections
|
||||
await stopAudioMonitoring()
|
||||
|
||||
enabled.value = true
|
||||
await start()
|
||||
await startStream()
|
||||
await until(stream).toBeTruthy()
|
||||
|
||||
// Create audio context
|
||||
@@ -104,7 +100,7 @@ async function stopAudioMonitoring() {
|
||||
}
|
||||
|
||||
await stopRecord()
|
||||
await stop()
|
||||
await stopStream()
|
||||
|
||||
analyser.value = undefined
|
||||
dataArray.value = undefined
|
||||
@@ -160,7 +156,7 @@ async function toggleMonitoring() {
|
||||
|
||||
onStopRecord(async (recording) => {
|
||||
try {
|
||||
if (recording) {
|
||||
if (recording && recording.size > 0) {
|
||||
audios.value.push(recording)
|
||||
const res = await props.generateTranscription(new File([recording], 'recording.wav'))
|
||||
transcriptions.value.push(res.text)
|
||||
@@ -186,15 +182,6 @@ const speakingIndicatorClass = computed(() => {
|
||||
: 'bg-white dark:bg-neutral-900 border-2 border-neutral-300 dark:border-neutral-600'
|
||||
})
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
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
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAudioMonitoring()
|
||||
})
|
||||
@@ -227,6 +214,19 @@ onUnmounted(() => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button class="my-4" w-full @click="toggleMonitoring">
|
||||
{{ isMonitoring ? 'Stop Monitoring' : 'Start Monitoring' }}
|
||||
</Button>
|
||||
|
||||
<div>
|
||||
<div v-for="(audio, index) in audioURLs" :key="index" class="mb-2">
|
||||
<audio :src="audio" controls class="w-full" />
|
||||
<div v-if="transcriptions[index]" class="mt-2 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{{ transcriptions[index] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audio Level Visualization -->
|
||||
<div class="space-y-3">
|
||||
<!-- Volume Meter -->
|
||||
@@ -265,18 +265,5 @@ onUnmounted(() => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button class="my-4" w-full @click="toggleMonitoring">
|
||||
{{ isMonitoring ? 'Stop Monitoring' : 'Start Monitoring' }}
|
||||
</Button>
|
||||
|
||||
<div>
|
||||
<div v-for="(audio, index) in audioURLs" :key="index" class="mb-2">
|
||||
<audio :src="audio" controls class="w-full" />
|
||||
<div v-if="transcriptions[index]" class="mt-2 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{{ transcriptions[index] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,7 +9,7 @@ export function useAudioRecorder(
|
||||
const audioRecorder = ref<MediaRecorder>()
|
||||
const mediaRef = toRef(media)
|
||||
const recordingChunk = shallowRef<Blob[]>([])
|
||||
const recording = ref<Blob>()
|
||||
const recording = shallowRef<Blob>()
|
||||
|
||||
const onStopRecordHooks = ref<Array<(recording: Blob | undefined) => Promise<void>>>([])
|
||||
|
||||
@@ -23,7 +23,9 @@ export function useAudioRecorder(
|
||||
recordingChunk.value = []
|
||||
|
||||
audioRecorder.value = new MediaRecorder(mediaRef.value)
|
||||
audioRecorder.value.start()
|
||||
// Whisper: problem with audio/mp4 blobs from Safari
|
||||
// https://community.openai.com/t/whisper-problem-with-audio-mp4-blobs-from-safari/322252
|
||||
audioRecorder.value.start(1000)
|
||||
|
||||
audioRecorder.value.onerror = (event) => {
|
||||
console.error('Error recording audio:', event)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useDevicesList, useUserMedia } from '@vueuse/core'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
|
||||
export function useAudioDevice() {
|
||||
const devices = useDevicesList({ constraints: { audio: true }, requestPermissions: true })
|
||||
const audioInputs = computed(() => devices.audioInputs.value)
|
||||
const selectedAudioInput = ref<string>(devices.audioInputs.value[0]?.deviceId || '')
|
||||
const deviceConstraints = computed<MediaStreamConstraints>(() => ({ 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[0]?.deviceId
|
||||
}
|
||||
})
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
audioInputs,
|
||||
selectedAudioInput,
|
||||
stream,
|
||||
stopStream,
|
||||
startStream,
|
||||
deviceConstraints,
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './audio-analyzer'
|
||||
export * from './audio-recorder'
|
||||
export * from './device'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useDevicesList, useUserMedia } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
function calculateVolumeWithLinearNormalize(analyser: AnalyserNode) {
|
||||
const dataBuffer = new Uint8Array(analyser.frequencyBinCount)
|
||||
@@ -65,12 +66,12 @@ function calculateVolume(analyser: AnalyserNode, mode: 'linear' | 'minmax' = 'li
|
||||
}
|
||||
|
||||
export const useAudioContext = defineStore('audio-context', () => {
|
||||
const audioContext = new AudioContext()
|
||||
const audioContext = ref<AudioContext>(new AudioContext())
|
||||
|
||||
onUnmounted(async () => {
|
||||
// Close audio context
|
||||
// Close audio context
|
||||
if (audioContext) {
|
||||
await audioContext.close()
|
||||
await audioContext.value.close()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -80,6 +81,51 @@ 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<string>(devices.audioInputs.value[0]?.deviceId || '')
|
||||
const deviceConstraints = computed<MediaStreamConstraints>(() => ({ 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[0]?.deviceId
|
||||
}
|
||||
})
|
||||
|
||||
function askPermission() {
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
if (requestPermission) {
|
||||
askPermission()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
audioInputs,
|
||||
selectedAudioInput,
|
||||
stream,
|
||||
deviceConstraints,
|
||||
|
||||
askPermission,
|
||||
startStream,
|
||||
stopStream,
|
||||
}
|
||||
}
|
||||
|
||||
export const useSpeakingStore = defineStore('character-speaking', () => {
|
||||
const nowSpeakingAvatarBorderOpacityMin = 30
|
||||
const nowSpeakingAvatarBorderOpacityMax = 100
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TranscriptionProviderWithExtraOptions } from '@xsai-ext/shared-pro
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { generateTranscription } from '@xsai/generate-transcription'
|
||||
import { defineStore, storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useProvidersStore } from '../providers'
|
||||
|
||||
@@ -14,6 +14,8 @@ export const useHearingStore = defineStore('hearing', () => {
|
||||
// State
|
||||
const activeTranscriptionProvider = useLocalStorage('settings/hearing/active-provider', '')
|
||||
const activeTranscriptionModel = useLocalStorage('settings/hearing/active-model', '')
|
||||
const activeCustomModelName = useLocalStorage('settings/hearing/active-custom-model', '')
|
||||
const transcriptionModelSearchQuery = ref('')
|
||||
|
||||
// Computed properties
|
||||
const availableProvidersMetadata = computed(() => allAudioTranscriptionProvidersMetadata.value)
|
||||
@@ -35,6 +37,20 @@ export const useHearingStore = defineStore('hearing', () => {
|
||||
return providersStore.modelLoadError[activeTranscriptionProvider.value] || null
|
||||
})
|
||||
|
||||
async function loadModelsForProvider(provider: string) {
|
||||
if (provider && providersStore.getProviderMetadata(provider)?.capabilities.listModels !== undefined) {
|
||||
await providersStore.fetchModelsForProvider(provider)
|
||||
}
|
||||
}
|
||||
|
||||
async function getModelsForProvider(provider: string) {
|
||||
if (provider && providersStore.getProviderMetadata(provider)?.capabilities.listModels !== undefined) {
|
||||
return providersStore.getModelsForProvider(provider)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
const configured = computed(() => {
|
||||
return !!activeTranscriptionProvider.value && !!activeTranscriptionModel.value
|
||||
})
|
||||
@@ -46,7 +62,7 @@ export const useHearingStore = defineStore('hearing', () => {
|
||||
format?: 'json' | 'verbose_json',
|
||||
) {
|
||||
const response = await generateTranscription({
|
||||
...provider.transcription(model, { responseFormat: format }),
|
||||
...provider.transcription(model),
|
||||
file,
|
||||
responseFormat: format,
|
||||
})
|
||||
@@ -58,6 +74,8 @@ export const useHearingStore = defineStore('hearing', () => {
|
||||
activeTranscriptionProvider,
|
||||
activeTranscriptionModel,
|
||||
availableProvidersMetadata,
|
||||
activeCustomModelName,
|
||||
transcriptionModelSearchQuery,
|
||||
|
||||
supportsModelListing,
|
||||
providerModels,
|
||||
@@ -66,5 +84,7 @@ export const useHearingStore = defineStore('hearing', () => {
|
||||
configured,
|
||||
|
||||
transcription,
|
||||
loadModelsForProvider,
|
||||
getModelsForProvider,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useDevicesList, useLocalStorage } from '@vueuse/core'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { converter } from 'culori'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { useAudioDevice } from './audio'
|
||||
|
||||
export const DEFAULT_THEME_COLORS_HUE = 220.44
|
||||
|
||||
@@ -17,10 +19,6 @@ export const useSettings = defineStore('settings', () => {
|
||||
const stageViewControlsEnabled = ref(false)
|
||||
const live2dDisableFocus = useLocalStorage('settings/live2d/disable-focus', false)
|
||||
|
||||
const isAudioInputOn = useLocalStorage('settings/audio/input', 'false')
|
||||
const selectedAudioDeviceId = computed(() => selectedAudioDevice.value?.deviceId)
|
||||
const { audioInputs, ensurePermissions } = useDevicesList({ constraints: { audio: true } })
|
||||
|
||||
const disableTransitions = useLocalStorage('settings/disable-transitions', true)
|
||||
const usePageSpecificTransitions = useLocalStorage('settings/use-page-specific-transitions', true)
|
||||
|
||||
@@ -58,23 +56,6 @@ export const useSettings = defineStore('settings', () => {
|
||||
return hueDifference < 0.01 || hueDifference > 359.99
|
||||
}
|
||||
|
||||
watch(isAudioInputOn, (value) => {
|
||||
if (value === 'false') {
|
||||
selectedAudioDevice.value = undefined
|
||||
}
|
||||
if (value === 'true') {
|
||||
ensurePermissions().then(() => {
|
||||
selectedAudioDevice.value = audioInputs.value[0]
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
watch(audioInputs, () => {
|
||||
if (isAudioInputOn.value === 'true' && !selectedAudioDevice.value) {
|
||||
selectedAudioDevice.value = audioInputs.value[0]
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
return {
|
||||
disableTransitions,
|
||||
usePageSpecificTransitions,
|
||||
@@ -84,9 +65,7 @@ export const useSettings = defineStore('settings', () => {
|
||||
stageViewControlsEnabled,
|
||||
themeColorsHue,
|
||||
themeColorsHueDynamic,
|
||||
isAudioInputOn,
|
||||
selectedAudioDevice,
|
||||
selectedAudioDeviceId,
|
||||
|
||||
allowVisibleOnAllWorkspaces,
|
||||
|
||||
@@ -95,3 +74,42 @@ export const useSettings = defineStore('settings', () => {
|
||||
isColorSelectedForPrimary,
|
||||
}
|
||||
})
|
||||
|
||||
export const useSettingsAudioDevice = defineStore('settings-audio-devices', () => {
|
||||
const { audioInputs, deviceConstraints, selectedAudioInput: selectedAudioInputNonPersist, startStream, stopStream, stream, askPermission } = useAudioDevice()
|
||||
|
||||
const selectedAudioInputPersist = useLocalStorage('settings/audio/input', selectedAudioInputNonPersist.value)
|
||||
const selectedAudioInputEnabledPersist = useLocalStorage('settings/audio/input-enabled', false)
|
||||
|
||||
watch(selectedAudioInputNonPersist, (newValue) => {
|
||||
selectedAudioInputPersist.value = newValue
|
||||
})
|
||||
|
||||
watch(selectedAudioInputEnabledPersist, (val) => {
|
||||
if (val) {
|
||||
startStream()
|
||||
}
|
||||
else {
|
||||
stopStream()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (selectedAudioInputEnabledPersist.value && selectedAudioInputNonPersist.value) {
|
||||
startStream()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
audioInputs,
|
||||
deviceConstraints,
|
||||
selectedAudioInput: selectedAudioInputNonPersist,
|
||||
enabled: selectedAudioInputEnabledPersist,
|
||||
|
||||
stream,
|
||||
|
||||
askPermission,
|
||||
startStream,
|
||||
stopStream,
|
||||
}
|
||||
})
|
||||
|
||||
Generated
+108
-7
@@ -1055,7 +1055,7 @@ importers:
|
||||
version: 0.1.3
|
||||
unplugin-yaml:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(astro@5.10.1(@types/node@24.0.15)(encoding@0.1.13)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.45.1)(terser@5.43.1)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0))(esbuild@0.25.5)(rolldown@1.0.0-beta.29)(rollup@4.45.1)(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))
|
||||
version: 3.0.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(astro@5.10.1(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.45.1)(terser@5.43.1)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0))(esbuild@0.25.5)(rolldown@1.0.0-beta.29)(rollup@4.45.1)(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))
|
||||
vitepress:
|
||||
specifier: ^2.0.0-alpha.8
|
||||
version: 2.0.0-alpha.8(@algolia/client-search@5.32.0)(@types/node@24.0.15)(change-case@5.4.4)(fuse.js@7.1.0)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(nprogress@0.2.0)(postcss@8.5.6)(react@18.3.1)(search-insights@2.17.3)(terser@5.43.1)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0)
|
||||
@@ -1101,7 +1101,7 @@ importers:
|
||||
devDependencies:
|
||||
unplugin-yaml:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(astro@5.10.1(@types/node@24.0.15)(encoding@0.1.13)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.45.1)(terser@5.43.1)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0))(esbuild@0.25.5)(rolldown@1.0.0-beta.29)(rollup@4.45.1)(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))
|
||||
version: 3.0.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(astro@5.10.1(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.45.1)(terser@5.43.1)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0))(esbuild@0.25.5)(rolldown@1.0.0-beta.29)(rollup@4.45.1)(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))
|
||||
|
||||
packages/memory-pgvector:
|
||||
dependencies:
|
||||
@@ -1478,7 +1478,7 @@ importers:
|
||||
version: 1.2.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(esbuild@0.25.5)(rollup@4.45.1)(vite@6.3.5(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))
|
||||
unplugin-yaml:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(astro@5.10.1(@types/node@24.0.15)(encoding@0.1.13)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.45.1)(terser@5.43.1)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0))(esbuild@0.25.5)(rolldown@1.0.0-beta.29)(rollup@4.45.1)(vite@6.3.5(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))
|
||||
version: 3.0.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(astro@5.10.1(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.45.1)(terser@5.43.1)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0))(esbuild@0.25.5)(rolldown@1.0.0-beta.29)(rollup@4.45.1)(vite@6.3.5(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))
|
||||
vite:
|
||||
specifier: ^6.3.5
|
||||
version: 6.3.5(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
|
||||
@@ -18698,6 +18698,107 @@ snapshots:
|
||||
- yaml
|
||||
optional: true
|
||||
|
||||
astro@5.10.1(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.45.1)(terser@5.43.1)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0):
|
||||
dependencies:
|
||||
'@astrojs/compiler': 2.12.2
|
||||
'@astrojs/internal-helpers': 0.6.1
|
||||
'@astrojs/markdown-remark': 6.3.2
|
||||
'@astrojs/telemetry': 3.3.0
|
||||
'@capsizecss/unpack': 2.4.0(encoding@0.1.13)
|
||||
'@oslojs/encoding': 1.1.0
|
||||
'@rollup/pluginutils': 5.2.0(rollup@4.45.1)
|
||||
acorn: 8.15.0
|
||||
aria-query: 5.3.2
|
||||
axobject-query: 4.1.0
|
||||
boxen: 8.0.1
|
||||
ci-info: 4.2.0
|
||||
clsx: 2.1.1
|
||||
common-ancestor-path: 1.0.1
|
||||
cookie: 1.0.2
|
||||
cssesc: 3.0.0
|
||||
debug: 4.4.1
|
||||
deterministic-object-hash: 2.0.2
|
||||
devalue: 5.1.1
|
||||
diff: 5.2.0
|
||||
dlv: 1.1.3
|
||||
dset: 3.1.4
|
||||
es-module-lexer: 1.7.0
|
||||
esbuild: 0.25.5
|
||||
estree-walker: 3.0.3
|
||||
flattie: 1.1.1
|
||||
fontace: 0.3.0
|
||||
github-slugger: 2.0.0
|
||||
html-escaper: 3.0.3
|
||||
http-cache-semantics: 4.2.0
|
||||
import-meta-resolve: 4.1.0
|
||||
js-yaml: 4.1.0
|
||||
kleur: 4.1.5
|
||||
magic-string: 0.30.17
|
||||
magicast: 0.3.5
|
||||
mrmime: 2.0.1
|
||||
neotraverse: 0.6.18
|
||||
p-limit: 6.2.0
|
||||
p-queue: 8.1.0
|
||||
package-manager-detector: 1.3.0
|
||||
picomatch: 4.0.2
|
||||
prompts: 2.4.2
|
||||
rehype: 13.0.2
|
||||
semver: 7.7.2
|
||||
shiki: 3.8.1
|
||||
tinyexec: 0.3.2
|
||||
tinyglobby: 0.2.14
|
||||
tsconfck: 3.1.6(typescript@5.8.3)
|
||||
ultrahtml: 1.6.0
|
||||
unifont: 0.5.2
|
||||
unist-util-visit: 5.0.0
|
||||
unstorage: 1.16.0
|
||||
vfile: 6.0.3
|
||||
vite: 6.3.5(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
|
||||
vitefu: 1.0.7(vite@6.3.5(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))
|
||||
xxhash-wasm: 1.1.0
|
||||
yargs-parser: 21.1.1
|
||||
yocto-spinner: 0.2.3
|
||||
zod: 3.25.76
|
||||
zod-to-json-schema: 3.24.6(zod@3.25.76)
|
||||
zod-to-ts: 1.2.0(typescript@5.8.3)(zod@3.25.76)
|
||||
optionalDependencies:
|
||||
sharp: 0.33.5
|
||||
transitivePeerDependencies:
|
||||
- '@azure/app-configuration'
|
||||
- '@azure/cosmos'
|
||||
- '@azure/data-tables'
|
||||
- '@azure/identity'
|
||||
- '@azure/keyvault-secrets'
|
||||
- '@azure/storage-blob'
|
||||
- '@capacitor/preferences'
|
||||
- '@deno/kv'
|
||||
- '@netlify/blobs'
|
||||
- '@planetscale/database'
|
||||
- '@types/node'
|
||||
- '@upstash/redis'
|
||||
- '@vercel/blob'
|
||||
- '@vercel/kv'
|
||||
- aws4fetch
|
||||
- db0
|
||||
- encoding
|
||||
- idb-keyval
|
||||
- ioredis
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- rollup
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
- tsx
|
||||
- typescript
|
||||
- uploadthing
|
||||
- yaml
|
||||
optional: true
|
||||
|
||||
async-mutex@0.3.2:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -25583,7 +25684,7 @@ snapshots:
|
||||
rollup: 4.45.1
|
||||
vite: rolldown-vite@7.0.9(@types/node@24.0.15)(esbuild@0.25.5)(jiti@2.4.2)(less@4.4.0)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
|
||||
|
||||
unplugin-yaml@3.0.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(astro@5.10.1(@types/node@24.0.15)(encoding@0.1.13)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.45.1)(terser@5.43.1)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0))(esbuild@0.25.5)(rolldown@1.0.0-beta.29)(rollup@4.45.1)(vite@6.3.5(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)):
|
||||
unplugin-yaml@3.0.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(astro@5.10.1(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.45.1)(terser@5.43.1)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0))(esbuild@0.25.5)(rolldown@1.0.0-beta.29)(rollup@4.45.1)(vite@6.3.5(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.2.0(rollup@4.45.1)
|
||||
unplugin: 2.3.5
|
||||
@@ -25591,13 +25692,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.45.1)
|
||||
'@nuxt/schema': 3.14.1592(magicast@0.3.5)(rollup@4.45.1)
|
||||
astro: 5.10.1(@types/node@24.0.15)(encoding@0.1.13)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.45.1)(terser@5.43.1)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0)
|
||||
astro: 5.10.1(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.45.1)(terser@5.43.1)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0)
|
||||
esbuild: 0.25.5
|
||||
rolldown: 1.0.0-beta.29
|
||||
rollup: 4.45.1
|
||||
vite: 6.3.5(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
|
||||
|
||||
unplugin-yaml@3.0.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(astro@5.10.1(@types/node@24.0.15)(encoding@0.1.13)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.45.1)(terser@5.43.1)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0))(esbuild@0.25.5)(rolldown@1.0.0-beta.29)(rollup@4.45.1)(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)):
|
||||
unplugin-yaml@3.0.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.45.1))(astro@5.10.1(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.45.1)(terser@5.43.1)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0))(esbuild@0.25.5)(rolldown@1.0.0-beta.29)(rollup@4.45.1)(vite@7.0.5(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.2.0(rollup@4.45.1)
|
||||
unplugin: 2.3.5
|
||||
@@ -25605,7 +25706,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.45.1)
|
||||
'@nuxt/schema': 3.14.1592(magicast@0.3.5)(rollup@4.45.1)
|
||||
astro: 5.10.1(@types/node@24.0.15)(encoding@0.1.13)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.45.1)(terser@5.43.1)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0)
|
||||
astro: 5.10.1(@types/node@24.0.15)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.45.1)(terser@5.43.1)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0)
|
||||
esbuild: 0.25.5
|
||||
rolldown: 1.0.0-beta.29
|
||||
rollup: 4.45.1
|
||||
|
||||
Reference in New Issue
Block a user