From 9d2aef70146125d3b0ce881a8c5e95e1ae98d442 Mon Sep 17 00:00:00 2001 From: Neko Ayaka Date: Wed, 16 Apr 2025 11:28:08 +0800 Subject: [PATCH] refactor: migrate out vad related examples --- apps/vad-asr-chat/index.html | 22 -- apps/vad-asr-chat/locales/en.yml | 1 - apps/vad-asr-chat/locales/zh-CN.yml | 1 - apps/vad-asr-chat/netlify.toml | 13 - apps/vad-asr-chat/package.json | 46 --- apps/vad-asr-chat/public/favicon.ico | Bin 15086 -> 0 bytes apps/vad-asr-chat/public/favicon.svg | 1 - apps/vad-asr-chat/src/App.vue | 56 --- .../src/components/Collapsable.vue | 39 --- .../src/components/FieldInput.vue | 36 -- apps/vad-asr-chat/src/components/Input.vue | 21 -- apps/vad-asr-chat/src/components/Section.vue | 38 -- .../src/components/TransitionVertical.vue | 134 -------- apps/vad-asr-chat/src/libs/vad/manager.ts | 175 ---------- .../src/libs/vad/process.worklet.ts | 53 --- apps/vad-asr-chat/src/libs/vad/vad.ts | 319 ----------------- apps/vad-asr-chat/src/libs/vad/wav.ts | 43 --- apps/vad-asr-chat/src/main.ts | 13 - apps/vad-asr-chat/src/pages/index.vue | 324 ------------------ apps/vad-asr-chat/tsconfig.json | 41 --- apps/vad-asr-chat/uno.config.ts | 37 -- apps/vad-asr-chat/vite.config.ts | 19 - apps/vad-asr/index.html | 22 -- apps/vad-asr/locales/en.yml | 1 - apps/vad-asr/locales/zh-CN.yml | 1 - apps/vad-asr/netlify.toml | 13 - apps/vad-asr/package.json | 42 --- apps/vad-asr/public/favicon.ico | Bin 15086 -> 0 bytes apps/vad-asr/public/favicon.svg | 1 - apps/vad-asr/src/App.vue | 56 --- apps/vad-asr/src/components/FieldInput.vue | 36 -- apps/vad-asr/src/components/Input.vue | 21 -- apps/vad-asr/src/libs/vad/manager.ts | 175 ---------- apps/vad-asr/src/libs/vad/process.worklet.ts | 53 --- apps/vad-asr/src/libs/vad/vad.ts | 319 ----------------- apps/vad-asr/src/libs/vad/wav.ts | 43 --- apps/vad-asr/src/main.ts | 13 - apps/vad-asr/src/pages/index.vue | 205 ----------- apps/vad-asr/tsconfig.json | 41 --- apps/vad-asr/uno.config.ts | 37 -- apps/vad-asr/vite.config.ts | 19 - apps/vad/index.html | 22 -- apps/vad/locales/en.yml | 1 - apps/vad/locales/zh-CN.yml | 1 - apps/vad/netlify.toml | 13 - apps/vad/package.json | 41 --- apps/vad/public/favicon.ico | Bin 15086 -> 0 bytes apps/vad/public/favicon.svg | 1 - apps/vad/src/App.vue | 56 --- apps/vad/src/libs/vad/manager.ts | 175 ---------- apps/vad/src/libs/vad/process.worklet.ts | 53 --- apps/vad/src/libs/vad/vad.ts | 319 ----------------- apps/vad/src/libs/vad/wav.ts | 43 --- apps/vad/src/main.ts | 13 - apps/vad/src/pages/index.vue | 169 --------- apps/vad/tsconfig.json | 41 --- apps/vad/uno.config.ts | 37 -- apps/vad/vite.config.ts | 19 - pnpm-lock.yaml | 196 +---------- 59 files changed, 5 insertions(+), 3725 deletions(-) delete mode 100644 apps/vad-asr-chat/index.html delete mode 100644 apps/vad-asr-chat/locales/en.yml delete mode 100644 apps/vad-asr-chat/locales/zh-CN.yml delete mode 100755 apps/vad-asr-chat/netlify.toml delete mode 100644 apps/vad-asr-chat/package.json delete mode 100644 apps/vad-asr-chat/public/favicon.ico delete mode 100644 apps/vad-asr-chat/public/favicon.svg delete mode 100644 apps/vad-asr-chat/src/App.vue delete mode 100644 apps/vad-asr-chat/src/components/Collapsable.vue delete mode 100644 apps/vad-asr-chat/src/components/FieldInput.vue delete mode 100644 apps/vad-asr-chat/src/components/Input.vue delete mode 100644 apps/vad-asr-chat/src/components/Section.vue delete mode 100644 apps/vad-asr-chat/src/components/TransitionVertical.vue delete mode 100644 apps/vad-asr-chat/src/libs/vad/manager.ts delete mode 100644 apps/vad-asr-chat/src/libs/vad/process.worklet.ts delete mode 100644 apps/vad-asr-chat/src/libs/vad/vad.ts delete mode 100644 apps/vad-asr-chat/src/libs/vad/wav.ts delete mode 100644 apps/vad-asr-chat/src/main.ts delete mode 100644 apps/vad-asr-chat/src/pages/index.vue delete mode 100644 apps/vad-asr-chat/tsconfig.json delete mode 100644 apps/vad-asr-chat/uno.config.ts delete mode 100644 apps/vad-asr-chat/vite.config.ts delete mode 100644 apps/vad-asr/index.html delete mode 100644 apps/vad-asr/locales/en.yml delete mode 100644 apps/vad-asr/locales/zh-CN.yml delete mode 100755 apps/vad-asr/netlify.toml delete mode 100644 apps/vad-asr/package.json delete mode 100644 apps/vad-asr/public/favicon.ico delete mode 100644 apps/vad-asr/public/favicon.svg delete mode 100644 apps/vad-asr/src/App.vue delete mode 100644 apps/vad-asr/src/components/FieldInput.vue delete mode 100644 apps/vad-asr/src/components/Input.vue delete mode 100644 apps/vad-asr/src/libs/vad/manager.ts delete mode 100644 apps/vad-asr/src/libs/vad/process.worklet.ts delete mode 100644 apps/vad-asr/src/libs/vad/vad.ts delete mode 100644 apps/vad-asr/src/libs/vad/wav.ts delete mode 100644 apps/vad-asr/src/main.ts delete mode 100644 apps/vad-asr/src/pages/index.vue delete mode 100644 apps/vad-asr/tsconfig.json delete mode 100644 apps/vad-asr/uno.config.ts delete mode 100644 apps/vad-asr/vite.config.ts delete mode 100644 apps/vad/index.html delete mode 100644 apps/vad/locales/en.yml delete mode 100644 apps/vad/locales/zh-CN.yml delete mode 100755 apps/vad/netlify.toml delete mode 100644 apps/vad/package.json delete mode 100644 apps/vad/public/favicon.ico delete mode 100644 apps/vad/public/favicon.svg delete mode 100644 apps/vad/src/App.vue delete mode 100644 apps/vad/src/libs/vad/manager.ts delete mode 100644 apps/vad/src/libs/vad/process.worklet.ts delete mode 100644 apps/vad/src/libs/vad/vad.ts delete mode 100644 apps/vad/src/libs/vad/wav.ts delete mode 100644 apps/vad/src/main.ts delete mode 100644 apps/vad/src/pages/index.vue delete mode 100644 apps/vad/tsconfig.json delete mode 100644 apps/vad/uno.config.ts delete mode 100644 apps/vad/vite.config.ts diff --git a/apps/vad-asr-chat/index.html b/apps/vad-asr-chat/index.html deleted file mode 100644 index 24501ea3f..000000000 --- a/apps/vad-asr-chat/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - Project AIRI VAD + ASR + LLM Chat Playground - - - - - -
- - - - diff --git a/apps/vad-asr-chat/locales/en.yml b/apps/vad-asr-chat/locales/en.yml deleted file mode 100644 index bf1d5461c..000000000 --- a/apps/vad-asr-chat/locales/en.yml +++ /dev/null @@ -1 +0,0 @@ -title: VAD diff --git a/apps/vad-asr-chat/locales/zh-CN.yml b/apps/vad-asr-chat/locales/zh-CN.yml deleted file mode 100644 index bf1d5461c..000000000 --- a/apps/vad-asr-chat/locales/zh-CN.yml +++ /dev/null @@ -1 +0,0 @@ -title: VAD diff --git a/apps/vad-asr-chat/netlify.toml b/apps/vad-asr-chat/netlify.toml deleted file mode 100755 index 4460d7b8e..000000000 --- a/apps/vad-asr-chat/netlify.toml +++ /dev/null @@ -1,13 +0,0 @@ -[build] -base = "/" -command = "pnpm -F @proj-airi/vad-asr-chat... run build" -publish = "/apps/vad-asr-chat/dist" - -[build.environment] -NODE_VERSION = "23" - -[[redirects]] -from = "/*" -to = "/index.html" -status = 200 -force = false diff --git a/apps/vad-asr-chat/package.json b/apps/vad-asr-chat/package.json deleted file mode 100644 index ef413273e..000000000 --- a/apps/vad-asr-chat/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "@proj-airi/vad-asr-chat", - "type": "module", - "private": true, - "description": "Voice Activity Detector & Automatic Speech Recognition & Chat", - "author": { - "name": "Neko Ayaka", - "email": "neko@ayaka.moe", - "url": "https://github.com/nekomeowww" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/moeru-ai/airi.git", - "directory": "apps/vad-asr-chat" - }, - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview", - "typecheck": "vue-tsc --noEmit" - }, - "dependencies": { - "@huggingface/transformers": "^3.4.2", - "@llama-flow/core": "^0.3.4", - "@vueuse/core": "^13.0.0", - "@xsai/generate-transcription": "catalog:", - "@xsai/shared": "catalog:", - "@xsai/shared-chat": "catalog:", - "@xsai/stream-text": "catalog:", - "defu": "^6.1.4", - "es-toolkit": "^1.34.1", - "vue": "^3.5.13" - }, - "devDependencies": { - "@iconify-json/solar": "^1.2.2", - "@iconify-json/svg-spinners": "^1.2.2", - "@types/audioworklet": "^0.0.72", - "@unocss/reset": "^66.1.0-beta.10", - "@vitejs/plugin-vue": "^5.2.3", - "unplugin-vue-router": "^0.12.0", - "vite": "^6.2.5", - "vue-router": "^4.5.0", - "vue-tsc": "^3.0.0-alpha.2" - } -} diff --git a/apps/vad-asr-chat/public/favicon.ico b/apps/vad-asr-chat/public/favicon.ico deleted file mode 100644 index aeddd290552c350de823b21ba553e240758152c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 zcmeI3OK(+G6vt0flMdk<(20pdMo#NTap;h5Ku3-ZBPK@0L`j68i4QO+I}lCOAWlda zplL~jDA0Qgr9!|S*YDi(Z&w%lyzVU)lHO!@?Y;JT{MXu#v+q8)MKM>*7e|j4Mi-0I zuNTFeMNup+*6qVZ@rUsa8>Ro9*NWm@6L`a9Or?0+3j86T+qWK$?%aA<`f~QfyOy zBl`5^>_q&mUE%RL5ElBV=WM}?u_4=~(`3u@d*ff-I0gp!^fAbuFy;Cr6Dj`N;*RpM z4W7uO9rI87w#AT>6%SjKh#wv}bGcGZqK<5~QFNMO&*2xZjw6|EN)^9wHFlvQ)F}F<7Hxfukb6kV8EuZ!uLKDTg0KZ^wn;o z$hO(*%DeiROTrJur^gRZF|_4ZGEmjq@YC0{rsxyCK2*;JpAT#+F5w|(9+Ph?cKZ9q zfLv2f`rtl$;_flz7=vg_A$u8*E=$EO%16Ll4*kK4=i!++17 zX*)4^ebw9ie|LIRZfuklcJNbI47u@>O;NDVnE&*}$$OiM)$DHXr}FXloz3$8-6vI{ zt~NOF4{j(j;8zUre2F+2ANry4?BTn>*FHLH2ws;(4n5 zh-VeDxj7(ta5u#!8KY?$+|r3_7fjA}V|_qw8*Nl`X23ov%$|b(!l|__Fq5ygeEPV$ z#>vZ4XYEiNU(cE3@eHjqe?0yk<6X^(K61jE!CsPk`iJ6;@)3WJwN}l0Ve3O#Ga>gH zhZOM)ZJ#-D_ngmv7asCHB=;@$X2wpeoO!$2B_lu-HrlAL;b%BbA4z` z2l59QILN?@$-v{=tA8-RMRD+ zCRQA8pw?OS*L1F?^G3lbx?t^R);?6zBgk3c6}7;YXd68aeOAns(6U&V(B-8GP345H z2KrCkHd5)eQ+}tlXyq*{eSfEA&)4mMu5TN>>cjKJ_rgR4*I;FWPObA5r{cBv$?2rN-{J62+?qEz|8ZQRjMj(vee+Ik6hHp` z*vS?&H!i-PdE(oBV~_s54b{7ROHOo1?D#jml+HGf`w;&_eY4+UpXXg${uNsn9g=rq z_xt^w)d%CI@$d3m_a|)5^5J*$J$>z(Z>E!G@#}RNLojGO7$4Oeu%YSC`anD#MOrBQtXPtt~ZwFt=RR(%2HX5 z?Y`oPm8aZW?0mSVIBaFnO4mJw)?V0i;=a4x_WP5WubwU1H~sU?%lUnlx_qI}N6!QC zy}ajXj}V<`PyVuY`RzgV`vbR!3Y&CjtM2u6KEt16pq#m&!r}Y9K6@tn`CorN+Tyzm z<Ry`Y_HMU*4*x_vJY?tV&t$482bap|; z%Sl(cA`g|mJ-0k=>c-#oJ}m0W269nX`u<$vaY(lbx3`CVBjmceUQTzvc C3_0)s diff --git a/apps/vad-asr-chat/public/favicon.svg b/apps/vad-asr-chat/public/favicon.svg deleted file mode 100644 index a52f4ddeb..000000000 --- a/apps/vad-asr-chat/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/vad-asr-chat/src/App.vue b/apps/vad-asr-chat/src/App.vue deleted file mode 100644 index 2f76294d0..000000000 --- a/apps/vad-asr-chat/src/App.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - - - diff --git a/apps/vad-asr-chat/src/components/Collapsable.vue b/apps/vad-asr-chat/src/components/Collapsable.vue deleted file mode 100644 index f22117852..000000000 --- a/apps/vad-asr-chat/src/components/Collapsable.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - diff --git a/apps/vad-asr-chat/src/components/FieldInput.vue b/apps/vad-asr-chat/src/components/FieldInput.vue deleted file mode 100644 index d1624a76f..000000000 --- a/apps/vad-asr-chat/src/components/FieldInput.vue +++ /dev/null @@ -1,36 +0,0 @@ - - - diff --git a/apps/vad-asr-chat/src/components/Input.vue b/apps/vad-asr-chat/src/components/Input.vue deleted file mode 100644 index 2221b5849..000000000 --- a/apps/vad-asr-chat/src/components/Input.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/apps/vad-asr-chat/src/components/Section.vue b/apps/vad-asr-chat/src/components/Section.vue deleted file mode 100644 index 04d022edc..000000000 --- a/apps/vad-asr-chat/src/components/Section.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - diff --git a/apps/vad-asr-chat/src/components/TransitionVertical.vue b/apps/vad-asr-chat/src/components/TransitionVertical.vue deleted file mode 100644 index 478cbb108..000000000 --- a/apps/vad-asr-chat/src/components/TransitionVertical.vue +++ /dev/null @@ -1,134 +0,0 @@ - - - diff --git a/apps/vad-asr-chat/src/libs/vad/manager.ts b/apps/vad-asr-chat/src/libs/vad/manager.ts deleted file mode 100644 index 979a95e25..000000000 --- a/apps/vad-asr-chat/src/libs/vad/manager.ts +++ /dev/null @@ -1,175 +0,0 @@ -// 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 -} - -/** - * 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 { - 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 startMicrophone(): Promise { - 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 = await navigator.mediaDevices.getUserMedia({ - audio: { - echoCancellation: true, - noiseSuppression: true, - autoGainControl: true, - sampleRate: this.audioContext.sampleRate, - }, - }) - - // 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 - } - } - - public async stopMicrophone(): Promise { - if (this.mediaStream) { - this.mediaStream.getTracks().forEach(track => track.stop()) - this.mediaStream = null - } - - if (this.sourceNode) { - this.sourceNode.disconnect() - this.sourceNode = null - } - - this.audioContext?.suspend() - this.audioWorkletNode?.disconnect() - } - - /** - * 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 = 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 - } -} diff --git a/apps/vad-asr-chat/src/libs/vad/process.worklet.ts b/apps/vad-asr-chat/src/libs/vad/process.worklet.ts deleted file mode 100644 index 899d933fe..000000000 --- a/apps/vad-asr-chat/src/libs/vad/process.worklet.ts +++ /dev/null @@ -1,53 +0,0 @@ -// vad-worklet-processor.ts -// This file needs to be registered as an AudioWorklet - -/** - * Minimum chunk size for processing audio - */ -const MIN_CHUNK_SIZE = 512 - -/** - * Global state for audio buffer accumulation - */ -let globalPointer = 0 -const globalBuffer = new Float32Array(MIN_CHUNK_SIZE) - -/** - * VAD AudioWorklet Processor - processes audio chunks and sends them to the main thread - */ -class VADProcessor extends AudioWorkletProcessor { - process(inputs: Float32Array[][], _outputs: Float32Array[][], _parameters: Record) { - const buffer = inputs[0][0] - if (!buffer) - return true // buffer is null when the stream ends - - if (buffer.length > MIN_CHUNK_SIZE) { - // If the buffer is larger than the minimum chunk size, send the entire buffer - this.port.postMessage({ buffer }) - } - else { - const remaining = MIN_CHUNK_SIZE - globalPointer - if (buffer.length >= remaining) { - // If the buffer is larger than (or equal to) the remaining space in the global buffer, copy the remaining space - globalBuffer.set(buffer.subarray(0, remaining), globalPointer) - - // Send the global buffer - this.port.postMessage({ buffer: globalBuffer }) - - // Reset the global buffer and set the remaining buffer - globalBuffer.fill(0) - globalBuffer.set(buffer.subarray(remaining), 0) - globalPointer = buffer.length - remaining - } - else { - // If the buffer is smaller than the remaining space in the global buffer, copy the buffer to the global buffer - globalBuffer.set(buffer, globalPointer) - globalPointer += buffer.length - } - } - - return true - } -} - -registerProcessor('vad-processor', VADProcessor) diff --git a/apps/vad-asr-chat/src/libs/vad/vad.ts b/apps/vad-asr-chat/src/libs/vad/vad.ts deleted file mode 100644 index bd28d0a18..000000000 --- a/apps/vad-asr-chat/src/libs/vad/vad.ts +++ /dev/null @@ -1,319 +0,0 @@ -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 = - (event: VADEvents[K]) => void - -/** - * Voice Activity Detection processor - */ -export class VAD { - private config: VADConfig - private model: any - private state: Tensor - private sampleRateTensor: Tensor - private buffer: Float32Array - private bufferPointer: number = 0 - private isRecording: boolean = false - private postSpeechSamples: number = 0 - private prevBuffers: Float32Array[] = [] - private inferenceChain: Promise = Promise.resolve() - private eventListeners: Partial[]>> = {} - private isReady: boolean = false - - constructor(userConfig: Partial = {}) { - // Default configuration - const defaultConfig: VADConfig = { - sampleRate: 16000, - speechThreshold: 0.3, - exitThreshold: 0.1, - minSilenceDurationMs: 400, - speechPadMs: 80, - minSpeechDurationMs: 250, - maxBufferDuration: 30, - newBufferSize: 512, - } - - this.config = { ...defaultConfig, ...userConfig } - - // Create buffer based on max duration - this.buffer = new Float32Array(this.config.maxBufferDuration * this.config.sampleRate) - - // Initialize state tensor for VAD model - this.state = new Tensor('float32', new Float32Array(2 * 1 * 128), [2, 1, 128]) - - // Sample rate tensor for the model - this.sampleRateTensor = new Tensor('int64', [this.config.sampleRate], []) - } - - /** - * Initialize the VAD model - */ - public async initialize(): Promise { - try { - this.emit('status', { type: 'info', message: 'Loading VAD model...' }) - - this.model = await AutoModel.from_pretrained('onnx-community/silero-vad', { - config: { model_type: 'custom' } as any, - dtype: 'fp32', // Full-precision - }) - - this.isReady = true - this.emit('status', { type: 'info', message: 'VAD model loaded successfully' }) - } - catch (error) { - this.emit('status', { type: 'error', message: `Failed to load VAD model: ${error}` }) - throw error - } - } - - /** - * Add event listener - */ - public on(event: K, callback: VADEventCallback): void { - if (!this.eventListeners[event]) { - this.eventListeners[event] = [] - } - this.eventListeners[event]!.push(callback as any) - } - - /** - * Remove event listener - */ - public off(event: K, callback: VADEventCallback): void { - if (!this.eventListeners[event]) - return - this.eventListeners[event] = this.eventListeners[event]!.filter(cb => cb !== callback) - } - - /** - * Emit event - */ - private emit(event: K, data: VADEvents[K]): void { - if (!this.eventListeners[event]) - return - for (const callback of this.eventListeners[event]!) { - callback(data) - } - } - - /** - * Process audio buffer for speech detection - */ - public async processAudio(inputBuffer: Float32Array): Promise { - if (!this.isReady) { - throw new Error('VAD model is not initialized. Call initialize() first.') - } - - const wasRecording = this.isRecording - - // Perform VAD on the input buffer - const isSpeech = await this.detectSpeech(inputBuffer) - - // Calculate derived constants - const sampleRateMs = this.config.sampleRate / 1000 - const minSilenceDurationSamples = this.config.minSilenceDurationMs * sampleRateMs - const speechPadSamples = this.config.speechPadMs * sampleRateMs - const minSpeechDurationSamples = this.config.minSpeechDurationMs * sampleRateMs - const maxPrevBuffers = Math.ceil(speechPadSamples / this.config.newBufferSize) - - // If not currently in speech and the current buffer isn't speech, - // store it in the previous buffers queue for potential padding - if (!wasRecording && !isSpeech) { - if (this.prevBuffers.length >= maxPrevBuffers) { - this.prevBuffers.shift() - } - this.prevBuffers.push(inputBuffer.slice(0)) - return - } - - // Check if we need to handle buffer overflow - const remaining = this.buffer.length - this.bufferPointer - if (inputBuffer.length >= remaining) { - // The buffer is full, process what we have - this.buffer.set(inputBuffer.subarray(0, remaining), this.bufferPointer) - this.bufferPointer += remaining - - // Process and reset with overflow - const overflow = inputBuffer.subarray(remaining) - this.processSpeechSegment(overflow) - return - } - else { - // Add input to the buffer - this.buffer.set(inputBuffer, this.bufferPointer) - this.bufferPointer += inputBuffer.length - } - - // Handle speech detection - if (isSpeech) { - if (!this.isRecording) { - // Speech just started - this.emit('speech-start', undefined) - this.emit('status', { type: 'info', message: 'Speech detected' }) - } - - // Update state - this.isRecording = true - this.postSpeechSamples = 0 - return - } - - // At this point, we were recording but the current buffer is not speech - this.postSpeechSamples += inputBuffer.length - - // Check if silence is long enough to consider speech ended - if (this.postSpeechSamples >= minSilenceDurationSamples) { - // Check if the speech segment is long enough to process - if (this.bufferPointer < minSpeechDurationSamples) { - // Too short, reset without processing - this.reset() - return - } - - // Process the speech segment - this.processSpeechSegment() - } - } - - /** - * Detect speech in an audio buffer - */ - private async detectSpeech(buffer: Float32Array): Promise { - const input = new Tensor('float32', buffer, [1, buffer.length]) - - const { stateN, output } = await (this.inferenceChain = this.inferenceChain.then(() => - this.model({ - input, - sr: this.sampleRateTensor, - state: this.state, - }), - )) - - // Update the state - this.state = stateN - - // Get the speech probability - const speechProb = output.data[0] - - this.emit('debug', { - message: 'VAD score', - data: { probability: speechProb }, - }) - - // Apply thresholds - return ( - speechProb > this.config.speechThreshold - || (this.isRecording && speechProb >= this.config.exitThreshold) - ) - } - - /** - * Process a complete speech segment - */ - private processSpeechSegment(overflow?: Float32Array): void { - const sampleRateMs = this.config.sampleRate / 1000 - const speechPadSamples = this.config.speechPadMs * sampleRateMs - - // Calculate duration info - const duration = (this.bufferPointer / this.config.sampleRate) * 1000 - const overflowLength = overflow?.length ?? 0 - - // Create the final buffer with padding - const prevLength = this.prevBuffers.reduce((acc, b) => acc + b.length, 0) - const finalBuffer = new Float32Array(prevLength + this.bufferPointer + speechPadSamples) - - // Add previous buffers for pre-speech padding - let offset = 0 - for (const prev of this.prevBuffers) { - finalBuffer.set(prev, offset) - offset += prev.length - } - - // Add the main speech segment - finalBuffer.set(this.buffer.slice(0, this.bufferPointer + speechPadSamples), offset) - - // Emit the speech segment - this.emit('speech-end', undefined) - this.emit('speech-ready', { - buffer: finalBuffer, - duration, - }) - - // Reset for the next segment - if (overflow) { - this.buffer.set(overflow, 0) - } - this.reset(overflowLength) - } - - /** - * Reset the VAD state - */ - private reset(offset: number = 0): void { - this.buffer.fill(0, offset) - this.bufferPointer = offset - this.isRecording = false - this.postSpeechSamples = 0 - this.prevBuffers = [] - } - - /** - * Update configuration - */ - public updateConfig(newConfig: Partial): void { - this.config = { ...this.config, ...newConfig } - - // If buffer size changed, create a new buffer - if (newConfig.maxBufferDuration || newConfig.sampleRate) { - this.buffer = new Float32Array(this.config.maxBufferDuration * this.config.sampleRate) - this.bufferPointer = 0 - } - - // Update sample rate tensor if needed - if (newConfig.sampleRate) { - this.sampleRateTensor = new Tensor('int64', [this.config.sampleRate], []) - } - } -} - -/** - * Create a VAD processor with the given configuration - */ -export async function createVAD(config?: Partial): Promise { - const vad = new VAD(config) - await vad.initialize() - return vad -} diff --git a/apps/vad-asr-chat/src/libs/vad/wav.ts b/apps/vad-asr-chat/src/libs/vad/wav.ts deleted file mode 100644 index 22cadbac9..000000000 --- a/apps/vad-asr-chat/src/libs/vad/wav.ts +++ /dev/null @@ -1,43 +0,0 @@ -function writeString(dataView: DataView, offset: number, string: string) { - for (let i = 0; i < string.length; i++) { - dataView.setUint8(offset + i, string.charCodeAt(i)) - } -} - -export function toWav(buffer: Float32Array, sampleRate: number) { - const numChannels = 1 - const numSamples = buffer.length - - // Create the WAV file container - const arrayBuffer = new ArrayBuffer(44 + numSamples * 2) - const dataView = new DataView(arrayBuffer) - - // RIFF chunk descriptor - writeString(dataView, 0, 'RIFF') - dataView.setUint32(4, 36 + numSamples * 2, true) - writeString(dataView, 8, 'WAVE') - - // fmt sub-chunk - writeString(dataView, 12, 'fmt ') - dataView.setUint32(16, 16, true) - dataView.setUint16(20, 1, true) // PCM format - dataView.setUint16(22, numChannels, true) - dataView.setUint32(24, sampleRate, true) - dataView.setUint32(28, sampleRate * numChannels * 2, true) // byte rate - dataView.setUint16(32, numChannels * 2, true) // block align - dataView.setUint16(34, 16, true) // bits per sample - - // data sub-chunk - writeString(dataView, 36, 'data') - dataView.setUint32(40, numSamples * 2, true) - - // Write the PCM samples - const offset = 44 - for (let i = 0; i < numSamples; i++) { - const sample = Math.max(-1, Math.min(1, buffer[i])) - const value = sample < 0 ? sample * 0x8000 : sample * 0x7FFF - dataView.setInt16(offset + i * 2, value, true) - } - - return arrayBuffer -} diff --git a/apps/vad-asr-chat/src/main.ts b/apps/vad-asr-chat/src/main.ts deleted file mode 100644 index 52e83a7af..000000000 --- a/apps/vad-asr-chat/src/main.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createApp } from 'vue' -import { createRouter, createWebHashHistory } from 'vue-router' -import { routes } from 'vue-router/auto-routes' - -import App from './App.vue' -import '@unocss/reset/tailwind.css' -import 'uno.css' - -const router = createRouter({ routes, history: createWebHashHistory() }) - -createApp(App) - .use(router) - .mount('#app') diff --git a/apps/vad-asr-chat/src/pages/index.vue b/apps/vad-asr-chat/src/pages/index.vue deleted file mode 100644 index 115cfaba6..000000000 --- a/apps/vad-asr-chat/src/pages/index.vue +++ /dev/null @@ -1,324 +0,0 @@ - - - diff --git a/apps/vad-asr-chat/tsconfig.json b/apps/vad-asr-chat/tsconfig.json deleted file mode 100644 index 4ccca08d0..000000000 --- a/apps/vad-asr-chat/tsconfig.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "jsx": "preserve", - "lib": [ - "DOM", - "ESNext", - "WebWorker" - ], - "module": "ESNext", - "moduleResolution": "Bundler", - "resolveJsonModule": true, - "types": [ - "vitest", - "vite/client", - // Currently AudioWorkletProcessor type is missing, we need to add it manually through @types/audioworklet - // https://github.com/microsoft/TypeScript/issues/28308#issuecomment-1512509870 - "@types/audioworklet", - "unplugin-vue-router/client" - ], - "allowJs": true, - "strict": true, - "strictNullChecks": true, - "noUnusedLocals": true, - "noEmit": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "isolatedModules": true, - "skipLibCheck": true - }, - "include": [ - "src/**/*.ts", - "src/**/*.tsx", - "src/**/*.vue", - "src/**/*.js" - ], - "exclude": [ - "dist", - "node_modules" - ] -} diff --git a/apps/vad-asr-chat/uno.config.ts b/apps/vad-asr-chat/uno.config.ts deleted file mode 100644 index 241b30eaa..000000000 --- a/apps/vad-asr-chat/uno.config.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - defineConfig, - presetAttributify, - presetIcons, - presetTypography, - presetWebFonts, - presetWind3, - transformerDirectives, - transformerVariantGroup, -} from 'unocss' - -export default defineConfig({ - presets: [ - presetWind3(), - presetAttributify(), - presetTypography(), - presetWebFonts({ - fonts: { - sans: 'DM Sans', - serif: 'DM Serif Display', - mono: 'DM Mono', - }, - timeouts: { - warning: 5000, - failure: 10000, - }, - }), - presetIcons({ - scale: 1.2, - }), - ], - transformers: [ - transformerDirectives(), - transformerVariantGroup(), - ], - safelist: 'prose prose-sm m-auto text-left'.split(' '), -}) diff --git a/apps/vad-asr-chat/vite.config.ts b/apps/vad-asr-chat/vite.config.ts deleted file mode 100644 index 5da74dfbe..000000000 --- a/apps/vad-asr-chat/vite.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { resolve } from 'node:path' -import Vue from '@vitejs/plugin-vue' -import Unocss from 'unocss/vite' -import VueRouter from 'unplugin-vue-router/vite' -import { defineConfig } from 'vite' - -export default defineConfig({ - plugins: [ - // https://github.com/posva/unplugin-vue-router - VueRouter({ - extensions: ['.vue', '.md'], - dts: resolve(import.meta.dirname, 'src', 'typed-router.d.ts'), - }), - Vue(), - // https://github.com/antfu/unocss - // see uno.config.ts for config - Unocss(), - ], -}) diff --git a/apps/vad-asr/index.html b/apps/vad-asr/index.html deleted file mode 100644 index 676793210..000000000 --- a/apps/vad-asr/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - Project AIRI VAD + ASR Playground - - - - - -
- - - - diff --git a/apps/vad-asr/locales/en.yml b/apps/vad-asr/locales/en.yml deleted file mode 100644 index bf1d5461c..000000000 --- a/apps/vad-asr/locales/en.yml +++ /dev/null @@ -1 +0,0 @@ -title: VAD diff --git a/apps/vad-asr/locales/zh-CN.yml b/apps/vad-asr/locales/zh-CN.yml deleted file mode 100644 index bf1d5461c..000000000 --- a/apps/vad-asr/locales/zh-CN.yml +++ /dev/null @@ -1 +0,0 @@ -title: VAD diff --git a/apps/vad-asr/netlify.toml b/apps/vad-asr/netlify.toml deleted file mode 100755 index 60602721a..000000000 --- a/apps/vad-asr/netlify.toml +++ /dev/null @@ -1,13 +0,0 @@ -[build] -base = "/" -command = "pnpm -F @proj-airi/vad-asr... run build" -publish = "/apps/vad-asr/dist" - -[build.environment] -NODE_VERSION = "23" - -[[redirects]] -from = "/*" -to = "/index.html" -status = 200 -force = false diff --git a/apps/vad-asr/package.json b/apps/vad-asr/package.json deleted file mode 100644 index 8e89bff4c..000000000 --- a/apps/vad-asr/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "@proj-airi/vad-asr", - "type": "module", - "private": true, - "description": "Voice Activity Detector & Automatic Speech Recognition", - "author": { - "name": "Neko Ayaka", - "email": "neko@ayaka.moe", - "url": "https://github.com/nekomeowww" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/moeru-ai/airi.git", - "directory": "apps/vad-asr" - }, - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview", - "typecheck": "vue-tsc --noEmit" - }, - "dependencies": { - "@huggingface/transformers": "^3.4.2", - "@vueuse/core": "^13.0.0", - "@xsai/generate-transcription": "catalog:", - "defu": "^6.1.4", - "es-toolkit": "^1.34.1", - "vue": "^3.5.13" - }, - "devDependencies": { - "@iconify-json/solar": "^1.2.2", - "@iconify-json/svg-spinners": "^1.2.2", - "@types/audioworklet": "^0.0.72", - "@unocss/reset": "^66.1.0-beta.10", - "@vitejs/plugin-vue": "^5.2.3", - "unplugin-vue-router": "^0.12.0", - "vite": "^6.2.5", - "vue-router": "^4.5.0", - "vue-tsc": "^3.0.0-alpha.2" - } -} diff --git a/apps/vad-asr/public/favicon.ico b/apps/vad-asr/public/favicon.ico deleted file mode 100644 index aeddd290552c350de823b21ba553e240758152c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 zcmeI3OK(+G6vt0flMdk<(20pdMo#NTap;h5Ku3-ZBPK@0L`j68i4QO+I}lCOAWlda zplL~jDA0Qgr9!|S*YDi(Z&w%lyzVU)lHO!@?Y;JT{MXu#v+q8)MKM>*7e|j4Mi-0I zuNTFeMNup+*6qVZ@rUsa8>Ro9*NWm@6L`a9Or?0+3j86T+qWK$?%aA<`f~QfyOy zBl`5^>_q&mUE%RL5ElBV=WM}?u_4=~(`3u@d*ff-I0gp!^fAbuFy;Cr6Dj`N;*RpM z4W7uO9rI87w#AT>6%SjKh#wv}bGcGZqK<5~QFNMO&*2xZjw6|EN)^9wHFlvQ)F}F<7Hxfukb6kV8EuZ!uLKDTg0KZ^wn;o z$hO(*%DeiROTrJur^gRZF|_4ZGEmjq@YC0{rsxyCK2*;JpAT#+F5w|(9+Ph?cKZ9q zfLv2f`rtl$;_flz7=vg_A$u8*E=$EO%16Ll4*kK4=i!++17 zX*)4^ebw9ie|LIRZfuklcJNbI47u@>O;NDVnE&*}$$OiM)$DHXr}FXloz3$8-6vI{ zt~NOF4{j(j;8zUre2F+2ANry4?BTn>*FHLH2ws;(4n5 zh-VeDxj7(ta5u#!8KY?$+|r3_7fjA}V|_qw8*Nl`X23ov%$|b(!l|__Fq5ygeEPV$ z#>vZ4XYEiNU(cE3@eHjqe?0yk<6X^(K61jE!CsPk`iJ6;@)3WJwN}l0Ve3O#Ga>gH zhZOM)ZJ#-D_ngmv7asCHB=;@$X2wpeoO!$2B_lu-HrlAL;b%BbA4z` z2l59QILN?@$-v{=tA8-RMRD+ zCRQA8pw?OS*L1F?^G3lbx?t^R);?6zBgk3c6}7;YXd68aeOAns(6U&V(B-8GP345H z2KrCkHd5)eQ+}tlXyq*{eSfEA&)4mMu5TN>>cjKJ_rgR4*I;FWPObA5r{cBv$?2rN-{J62+?qEz|8ZQRjMj(vee+Ik6hHp` z*vS?&H!i-PdE(oBV~_s54b{7ROHOo1?D#jml+HGf`w;&_eY4+UpXXg${uNsn9g=rq z_xt^w)d%CI@$d3m_a|)5^5J*$J$>z(Z>E!G@#}RNLojGO7$4Oeu%YSC`anD#MOrBQtXPtt~ZwFt=RR(%2HX5 z?Y`oPm8aZW?0mSVIBaFnO4mJw)?V0i;=a4x_WP5WubwU1H~sU?%lUnlx_qI}N6!QC zy}ajXj}V<`PyVuY`RzgV`vbR!3Y&CjtM2u6KEt16pq#m&!r}Y9K6@tn`CorN+Tyzm z<Ry`Y_HMU*4*x_vJY?tV&t$482bap|; z%Sl(cA`g|mJ-0k=>c-#oJ}m0W269nX`u<$vaY(lbx3`CVBjmceUQTzvc C3_0)s diff --git a/apps/vad-asr/public/favicon.svg b/apps/vad-asr/public/favicon.svg deleted file mode 100644 index a52f4ddeb..000000000 --- a/apps/vad-asr/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/vad-asr/src/App.vue b/apps/vad-asr/src/App.vue deleted file mode 100644 index e9982364f..000000000 --- a/apps/vad-asr/src/App.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - - - diff --git a/apps/vad-asr/src/components/FieldInput.vue b/apps/vad-asr/src/components/FieldInput.vue deleted file mode 100644 index d1624a76f..000000000 --- a/apps/vad-asr/src/components/FieldInput.vue +++ /dev/null @@ -1,36 +0,0 @@ - - - diff --git a/apps/vad-asr/src/components/Input.vue b/apps/vad-asr/src/components/Input.vue deleted file mode 100644 index 2221b5849..000000000 --- a/apps/vad-asr/src/components/Input.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/apps/vad-asr/src/libs/vad/manager.ts b/apps/vad-asr/src/libs/vad/manager.ts deleted file mode 100644 index 979a95e25..000000000 --- a/apps/vad-asr/src/libs/vad/manager.ts +++ /dev/null @@ -1,175 +0,0 @@ -// 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 -} - -/** - * 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 { - 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 startMicrophone(): Promise { - 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 = await navigator.mediaDevices.getUserMedia({ - audio: { - echoCancellation: true, - noiseSuppression: true, - autoGainControl: true, - sampleRate: this.audioContext.sampleRate, - }, - }) - - // 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 - } - } - - public async stopMicrophone(): Promise { - if (this.mediaStream) { - this.mediaStream.getTracks().forEach(track => track.stop()) - this.mediaStream = null - } - - if (this.sourceNode) { - this.sourceNode.disconnect() - this.sourceNode = null - } - - this.audioContext?.suspend() - this.audioWorkletNode?.disconnect() - } - - /** - * 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 = 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 - } -} diff --git a/apps/vad-asr/src/libs/vad/process.worklet.ts b/apps/vad-asr/src/libs/vad/process.worklet.ts deleted file mode 100644 index 899d933fe..000000000 --- a/apps/vad-asr/src/libs/vad/process.worklet.ts +++ /dev/null @@ -1,53 +0,0 @@ -// vad-worklet-processor.ts -// This file needs to be registered as an AudioWorklet - -/** - * Minimum chunk size for processing audio - */ -const MIN_CHUNK_SIZE = 512 - -/** - * Global state for audio buffer accumulation - */ -let globalPointer = 0 -const globalBuffer = new Float32Array(MIN_CHUNK_SIZE) - -/** - * VAD AudioWorklet Processor - processes audio chunks and sends them to the main thread - */ -class VADProcessor extends AudioWorkletProcessor { - process(inputs: Float32Array[][], _outputs: Float32Array[][], _parameters: Record) { - const buffer = inputs[0][0] - if (!buffer) - return true // buffer is null when the stream ends - - if (buffer.length > MIN_CHUNK_SIZE) { - // If the buffer is larger than the minimum chunk size, send the entire buffer - this.port.postMessage({ buffer }) - } - else { - const remaining = MIN_CHUNK_SIZE - globalPointer - if (buffer.length >= remaining) { - // If the buffer is larger than (or equal to) the remaining space in the global buffer, copy the remaining space - globalBuffer.set(buffer.subarray(0, remaining), globalPointer) - - // Send the global buffer - this.port.postMessage({ buffer: globalBuffer }) - - // Reset the global buffer and set the remaining buffer - globalBuffer.fill(0) - globalBuffer.set(buffer.subarray(remaining), 0) - globalPointer = buffer.length - remaining - } - else { - // If the buffer is smaller than the remaining space in the global buffer, copy the buffer to the global buffer - globalBuffer.set(buffer, globalPointer) - globalPointer += buffer.length - } - } - - return true - } -} - -registerProcessor('vad-processor', VADProcessor) diff --git a/apps/vad-asr/src/libs/vad/vad.ts b/apps/vad-asr/src/libs/vad/vad.ts deleted file mode 100644 index bd28d0a18..000000000 --- a/apps/vad-asr/src/libs/vad/vad.ts +++ /dev/null @@ -1,319 +0,0 @@ -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 = - (event: VADEvents[K]) => void - -/** - * Voice Activity Detection processor - */ -export class VAD { - private config: VADConfig - private model: any - private state: Tensor - private sampleRateTensor: Tensor - private buffer: Float32Array - private bufferPointer: number = 0 - private isRecording: boolean = false - private postSpeechSamples: number = 0 - private prevBuffers: Float32Array[] = [] - private inferenceChain: Promise = Promise.resolve() - private eventListeners: Partial[]>> = {} - private isReady: boolean = false - - constructor(userConfig: Partial = {}) { - // Default configuration - const defaultConfig: VADConfig = { - sampleRate: 16000, - speechThreshold: 0.3, - exitThreshold: 0.1, - minSilenceDurationMs: 400, - speechPadMs: 80, - minSpeechDurationMs: 250, - maxBufferDuration: 30, - newBufferSize: 512, - } - - this.config = { ...defaultConfig, ...userConfig } - - // Create buffer based on max duration - this.buffer = new Float32Array(this.config.maxBufferDuration * this.config.sampleRate) - - // Initialize state tensor for VAD model - this.state = new Tensor('float32', new Float32Array(2 * 1 * 128), [2, 1, 128]) - - // Sample rate tensor for the model - this.sampleRateTensor = new Tensor('int64', [this.config.sampleRate], []) - } - - /** - * Initialize the VAD model - */ - public async initialize(): Promise { - try { - this.emit('status', { type: 'info', message: 'Loading VAD model...' }) - - this.model = await AutoModel.from_pretrained('onnx-community/silero-vad', { - config: { model_type: 'custom' } as any, - dtype: 'fp32', // Full-precision - }) - - this.isReady = true - this.emit('status', { type: 'info', message: 'VAD model loaded successfully' }) - } - catch (error) { - this.emit('status', { type: 'error', message: `Failed to load VAD model: ${error}` }) - throw error - } - } - - /** - * Add event listener - */ - public on(event: K, callback: VADEventCallback): void { - if (!this.eventListeners[event]) { - this.eventListeners[event] = [] - } - this.eventListeners[event]!.push(callback as any) - } - - /** - * Remove event listener - */ - public off(event: K, callback: VADEventCallback): void { - if (!this.eventListeners[event]) - return - this.eventListeners[event] = this.eventListeners[event]!.filter(cb => cb !== callback) - } - - /** - * Emit event - */ - private emit(event: K, data: VADEvents[K]): void { - if (!this.eventListeners[event]) - return - for (const callback of this.eventListeners[event]!) { - callback(data) - } - } - - /** - * Process audio buffer for speech detection - */ - public async processAudio(inputBuffer: Float32Array): Promise { - if (!this.isReady) { - throw new Error('VAD model is not initialized. Call initialize() first.') - } - - const wasRecording = this.isRecording - - // Perform VAD on the input buffer - const isSpeech = await this.detectSpeech(inputBuffer) - - // Calculate derived constants - const sampleRateMs = this.config.sampleRate / 1000 - const minSilenceDurationSamples = this.config.minSilenceDurationMs * sampleRateMs - const speechPadSamples = this.config.speechPadMs * sampleRateMs - const minSpeechDurationSamples = this.config.minSpeechDurationMs * sampleRateMs - const maxPrevBuffers = Math.ceil(speechPadSamples / this.config.newBufferSize) - - // If not currently in speech and the current buffer isn't speech, - // store it in the previous buffers queue for potential padding - if (!wasRecording && !isSpeech) { - if (this.prevBuffers.length >= maxPrevBuffers) { - this.prevBuffers.shift() - } - this.prevBuffers.push(inputBuffer.slice(0)) - return - } - - // Check if we need to handle buffer overflow - const remaining = this.buffer.length - this.bufferPointer - if (inputBuffer.length >= remaining) { - // The buffer is full, process what we have - this.buffer.set(inputBuffer.subarray(0, remaining), this.bufferPointer) - this.bufferPointer += remaining - - // Process and reset with overflow - const overflow = inputBuffer.subarray(remaining) - this.processSpeechSegment(overflow) - return - } - else { - // Add input to the buffer - this.buffer.set(inputBuffer, this.bufferPointer) - this.bufferPointer += inputBuffer.length - } - - // Handle speech detection - if (isSpeech) { - if (!this.isRecording) { - // Speech just started - this.emit('speech-start', undefined) - this.emit('status', { type: 'info', message: 'Speech detected' }) - } - - // Update state - this.isRecording = true - this.postSpeechSamples = 0 - return - } - - // At this point, we were recording but the current buffer is not speech - this.postSpeechSamples += inputBuffer.length - - // Check if silence is long enough to consider speech ended - if (this.postSpeechSamples >= minSilenceDurationSamples) { - // Check if the speech segment is long enough to process - if (this.bufferPointer < minSpeechDurationSamples) { - // Too short, reset without processing - this.reset() - return - } - - // Process the speech segment - this.processSpeechSegment() - } - } - - /** - * Detect speech in an audio buffer - */ - private async detectSpeech(buffer: Float32Array): Promise { - const input = new Tensor('float32', buffer, [1, buffer.length]) - - const { stateN, output } = await (this.inferenceChain = this.inferenceChain.then(() => - this.model({ - input, - sr: this.sampleRateTensor, - state: this.state, - }), - )) - - // Update the state - this.state = stateN - - // Get the speech probability - const speechProb = output.data[0] - - this.emit('debug', { - message: 'VAD score', - data: { probability: speechProb }, - }) - - // Apply thresholds - return ( - speechProb > this.config.speechThreshold - || (this.isRecording && speechProb >= this.config.exitThreshold) - ) - } - - /** - * Process a complete speech segment - */ - private processSpeechSegment(overflow?: Float32Array): void { - const sampleRateMs = this.config.sampleRate / 1000 - const speechPadSamples = this.config.speechPadMs * sampleRateMs - - // Calculate duration info - const duration = (this.bufferPointer / this.config.sampleRate) * 1000 - const overflowLength = overflow?.length ?? 0 - - // Create the final buffer with padding - const prevLength = this.prevBuffers.reduce((acc, b) => acc + b.length, 0) - const finalBuffer = new Float32Array(prevLength + this.bufferPointer + speechPadSamples) - - // Add previous buffers for pre-speech padding - let offset = 0 - for (const prev of this.prevBuffers) { - finalBuffer.set(prev, offset) - offset += prev.length - } - - // Add the main speech segment - finalBuffer.set(this.buffer.slice(0, this.bufferPointer + speechPadSamples), offset) - - // Emit the speech segment - this.emit('speech-end', undefined) - this.emit('speech-ready', { - buffer: finalBuffer, - duration, - }) - - // Reset for the next segment - if (overflow) { - this.buffer.set(overflow, 0) - } - this.reset(overflowLength) - } - - /** - * Reset the VAD state - */ - private reset(offset: number = 0): void { - this.buffer.fill(0, offset) - this.bufferPointer = offset - this.isRecording = false - this.postSpeechSamples = 0 - this.prevBuffers = [] - } - - /** - * Update configuration - */ - public updateConfig(newConfig: Partial): void { - this.config = { ...this.config, ...newConfig } - - // If buffer size changed, create a new buffer - if (newConfig.maxBufferDuration || newConfig.sampleRate) { - this.buffer = new Float32Array(this.config.maxBufferDuration * this.config.sampleRate) - this.bufferPointer = 0 - } - - // Update sample rate tensor if needed - if (newConfig.sampleRate) { - this.sampleRateTensor = new Tensor('int64', [this.config.sampleRate], []) - } - } -} - -/** - * Create a VAD processor with the given configuration - */ -export async function createVAD(config?: Partial): Promise { - const vad = new VAD(config) - await vad.initialize() - return vad -} diff --git a/apps/vad-asr/src/libs/vad/wav.ts b/apps/vad-asr/src/libs/vad/wav.ts deleted file mode 100644 index 22cadbac9..000000000 --- a/apps/vad-asr/src/libs/vad/wav.ts +++ /dev/null @@ -1,43 +0,0 @@ -function writeString(dataView: DataView, offset: number, string: string) { - for (let i = 0; i < string.length; i++) { - dataView.setUint8(offset + i, string.charCodeAt(i)) - } -} - -export function toWav(buffer: Float32Array, sampleRate: number) { - const numChannels = 1 - const numSamples = buffer.length - - // Create the WAV file container - const arrayBuffer = new ArrayBuffer(44 + numSamples * 2) - const dataView = new DataView(arrayBuffer) - - // RIFF chunk descriptor - writeString(dataView, 0, 'RIFF') - dataView.setUint32(4, 36 + numSamples * 2, true) - writeString(dataView, 8, 'WAVE') - - // fmt sub-chunk - writeString(dataView, 12, 'fmt ') - dataView.setUint32(16, 16, true) - dataView.setUint16(20, 1, true) // PCM format - dataView.setUint16(22, numChannels, true) - dataView.setUint32(24, sampleRate, true) - dataView.setUint32(28, sampleRate * numChannels * 2, true) // byte rate - dataView.setUint16(32, numChannels * 2, true) // block align - dataView.setUint16(34, 16, true) // bits per sample - - // data sub-chunk - writeString(dataView, 36, 'data') - dataView.setUint32(40, numSamples * 2, true) - - // Write the PCM samples - const offset = 44 - for (let i = 0; i < numSamples; i++) { - const sample = Math.max(-1, Math.min(1, buffer[i])) - const value = sample < 0 ? sample * 0x8000 : sample * 0x7FFF - dataView.setInt16(offset + i * 2, value, true) - } - - return arrayBuffer -} diff --git a/apps/vad-asr/src/main.ts b/apps/vad-asr/src/main.ts deleted file mode 100644 index 52e83a7af..000000000 --- a/apps/vad-asr/src/main.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createApp } from 'vue' -import { createRouter, createWebHashHistory } from 'vue-router' -import { routes } from 'vue-router/auto-routes' - -import App from './App.vue' -import '@unocss/reset/tailwind.css' -import 'uno.css' - -const router = createRouter({ routes, history: createWebHashHistory() }) - -createApp(App) - .use(router) - .mount('#app') diff --git a/apps/vad-asr/src/pages/index.vue b/apps/vad-asr/src/pages/index.vue deleted file mode 100644 index 952700048..000000000 --- a/apps/vad-asr/src/pages/index.vue +++ /dev/null @@ -1,205 +0,0 @@ - - - diff --git a/apps/vad-asr/tsconfig.json b/apps/vad-asr/tsconfig.json deleted file mode 100644 index 4ccca08d0..000000000 --- a/apps/vad-asr/tsconfig.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "jsx": "preserve", - "lib": [ - "DOM", - "ESNext", - "WebWorker" - ], - "module": "ESNext", - "moduleResolution": "Bundler", - "resolveJsonModule": true, - "types": [ - "vitest", - "vite/client", - // Currently AudioWorkletProcessor type is missing, we need to add it manually through @types/audioworklet - // https://github.com/microsoft/TypeScript/issues/28308#issuecomment-1512509870 - "@types/audioworklet", - "unplugin-vue-router/client" - ], - "allowJs": true, - "strict": true, - "strictNullChecks": true, - "noUnusedLocals": true, - "noEmit": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "isolatedModules": true, - "skipLibCheck": true - }, - "include": [ - "src/**/*.ts", - "src/**/*.tsx", - "src/**/*.vue", - "src/**/*.js" - ], - "exclude": [ - "dist", - "node_modules" - ] -} diff --git a/apps/vad-asr/uno.config.ts b/apps/vad-asr/uno.config.ts deleted file mode 100644 index 241b30eaa..000000000 --- a/apps/vad-asr/uno.config.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - defineConfig, - presetAttributify, - presetIcons, - presetTypography, - presetWebFonts, - presetWind3, - transformerDirectives, - transformerVariantGroup, -} from 'unocss' - -export default defineConfig({ - presets: [ - presetWind3(), - presetAttributify(), - presetTypography(), - presetWebFonts({ - fonts: { - sans: 'DM Sans', - serif: 'DM Serif Display', - mono: 'DM Mono', - }, - timeouts: { - warning: 5000, - failure: 10000, - }, - }), - presetIcons({ - scale: 1.2, - }), - ], - transformers: [ - transformerDirectives(), - transformerVariantGroup(), - ], - safelist: 'prose prose-sm m-auto text-left'.split(' '), -}) diff --git a/apps/vad-asr/vite.config.ts b/apps/vad-asr/vite.config.ts deleted file mode 100644 index 5da74dfbe..000000000 --- a/apps/vad-asr/vite.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { resolve } from 'node:path' -import Vue from '@vitejs/plugin-vue' -import Unocss from 'unocss/vite' -import VueRouter from 'unplugin-vue-router/vite' -import { defineConfig } from 'vite' - -export default defineConfig({ - plugins: [ - // https://github.com/posva/unplugin-vue-router - VueRouter({ - extensions: ['.vue', '.md'], - dts: resolve(import.meta.dirname, 'src', 'typed-router.d.ts'), - }), - Vue(), - // https://github.com/antfu/unocss - // see uno.config.ts for config - Unocss(), - ], -}) diff --git a/apps/vad/index.html b/apps/vad/index.html deleted file mode 100644 index 42e6a40d9..000000000 --- a/apps/vad/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - Project AIRI VAD Playground - - - - - -
- - - - diff --git a/apps/vad/locales/en.yml b/apps/vad/locales/en.yml deleted file mode 100644 index bf1d5461c..000000000 --- a/apps/vad/locales/en.yml +++ /dev/null @@ -1 +0,0 @@ -title: VAD diff --git a/apps/vad/locales/zh-CN.yml b/apps/vad/locales/zh-CN.yml deleted file mode 100644 index bf1d5461c..000000000 --- a/apps/vad/locales/zh-CN.yml +++ /dev/null @@ -1 +0,0 @@ -title: VAD diff --git a/apps/vad/netlify.toml b/apps/vad/netlify.toml deleted file mode 100755 index e26eeac5d..000000000 --- a/apps/vad/netlify.toml +++ /dev/null @@ -1,13 +0,0 @@ -[build] -base = "/" -command = "pnpm -F @proj-airi/vad... run build" -publish = "/apps/vad/dist" - -[build.environment] -NODE_VERSION = "23" - -[[redirects]] -from = "/*" -to = "/index.html" -status = 200 -force = false diff --git a/apps/vad/package.json b/apps/vad/package.json deleted file mode 100644 index 49fa3a9ac..000000000 --- a/apps/vad/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "@proj-airi/vad", - "type": "module", - "private": true, - "description": "Voice Activity Detector", - "author": { - "name": "Neko Ayaka", - "email": "neko@ayaka.moe", - "url": "https://github.com/nekomeowww" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/moeru-ai/airi.git", - "directory": "apps/vad" - }, - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview", - "typecheck": "vue-tsc --noEmit" - }, - "dependencies": { - "@huggingface/transformers": "^3.4.2", - "@vueuse/core": "^13.0.0", - "defu": "^6.1.4", - "es-toolkit": "^1.34.1", - "vue": "^3.5.13" - }, - "devDependencies": { - "@iconify-json/solar": "^1.2.2", - "@iconify-json/svg-spinners": "^1.2.2", - "@types/audioworklet": "^0.0.72", - "@unocss/reset": "^66.1.0-beta.10", - "@vitejs/plugin-vue": "^5.2.3", - "unplugin-vue-router": "^0.12.0", - "vite": "^6.2.5", - "vue-router": "^4.5.0", - "vue-tsc": "^3.0.0-alpha.2" - } -} diff --git a/apps/vad/public/favicon.ico b/apps/vad/public/favicon.ico deleted file mode 100644 index aeddd290552c350de823b21ba553e240758152c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 zcmeI3OK(+G6vt0flMdk<(20pdMo#NTap;h5Ku3-ZBPK@0L`j68i4QO+I}lCOAWlda zplL~jDA0Qgr9!|S*YDi(Z&w%lyzVU)lHO!@?Y;JT{MXu#v+q8)MKM>*7e|j4Mi-0I zuNTFeMNup+*6qVZ@rUsa8>Ro9*NWm@6L`a9Or?0+3j86T+qWK$?%aA<`f~QfyOy zBl`5^>_q&mUE%RL5ElBV=WM}?u_4=~(`3u@d*ff-I0gp!^fAbuFy;Cr6Dj`N;*RpM z4W7uO9rI87w#AT>6%SjKh#wv}bGcGZqK<5~QFNMO&*2xZjw6|EN)^9wHFlvQ)F}F<7Hxfukb6kV8EuZ!uLKDTg0KZ^wn;o z$hO(*%DeiROTrJur^gRZF|_4ZGEmjq@YC0{rsxyCK2*;JpAT#+F5w|(9+Ph?cKZ9q zfLv2f`rtl$;_flz7=vg_A$u8*E=$EO%16Ll4*kK4=i!++17 zX*)4^ebw9ie|LIRZfuklcJNbI47u@>O;NDVnE&*}$$OiM)$DHXr}FXloz3$8-6vI{ zt~NOF4{j(j;8zUre2F+2ANry4?BTn>*FHLH2ws;(4n5 zh-VeDxj7(ta5u#!8KY?$+|r3_7fjA}V|_qw8*Nl`X23ov%$|b(!l|__Fq5ygeEPV$ z#>vZ4XYEiNU(cE3@eHjqe?0yk<6X^(K61jE!CsPk`iJ6;@)3WJwN}l0Ve3O#Ga>gH zhZOM)ZJ#-D_ngmv7asCHB=;@$X2wpeoO!$2B_lu-HrlAL;b%BbA4z` z2l59QILN?@$-v{=tA8-RMRD+ zCRQA8pw?OS*L1F?^G3lbx?t^R);?6zBgk3c6}7;YXd68aeOAns(6U&V(B-8GP345H z2KrCkHd5)eQ+}tlXyq*{eSfEA&)4mMu5TN>>cjKJ_rgR4*I;FWPObA5r{cBv$?2rN-{J62+?qEz|8ZQRjMj(vee+Ik6hHp` z*vS?&H!i-PdE(oBV~_s54b{7ROHOo1?D#jml+HGf`w;&_eY4+UpXXg${uNsn9g=rq z_xt^w)d%CI@$d3m_a|)5^5J*$J$>z(Z>E!G@#}RNLojGO7$4Oeu%YSC`anD#MOrBQtXPtt~ZwFt=RR(%2HX5 z?Y`oPm8aZW?0mSVIBaFnO4mJw)?V0i;=a4x_WP5WubwU1H~sU?%lUnlx_qI}N6!QC zy}ajXj}V<`PyVuY`RzgV`vbR!3Y&CjtM2u6KEt16pq#m&!r}Y9K6@tn`CorN+Tyzm z<Ry`Y_HMU*4*x_vJY?tV&t$482bap|; z%Sl(cA`g|mJ-0k=>c-#oJ}m0W269nX`u<$vaY(lbx3`CVBjmceUQTzvc C3_0)s diff --git a/apps/vad/public/favicon.svg b/apps/vad/public/favicon.svg deleted file mode 100644 index a52f4ddeb..000000000 --- a/apps/vad/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/vad/src/App.vue b/apps/vad/src/App.vue deleted file mode 100644 index 61b6b87ef..000000000 --- a/apps/vad/src/App.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - - - diff --git a/apps/vad/src/libs/vad/manager.ts b/apps/vad/src/libs/vad/manager.ts deleted file mode 100644 index 979a95e25..000000000 --- a/apps/vad/src/libs/vad/manager.ts +++ /dev/null @@ -1,175 +0,0 @@ -// 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 -} - -/** - * 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 { - 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 startMicrophone(): Promise { - 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 = await navigator.mediaDevices.getUserMedia({ - audio: { - echoCancellation: true, - noiseSuppression: true, - autoGainControl: true, - sampleRate: this.audioContext.sampleRate, - }, - }) - - // 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 - } - } - - public async stopMicrophone(): Promise { - if (this.mediaStream) { - this.mediaStream.getTracks().forEach(track => track.stop()) - this.mediaStream = null - } - - if (this.sourceNode) { - this.sourceNode.disconnect() - this.sourceNode = null - } - - this.audioContext?.suspend() - this.audioWorkletNode?.disconnect() - } - - /** - * 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 = 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 - } -} diff --git a/apps/vad/src/libs/vad/process.worklet.ts b/apps/vad/src/libs/vad/process.worklet.ts deleted file mode 100644 index 899d933fe..000000000 --- a/apps/vad/src/libs/vad/process.worklet.ts +++ /dev/null @@ -1,53 +0,0 @@ -// vad-worklet-processor.ts -// This file needs to be registered as an AudioWorklet - -/** - * Minimum chunk size for processing audio - */ -const MIN_CHUNK_SIZE = 512 - -/** - * Global state for audio buffer accumulation - */ -let globalPointer = 0 -const globalBuffer = new Float32Array(MIN_CHUNK_SIZE) - -/** - * VAD AudioWorklet Processor - processes audio chunks and sends them to the main thread - */ -class VADProcessor extends AudioWorkletProcessor { - process(inputs: Float32Array[][], _outputs: Float32Array[][], _parameters: Record) { - const buffer = inputs[0][0] - if (!buffer) - return true // buffer is null when the stream ends - - if (buffer.length > MIN_CHUNK_SIZE) { - // If the buffer is larger than the minimum chunk size, send the entire buffer - this.port.postMessage({ buffer }) - } - else { - const remaining = MIN_CHUNK_SIZE - globalPointer - if (buffer.length >= remaining) { - // If the buffer is larger than (or equal to) the remaining space in the global buffer, copy the remaining space - globalBuffer.set(buffer.subarray(0, remaining), globalPointer) - - // Send the global buffer - this.port.postMessage({ buffer: globalBuffer }) - - // Reset the global buffer and set the remaining buffer - globalBuffer.fill(0) - globalBuffer.set(buffer.subarray(remaining), 0) - globalPointer = buffer.length - remaining - } - else { - // If the buffer is smaller than the remaining space in the global buffer, copy the buffer to the global buffer - globalBuffer.set(buffer, globalPointer) - globalPointer += buffer.length - } - } - - return true - } -} - -registerProcessor('vad-processor', VADProcessor) diff --git a/apps/vad/src/libs/vad/vad.ts b/apps/vad/src/libs/vad/vad.ts deleted file mode 100644 index bd28d0a18..000000000 --- a/apps/vad/src/libs/vad/vad.ts +++ /dev/null @@ -1,319 +0,0 @@ -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 = - (event: VADEvents[K]) => void - -/** - * Voice Activity Detection processor - */ -export class VAD { - private config: VADConfig - private model: any - private state: Tensor - private sampleRateTensor: Tensor - private buffer: Float32Array - private bufferPointer: number = 0 - private isRecording: boolean = false - private postSpeechSamples: number = 0 - private prevBuffers: Float32Array[] = [] - private inferenceChain: Promise = Promise.resolve() - private eventListeners: Partial[]>> = {} - private isReady: boolean = false - - constructor(userConfig: Partial = {}) { - // Default configuration - const defaultConfig: VADConfig = { - sampleRate: 16000, - speechThreshold: 0.3, - exitThreshold: 0.1, - minSilenceDurationMs: 400, - speechPadMs: 80, - minSpeechDurationMs: 250, - maxBufferDuration: 30, - newBufferSize: 512, - } - - this.config = { ...defaultConfig, ...userConfig } - - // Create buffer based on max duration - this.buffer = new Float32Array(this.config.maxBufferDuration * this.config.sampleRate) - - // Initialize state tensor for VAD model - this.state = new Tensor('float32', new Float32Array(2 * 1 * 128), [2, 1, 128]) - - // Sample rate tensor for the model - this.sampleRateTensor = new Tensor('int64', [this.config.sampleRate], []) - } - - /** - * Initialize the VAD model - */ - public async initialize(): Promise { - try { - this.emit('status', { type: 'info', message: 'Loading VAD model...' }) - - this.model = await AutoModel.from_pretrained('onnx-community/silero-vad', { - config: { model_type: 'custom' } as any, - dtype: 'fp32', // Full-precision - }) - - this.isReady = true - this.emit('status', { type: 'info', message: 'VAD model loaded successfully' }) - } - catch (error) { - this.emit('status', { type: 'error', message: `Failed to load VAD model: ${error}` }) - throw error - } - } - - /** - * Add event listener - */ - public on(event: K, callback: VADEventCallback): void { - if (!this.eventListeners[event]) { - this.eventListeners[event] = [] - } - this.eventListeners[event]!.push(callback as any) - } - - /** - * Remove event listener - */ - public off(event: K, callback: VADEventCallback): void { - if (!this.eventListeners[event]) - return - this.eventListeners[event] = this.eventListeners[event]!.filter(cb => cb !== callback) - } - - /** - * Emit event - */ - private emit(event: K, data: VADEvents[K]): void { - if (!this.eventListeners[event]) - return - for (const callback of this.eventListeners[event]!) { - callback(data) - } - } - - /** - * Process audio buffer for speech detection - */ - public async processAudio(inputBuffer: Float32Array): Promise { - if (!this.isReady) { - throw new Error('VAD model is not initialized. Call initialize() first.') - } - - const wasRecording = this.isRecording - - // Perform VAD on the input buffer - const isSpeech = await this.detectSpeech(inputBuffer) - - // Calculate derived constants - const sampleRateMs = this.config.sampleRate / 1000 - const minSilenceDurationSamples = this.config.minSilenceDurationMs * sampleRateMs - const speechPadSamples = this.config.speechPadMs * sampleRateMs - const minSpeechDurationSamples = this.config.minSpeechDurationMs * sampleRateMs - const maxPrevBuffers = Math.ceil(speechPadSamples / this.config.newBufferSize) - - // If not currently in speech and the current buffer isn't speech, - // store it in the previous buffers queue for potential padding - if (!wasRecording && !isSpeech) { - if (this.prevBuffers.length >= maxPrevBuffers) { - this.prevBuffers.shift() - } - this.prevBuffers.push(inputBuffer.slice(0)) - return - } - - // Check if we need to handle buffer overflow - const remaining = this.buffer.length - this.bufferPointer - if (inputBuffer.length >= remaining) { - // The buffer is full, process what we have - this.buffer.set(inputBuffer.subarray(0, remaining), this.bufferPointer) - this.bufferPointer += remaining - - // Process and reset with overflow - const overflow = inputBuffer.subarray(remaining) - this.processSpeechSegment(overflow) - return - } - else { - // Add input to the buffer - this.buffer.set(inputBuffer, this.bufferPointer) - this.bufferPointer += inputBuffer.length - } - - // Handle speech detection - if (isSpeech) { - if (!this.isRecording) { - // Speech just started - this.emit('speech-start', undefined) - this.emit('status', { type: 'info', message: 'Speech detected' }) - } - - // Update state - this.isRecording = true - this.postSpeechSamples = 0 - return - } - - // At this point, we were recording but the current buffer is not speech - this.postSpeechSamples += inputBuffer.length - - // Check if silence is long enough to consider speech ended - if (this.postSpeechSamples >= minSilenceDurationSamples) { - // Check if the speech segment is long enough to process - if (this.bufferPointer < minSpeechDurationSamples) { - // Too short, reset without processing - this.reset() - return - } - - // Process the speech segment - this.processSpeechSegment() - } - } - - /** - * Detect speech in an audio buffer - */ - private async detectSpeech(buffer: Float32Array): Promise { - const input = new Tensor('float32', buffer, [1, buffer.length]) - - const { stateN, output } = await (this.inferenceChain = this.inferenceChain.then(() => - this.model({ - input, - sr: this.sampleRateTensor, - state: this.state, - }), - )) - - // Update the state - this.state = stateN - - // Get the speech probability - const speechProb = output.data[0] - - this.emit('debug', { - message: 'VAD score', - data: { probability: speechProb }, - }) - - // Apply thresholds - return ( - speechProb > this.config.speechThreshold - || (this.isRecording && speechProb >= this.config.exitThreshold) - ) - } - - /** - * Process a complete speech segment - */ - private processSpeechSegment(overflow?: Float32Array): void { - const sampleRateMs = this.config.sampleRate / 1000 - const speechPadSamples = this.config.speechPadMs * sampleRateMs - - // Calculate duration info - const duration = (this.bufferPointer / this.config.sampleRate) * 1000 - const overflowLength = overflow?.length ?? 0 - - // Create the final buffer with padding - const prevLength = this.prevBuffers.reduce((acc, b) => acc + b.length, 0) - const finalBuffer = new Float32Array(prevLength + this.bufferPointer + speechPadSamples) - - // Add previous buffers for pre-speech padding - let offset = 0 - for (const prev of this.prevBuffers) { - finalBuffer.set(prev, offset) - offset += prev.length - } - - // Add the main speech segment - finalBuffer.set(this.buffer.slice(0, this.bufferPointer + speechPadSamples), offset) - - // Emit the speech segment - this.emit('speech-end', undefined) - this.emit('speech-ready', { - buffer: finalBuffer, - duration, - }) - - // Reset for the next segment - if (overflow) { - this.buffer.set(overflow, 0) - } - this.reset(overflowLength) - } - - /** - * Reset the VAD state - */ - private reset(offset: number = 0): void { - this.buffer.fill(0, offset) - this.bufferPointer = offset - this.isRecording = false - this.postSpeechSamples = 0 - this.prevBuffers = [] - } - - /** - * Update configuration - */ - public updateConfig(newConfig: Partial): void { - this.config = { ...this.config, ...newConfig } - - // If buffer size changed, create a new buffer - if (newConfig.maxBufferDuration || newConfig.sampleRate) { - this.buffer = new Float32Array(this.config.maxBufferDuration * this.config.sampleRate) - this.bufferPointer = 0 - } - - // Update sample rate tensor if needed - if (newConfig.sampleRate) { - this.sampleRateTensor = new Tensor('int64', [this.config.sampleRate], []) - } - } -} - -/** - * Create a VAD processor with the given configuration - */ -export async function createVAD(config?: Partial): Promise { - const vad = new VAD(config) - await vad.initialize() - return vad -} diff --git a/apps/vad/src/libs/vad/wav.ts b/apps/vad/src/libs/vad/wav.ts deleted file mode 100644 index 22cadbac9..000000000 --- a/apps/vad/src/libs/vad/wav.ts +++ /dev/null @@ -1,43 +0,0 @@ -function writeString(dataView: DataView, offset: number, string: string) { - for (let i = 0; i < string.length; i++) { - dataView.setUint8(offset + i, string.charCodeAt(i)) - } -} - -export function toWav(buffer: Float32Array, sampleRate: number) { - const numChannels = 1 - const numSamples = buffer.length - - // Create the WAV file container - const arrayBuffer = new ArrayBuffer(44 + numSamples * 2) - const dataView = new DataView(arrayBuffer) - - // RIFF chunk descriptor - writeString(dataView, 0, 'RIFF') - dataView.setUint32(4, 36 + numSamples * 2, true) - writeString(dataView, 8, 'WAVE') - - // fmt sub-chunk - writeString(dataView, 12, 'fmt ') - dataView.setUint32(16, 16, true) - dataView.setUint16(20, 1, true) // PCM format - dataView.setUint16(22, numChannels, true) - dataView.setUint32(24, sampleRate, true) - dataView.setUint32(28, sampleRate * numChannels * 2, true) // byte rate - dataView.setUint16(32, numChannels * 2, true) // block align - dataView.setUint16(34, 16, true) // bits per sample - - // data sub-chunk - writeString(dataView, 36, 'data') - dataView.setUint32(40, numSamples * 2, true) - - // Write the PCM samples - const offset = 44 - for (let i = 0; i < numSamples; i++) { - const sample = Math.max(-1, Math.min(1, buffer[i])) - const value = sample < 0 ? sample * 0x8000 : sample * 0x7FFF - dataView.setInt16(offset + i * 2, value, true) - } - - return arrayBuffer -} diff --git a/apps/vad/src/main.ts b/apps/vad/src/main.ts deleted file mode 100644 index 52e83a7af..000000000 --- a/apps/vad/src/main.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createApp } from 'vue' -import { createRouter, createWebHashHistory } from 'vue-router' -import { routes } from 'vue-router/auto-routes' - -import App from './App.vue' -import '@unocss/reset/tailwind.css' -import 'uno.css' - -const router = createRouter({ routes, history: createWebHashHistory() }) - -createApp(App) - .use(router) - .mount('#app') diff --git a/apps/vad/src/pages/index.vue b/apps/vad/src/pages/index.vue deleted file mode 100644 index 3b5913e2c..000000000 --- a/apps/vad/src/pages/index.vue +++ /dev/null @@ -1,169 +0,0 @@ - - - diff --git a/apps/vad/tsconfig.json b/apps/vad/tsconfig.json deleted file mode 100644 index 4ccca08d0..000000000 --- a/apps/vad/tsconfig.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "jsx": "preserve", - "lib": [ - "DOM", - "ESNext", - "WebWorker" - ], - "module": "ESNext", - "moduleResolution": "Bundler", - "resolveJsonModule": true, - "types": [ - "vitest", - "vite/client", - // Currently AudioWorkletProcessor type is missing, we need to add it manually through @types/audioworklet - // https://github.com/microsoft/TypeScript/issues/28308#issuecomment-1512509870 - "@types/audioworklet", - "unplugin-vue-router/client" - ], - "allowJs": true, - "strict": true, - "strictNullChecks": true, - "noUnusedLocals": true, - "noEmit": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "isolatedModules": true, - "skipLibCheck": true - }, - "include": [ - "src/**/*.ts", - "src/**/*.tsx", - "src/**/*.vue", - "src/**/*.js" - ], - "exclude": [ - "dist", - "node_modules" - ] -} diff --git a/apps/vad/uno.config.ts b/apps/vad/uno.config.ts deleted file mode 100644 index 241b30eaa..000000000 --- a/apps/vad/uno.config.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - defineConfig, - presetAttributify, - presetIcons, - presetTypography, - presetWebFonts, - presetWind3, - transformerDirectives, - transformerVariantGroup, -} from 'unocss' - -export default defineConfig({ - presets: [ - presetWind3(), - presetAttributify(), - presetTypography(), - presetWebFonts({ - fonts: { - sans: 'DM Sans', - serif: 'DM Serif Display', - mono: 'DM Mono', - }, - timeouts: { - warning: 5000, - failure: 10000, - }, - }), - presetIcons({ - scale: 1.2, - }), - ], - transformers: [ - transformerDirectives(), - transformerVariantGroup(), - ], - safelist: 'prose prose-sm m-auto text-left'.split(' '), -}) diff --git a/apps/vad/vite.config.ts b/apps/vad/vite.config.ts deleted file mode 100644 index 5da74dfbe..000000000 --- a/apps/vad/vite.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { resolve } from 'node:path' -import Vue from '@vitejs/plugin-vue' -import Unocss from 'unocss/vite' -import VueRouter from 'unplugin-vue-router/vite' -import { defineConfig } from 'vite' - -export default defineConfig({ - plugins: [ - // https://github.com/posva/unplugin-vue-router - VueRouter({ - extensions: ['.vue', '.md'], - dts: resolve(import.meta.dirname, 'src', 'typed-router.d.ts'), - }), - Vue(), - // https://github.com/antfu/unocss - // see uno.config.ts for config - Unocss(), - ], -}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7150d4041..864e6cea3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -524,7 +524,7 @@ importers: version: 28.4.1(@babel/parser@7.26.10)(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(vue@3.5.13(typescript@5.8.3)) unplugin-vue-macros: specifier: ^2.14.5 - version: 2.14.5(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.3)))(esbuild@0.25.0)(rollup@2.79.1)(typescript@5.8.3)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@3.0.0-alpha.2(typescript@5.8.3))(vue@3.5.13(typescript@5.8.3)) + version: 2.14.5(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.3)))(esbuild@0.19.12)(rollup@2.79.1)(typescript@5.8.3)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@3.0.0-alpha.2(typescript@5.8.3))(vue@3.5.13(typescript@5.8.3)) unplugin-vue-markdown: specifier: ^28.3.1 version: 28.3.1(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)) @@ -887,162 +887,6 @@ importers: specifier: ^3.0.0-alpha.2 version: 3.0.0-alpha.2(typescript@5.8.3) - apps/vad: - dependencies: - '@huggingface/transformers': - specifier: ^3.4.2 - version: 3.4.2 - '@vueuse/core': - specifier: ^13.0.0 - version: 13.0.0(vue@3.5.13(typescript@5.8.3)) - defu: - specifier: ^6.1.4 - version: 6.1.4 - es-toolkit: - specifier: ^1.34.1 - version: 1.34.1 - vue: - specifier: ^3.5.13 - version: 3.5.13(typescript@5.8.3) - devDependencies: - '@iconify-json/solar': - specifier: ^1.2.2 - version: 1.2.2 - '@iconify-json/svg-spinners': - specifier: ^1.2.2 - version: 1.2.2 - '@types/audioworklet': - specifier: ^0.0.72 - version: 0.0.72 - '@unocss/reset': - specifier: ^66.1.0-beta.10 - version: 66.1.0-beta.10 - '@vitejs/plugin-vue': - specifier: ^5.2.3 - version: 5.2.3(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.3)) - unplugin-vue-router: - specifier: ^0.12.0 - version: 0.12.0(vue-router@4.5.0(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3)) - vite: - specifier: ^6.2.5 - version: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0) - vue-router: - specifier: ^4.5.0 - version: 4.5.0(vue@3.5.13(typescript@5.8.3)) - vue-tsc: - specifier: ^3.0.0-alpha.2 - version: 3.0.0-alpha.2(typescript@5.8.3) - - apps/vad-asr: - dependencies: - '@huggingface/transformers': - specifier: ^3.4.2 - version: 3.4.2 - '@vueuse/core': - specifier: ^13.0.0 - version: 13.0.0(vue@3.5.13(typescript@5.8.3)) - '@xsai/generate-transcription': - specifier: 'catalog:' - version: 0.2.0-beta.3 - defu: - specifier: ^6.1.4 - version: 6.1.4 - es-toolkit: - specifier: ^1.34.1 - version: 1.34.1 - vue: - specifier: ^3.5.13 - version: 3.5.13(typescript@5.8.3) - devDependencies: - '@iconify-json/solar': - specifier: ^1.2.2 - version: 1.2.2 - '@iconify-json/svg-spinners': - specifier: ^1.2.2 - version: 1.2.2 - '@types/audioworklet': - specifier: ^0.0.72 - version: 0.0.72 - '@unocss/reset': - specifier: ^66.1.0-beta.10 - version: 66.1.0-beta.10 - '@vitejs/plugin-vue': - specifier: ^5.2.3 - version: 5.2.3(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.3)) - unplugin-vue-router: - specifier: ^0.12.0 - version: 0.12.0(vue-router@4.5.0(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3)) - vite: - specifier: ^6.2.5 - version: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0) - vue-router: - specifier: ^4.5.0 - version: 4.5.0(vue@3.5.13(typescript@5.8.3)) - vue-tsc: - specifier: ^3.0.0-alpha.2 - version: 3.0.0-alpha.2(typescript@5.8.3) - - apps/vad-asr-chat: - dependencies: - '@huggingface/transformers': - specifier: ^3.4.2 - version: 3.4.2 - '@llama-flow/core': - specifier: ^0.3.4 - version: 0.3.4(@modelcontextprotocol/sdk@1.8.0)(zod@3.24.2) - '@vueuse/core': - specifier: ^13.0.0 - version: 13.0.0(vue@3.5.13(typescript@5.8.3)) - '@xsai/generate-transcription': - specifier: 'catalog:' - version: 0.2.0-beta.3 - '@xsai/shared': - specifier: 'catalog:' - version: 0.2.0-beta.3 - '@xsai/shared-chat': - specifier: 'catalog:' - version: 0.2.0-beta.3 - '@xsai/stream-text': - specifier: 'catalog:' - version: 0.2.0-beta.3 - defu: - specifier: ^6.1.4 - version: 6.1.4 - es-toolkit: - specifier: ^1.34.1 - version: 1.34.1 - vue: - specifier: ^3.5.13 - version: 3.5.13(typescript@5.8.3) - devDependencies: - '@iconify-json/solar': - specifier: ^1.2.2 - version: 1.2.2 - '@iconify-json/svg-spinners': - specifier: ^1.2.2 - version: 1.2.2 - '@types/audioworklet': - specifier: ^0.0.72 - version: 0.0.72 - '@unocss/reset': - specifier: ^66.1.0-beta.10 - version: 66.1.0-beta.10 - '@vitejs/plugin-vue': - specifier: ^5.2.3 - version: 5.2.3(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.3)) - unplugin-vue-router: - specifier: ^0.12.0 - version: 0.12.0(vue-router@4.5.0(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3)) - vite: - specifier: ^6.2.5 - version: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0) - vue-router: - specifier: ^4.5.0 - version: 4.5.0(vue@3.5.13(typescript@5.8.3)) - vue-tsc: - specifier: ^3.0.0-alpha.2 - version: 3.0.0-alpha.2(typescript@5.8.3) - docs: devDependencies: 98.css: @@ -3765,26 +3609,6 @@ packages: '@lezer/lr@1.4.2': resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==} - '@llama-flow/core@0.3.4': - resolution: {integrity: sha512-BOe23pfm7j9hKMH7u0jFS8bPKLsShKgz8KdN/rXeicgnopCwhDMO4ppOg7Cy7tWap8kYZIY8ZliN7Q9SmNfjkg==} - peerDependencies: - '@modelcontextprotocol/sdk': ^1.7.0 - hono: ^4.7.4 - next: ^15.2.2 - p-retry: ^6.2.1 - zod: ^3.24.2 - peerDependenciesMeta: - '@modelcontextprotocol/sdk': - optional: true - hono: - optional: true - next: - optional: true - p-retry: - optional: true - zod: - optional: true - '@marijn/find-cluster-break@1.0.2': resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} @@ -4909,9 +4733,6 @@ packages: '@types/acorn@4.0.6': resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} - '@types/audioworklet@0.0.72': - resolution: {integrity: sha512-qrsjMZxB0k5DJSLpcDXlX/OYa5iDEAc9dgEwCZO1op5jka7Jgc/fMEGq8LuHO+8MmpOInqco6hET1gawCTVeEg==} - '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -14331,11 +14152,6 @@ snapshots: dependencies: '@lezer/common': 1.2.3 - '@llama-flow/core@0.3.4(@modelcontextprotocol/sdk@1.8.0)(zod@3.24.2)': - optionalDependencies: - '@modelcontextprotocol/sdk': 1.8.0 - zod: 3.24.2 - '@marijn/find-cluster-break@1.0.2': {} '@mdit-vue/plugin-component@2.1.3': @@ -15571,8 +15387,6 @@ snapshots: dependencies: '@types/estree': 1.0.7 - '@types/audioworklet@0.0.72': {} - '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.26.10 @@ -23856,9 +23670,9 @@ snapshots: '@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.39.0) '@vueuse/core': 13.0.0(vue@3.5.13(typescript@5.8.3)) - unplugin-combine@1.2.1(esbuild@0.25.0)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)): + unplugin-combine@1.2.1(esbuild@0.19.12)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)): optionalDependencies: - esbuild: 0.25.0 + esbuild: 0.19.12 rollup: 2.79.1 unplugin: 1.16.1 vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0) @@ -23917,7 +23731,7 @@ snapshots: transitivePeerDependencies: - vue - unplugin-vue-macros@2.14.5(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.3)))(esbuild@0.25.0)(rollup@2.79.1)(typescript@5.8.3)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@3.0.0-alpha.2(typescript@5.8.3))(vue@3.5.13(typescript@5.8.3)): + unplugin-vue-macros@2.14.5(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.3)))(esbuild@0.19.12)(rollup@2.79.1)(typescript@5.8.3)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@3.0.0-alpha.2(typescript@5.8.3))(vue@3.5.13(typescript@5.8.3)): dependencies: '@vue-macros/better-define': 1.11.4(vue@3.5.13(typescript@5.8.3)) '@vue-macros/boolean-prop': 0.5.5(vue@3.5.13(typescript@5.8.3)) @@ -23949,7 +23763,7 @@ snapshots: '@vue-macros/short-vmodel': 1.5.5(vue@3.5.13(typescript@5.8.3)) '@vue-macros/volar': 0.30.15(typescript@5.8.3)(vue-tsc@3.0.0-alpha.2(typescript@5.8.3))(vue@3.5.13(typescript@5.8.3)) unplugin: 1.16.1 - unplugin-combine: 1.2.1(esbuild@0.25.0)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)) + unplugin-combine: 1.2.1(esbuild@0.19.12)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)) unplugin-vue-define-options: 1.5.5(vue@3.5.13(typescript@5.8.3)) vue: 3.5.13(typescript@5.8.3) transitivePeerDependencies: