P4
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { pcmRms } from './audio-pipewire.js';
|
||||
|
||||
export const DEFAULT_VAD = Object.freeze({
|
||||
threshold: 0.6,
|
||||
minSpeechDurationMs: 300,
|
||||
minSilenceDurationMs: 700,
|
||||
maxSpeechDurationMs: 15_000,
|
||||
speechPadMs: 200,
|
||||
});
|
||||
|
||||
/** A conservative local gate around QVAC's stream. It bounds audio sent to
|
||||
* ASR and gives the loop deterministic utterance boundaries in tests. */
|
||||
export class VadSegmenter extends EventEmitter {
|
||||
constructor({ sampleRate = 16_000, frameMs = 20, params = {} } = {}) {
|
||||
super();
|
||||
this.sampleRate = sampleRate;
|
||||
this.frameMs = frameMs;
|
||||
this.params = { ...DEFAULT_VAD, ...params };
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() { this.speaking = false; this.buffer = []; this.speechMs = 0; this.silenceMs = 0; }
|
||||
|
||||
push(frame) {
|
||||
const chunk = Buffer.from(frame || '');
|
||||
const rms = pcmRms(chunk);
|
||||
const voiced = rms >= this.params.threshold / 10; // PCM RMS is 0..1; QVAC threshold is posterior-like.
|
||||
this.emit('level', rms);
|
||||
if (!this.speaking && voiced) {
|
||||
this.speaking = true; this.speechMs = 0; this.silenceMs = 0; this.buffer = [];
|
||||
this.emit('speechStart');
|
||||
}
|
||||
if (!this.speaking) return;
|
||||
this.buffer.push(chunk);
|
||||
if (voiced) { this.speechMs += this.frameMs; this.silenceMs = 0; }
|
||||
else { this.silenceMs += this.frameMs; }
|
||||
if (this.speechMs >= this.params.maxSpeechDurationMs ||
|
||||
(this.speechMs >= this.params.minSpeechDurationMs && this.silenceMs >= this.params.minSilenceDurationMs)) {
|
||||
this.end();
|
||||
}
|
||||
}
|
||||
|
||||
end() {
|
||||
if (!this.speaking) return null;
|
||||
const audio = Buffer.concat(this.buffer);
|
||||
this.reset();
|
||||
this.emit('utterance', audio);
|
||||
return audio;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user