diff --git a/components/VoiceController.tsx b/components/VoiceController.tsx index a4036f2..c9c1b85 100644 --- a/components/VoiceController.tsx +++ b/components/VoiceController.tsx @@ -176,11 +176,23 @@ export default function VoiceController() { // 2. Запуск wake-word try { + // Логируем периодически max-score и просто что pipeline жив, чтобы было + // видно, что инференс идёт. + let maxScore = 0 + let scoreCount = 0 const wake = new WakeWordDetector({ modelPath: '/wake/cosmo.onnx', threshold: WAKE_THRESHOLD, onWake: (s) => onWakeDetected(s), - // onScore: (s) => { if (s > 0.1) console.log('[wake] score', s.toFixed(3)) }, + onScore: (s) => { + if (s > maxScore) maxScore = s + scoreCount++ + if (scoreCount % 25 === 0) { + console.log(`[wake] alive · max score за окно=${maxScore.toFixed(3)} · scoreCount=${scoreCount}`) + maxScore = 0 + } + if (s > 0.15) console.log(`[wake] score=${s.toFixed(3)}`) + }, onError: (e) => console.warn('[wake] error', e), }) await wake.start() diff --git a/lib/wake-word.ts b/lib/wake-word.ts index 78342de..e3c90e6 100644 --- a/lib/wake-word.ts +++ b/lib/wake-word.ts @@ -102,7 +102,10 @@ export class WakeWordDetector { async start(externalStream?: MediaStream): Promise { if (this.running) return + console.log('[wake] start: loading ort + models') + const t0 = performance.now() const ort = await getOrt() + console.log(`[wake] ort ready in ${(performance.now() - t0).toFixed(0)}ms`) // 1. Загружаем модели параллельно (до user gesture, чтобы AudioContext не висел) const [mel, emb, cls] = await Promise.all([ @@ -116,14 +119,18 @@ export class WakeWordDetector { this.melInName = mel.inputNames[0]; this.melOutName = mel.outputNames[0] this.embInName = emb.inputNames[0]; this.embOutName = emb.outputNames[0] this.clsInName = cls.inputNames[0]; this.clsOutName = cls.outputNames[0] + console.log(`[wake] models loaded in ${(performance.now() - t0).toFixed(0)}ms`, + { mel: { in: this.melInName, out: this.melOutName }, + emb: { in: this.embInName, out: this.embOutName }, + cls: { in: this.clsInName, out: this.clsOutName } }) // 2. Audio context @ 16kHz (если браузер не уважит — обработаем на стороне) this.ctx = new AudioContext({ sampleRate: 16000 }) if (this.ctx.state === 'suspended') await this.ctx.resume() + console.log(`[wake] AudioContext sampleRate=${this.ctx.sampleRate} state=${this.ctx.state}`) if (this.ctx.sampleRate !== 16000) { - // На некоторых платформах sampleRate может не получиться. Не валим — ниже даунсэмпл-ниже не делаем, - // openWakeWord не выдержит другую частоту. Сообщим в onError, но попробуем работать. - this.opts.onError?.(new Error(`AudioContext sampleRate=${this.ctx.sampleRate}, нужен 16000`)) + console.warn(`[wake] AudioContext sampleRate=${this.ctx.sampleRate}, ожидается 16000 — wake-word скорее всего не сработает`) + this.opts.onError?.(new Error(`AudioContext sampleRate=${this.ctx.sampleRate}`)) } // 3. Mic stream @@ -135,11 +142,17 @@ export class WakeWordDetector { await this.ctx.audioWorklet.addModule(this.opts.workletPath) this.source = this.ctx.createMediaStreamSource(this.stream) this.worklet = new AudioWorkletNode(this.ctx, 'wake-capture') - this.worklet.port.onmessage = (e) => this.onChunk(e.data as Float32Array) + let chunkCount = 0 + this.worklet.port.onmessage = (e) => { + if (chunkCount === 0) console.log('[wake] first audio chunk received') + chunkCount++ + this.onChunk(e.data as Float32Array) + } this.source.connect(this.worklet) // Worklet не подключается к destination → не звучит в колонках. this.running = true + console.log('[wake] running') } async stop(): Promise {