diff --git a/apps/realtime-audio/tsconfig.json b/apps/realtime-audio/tsconfig.json index 4cdfa5b2f..65b580d32 100644 --- a/apps/realtime-audio/tsconfig.json +++ b/apps/realtime-audio/tsconfig.json @@ -23,10 +23,6 @@ "include": [ "src/**/*.ts", "src/**/*.d.ts", - "src/**/*.mts", - "playground/**/*.ts", - "playground/**/*.d.ts", - "playground/**/*.mts", - "playground/**/*.vue" + "src/**/*.mts" ] } diff --git a/apps/vad/index.html b/apps/vad/index.html new file mode 100644 index 000000000..42e6a40d9 --- /dev/null +++ b/apps/vad/index.html @@ -0,0 +1,22 @@ + + + + + Project AIRI VAD Playground + + + + + +
+ + + + diff --git a/apps/vad/locales/en.yml b/apps/vad/locales/en.yml new file mode 100644 index 000000000..bf1d5461c --- /dev/null +++ b/apps/vad/locales/en.yml @@ -0,0 +1 @@ +title: VAD diff --git a/apps/vad/locales/zh-CN.yml b/apps/vad/locales/zh-CN.yml new file mode 100644 index 000000000..bf1d5461c --- /dev/null +++ b/apps/vad/locales/zh-CN.yml @@ -0,0 +1 @@ +title: VAD diff --git a/apps/vad/netlify.toml b/apps/vad/netlify.toml new file mode 100755 index 000000000..e26eeac5d --- /dev/null +++ b/apps/vad/netlify.toml @@ -0,0 +1,13 @@ +[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 new file mode 100644 index 000000000..49fa3a9ac --- /dev/null +++ b/apps/vad/package.json @@ -0,0 +1,41 @@ +{ + "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 new file mode 100644 index 000000000..aeddd2905 Binary files /dev/null and b/apps/vad/public/favicon.ico differ diff --git a/apps/vad/public/favicon.svg b/apps/vad/public/favicon.svg new file mode 100644 index 000000000..a52f4ddeb --- /dev/null +++ b/apps/vad/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/vad/src/App.vue b/apps/vad/src/App.vue new file mode 100644 index 000000000..61b6b87ef --- /dev/null +++ b/apps/vad/src/App.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/apps/vad/src/libs/vad/manager.ts b/apps/vad/src/libs/vad/manager.ts new file mode 100644 index 000000000..4c5c20a91 --- /dev/null +++ b/apps/vad/src/libs/vad/manager.ts @@ -0,0 +1,160 @@ +// 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 + } + } + + /** + * 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 new file mode 100644 index 000000000..899d933fe --- /dev/null +++ b/apps/vad/src/libs/vad/process.worklet.ts @@ -0,0 +1,53 @@ +// 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 new file mode 100644 index 000000000..bca783d93 --- /dev/null +++ b/apps/vad/src/libs/vad/vad.ts @@ -0,0 +1,319 @@ +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' }, + 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 new file mode 100644 index 000000000..22cadbac9 --- /dev/null +++ b/apps/vad/src/libs/vad/wav.ts @@ -0,0 +1,43 @@ +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 new file mode 100644 index 000000000..52e83a7af --- /dev/null +++ b/apps/vad/src/main.ts @@ -0,0 +1,13 @@ +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 new file mode 100644 index 000000000..bfae9587b --- /dev/null +++ b/apps/vad/src/pages/index.vue @@ -0,0 +1,168 @@ + + + diff --git a/apps/vad/src/utils/loader.ts b/apps/vad/src/utils/loader.ts new file mode 100644 index 000000000..5afb6cb09 --- /dev/null +++ b/apps/vad/src/utils/loader.ts @@ -0,0 +1,40 @@ +import type { WasmModule } from '../libs/vad' + +export async function loadWasmModule(wasmUrl: string): Promise { + return new Promise((resolve, reject) => { + // Create a script element to load the WASM module + const script = document.createElement('script') + script.src = wasmUrl + script.async = true + + script.onload = () => { + // Access the global Module object + const Module = (window as any).Module + + if (!Module) { + reject(new Error('Failed to load WASM module: Module not found')) + return + } + + if (Module.onRuntimeInitialized) { + const originalOnRuntimeInitialized = Module.onRuntimeInitialized + Module.onRuntimeInitialized = () => { + if (originalOnRuntimeInitialized) { + originalOnRuntimeInitialized() + } + resolve(Module) + } + } + else { + // Module is already initialized + resolve(Module) + } + } + + script.onerror = () => { + reject(new Error('Failed to load WASM module')) + } + + document.head.appendChild(script) + }) +} diff --git a/apps/vad/tsconfig.json b/apps/vad/tsconfig.json new file mode 100644 index 000000000..75af8ab4f --- /dev/null +++ b/apps/vad/tsconfig.json @@ -0,0 +1,31 @@ +{ + "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" + ], + "allowJs": true, + "strict": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "noEmit": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "skipLibCheck": true + }, + "exclude": ["dist", "node_modules"] +} diff --git a/apps/vad/uno.config.ts b/apps/vad/uno.config.ts new file mode 100644 index 000000000..241b30eaa --- /dev/null +++ b/apps/vad/uno.config.ts @@ -0,0 +1,37 @@ +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 new file mode 100644 index 000000000..5da74dfbe --- /dev/null +++ b/apps/vad/vite.config.ts @@ -0,0 +1,19 @@ +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/packages/gpuu/src/webgpu/checker.ts b/packages/gpuu/src/webgpu/checker.ts index 778e570e3..9a8fc17da 100644 --- a/packages/gpuu/src/webgpu/checker.ts +++ b/packages/gpuu/src/webgpu/checker.ts @@ -34,3 +34,7 @@ function isInNodejsRuntime() { // eslint-disable-next-line node/prefer-global/process && 'node' in process.versions && process.versions.node != null } + +export async function isWebGPUSupported() { + return check().then(result => result.supported) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc6711554..1bb6731e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -458,7 +458,7 @@ importers: version: 2.3.0 '@intlify/unplugin-vue-i18n': specifier: ^6.0.5 - version: 6.0.5(@vue/compiler-dom@3.5.13)(eslint@9.24.0(jiti@2.4.2))(rollup@4.39.0)(typescript@5.8.3)(vue-i18n@11.1.3(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3)) + version: 6.0.5(@vue/compiler-dom@3.5.13)(eslint@9.24.0(jiti@2.4.2))(rollup@2.79.1)(typescript@5.8.3)(vue-i18n@11.1.3(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3)) '@proj-airi/lobe-icons': specifier: ^1.0.5 version: 1.0.5 @@ -506,7 +506,7 @@ importers: version: 3.0.0-beta.7(typescript@5.8.3)(vue-tsc@3.0.0-alpha.2(typescript@5.8.3))(vue@3.5.13(typescript@5.8.3)) '@vueuse/motion': specifier: ^3.0.3 - version: 3.0.3(magicast@0.3.5)(rollup@4.39.0)(vue@3.5.13(typescript@5.8.3)) + version: 3.0.3(magicast@0.3.5)(rollup@2.79.1)(vue@3.5.13(typescript@5.8.3)) less: specifier: ^4.3.0 version: 4.3.0 @@ -518,13 +518,13 @@ importers: version: 3.2.0(unocss@66.1.0-beta.10(postcss@8.5.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-auto-import: specifier: ^19.1.2 - version: 19.1.2(@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))) + version: 19.1.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.3))) unplugin-vue-components: specifier: ^28.4.1 - version: 28.4.1(@babel/parser@7.26.10)(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.39.0))(vue@3.5.13(typescript@5.8.3)) + 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@4.39.0)(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.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-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)) @@ -533,13 +533,13 @@ importers: 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-bundle-visualizer: specifier: ^1.2.1 - version: 1.2.1(rollup@4.39.0) + version: 1.2.1(rollup@2.79.1) vite-plugin-pwa: specifier: ^1.0.0 version: 1.0.0(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))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0) vite-plugin-vue-devtools: specifier: ^7.7.2 - version: 7.7.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.39.0))(rollup@4.39.0)(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)) + version: 7.7.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(rollup@2.79.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))(vue@3.5.13(typescript@5.8.3)) vite-plugin-vue-layouts: specifier: ^0.11.0 version: 0.11.0(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-router@4.5.0(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3)) @@ -807,7 +807,7 @@ importers: version: 2.3.0 '@intlify/unplugin-vue-i18n': specifier: ^6.0.5 - version: 6.0.5(@vue/compiler-dom@3.5.13)(eslint@9.24.0(jiti@2.4.2))(rollup@2.79.1)(typescript@5.8.3)(vue-i18n@11.1.3(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3)) + version: 6.0.5(@vue/compiler-dom@3.5.13)(eslint@9.24.0(jiti@2.4.2))(rollup@4.39.0)(typescript@5.8.3)(vue-i18n@11.1.3(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3)) '@proj-airi/lobe-icons': specifier: ^1.0.5 version: 1.0.5 @@ -846,7 +846,7 @@ importers: version: 3.0.0-beta.7(typescript@5.8.3)(vue-tsc@3.0.0-alpha.2(typescript@5.8.3))(vue@3.5.13(typescript@5.8.3)) '@vueuse/motion': specifier: ^3.0.3 - version: 3.0.3(magicast@0.3.5)(rollup@2.79.1)(vue@3.5.13(typescript@5.8.3)) + version: 3.0.3(magicast@0.3.5)(rollup@4.39.0)(vue@3.5.13(typescript@5.8.3)) hfup: specifier: ^0.5.0 version: 0.5.0(@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) @@ -858,13 +858,13 @@ importers: version: 4.0.1 unplugin-auto-import: specifier: ^19.1.2 - version: 19.1.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.3))) + version: 19.1.2(@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-vue-components: specifier: ^28.4.1 - 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)) + version: 28.4.1(@babel/parser@7.26.10)(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.39.0))(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.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)) + version: 2.14.5(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.3)))(esbuild@0.25.0)(rollup@4.39.0)(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)) @@ -873,13 +873,13 @@ importers: 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-bundle-visualizer: specifier: ^1.2.1 - version: 1.2.1(rollup@2.79.1) + version: 1.2.1(rollup@4.39.0) vite-plugin-pwa: specifier: ^1.0.0 version: 1.0.0(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))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0) vite-plugin-vue-devtools: specifier: ^7.7.2 - version: 7.7.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(rollup@2.79.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))(vue@3.5.13(typescript@5.8.3)) + version: 7.7.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.39.0))(rollup@4.39.0)(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)) vite-plugin-vue-layouts: specifier: ^0.11.0 version: 0.11.0(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-router@4.5.0(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3)) @@ -887,6 +887,52 @@ 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) + docs: devDependencies: 98.css: @@ -4730,6 +4776,9 @@ 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==} @@ -15384,6 +15433,8 @@ snapshots: dependencies: '@types/estree': 1.0.7 + '@types/audioworklet@0.0.72': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.26.10 @@ -23667,9 +23718,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.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-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)): optionalDependencies: - esbuild: 0.19.12 + 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) @@ -23728,7 +23779,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.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-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)): 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)) @@ -23760,7 +23811,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.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-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-vue-define-options: 1.5.5(vue@3.5.13(typescript@5.8.3)) vue: 3.5.13(typescript@5.8.3) transitivePeerDependencies: