File size: 2,803 Bytes
7496c22
 
 
 
 
 
 
 
 
 
 
 
 
 
8545065
7496c22
 
 
 
 
 
8545065
7496c22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
// @ts-check
/**
 * Turns per-frame speech probabilities into utterances. Pure logic (no model,
 * no audio I/O) so it can be unit tested.
 *
 * Two thresholds: normally a short confident run opens an utterance, but while
 * she is talking ("guarded") the bar is higher and longer, so a cough or leaked
 * speaker audio doesn't cut her off while a real interruption still does.
 */

export const FRAME_SAMPLES = 512; // 32 ms at 16 kHz

export const DEFAULTS = {
  startProb: 0.5,
  startFrames: 4, // 128 ms
  guardedStartProb: 0.75,
  guardedStartFrames: 9, // 288 ms
  endProb: 0.35,
  endFrames: 17, // 544 ms of quiet ends the turn
  trailFrames: 6, // keep 192 ms of the quiet tail
  prerollFrames: 12, // 384 ms before the trigger, so onsets aren't clipped
  minSpeechFrames: 8, // 256 ms of actual speech, or it was a misfire
  maxFrames: 938, // 30 s
};

export class Segmenter {
  /** @param {Partial<typeof DEFAULTS>} [opt] */
  constructor(opt = {}) {
    this.opt = { ...DEFAULTS, ...opt };
    this.guarded = false;
    this.reset();
  }

  reset() {
    /** @type {Float32Array[]} */
    this._preroll = [];
    /** @type {Float32Array[]} */
    this._speech = [];
    this._inSpeech = false;
    this._run = 0;
    this._quiet = 0;
    this._voiced = 0;
  }

  /**
   * @param {Float32Array} frame
   * @param {number} prob
   * @returns {{ kind: "start" } | { kind: "end", samples: Float32Array } | { kind: "misfire" } | null}
   */
  push(frame, prob) {
    const o = this.opt;
    if (!this._inSpeech) {
      this._preroll.push(frame);
      if (this._preroll.length > o.prerollFrames + o.guardedStartFrames) this._preroll.shift();
      const startProb = this.guarded ? o.guardedStartProb : o.startProb;
      const startFrames = this.guarded ? o.guardedStartFrames : o.startFrames;
      this._run = prob >= startProb ? this._run + 1 : 0;
      if (this._run < startFrames) return null;
      this._inSpeech = true;
      this._speech = this._preroll;
      this._preroll = [];
      this._voiced = this._run;
      this._quiet = 0;
      this._run = 0;
      return { kind: "start" };
    }

    this._speech.push(frame);
    if (prob >= o.endProb) {
      this._quiet = 0;
      this._voiced += 1;
    } else {
      this._quiet += 1;
    }
    if (this._quiet >= o.endFrames || this._speech.length >= o.maxFrames) {
      const dropTail = Math.max(0, this._quiet - o.trailFrames);
      const kept = this._speech.slice(0, this._speech.length - dropTail);
      const voiced = this._voiced;
      this.reset();
      if (voiced < o.minSpeechFrames) return { kind: "misfire" };
      const samples = new Float32Array(kept.length * FRAME_SAMPLES);
      kept.forEach((f, i) => samples.set(f, i * FRAME_SAMPLES));
      return { kind: "end", samples };
    }
    return null;
  }
}