fix(stage-*): async audio implementation

This commit is contained in:
Neko Ayaka
2025-07-25 03:59:36 +08:00
parent 94f005a6f3
commit d56823f75c
14 changed files with 491 additions and 159 deletions
@@ -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()
}
})
+8 -10
View File
@@ -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'
+50 -4
View File
@@ -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,
}
})
+43 -25
View File
@@ -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,
}
})
+108 -7
View File
@@ -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