refactor(stage-*): moved vad state manager
This commit is contained in:
@@ -49,6 +49,7 @@
|
||||
"@xsai/generate-speech": "catalog:",
|
||||
"@xsai/generate-text": "catalog:",
|
||||
"@xsai/model": "catalog:",
|
||||
"@xsai/shared": "catalog:",
|
||||
"@xsai/shared-chat": "catalog:",
|
||||
"@xsai/stream-text": "catalog:",
|
||||
"@xsai/utils-chat": "catalog:",
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
// Language constants
|
||||
const LANGUAGES: Record<string, string>[] = [
|
||||
{ en: 'english' },
|
||||
{ zh: 'chinese' },
|
||||
{ 'zh-Hans': 'chinese' },
|
||||
{ 'zh-Hant': 'chinese' },
|
||||
{ 'zh-CN': 'chinese' },
|
||||
{ 'zh-TW': 'chinese' },
|
||||
{ 'zh-HK': 'chinese' },
|
||||
{ de: 'german' },
|
||||
{ es: 'spanish' },
|
||||
{ ru: 'russian' },
|
||||
{ ko: 'korean' },
|
||||
{ fr: 'french' },
|
||||
{ ja: 'japanese' },
|
||||
{ pt: 'portuguese' },
|
||||
{ tr: 'turkish' },
|
||||
{ pl: 'polish' },
|
||||
{ ca: 'catalan' },
|
||||
{ nl: 'dutch' },
|
||||
{ ar: 'arabic' },
|
||||
{ sv: 'swedish' },
|
||||
{ it: 'italian' },
|
||||
{ id: 'indonesian' },
|
||||
{ hi: 'hindi' },
|
||||
{ fi: 'finnish' },
|
||||
{ vi: 'vietnamese' },
|
||||
{ he: 'hebrew' },
|
||||
{ uk: 'ukrainian' },
|
||||
{ el: 'greek' },
|
||||
{ ms: 'malay' },
|
||||
{ cs: 'czech' },
|
||||
{ ro: 'romanian' },
|
||||
{ da: 'danish' },
|
||||
{ hu: 'hungarian' },
|
||||
{ ta: 'tamil' },
|
||||
{ no: 'norwegian' },
|
||||
{ th: 'thai' },
|
||||
{ ur: 'urdu' },
|
||||
{ hr: 'croatian' },
|
||||
{ bg: 'bulgarian' },
|
||||
{ lt: 'lithuanian' },
|
||||
{ la: 'latin' },
|
||||
{ mi: 'maori' },
|
||||
{ ml: 'malayalam' },
|
||||
{ cy: 'welsh' },
|
||||
{ sk: 'slovak' },
|
||||
{ te: 'telugu' },
|
||||
{ fa: 'persian' },
|
||||
{ lv: 'latvian' },
|
||||
{ bn: 'bengali' },
|
||||
{ sr: 'serbian' },
|
||||
{ az: 'azerbaijani' },
|
||||
{ sl: 'slovenian' },
|
||||
{ kn: 'kannada' },
|
||||
{ et: 'estonian' },
|
||||
{ mk: 'macedonian' },
|
||||
{ br: 'breton' },
|
||||
{ eu: 'basque' },
|
||||
{ is: 'icelandic' },
|
||||
{ hy: 'armenian' },
|
||||
{ ne: 'nepali' },
|
||||
{ mn: 'mongolian' },
|
||||
{ bs: 'bosnian' },
|
||||
{ kk: 'kazakh' },
|
||||
{ sq: 'albanian' },
|
||||
{ sw: 'swahili' },
|
||||
{ gl: 'galician' },
|
||||
{ mr: 'marathi' },
|
||||
{ pa: 'punjabi' },
|
||||
{ si: 'sinhala' },
|
||||
{ km: 'khmer' },
|
||||
{ sn: 'shona' },
|
||||
{ yo: 'yoruba' },
|
||||
{ so: 'somali' },
|
||||
{ af: 'afrikaans' },
|
||||
{ oc: 'occitan' },
|
||||
{ ka: 'georgian' },
|
||||
{ be: 'belarusian' },
|
||||
{ tg: 'tajik' },
|
||||
{ sd: 'sindhi' },
|
||||
{ gu: 'gujarati' },
|
||||
{ am: 'amharic' },
|
||||
{ yi: 'yiddish' },
|
||||
{ lo: 'lao' },
|
||||
{ uz: 'uzbek' },
|
||||
{ fo: 'faroese' },
|
||||
{ ht: 'haitian creole' },
|
||||
{ ps: 'pashto' },
|
||||
{ tk: 'turkmen' },
|
||||
{ nn: 'nynorsk' },
|
||||
{ mt: 'maltese' },
|
||||
{ sa: 'sanskrit' },
|
||||
{ lb: 'luxembourgish' },
|
||||
{ my: 'myanmar' },
|
||||
{ bo: 'tibetan' },
|
||||
{ tl: 'tagalog' },
|
||||
{ mg: 'malagasy' },
|
||||
{ as: 'assamese' },
|
||||
{ tt: 'tatar' },
|
||||
{ haw: 'hawaiian' },
|
||||
{ ln: 'lingala' },
|
||||
{ ha: 'hausa' },
|
||||
{ ba: 'bashkir' },
|
||||
{ jw: 'javanese' },
|
||||
{ su: 'sundanese' },
|
||||
]
|
||||
|
||||
export function mapLanguageCodeToName(code: string): string {
|
||||
const lang = LANGUAGES.find(lang => lang[code])
|
||||
return lang ? lang[code] : 'Unknown Language'
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
import { computed, readonly, ref } from 'vue'
|
||||
|
||||
import { useTauriCore } from '../tauri'
|
||||
import { mapLanguageCodeToName } from './extract-whisper-languages'
|
||||
|
||||
export interface WhisperConfig {
|
||||
modelSize: 'tiny' | 'base' | 'small' | 'medium' | 'large'
|
||||
language?: string
|
||||
temperature: number
|
||||
beamSize: number
|
||||
bestOf: number
|
||||
}
|
||||
|
||||
export interface VADSegment {
|
||||
id: string
|
||||
audioData: Float32Array
|
||||
startTime: number
|
||||
endTime: number
|
||||
probability: number
|
||||
isComplete: boolean
|
||||
}
|
||||
|
||||
export interface TranscriptionResult {
|
||||
id: string
|
||||
segmentId: string
|
||||
text: string
|
||||
language?: string
|
||||
processingTimeMs: number
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export function useWhisperTranscription(config: Partial<WhisperConfig> = {}) {
|
||||
const { invoke } = useTauriCore()
|
||||
|
||||
const isModelLoaded = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const error = ref('')
|
||||
const isProcessing = ref(false)
|
||||
|
||||
const whisperConfig = ref<WhisperConfig>({
|
||||
modelSize: 'base',
|
||||
language: undefined, // Auto-detect
|
||||
temperature: 0.0,
|
||||
beamSize: 5,
|
||||
bestOf: 5,
|
||||
...config,
|
||||
})
|
||||
|
||||
const transcriptionQueue = ref<VADSegment[]>([])
|
||||
const transcriptionResults = ref<TranscriptionResult[]>([])
|
||||
const maxResults = 50
|
||||
|
||||
const onTranscriptionResultHooks = ref<((result: TranscriptionResult) => void)[]>([])
|
||||
|
||||
const currentTranscription = computed(() =>
|
||||
transcriptionResults.value[transcriptionResults.value.length - 1],
|
||||
)
|
||||
|
||||
async function loadModel(modelType: 'base' | 'largev3' | 'tiny' | 'medium' = 'base') {
|
||||
if (isModelLoaded.value || isLoading.value)
|
||||
return
|
||||
|
||||
isLoading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
await invoke('plugin:ipc-audio-transcription-ort|load_ort_model_whisper', { modelType })
|
||||
isModelLoaded.value = true
|
||||
}
|
||||
catch (err) {
|
||||
error.value = err instanceof Error ? err.message : String(err)
|
||||
console.error('Failed to load Whisper model:', err)
|
||||
}
|
||||
finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function transcribeSegment(segment: VADSegment, locale: string): Promise<TranscriptionResult | null> {
|
||||
if (!isModelLoaded.value || !segment.isComplete)
|
||||
return null
|
||||
|
||||
const startTime = performance.now()
|
||||
|
||||
try {
|
||||
isProcessing.value = true
|
||||
|
||||
const audioArray = Array.from(segment.audioData)
|
||||
const result = await invoke('plugin:ipc-audio-transcription-ort|ipc_audio_transcription', {
|
||||
chunk: audioArray,
|
||||
language: mapLanguageCodeToName(locale),
|
||||
}) || ''
|
||||
|
||||
const transcription: TranscriptionResult = {
|
||||
id: `transcription_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
segmentId: segment.id,
|
||||
text: result?.trim() || '',
|
||||
processingTimeMs: performance.now() - startTime,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
// Add to results
|
||||
transcriptionResults.value.push(transcription)
|
||||
if (transcriptionResults.value.length > maxResults) {
|
||||
transcriptionResults.value.shift()
|
||||
}
|
||||
|
||||
// Notify hooks
|
||||
onTranscriptionResultHooks.value.forEach(cb => cb(transcription))
|
||||
|
||||
return transcription
|
||||
}
|
||||
catch (err) {
|
||||
error.value = err instanceof Error ? err.message : String(err)
|
||||
console.error('Transcription error:', err)
|
||||
return null
|
||||
}
|
||||
finally {
|
||||
isProcessing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function transcribeAudioData(audioData: Float32Array, locale: string): Promise<TranscriptionResult | null> {
|
||||
if (!isModelLoaded.value)
|
||||
return null
|
||||
|
||||
const fakeSegment: VADSegment = {
|
||||
id: `manual_${Date.now()}`,
|
||||
audioData,
|
||||
startTime: 0,
|
||||
endTime: (audioData.length / 16000) * 1000,
|
||||
probability: 1.0,
|
||||
isComplete: true,
|
||||
}
|
||||
|
||||
return await transcribeSegment(fakeSegment, locale)
|
||||
}
|
||||
|
||||
function queueSegment(segment: VADSegment, locale: string) {
|
||||
if (segment.isComplete) {
|
||||
transcriptionQueue.value.push(segment)
|
||||
processQueue(locale)
|
||||
}
|
||||
}
|
||||
|
||||
async function processQueue(locale: string) {
|
||||
if (isProcessing.value || transcriptionQueue.value.length === 0)
|
||||
return
|
||||
|
||||
const segment = transcriptionQueue.value.shift()
|
||||
if (segment) {
|
||||
await transcribeSegment(segment, locale)
|
||||
// Process next item in queue
|
||||
if (transcriptionQueue.value.length > 0) {
|
||||
setTimeout(() => processQueue(locale), 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function onTranscriptionResult(cb: (result: TranscriptionResult) => void) {
|
||||
onTranscriptionResultHooks.value.push(cb)
|
||||
|
||||
// Immediately call with current results
|
||||
transcriptionResults.value.forEach(result => cb(result))
|
||||
|
||||
// Return a cleanup function
|
||||
return () => {
|
||||
const index = onTranscriptionResultHooks.value.indexOf(cb)
|
||||
if (index !== -1) {
|
||||
onTranscriptionResultHooks.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearResults() {
|
||||
transcriptionResults.value = []
|
||||
}
|
||||
|
||||
function clearQueue() {
|
||||
transcriptionQueue.value = []
|
||||
}
|
||||
|
||||
function getResultBySegmentId(segmentId: string): TranscriptionResult | undefined {
|
||||
return transcriptionResults.value.find(r => r.segmentId === segmentId)
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
isModelLoaded: readonly(isModelLoaded),
|
||||
isLoading: readonly(isLoading),
|
||||
isProcessing: readonly(isProcessing),
|
||||
error: readonly(error),
|
||||
|
||||
// Configuration
|
||||
config: whisperConfig,
|
||||
|
||||
// Results
|
||||
transcriptionResults: readonly(transcriptionResults),
|
||||
currentTranscription,
|
||||
queueLength: computed(() => transcriptionQueue.value.length),
|
||||
|
||||
// Controls
|
||||
loadModel,
|
||||
transcribeSegment,
|
||||
transcribeAudioData,
|
||||
queueSegment,
|
||||
clearResults,
|
||||
clearQueue,
|
||||
getResultBySegmentId,
|
||||
onTranscriptionResult,
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import workletUrl from '../../../tauri/vad/process.worklet?worker&url'
|
||||
|
||||
import { createVAD, VADAudioManager } from '../../../tauri/vad'
|
||||
import { createVAD, createVADStates } from '../../../tauri/vad'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -50,7 +50,7 @@ const speakingThreshold = ref(25) // 0-100 (for volume-based fallback)
|
||||
const monitorVolume = ref(50) // 0-100
|
||||
|
||||
// VAD integration
|
||||
const vadManager = ref<VADAudioManager>()
|
||||
const vadManager = ref<ReturnType<typeof createVADStates>>()
|
||||
const isVADModelLoaded = ref(false)
|
||||
const isLoadingVADModel = ref(false)
|
||||
const useVADModel = ref(true) // Toggle between VAD and volume-based detection
|
||||
@@ -119,7 +119,7 @@ async function loadVADModel() {
|
||||
})
|
||||
|
||||
// Create and initialize audio manager
|
||||
const manager = new VADAudioManager(vad, {
|
||||
const manager = createVADStates(vad, workletUrl, {
|
||||
minChunkSize: 512,
|
||||
// NOTICE: VAD will have it's own audio context since
|
||||
// it needs special sample rate and latency settings
|
||||
@@ -129,7 +129,7 @@ async function loadVADModel() {
|
||||
},
|
||||
})
|
||||
|
||||
await manager.initialize(workletUrl)
|
||||
await manager.initialize()
|
||||
vadManager.value = manager
|
||||
isVADModelLoaded.value = true
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export { VADAudioManager } from './manager'
|
||||
export { createVADStates } from './manager'
|
||||
export type { VADAudioOptions } from './manager'
|
||||
export { createVAD, VAD } from './vad'
|
||||
export type { VADConfig, VADEventCallback, VADEvents } from './vad'
|
||||
|
||||
@@ -1,150 +1,2 @@
|
||||
import type { VAD, VADConfig } from './vad'
|
||||
|
||||
export interface VADAudioOptions {
|
||||
/**
|
||||
* Audio context options
|
||||
*/
|
||||
audioContextOptions?: AudioContextOptions
|
||||
|
||||
/**
|
||||
* The minimum size of audio chunks to process
|
||||
*/
|
||||
minChunkSize?: number
|
||||
|
||||
/**
|
||||
* VAD configuration options
|
||||
*/
|
||||
vadConfig?: Partial<VADConfig>
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages audio input and worklet processing for the VAD module
|
||||
*/
|
||||
export class VADAudioManager {
|
||||
private audioContext: AudioContext | null = null
|
||||
private audioWorkletNode: AudioWorkletNode | null = null
|
||||
private mediaStream: MediaStream | null = null
|
||||
private sourceNode: MediaStreamAudioSourceNode | null = null
|
||||
private vad: VAD // Updated type
|
||||
private workletInitialized: boolean = false
|
||||
|
||||
/**
|
||||
* Create a new VAD audio manager
|
||||
*/
|
||||
constructor(vad: VAD, options: VADAudioOptions = {}) { // Updated parameter type
|
||||
this.vad = vad
|
||||
|
||||
// Create audio context with user options or defaults
|
||||
this.audioContext = new AudioContext(options.audioContextOptions || {
|
||||
sampleRate: 16000, // Match the VAD sample rate
|
||||
latencyHint: 'interactive',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the audio worklet and connect to microphone
|
||||
*/
|
||||
public async initialize(workletUrl: string): Promise<void> {
|
||||
if (!this.audioContext) {
|
||||
throw new Error('Audio context not created')
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this.workletInitialized) {
|
||||
await this.audioContext.audioWorklet.addModule(workletUrl)
|
||||
URL.revokeObjectURL(workletUrl)
|
||||
this.workletInitialized = true
|
||||
}
|
||||
|
||||
// Create the worklet node
|
||||
this.audioWorkletNode = new AudioWorkletNode(this.audioContext, 'vad-processor')
|
||||
|
||||
// Set up message handling from the worklet
|
||||
this.audioWorkletNode.port.onmessage = async (event) => {
|
||||
const { buffer } = event.data
|
||||
if (buffer && buffer.length > 0) {
|
||||
await this.vad.processAudio(new Float32Array(buffer))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to initialize audio worklet:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start capturing audio from the microphone
|
||||
*/
|
||||
public async start(mediaStream: MediaStream): Promise<void> {
|
||||
if (!this.audioContext || !this.audioWorkletNode) {
|
||||
throw new Error('Audio system not initialized. Call initialize() first.')
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.audioContext.state === 'suspended') {
|
||||
await this.audioContext.resume()
|
||||
}
|
||||
|
||||
// Request microphone access
|
||||
this.mediaStream = mediaStream
|
||||
|
||||
// Create source node and connect to worklet
|
||||
this.sourceNode = this.audioContext.createMediaStreamSource(this.mediaStream)
|
||||
this.sourceNode.connect(this.audioWorkletNode)
|
||||
|
||||
// Connect worklet to a silent destination (to keep the audio graph active)
|
||||
// Using a GainNode with gain=0 to ensure no sound is output
|
||||
const silentGain = this.audioContext.createGain()
|
||||
silentGain.gain.value = 0
|
||||
this.audioWorkletNode.connect(silentGain)
|
||||
silentGain.connect(this.audioContext.destination)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to start microphone:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop capturing audio
|
||||
*/
|
||||
public stop(): void {
|
||||
// Disconnect nodes
|
||||
if (this.sourceNode && this.audioWorkletNode) {
|
||||
this.sourceNode.disconnect()
|
||||
this.audioWorkletNode.disconnect()
|
||||
}
|
||||
|
||||
// Stop all tracks in the media stream
|
||||
if (this.mediaStream) {
|
||||
this.mediaStream.getTracks().forEach(track => track.stop())
|
||||
this.mediaStream = null
|
||||
}
|
||||
|
||||
// Suspend the audio context rather than closing it
|
||||
// This allows us to reuse it later
|
||||
if (this.audioContext && this.audioContext.state !== 'closed') {
|
||||
this.audioContext.suspend()
|
||||
}
|
||||
|
||||
this.sourceNode = null
|
||||
this.audioWorkletNode?.disconnect()
|
||||
this.audioWorkletNode = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all resources
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.stop()
|
||||
|
||||
// Now fully close the audio context
|
||||
if (this.audioContext && this.audioContext.state !== 'closed') {
|
||||
this.audioContext.close()
|
||||
this.audioContext = null
|
||||
}
|
||||
|
||||
this.workletInitialized = false
|
||||
}
|
||||
}
|
||||
export type { VADAudioOptions } from '@proj-airi/stage-ui/libs/audio/vad'
|
||||
export { createVADStates } from '@proj-airi/stage-ui/libs/audio/vad'
|
||||
|
||||
@@ -1,28 +1,9 @@
|
||||
import type { BaseVAD, BaseVADConfig, VADEventCallback, VADEvents } from '@proj-airi/stage-ui/libs/audio/vad'
|
||||
|
||||
import { invoke } from '../invoke'
|
||||
|
||||
export interface VADConfig {
|
||||
sampleRate: number
|
||||
speechThreshold: number
|
||||
exitThreshold: number
|
||||
minSilenceDurationMs: number
|
||||
speechPadMs: number
|
||||
minSpeechDurationMs: number
|
||||
maxBufferDuration: number
|
||||
newBufferSize: number
|
||||
}
|
||||
|
||||
export interface VADEvents {
|
||||
'speech-start': void
|
||||
'speech-end': void
|
||||
'speech-ready': { buffer: Float32Array, duration: number }
|
||||
'status': { type: string, message: string }
|
||||
'debug': { message: string, data?: any }
|
||||
}
|
||||
|
||||
export type VADEventCallback<K extends keyof VADEvents> = (event: VADEvents[K]) => void
|
||||
|
||||
export class VAD {
|
||||
private config: VADConfig
|
||||
export class VAD implements BaseVAD {
|
||||
private config: BaseVADConfig
|
||||
private state: Float32Array = new Float32Array(2 * 1 * 128) // 2, 1, 128
|
||||
private buffer: Float32Array
|
||||
private bufferPointer: number = 0
|
||||
@@ -33,8 +14,8 @@ export class VAD {
|
||||
private eventListeners: Partial<Record<keyof VADEvents, VADEventCallback<any>[]>> = {}
|
||||
private isReady: boolean = false
|
||||
|
||||
constructor(userConfig: Partial<VADConfig> = {}) {
|
||||
const defaultConfig: VADConfig = {
|
||||
constructor(userConfig: Partial<BaseVADConfig> = {}) {
|
||||
const defaultConfig: BaseVADConfig = {
|
||||
sampleRate: 16000,
|
||||
speechThreshold: 0.3,
|
||||
exitThreshold: 0.1,
|
||||
@@ -215,7 +196,7 @@ export class VAD {
|
||||
this.prevBuffers = []
|
||||
}
|
||||
|
||||
public updateConfig(newConfig: Partial<VADConfig>): void {
|
||||
public updateConfig(newConfig: Partial<BaseVADConfig>): void {
|
||||
this.config = { ...this.config, ...newConfig }
|
||||
|
||||
if (newConfig.maxBufferDuration || newConfig.sampleRate) {
|
||||
@@ -229,7 +210,7 @@ export class VAD {
|
||||
}
|
||||
}
|
||||
|
||||
export async function createVAD(config?: Partial<VADConfig>): Promise<VAD> {
|
||||
export async function createVAD(config?: Partial<BaseVADConfig>): Promise<VAD> {
|
||||
const vad = new VAD(config)
|
||||
await vad.initialize()
|
||||
return vad
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import workletUrl from '../../../workers/vad/process.worklet?worker&url'
|
||||
|
||||
import { createVAD, VADAudioManager } from '../../../workers/vad'
|
||||
import { createVAD, createVADStates } from '../../../workers/vad'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -51,7 +51,7 @@ const speakingThreshold = ref(25) // 0-100 (for volume-based fallback)
|
||||
const monitorVolume = ref(50) // 0-100
|
||||
|
||||
// VAD integration
|
||||
const vadManager = ref<VADAudioManager>()
|
||||
const vadManager = ref<ReturnType<typeof createVADStates>>()
|
||||
const isVADModelLoaded = ref(false)
|
||||
const isLoadingVADModel = ref(false)
|
||||
const useVADModel = ref(true) // Toggle between VAD and volume-based detection
|
||||
@@ -120,7 +120,7 @@ async function loadVADModel() {
|
||||
})
|
||||
|
||||
// Create and initialize audio manager
|
||||
const manager = new VADAudioManager(vad, {
|
||||
const manager = createVADStates(vad, workletUrl, {
|
||||
minChunkSize: 512,
|
||||
// NOTICE: VAD will have it's own audio context since
|
||||
// it needs special sample rate and latency settings
|
||||
@@ -130,7 +130,7 @@ async function loadVADModel() {
|
||||
},
|
||||
})
|
||||
|
||||
await manager.initialize(workletUrl)
|
||||
await manager.initialize()
|
||||
vadManager.value = manager
|
||||
isVADModelLoaded.value = true
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export { VADAudioManager } from './manager'
|
||||
export { createVADStates } from './manager'
|
||||
export type { VADAudioOptions } from './manager'
|
||||
export { createVAD, VAD } from './vad'
|
||||
export type { VADConfig, VADEventCallback, VADEvents } from './vad'
|
||||
|
||||
@@ -1,154 +1,2 @@
|
||||
// vad-audio-manager.ts
|
||||
|
||||
import type { VAD, VADConfig } from './vad'
|
||||
|
||||
export interface VADAudioOptions {
|
||||
/**
|
||||
* Audio context options
|
||||
*/
|
||||
audioContextOptions?: AudioContextOptions
|
||||
|
||||
/**
|
||||
* The minimum size of audio chunks to process
|
||||
*/
|
||||
minChunkSize?: number
|
||||
|
||||
/**
|
||||
* VAD configuration options
|
||||
*/
|
||||
vadConfig?: Partial<VADConfig>
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages audio input and worklet processing for the VAD module
|
||||
*/
|
||||
export class VADAudioManager {
|
||||
private audioContext: AudioContext | null = null
|
||||
private audioWorkletNode: AudioWorkletNode | null = null
|
||||
private mediaStream: MediaStream | null = null
|
||||
private sourceNode: MediaStreamAudioSourceNode | null = null
|
||||
private vad: VAD
|
||||
// private minChunkSize: number;
|
||||
private workletInitialized: boolean = false
|
||||
|
||||
/**
|
||||
* Create a new VAD audio manager
|
||||
*/
|
||||
constructor(vad: VAD, options: VADAudioOptions = {}) {
|
||||
this.vad = vad
|
||||
// this.minChunkSize = options.minChunkSize || 512;
|
||||
|
||||
// Create audio context with user options or defaults
|
||||
this.audioContext = new AudioContext(options.audioContextOptions || {
|
||||
sampleRate: 16000, // Match the VAD sample rate
|
||||
latencyHint: 'interactive',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the audio worklet and connect to microphone
|
||||
*/
|
||||
public async initialize(workletUrl: string): Promise<void> {
|
||||
if (!this.audioContext) {
|
||||
throw new Error('Audio context not created')
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this.workletInitialized) {
|
||||
await this.audioContext.audioWorklet.addModule(workletUrl)
|
||||
URL.revokeObjectURL(workletUrl)
|
||||
this.workletInitialized = true
|
||||
}
|
||||
|
||||
// Create the worklet node
|
||||
this.audioWorkletNode = new AudioWorkletNode(this.audioContext, 'vad-processor')
|
||||
|
||||
// Set up message handling from the worklet
|
||||
this.audioWorkletNode.port.onmessage = async (event) => {
|
||||
const { buffer } = event.data
|
||||
if (buffer && buffer.length > 0) {
|
||||
await this.vad.processAudio(new Float32Array(buffer))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to initialize audio worklet:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start capturing audio from the microphone
|
||||
*/
|
||||
public async start(mediaStream: MediaStream): Promise<void> {
|
||||
if (!this.audioContext || !this.audioWorkletNode) {
|
||||
throw new Error('Audio system not initialized. Call initialize() first.')
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.audioContext.state === 'suspended') {
|
||||
await this.audioContext.resume()
|
||||
}
|
||||
|
||||
// Request microphone access
|
||||
this.mediaStream = mediaStream
|
||||
|
||||
// Create source node and connect to worklet
|
||||
this.sourceNode = this.audioContext.createMediaStreamSource(this.mediaStream)
|
||||
this.sourceNode.connect(this.audioWorkletNode)
|
||||
|
||||
// Connect worklet to a silent destination (to keep the audio graph active)
|
||||
// Using a GainNode with gain=0 to ensure no sound is output
|
||||
const silentGain = this.audioContext.createGain()
|
||||
silentGain.gain.value = 0
|
||||
this.audioWorkletNode.connect(silentGain)
|
||||
silentGain.connect(this.audioContext.destination)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to start microphone:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop capturing audio
|
||||
*/
|
||||
public stop(): void {
|
||||
// Disconnect nodes
|
||||
if (this.sourceNode && this.audioWorkletNode) {
|
||||
this.sourceNode.disconnect()
|
||||
this.audioWorkletNode.disconnect()
|
||||
}
|
||||
|
||||
// Stop all tracks in the media stream
|
||||
if (this.mediaStream) {
|
||||
this.mediaStream.getTracks().forEach(track => track.stop())
|
||||
this.mediaStream = null
|
||||
}
|
||||
|
||||
// Suspend the audio context rather than closing it
|
||||
// This allows us to reuse it later
|
||||
if (this.audioContext && this.audioContext.state !== 'closed') {
|
||||
this.audioContext.suspend()
|
||||
}
|
||||
|
||||
this.sourceNode = null
|
||||
this.audioWorkletNode?.disconnect()
|
||||
this.audioWorkletNode = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all resources
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.stop()
|
||||
|
||||
// Now fully close the audio context
|
||||
if (this.audioContext && this.audioContext.state !== 'closed') {
|
||||
this.audioContext.close()
|
||||
this.audioContext = null
|
||||
}
|
||||
|
||||
this.workletInitialized = false
|
||||
}
|
||||
}
|
||||
export type { VADAudioOptions } from '@proj-airi/stage-ui/libs/audio/vad'
|
||||
export { createVADStates } from '@proj-airi/stage-ui/libs/audio/vad'
|
||||
|
||||
@@ -1,48 +1,13 @@
|
||||
import type { PreTrainedModel } from '@huggingface/transformers'
|
||||
import type { BaseVAD, BaseVADConfig, VADEventCallback, VADEvents } from '@proj-airi/stage-ui/libs/audio/vad'
|
||||
|
||||
import { AutoModel, Tensor } from '@huggingface/transformers'
|
||||
|
||||
// Default configuration parameters
|
||||
export interface VADConfig {
|
||||
// Sample rate of the audio
|
||||
sampleRate: number
|
||||
// Probabilities above this value are considered speech
|
||||
speechThreshold: number
|
||||
// Threshold to exit speech state
|
||||
exitThreshold: number
|
||||
// Minimum silence duration to consider speech ended (ms)
|
||||
minSilenceDurationMs: number
|
||||
// Padding to add before and after speech (ms)
|
||||
speechPadMs: number
|
||||
// Minimum duration of speech to consider valid (ms)
|
||||
minSpeechDurationMs: number
|
||||
// Maximum buffer duration in seconds
|
||||
maxBufferDuration: number
|
||||
// Size of input buffers from audio source
|
||||
newBufferSize: number
|
||||
}
|
||||
|
||||
export interface VADEvents {
|
||||
// Emitted when speech is detected
|
||||
'speech-start': void
|
||||
// Emitted when speech has ended
|
||||
'speech-end': void
|
||||
// Emitted when a complete speech segment is ready for transcription
|
||||
'speech-ready': { buffer: Float32Array, duration: number }
|
||||
// Emitted for status updates and errors
|
||||
'status': { type: string, message: string }
|
||||
// Debug info
|
||||
'debug': { message: string, data?: any }
|
||||
}
|
||||
|
||||
export type VADEventCallback<K extends keyof VADEvents>
|
||||
= (event: VADEvents[K]) => void
|
||||
|
||||
/**
|
||||
* Voice Activity Detection processor
|
||||
*/
|
||||
export class VAD {
|
||||
private config: VADConfig
|
||||
export class VAD implements BaseVAD {
|
||||
private config: BaseVADConfig
|
||||
private model: PreTrainedModel | undefined
|
||||
private state: Tensor
|
||||
private sampleRateTensor: Tensor
|
||||
@@ -55,9 +20,9 @@ export class VAD {
|
||||
private eventListeners: Partial<Record<keyof VADEvents, VADEventCallback<any>[]>> = {}
|
||||
private isReady: boolean = false
|
||||
|
||||
constructor(userConfig: Partial<VADConfig> = {}) {
|
||||
constructor(userConfig: Partial<BaseVADConfig> = {}) {
|
||||
// Default configuration
|
||||
const defaultConfig: VADConfig = {
|
||||
const defaultConfig: BaseVADConfig = {
|
||||
sampleRate: 16000,
|
||||
speechThreshold: 0.3,
|
||||
exitThreshold: 0.1,
|
||||
@@ -290,7 +255,7 @@ export class VAD {
|
||||
/**
|
||||
* Update configuration
|
||||
*/
|
||||
public updateConfig(newConfig: Partial<VADConfig>): void {
|
||||
public updateConfig(newConfig: Partial<BaseVADConfig>): void {
|
||||
this.config = { ...this.config, ...newConfig }
|
||||
|
||||
// If buffer size changed, create a new buffer
|
||||
@@ -325,7 +290,7 @@ export class VAD {
|
||||
/**
|
||||
* Create a VAD processor with the given configuration
|
||||
*/
|
||||
export async function createVAD(config?: Partial<VADConfig>): Promise<VAD> {
|
||||
export async function createVAD(config?: Partial<BaseVADConfig>): Promise<VAD> {
|
||||
const vad = new VAD(config)
|
||||
await vad.initialize()
|
||||
return vad
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
export interface BaseVADConfig {
|
||||
// Sample rate of the audio
|
||||
sampleRate: number
|
||||
// Probabilities above this value are considered speech
|
||||
speechThreshold: number
|
||||
// Threshold to exit speech state
|
||||
exitThreshold: number
|
||||
// Minimum silence duration to consider speech ended (ms)
|
||||
minSilenceDurationMs: number
|
||||
// Padding to add before and after speech (ms)
|
||||
speechPadMs: number
|
||||
// Minimum duration of speech to consider valid (ms)
|
||||
minSpeechDurationMs: number
|
||||
// Maximum buffer duration in seconds
|
||||
maxBufferDuration: number
|
||||
// Size of input buffers from audio source
|
||||
newBufferSize: number
|
||||
}
|
||||
|
||||
export interface VADEvents {
|
||||
// Emitted when speech is detected
|
||||
'speech-start': void
|
||||
// Emitted when speech has ended
|
||||
'speech-end': void
|
||||
// Emitted when a complete speech segment is ready for transcription
|
||||
'speech-ready': { buffer: Float32Array, duration: number }
|
||||
// Emitted for status updates and errors
|
||||
'status': { type: string, message: string }
|
||||
// Debug info
|
||||
'debug': { message: string, data?: any }
|
||||
}
|
||||
|
||||
export type VADEventCallback<K extends keyof VADEvents> = (event: VADEvents[K]) => void
|
||||
|
||||
export interface BaseVAD {
|
||||
initialize: () => Promise<void>
|
||||
processAudio: (inputBuffer: Float32Array) => Promise<void>
|
||||
on: <K extends keyof VADEvents>(event: K, callback: VADEventCallback<K>) => void
|
||||
off: <K extends keyof VADEvents>(event: K, callback: VADEventCallback<K>) => void
|
||||
}
|
||||
|
||||
export interface VADAudioOptions {
|
||||
/**
|
||||
* Audio context options
|
||||
*/
|
||||
audioContextOptions?: AudioContextOptions
|
||||
|
||||
/**
|
||||
* The minimum size of audio chunks to process
|
||||
*/
|
||||
minChunkSize?: number
|
||||
|
||||
/**
|
||||
* VAD configuration options
|
||||
*/
|
||||
vadConfig?: Partial<BaseVADConfig>
|
||||
}
|
||||
|
||||
export function createVADStates(vad: BaseVAD, vadAudioWorkletUrl: string, options?: VADAudioOptions) {
|
||||
let audioWorkletNode: AudioWorkletNode | null
|
||||
let mediaStream: MediaStream | null
|
||||
let sourceNode: MediaStreamAudioSourceNode | null
|
||||
let workletInitialized: boolean
|
||||
|
||||
const {
|
||||
audioContextOptions = {
|
||||
sampleRate: 16000,
|
||||
latencyHint: 'interactive',
|
||||
},
|
||||
} = options || {}
|
||||
|
||||
let audioContext = new AudioContext(audioContextOptions)
|
||||
|
||||
async function initialize() {
|
||||
if (!audioContext || audioContext.state === 'closed') {
|
||||
audioContext = new AudioContext(audioContextOptions)
|
||||
}
|
||||
|
||||
try {
|
||||
if (!workletInitialized) {
|
||||
await audioContext.audioWorklet.addModule(vadAudioWorkletUrl)
|
||||
workletInitialized = true
|
||||
}
|
||||
|
||||
audioWorkletNode = new AudioWorkletNode(audioContext, 'vad-audio-worklet-processor')
|
||||
audioWorkletNode.port.onmessage = async (event) => {
|
||||
const { buffer } = event.data
|
||||
if (buffer && buffer.length > 0) {
|
||||
await vad.processAudio(new Float32Array(buffer))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to initialize audio worklet:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function start(stream: MediaStream) {
|
||||
if (!audioContext || !audioWorkletNode) {
|
||||
throw new Error('Audio system not initialized. Call initialize() first.')
|
||||
}
|
||||
|
||||
try {
|
||||
if (audioContext.state === 'suspended') {
|
||||
await audioContext.resume()
|
||||
}
|
||||
|
||||
// Request microphone access
|
||||
mediaStream = stream
|
||||
|
||||
// Create source node and connect to worklet
|
||||
sourceNode = audioContext.createMediaStreamSource(mediaStream)
|
||||
sourceNode.connect(audioWorkletNode)
|
||||
|
||||
// Connect worklet to a silent destination (to keep the audio graph active)
|
||||
// Using a GainNode with gain=0 to ensure no sound is output
|
||||
const silentGain = audioContext.createGain()
|
||||
silentGain.gain.value = 0
|
||||
audioWorkletNode.connect(silentGain)
|
||||
silentGain.connect(audioContext.destination)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to start microphone:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (sourceNode) {
|
||||
sourceNode.disconnect()
|
||||
sourceNode = null
|
||||
}
|
||||
if (audioWorkletNode) {
|
||||
audioWorkletNode.disconnect()
|
||||
audioWorkletNode = null
|
||||
}
|
||||
if (mediaStream) {
|
||||
mediaStream.getTracks().forEach(track => track.stop())
|
||||
mediaStream = null
|
||||
}
|
||||
if (audioContext) {
|
||||
audioContext.suspend()
|
||||
}
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
stop()
|
||||
|
||||
if (audioContext && audioContext.state !== 'closed') {
|
||||
audioContext.close()
|
||||
}
|
||||
|
||||
workletInitialized = false
|
||||
}
|
||||
|
||||
return {
|
||||
initialize,
|
||||
start,
|
||||
stop,
|
||||
dispose,
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { computed, ref } from 'vue'
|
||||
|
||||
import { useProvidersStore } from '../providers'
|
||||
|
||||
export const useHearingStore = defineStore('hearing', () => {
|
||||
export const useHearingStore = defineStore('hearing-store', () => {
|
||||
const providersStore = useProvidersStore()
|
||||
const { allAudioTranscriptionProvidersMetadata } = storeToRefs(providersStore)
|
||||
|
||||
|
||||
Generated
+118
-9
@@ -37,7 +37,7 @@ catalogs:
|
||||
specifier: ^0.3.4
|
||||
version: 0.3.4
|
||||
'@xsai/shared':
|
||||
specifier: ^0.3.4
|
||||
specifier: 0.3.4
|
||||
version: 0.3.4
|
||||
'@xsai/shared-chat':
|
||||
specifier: ^0.3.4
|
||||
@@ -427,6 +427,9 @@ importers:
|
||||
'@xsai/model':
|
||||
specifier: 'catalog:'
|
||||
version: 0.3.4
|
||||
'@xsai/shared':
|
||||
specifier: 'catalog:'
|
||||
version: 0.3.4
|
||||
'@xsai/shared-chat':
|
||||
specifier: 'catalog:'
|
||||
version: 0.3.4
|
||||
@@ -1128,7 +1131,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.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(astro@5.10.1(@types/node@24.1.0)(encoding@0.1.13)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0))(esbuild@0.25.8)(rolldown@1.0.0-beta.30)(rollup@4.46.2)(vite@7.0.6(@types/node@24.1.0)(jiti@2.5.1)(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.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(astro@5.10.1(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0))(esbuild@0.25.8)(rolldown@1.0.0-beta.30)(rollup@4.46.2)(vite@7.0.6(@types/node@24.1.0)(jiti@2.5.1)(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.9
|
||||
version: 2.0.0-alpha.9(@types/node@24.1.0)(change-case@5.4.4)(fuse.js@7.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(nprogress@0.2.0)(postcss@8.5.6)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0)
|
||||
@@ -1171,7 +1174,7 @@ importers:
|
||||
devDependencies:
|
||||
unplugin-yaml:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(astro@5.10.1(@types/node@24.1.0)(encoding@0.1.13)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0))(esbuild@0.25.8)(rolldown@1.0.0-beta.30)(rollup@4.46.2)(vite@7.0.6(@types/node@24.1.0)(jiti@2.5.1)(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.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(astro@5.10.1(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0))(esbuild@0.25.8)(rolldown@1.0.0-beta.30)(rollup@4.46.2)(vite@7.0.6(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))
|
||||
|
||||
packages/memory-pgvector:
|
||||
dependencies:
|
||||
@@ -1554,7 +1557,7 @@ importers:
|
||||
version: 1.2.4(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(esbuild@0.25.8)(rollup@4.46.2)(vite@6.3.5(@types/node@24.1.0)(jiti@2.5.1)(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.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(astro@5.10.1(@types/node@24.1.0)(encoding@0.1.13)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0))(esbuild@0.25.8)(rolldown@1.0.0-beta.30)(rollup@4.46.2)(vite@6.3.5(@types/node@24.1.0)(jiti@2.5.1)(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.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(astro@5.10.1(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0))(esbuild@0.25.8)(rolldown@1.0.0-beta.30)(rollup@4.46.2)(vite@6.3.5(@types/node@24.1.0)(jiti@2.5.1)(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.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
|
||||
@@ -6307,6 +6310,9 @@ packages:
|
||||
'@xsai/shared@0.3.4':
|
||||
resolution: {integrity: sha512-Age17VqIqiu3X/yTlEP+tQRqY9Xb/0yTlxkvPIN9HAeky0WGWkXAt5RIgR0GMgwOVjBRYXsyZA5jYvCbmvF5hQ==}
|
||||
|
||||
'@xsai/shared@0.3.5':
|
||||
resolution: {integrity: sha512-gxWH+9UjhXgqqKeU/o9UteC/ih1FzxEEsmPojImd/jvq4j/wtIg4aP3dCFnu+EhCT/MRQ38dkBSra99iY9cC3w==}
|
||||
|
||||
'@xsai/stream-text@0.3.4':
|
||||
resolution: {integrity: sha512-5hI1zvBGwC8J9BiuChHwIxIJOetIwl1kmt4OR9b/CGLuhK1OR5JnxhbMcwXjJKqfOMKMkqM74KHTQG+QSVfsGg==}
|
||||
|
||||
@@ -17879,7 +17885,7 @@ snapshots:
|
||||
|
||||
'@xsai-ext/shared-providers@0.2.2':
|
||||
dependencies:
|
||||
'@xsai/shared': 0.3.4
|
||||
'@xsai/shared': 0.3.5
|
||||
|
||||
'@xsai-ext/shared-providers@0.3.4':
|
||||
dependencies:
|
||||
@@ -17933,6 +17939,8 @@ snapshots:
|
||||
|
||||
'@xsai/shared@0.3.4': {}
|
||||
|
||||
'@xsai/shared@0.3.5': {}
|
||||
|
||||
'@xsai/stream-text@0.3.4':
|
||||
dependencies:
|
||||
'@xsai/shared-chat': 0.3.4
|
||||
@@ -18369,6 +18377,107 @@ snapshots:
|
||||
- yaml
|
||||
optional: true
|
||||
|
||||
astro@5.10.1(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(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.46.2)
|
||||
acorn: 8.15.0
|
||||
aria-query: 5.3.2
|
||||
axobject-query: 4.1.0
|
||||
boxen: 8.0.1
|
||||
ci-info: 4.3.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.8
|
||||
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.3
|
||||
prompts: 2.4.2
|
||||
rehype: 13.0.2
|
||||
semver: 7.7.2
|
||||
shiki: 3.9.1
|
||||
tinyexec: 0.3.2
|
||||
tinyglobby: 0.2.14
|
||||
tsconfck: 3.1.6(typescript@5.9.2)
|
||||
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.1.0)(jiti@2.5.1)(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.1.0)(jiti@2.5.1)(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.9.2)(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
|
||||
@@ -25217,7 +25326,7 @@ snapshots:
|
||||
rollup: 4.46.2
|
||||
vite: rolldown-vite@7.0.12(@types/node@24.1.0)(esbuild@0.25.8)(jiti@2.5.1)(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.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(astro@5.10.1(@types/node@24.1.0)(encoding@0.1.13)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0))(esbuild@0.25.8)(rolldown@1.0.0-beta.30)(rollup@4.46.2)(vite@6.3.5(@types/node@24.1.0)(jiti@2.5.1)(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.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(astro@5.10.1(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0))(esbuild@0.25.8)(rolldown@1.0.0-beta.30)(rollup@4.46.2)(vite@6.3.5(@types/node@24.1.0)(jiti@2.5.1)(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.46.2)
|
||||
unplugin: 2.3.5
|
||||
@@ -25225,13 +25334,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.46.2)
|
||||
'@nuxt/schema': 3.14.1592(magicast@0.3.5)(rollup@4.46.2)
|
||||
astro: 5.10.1(@types/node@24.1.0)(encoding@0.1.13)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0)
|
||||
astro: 5.10.1(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0)
|
||||
esbuild: 0.25.8
|
||||
rolldown: 1.0.0-beta.30
|
||||
rollup: 4.46.2
|
||||
vite: 6.3.5(@types/node@24.1.0)(jiti@2.5.1)(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.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(astro@5.10.1(@types/node@24.1.0)(encoding@0.1.13)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0))(esbuild@0.25.8)(rolldown@1.0.0-beta.30)(rollup@4.46.2)(vite@7.0.6(@types/node@24.1.0)(jiti@2.5.1)(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.46.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.46.2))(astro@5.10.1(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0))(esbuild@0.25.8)(rolldown@1.0.0-beta.30)(rollup@4.46.2)(vite@7.0.6(@types/node@24.1.0)(jiti@2.5.1)(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.46.2)
|
||||
unplugin: 2.3.5
|
||||
@@ -25239,7 +25348,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.46.2)
|
||||
'@nuxt/schema': 3.14.1592(magicast@0.3.5)(rollup@4.46.2)
|
||||
astro: 5.10.1(@types/node@24.1.0)(encoding@0.1.13)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0)
|
||||
astro: 5.10.1(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(rollup@4.46.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.9.2)(yaml@2.8.0)
|
||||
esbuild: 0.25.8
|
||||
rolldown: 1.0.0-beta.30
|
||||
rollup: 4.46.2
|
||||
|
||||
Reference in New Issue
Block a user