debug(voice): verbose logging для wake-word pipeline
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
This commit is contained in:
@@ -176,11 +176,23 @@ export default function VoiceController() {
|
|||||||
|
|
||||||
// 2. Запуск wake-word
|
// 2. Запуск wake-word
|
||||||
try {
|
try {
|
||||||
|
// Логируем периодически max-score и просто что pipeline жив, чтобы было
|
||||||
|
// видно, что инференс идёт.
|
||||||
|
let maxScore = 0
|
||||||
|
let scoreCount = 0
|
||||||
const wake = new WakeWordDetector({
|
const wake = new WakeWordDetector({
|
||||||
modelPath: '/wake/cosmo.onnx',
|
modelPath: '/wake/cosmo.onnx',
|
||||||
threshold: WAKE_THRESHOLD,
|
threshold: WAKE_THRESHOLD,
|
||||||
onWake: (s) => onWakeDetected(s),
|
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),
|
onError: (e) => console.warn('[wake] error', e),
|
||||||
})
|
})
|
||||||
await wake.start()
|
await wake.start()
|
||||||
|
|||||||
@@ -102,7 +102,10 @@ export class WakeWordDetector {
|
|||||||
|
|
||||||
async start(externalStream?: MediaStream): Promise<void> {
|
async start(externalStream?: MediaStream): Promise<void> {
|
||||||
if (this.running) return
|
if (this.running) return
|
||||||
|
console.log('[wake] start: loading ort + models')
|
||||||
|
const t0 = performance.now()
|
||||||
const ort = await getOrt()
|
const ort = await getOrt()
|
||||||
|
console.log(`[wake] ort ready in ${(performance.now() - t0).toFixed(0)}ms`)
|
||||||
|
|
||||||
// 1. Загружаем модели параллельно (до user gesture, чтобы AudioContext не висел)
|
// 1. Загружаем модели параллельно (до user gesture, чтобы AudioContext не висел)
|
||||||
const [mel, emb, cls] = await Promise.all([
|
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.melInName = mel.inputNames[0]; this.melOutName = mel.outputNames[0]
|
||||||
this.embInName = emb.inputNames[0]; this.embOutName = emb.outputNames[0]
|
this.embInName = emb.inputNames[0]; this.embOutName = emb.outputNames[0]
|
||||||
this.clsInName = cls.inputNames[0]; this.clsOutName = cls.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 (если браузер не уважит — обработаем на стороне)
|
// 2. Audio context @ 16kHz (если браузер не уважит — обработаем на стороне)
|
||||||
this.ctx = new AudioContext({ sampleRate: 16000 })
|
this.ctx = new AudioContext({ sampleRate: 16000 })
|
||||||
if (this.ctx.state === 'suspended') await this.ctx.resume()
|
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) {
|
if (this.ctx.sampleRate !== 16000) {
|
||||||
// На некоторых платформах sampleRate может не получиться. Не валим — ниже даунсэмпл-ниже не делаем,
|
console.warn(`[wake] AudioContext sampleRate=${this.ctx.sampleRate}, ожидается 16000 — wake-word скорее всего не сработает`)
|
||||||
// openWakeWord не выдержит другую частоту. Сообщим в onError, но попробуем работать.
|
this.opts.onError?.(new Error(`AudioContext sampleRate=${this.ctx.sampleRate}`))
|
||||||
this.opts.onError?.(new Error(`AudioContext sampleRate=${this.ctx.sampleRate}, нужен 16000`))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Mic stream
|
// 3. Mic stream
|
||||||
@@ -135,11 +142,17 @@ export class WakeWordDetector {
|
|||||||
await this.ctx.audioWorklet.addModule(this.opts.workletPath)
|
await this.ctx.audioWorklet.addModule(this.opts.workletPath)
|
||||||
this.source = this.ctx.createMediaStreamSource(this.stream)
|
this.source = this.ctx.createMediaStreamSource(this.stream)
|
||||||
this.worklet = new AudioWorkletNode(this.ctx, 'wake-capture')
|
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)
|
this.source.connect(this.worklet)
|
||||||
// Worklet не подключается к destination → не звучит в колонках.
|
// Worklet не подключается к destination → не звучит в колонках.
|
||||||
|
|
||||||
this.running = true
|
this.running = true
|
||||||
|
console.log('[wake] running')
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
|
|||||||
Reference in New Issue
Block a user