fix(voice): ship non-asyncify ort-wasm + force single-thread
All checks were successful
Deploy / deploy (push) Successful in 3m46s

В public/vad/ были только asyncify-варианты, а onnxruntime-web по дефолту
просит ort-wasm-simd-threaded.{mjs,wasm} → 404 → MicVAD init falls.

- Положили ort-wasm-simd-threaded.{mjs,wasm} рядом.
- ortConfig forces numThreads=1, чтобы не требовать SharedArrayBuffer
  (нет COOP/COEP headers и не хотим их вешать на весь сайт).
- Раздельный getUserMedia probe перед VAD init, чтобы отличить отказ
  по микрофону от ошибки VAD/wasm в UI-сообщении.
This commit is contained in:
Cosmo
2026-04-27 09:01:52 +00:00
parent 93bf34f216
commit 96bd846a08
3 changed files with 86 additions and 6 deletions

View File

@@ -112,17 +112,38 @@ export default function VoiceController() {
const start = async () => {
if (state !== 'idle' && state !== 'error') return
setState('loading')
// 1. Сначала отдельно проверяем mic — иначе ошибка VAD маскирует разрешения.
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
// VAD откроет свой stream; этот закрываем, мы только проверяли разрешение.
stream.getTracks().forEach((t) => t.stop())
} catch (e: any) {
console.error('[voice] mic permission failed:', e?.name, e?.message)
setState('error')
const reason = e?.name === 'NotAllowedError' ? 'Нет доступа к микрофону'
: e?.name === 'NotFoundError' ? 'Микрофон не найден'
: 'Микрофон не открылся'
emitLocal('error', AGENT, reason)
return
}
// 2. Инициализируем VAD. Single-threaded WASM — без COOP/COEP headers
// threaded режим всё равно не заработает, плюс не нужна SharedArrayBuffer.
try {
const { MicVAD } = await import('@ricky0123/vad-web')
const vad = await MicVAD.new({
model: 'v5',
baseAssetPath: '/vad/',
onnxWASMBasePath: '/vad/',
// sensitivity tuned conservatively — тише = меньше ложных срабатываний эха
ortConfig: (ort: any) => {
ort.env.wasm.numThreads = 1
ort.env.wasm.simd = true
},
positiveSpeechThreshold: 0.6,
negativeSpeechThreshold: 0.45,
minSpeechMs: 160, // ~5 фреймов по 32мс
redemptionMs: 750, // тишина перед onSpeechEnd
minSpeechMs: 160,
redemptionMs: 750,
onSpeechStart: () => {
emitLocal('wake', AGENT)
},
@@ -131,10 +152,10 @@ export default function VoiceController() {
vadRef.current = vad
vad.start()
setState('active')
} catch (e) {
console.error('[voice] VAD init failed:', e)
} catch (e: any) {
console.error('[voice] VAD init failed:', e?.name, e?.message, e)
setState('error')
emitLocal('error', AGENT, 'Микрофон недоступен')
emitLocal('error', AGENT, `VAD: ${e?.message?.slice(0, 60) || 'init'}`)
}
}