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 aeddd2905..000000000
Binary files a/apps/vad-asr-chat/public/favicon.ico and /dev/null differ
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 @@
-
-
-
-
-
-
- VAD + ASR + LLM Chat Playground
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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 @@
-
-
-
-
-
-
-
-
-
-
-
-
- {{ error }}
-
-
-
-
-
- {{ message.content }}
-
-
- {{ message.content }}
-
-
-
-
-
-
-
-
-
-
-
-
-
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 aeddd2905..000000000
Binary files a/apps/vad-asr/public/favicon.ico and /dev/null differ
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 @@
-
-
-
-
-
-
- VAD + ASR Playground
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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 @@
-
-
-
-
-
-
-
-
-
-
- {{ error }}
-
-
-
-
Voice Segments ({{ segments.length }})
-
-
-
-
-
-
-
-
-
-
-
-
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 aeddd2905..000000000
Binary files a/apps/vad/public/favicon.ico and /dev/null differ
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 @@
-
-
-
-
-
-
- VAD Playground
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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 @@
-
-
-
-
-
-
-
-
- {{ error }}
-
-
-
-
Voice Segments ({{ segments.length }})
-
-
-
-
-
-
-
-
-
-
-
-
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: