11<script setup lang="ts">
22import type { ChatProvider } from ' @xsai-ext/shared-providers'
33
4- import WhisperWorker from ' @proj-airi/stage-ui/libs/workers/worker?worker&url'
5-
6- import { toWAVBase64 } from ' @proj-airi/audio'
74import { HearingConfigDialog } from ' @proj-airi/stage-ui/components'
8- import { useMicVAD , useWhisper } from ' @proj-airi/stage-ui/composables'
5+ import { useAudioAnalyzer } from ' @proj-airi/stage-ui/composables'
96import { useAudioContext } from ' @proj-airi/stage-ui/stores/audio'
107import { useChatStore } from ' @proj-airi/stage-ui/stores/chat'
118import { useConsciousnessStore } from ' @proj-airi/stage-ui/stores/modules/consciousness'
@@ -21,7 +18,6 @@ import ChatHistory from '../Widgets/ChatHistory.vue'
2118import IndicatorMicVolume from ' ../Widgets/IndicatorMicVolume.vue'
2219
2320const messageInput = ref (' ' )
24- const listening = ref (false )
2521const hearingDialogOpen = ref (false )
2622const isComposing = ref (false )
2723
@@ -30,29 +26,15 @@ const { activeProvider, activeModel } = storeToRefs(useConsciousnessStore())
3026const { themeColorsHueDynamic } = storeToRefs (useSettings ())
3127
3228const { askPermission } = useSettingsAudioDevice ()
33- const { enabled, selectedAudioInput } = storeToRefs (useSettingsAudioDevice ())
29+ const { enabled, selectedAudioInput, stream, audioInputs } = storeToRefs (useSettingsAudioDevice ())
3430const { send, onAfterMessageComposed, discoverToolsCompatibility, cleanupMessages } = useChatStore ()
3531const { messages } = storeToRefs (useChatStore ())
3632const { audioContext } = useAudioContext ()
3733const { t } = useI18n ()
3834
3935const isDark = useDark ({ disableTransition: false })
4036
41- const { transcribe : generate, terminate } = useWhisper (WhisperWorker , {
42- onComplete : async (res ) => {
43- if (! res || ! res .trim ()) {
44- return
45- }
46-
47- const providerConfig = providersStore .getProviderConfig (activeProvider .value )
48-
49- await send (res , {
50- chatProvider: await providersStore .getProviderInstance (activeProvider .value ) as ChatProvider ,
51- model: activeModel .value ,
52- providerConfig ,
53- })
54- },
55- })
37+ // Legacy whisper pipeline removed; audio pipeline handled at page level
5638
5739async function handleSend() {
5840 if (! messageInput .value .trim () || isComposing .value ) {
@@ -77,46 +59,7 @@ async function handleSend() {
7759 }
7860}
7961
80- const { destroy, start } = useMicVAD (selectedAudioInput , {
81- onSpeechStart : () => {
82- // TODO: interrupt the playback
83- // TODO: interrupt any of the ongoing TTS
84- // TODO: interrupt any of the ongoing LLM requests
85- // TODO: interrupt any of the ongoing animation of Live2D or VRM
86- // TODO: once interrupted, we should somehow switch to listen or thinking
87- // emotion / expression?
88- listening .value = true
89- },
90- // VAD misfire means while speech end is detected but
91- // the frames of the segment of the audio buffer
92- // is not enough to be considered as a speech segment
93- // which controlled by the `minSpeechFrames` parameter
94- onVADMisfire : () => {
95- // TODO: do audio buffer send to whisper
96- listening .value = false
97- },
98- onSpeechEnd : (buffer ) => {
99- // TODO: do audio buffer send to whisper
100- listening .value = false
101- handleTranscription (buffer .buffer )
102- },
103- auto: false ,
104- })
105-
106- async function handleTranscription(buffer : ArrayBufferLike ) {
107- await audioContext .resume ()
108-
109- // Convert Float32Array to WAV format
110- const audioBase64 = await toWAVBase64 (buffer , audioContext .sampleRate )
111- generate ({ type: ' generate' , data: { audio: audioBase64 , language: ' en' } })
112- }
113-
114- watch (enabled , async (value ) => {
115- if (value === false ) {
116- destroy ()
117- terminate ()
118- }
119- })
62+ // No inline VAD/whisper here; see pages/index.vue pipeline
12063
12164watch (hearingDialogOpen , async (value ) => {
12265 if (value ) {
@@ -130,14 +73,38 @@ watch([activeProvider, activeModel], async () => {
13073 }
13174})
13275
133- onMounted (() => {
134- // loadWhisper()
135- start ()
136- })
76+ onMounted (() => {})
13777
13878onAfterMessageComposed (async () => {
13979 messageInput .value = ' '
14080})
81+
82+ const { startAnalyzer, stopAnalyzer, volumeLevel } = useAudioAnalyzer ()
83+ let analyzerSource: MediaStreamAudioSourceNode | undefined
84+
85+ function teardownAnalyzer() {
86+ try { analyzerSource ?.disconnect () }
87+ catch {}
88+ analyzerSource = undefined
89+ stopAnalyzer ()
90+ }
91+
92+ async function setupAnalyzer() {
93+ teardownAnalyzer ()
94+ if (! hearingDialogOpen .value || ! enabled .value || ! stream .value )
95+ return
96+ if (audioContext .state === ' suspended' )
97+ await audioContext .resume ()
98+ const analyser = startAnalyzer (audioContext )
99+ if (! analyser )
100+ return
101+ analyzerSource = audioContext .createMediaStreamSource (stream .value )
102+ analyzerSource .connect (analyser )
103+ }
104+
105+ watch ([hearingDialogOpen , enabled , stream ], () => {
106+ setupAnalyzer ()
107+ }, { immediate: true })
141108 </script >
142109
143110<template >
@@ -167,12 +134,23 @@ onAfterMessageComposed(async () => {
167134 @compositionend =" isComposing = false"
168135 />
169136
170- <HearingConfigDialog v-model:show =" hearingDialogOpen" :overlay-dim =" true" :overlay-blur =" true" >
137+ <HearingConfigDialog
138+ v-model:show =" hearingDialogOpen"
139+ :overlay-dim =" true"
140+ :overlay-blur =" true"
141+ :enabled =" enabled"
142+ :audio-input-options =" audioInputs"
143+ :selected-audio-input =" selectedAudioInput"
144+ :has-devices =" (audioInputs || []).length > 0"
145+ :volume-level =" volumeLevel"
146+ @toggle-enabled =" enabled = !enabled"
147+ @update:selected-audio-input =" val => selectedAudioInput = val as string"
148+ >
171149 <button
172150 class =" max-h-[10lh] min-h-[1lh]"
173151 bg =" neutral-100 dark:neutral-800"
174152 text =" lg neutral-500 dark:neutral-400"
175- :class =" { 'ring-2 ring-primary-400/60 ring-offset-2 dark:ring-offset-neutral-900': listening }"
153+ :class =" { 'ring-2 ring-primary-400/60 ring-offset-2 dark:ring-offset-neutral-900': enabled }"
176154 flex items-center justify-center rounded-md p-2 outline-none
177155 transition =" colors duration-200, transform duration-100" active:scale-95
178156 :title =" t('settings.hearing.title')"
0 commit comments