@framers/agentos-ext-diarization 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +23 -0
  2. package/SKILL.md +71 -0
  3. package/dist/ClusteringStrategy.d.ts +56 -0
  4. package/dist/ClusteringStrategy.d.ts.map +1 -0
  5. package/dist/ClusteringStrategy.js +123 -0
  6. package/dist/ClusteringStrategy.js.map +1 -0
  7. package/dist/DiarizationEngine.d.ts +45 -0
  8. package/dist/DiarizationEngine.d.ts.map +1 -0
  9. package/dist/DiarizationEngine.js +84 -0
  10. package/dist/DiarizationEngine.js.map +1 -0
  11. package/dist/DiarizationSession.d.ts +90 -0
  12. package/dist/DiarizationSession.d.ts.map +1 -0
  13. package/dist/DiarizationSession.js +139 -0
  14. package/dist/DiarizationSession.js.map +1 -0
  15. package/dist/LocalDiarizationBackend.d.ts +90 -0
  16. package/dist/LocalDiarizationBackend.d.ts.map +1 -0
  17. package/dist/LocalDiarizationBackend.js +177 -0
  18. package/dist/LocalDiarizationBackend.js.map +1 -0
  19. package/dist/ProviderDiarizationBackend.d.ts +54 -0
  20. package/dist/ProviderDiarizationBackend.d.ts.map +1 -0
  21. package/dist/ProviderDiarizationBackend.js +101 -0
  22. package/dist/ProviderDiarizationBackend.js.map +1 -0
  23. package/dist/SlidingWindowExtractor.d.ts +85 -0
  24. package/dist/SlidingWindowExtractor.d.ts.map +1 -0
  25. package/dist/SlidingWindowExtractor.js +140 -0
  26. package/dist/SlidingWindowExtractor.js.map +1 -0
  27. package/dist/SpeakerEmbeddingCache.d.ts +117 -0
  28. package/dist/SpeakerEmbeddingCache.d.ts.map +1 -0
  29. package/dist/SpeakerEmbeddingCache.js +193 -0
  30. package/dist/SpeakerEmbeddingCache.js.map +1 -0
  31. package/dist/index.d.ts +67 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +73 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/types.d.ts +177 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/dist/types.js +13 -0
  38. package/dist/types.js.map +1 -0
  39. package/manifest.json +8 -0
  40. package/package.json +48 -0
  41. package/src/ClusteringStrategy.ts +139 -0
  42. package/src/DiarizationEngine.ts +94 -0
  43. package/src/DiarizationSession.ts +178 -0
  44. package/src/LocalDiarizationBackend.ts +201 -0
  45. package/src/ProviderDiarizationBackend.ts +119 -0
  46. package/src/SlidingWindowExtractor.ts +174 -0
  47. package/src/SpeakerEmbeddingCache.ts +228 -0
  48. package/src/index.ts +105 -0
  49. package/src/types.ts +221 -0
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @file LocalDiarizationBackend.ts
3
+ * @description Offline speaker diarization backend using a sliding-window
4
+ * spectral-centroid voiceprint and cosine similarity matching.
5
+ *
6
+ * Audio is accumulated in a {@link SlidingWindowExtractor}. When a full chunk
7
+ * is ready, a 16-dimensional feature vector is extracted and matched against
8
+ * cached speaker centroids. If no centroid exceeds the similarity threshold a
9
+ * new `Speaker_N` identity is created.
10
+ *
11
+ * The built-in feature extractor is intentionally lightweight. An ONNX
12
+ * x-vector model can replace {@link LocalDiarizationBackend.extractSimpleEmbedding}
13
+ * later without changing the surrounding API.
14
+ *
15
+ * @module diarization/LocalDiarizationBackend
16
+ */
17
+ import { EventEmitter } from 'node:events';
18
+ import type { SpeakerEmbeddingCache } from './SpeakerEmbeddingCache.js';
19
+ import type { SlidingWindowExtractor } from './SlidingWindowExtractor.js';
20
+ /**
21
+ * Fully offline diarization backend.
22
+ *
23
+ * Connects a {@link SlidingWindowExtractor} to a {@link SpeakerEmbeddingCache}
24
+ * and emits `speaker_identified` events whenever a speaker label is resolved
25
+ * for a new audio chunk.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const cache = new SpeakerEmbeddingCache(0.7);
30
+ * const extractor = new SlidingWindowExtractor(1500, 500, 16000);
31
+ * const backend = new LocalDiarizationBackend(cache, extractor);
32
+ *
33
+ * backend.on('speaker_identified', ({ speakerId }) => console.log(speakerId));
34
+ * backend.start();
35
+ *
36
+ * microphone.on('frame', (f) => extractor.pushAudio(f));
37
+ * ```
38
+ */
39
+ export declare class LocalDiarizationBackend extends EventEmitter {
40
+ private readonly cache;
41
+ private readonly extractor;
42
+ /**
43
+ * @param cache - Speaker voiceprint cache used for matching and centroid
44
+ * updates.
45
+ * @param extractor - Sliding-window extractor that supplies audio chunks.
46
+ */
47
+ constructor(cache: SpeakerEmbeddingCache, extractor: SlidingWindowExtractor);
48
+ /**
49
+ * Attach the `'chunk_ready'` listener to the extractor and begin processing
50
+ * audio chunks.
51
+ *
52
+ * Calling `start()` more than once is safe — subsequent calls are no-ops
53
+ * because the same bound listener is reused.
54
+ */
55
+ start(): void;
56
+ /**
57
+ * Detach from the extractor and stop processing.
58
+ */
59
+ stop(): void;
60
+ /**
61
+ * Bound handler for `'chunk_ready'` events from the extractor.
62
+ *
63
+ * Stored as an arrow-function property so the same reference is used for
64
+ * both `on()` and `off()`.
65
+ */
66
+ private readonly _onChunkReady;
67
+ /**
68
+ * Extract a 16-dimensional voiceprint from a raw PCM chunk.
69
+ *
70
+ * Feature layout (4 octave bands × 4 features per band):
71
+ * - Band 0: sub-bass (0 – 500 Hz equivalent bucket)
72
+ * - Band 1: bass (500 – 2000 Hz equivalent bucket)
73
+ * - Band 2: mid (2000 – 4000 Hz equivalent bucket)
74
+ * - Band 3: high (4000 Hz+ equivalent bucket)
75
+ *
76
+ * For each band the following statistics are computed:
77
+ * - [0] RMS energy
78
+ * - [1] Spectral centroid (normalised to band range)
79
+ * - [2] Zero-crossing rate
80
+ * - [3] Delta energy (mean absolute frame-to-frame change)
81
+ *
82
+ * This is a placeholder implementation suitable for demos and testing.
83
+ * Replace with an ONNX x-vector model for production accuracy.
84
+ *
85
+ * @param chunk - Float32 PCM audio data.
86
+ * @returns 16-dimensional embedding vector.
87
+ */
88
+ extractSimpleEmbedding(chunk: Float32Array): Float32Array;
89
+ }
90
+ //# sourceMappingURL=LocalDiarizationBackend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalDiarizationBackend.d.ts","sourceRoot":"","sources":["../src/LocalDiarizationBackend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACxE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAoB1E;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,uBAAwB,SAAQ,YAAY;IAWrD,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAP5B;;;;OAIG;gBAEgB,KAAK,EAAE,qBAAqB,EAC5B,SAAS,EAAE,sBAAsB;IASpD;;;;;;OAMG;IACH,KAAK,IAAI,IAAI;IAIb;;OAEG;IACH,IAAI,IAAI,IAAI;IAQZ;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,aAAa,CAW5B;IAMF;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,sBAAsB,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY;CAuD1D"}
@@ -0,0 +1,177 @@
1
+ /**
2
+ * @file LocalDiarizationBackend.ts
3
+ * @description Offline speaker diarization backend using a sliding-window
4
+ * spectral-centroid voiceprint and cosine similarity matching.
5
+ *
6
+ * Audio is accumulated in a {@link SlidingWindowExtractor}. When a full chunk
7
+ * is ready, a 16-dimensional feature vector is extracted and matched against
8
+ * cached speaker centroids. If no centroid exceeds the similarity threshold a
9
+ * new `Speaker_N` identity is created.
10
+ *
11
+ * The built-in feature extractor is intentionally lightweight. An ONNX
12
+ * x-vector model can replace {@link LocalDiarizationBackend.extractSimpleEmbedding}
13
+ * later without changing the surrounding API.
14
+ *
15
+ * @module diarization/LocalDiarizationBackend
16
+ */
17
+ import { EventEmitter } from 'node:events';
18
+ // ---------------------------------------------------------------------------
19
+ // Constants
20
+ // ---------------------------------------------------------------------------
21
+ /** Size of the local feature vector in dimensions. */
22
+ const FEATURE_DIMS = 16;
23
+ /** Number of octave-band buckets used by the feature extractor. */
24
+ const OCTAVE_BANDS = 4;
25
+ /** Dimensions per octave band (FEATURE_DIMS / OCTAVE_BANDS). */
26
+ const DIMS_PER_BAND = FEATURE_DIMS / OCTAVE_BANDS;
27
+ // ---------------------------------------------------------------------------
28
+ // Main class
29
+ // ---------------------------------------------------------------------------
30
+ /**
31
+ * Fully offline diarization backend.
32
+ *
33
+ * Connects a {@link SlidingWindowExtractor} to a {@link SpeakerEmbeddingCache}
34
+ * and emits `speaker_identified` events whenever a speaker label is resolved
35
+ * for a new audio chunk.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * const cache = new SpeakerEmbeddingCache(0.7);
40
+ * const extractor = new SlidingWindowExtractor(1500, 500, 16000);
41
+ * const backend = new LocalDiarizationBackend(cache, extractor);
42
+ *
43
+ * backend.on('speaker_identified', ({ speakerId }) => console.log(speakerId));
44
+ * backend.start();
45
+ *
46
+ * microphone.on('frame', (f) => extractor.pushAudio(f));
47
+ * ```
48
+ */
49
+ export class LocalDiarizationBackend extends EventEmitter {
50
+ cache;
51
+ extractor;
52
+ // -------------------------------------------------------------------------
53
+ // Constructor
54
+ // -------------------------------------------------------------------------
55
+ /**
56
+ * @param cache - Speaker voiceprint cache used for matching and centroid
57
+ * updates.
58
+ * @param extractor - Sliding-window extractor that supplies audio chunks.
59
+ */
60
+ constructor(cache, extractor) {
61
+ super();
62
+ this.cache = cache;
63
+ this.extractor = extractor;
64
+ }
65
+ // -------------------------------------------------------------------------
66
+ // Public API
67
+ // -------------------------------------------------------------------------
68
+ /**
69
+ * Attach the `'chunk_ready'` listener to the extractor and begin processing
70
+ * audio chunks.
71
+ *
72
+ * Calling `start()` more than once is safe — subsequent calls are no-ops
73
+ * because the same bound listener is reused.
74
+ */
75
+ start() {
76
+ this.extractor.on('chunk_ready', this._onChunkReady);
77
+ }
78
+ /**
79
+ * Detach from the extractor and stop processing.
80
+ */
81
+ stop() {
82
+ this.extractor.off('chunk_ready', this._onChunkReady);
83
+ }
84
+ // -------------------------------------------------------------------------
85
+ // Internal event handler
86
+ // -------------------------------------------------------------------------
87
+ /**
88
+ * Bound handler for `'chunk_ready'` events from the extractor.
89
+ *
90
+ * Stored as an arrow-function property so the same reference is used for
91
+ * both `on()` and `off()`.
92
+ */
93
+ _onChunkReady = (chunk) => {
94
+ const embedding = this.extractSimpleEmbedding(chunk);
95
+ const { speakerId } = this.cache.getOrCreateSpeaker(embedding);
96
+ const payload = {
97
+ speakerId,
98
+ confidence: 0.7,
99
+ timestamp: Date.now(),
100
+ };
101
+ this.emit('speaker_identified', payload);
102
+ };
103
+ // -------------------------------------------------------------------------
104
+ // Feature extraction
105
+ // -------------------------------------------------------------------------
106
+ /**
107
+ * Extract a 16-dimensional voiceprint from a raw PCM chunk.
108
+ *
109
+ * Feature layout (4 octave bands × 4 features per band):
110
+ * - Band 0: sub-bass (0 – 500 Hz equivalent bucket)
111
+ * - Band 1: bass (500 – 2000 Hz equivalent bucket)
112
+ * - Band 2: mid (2000 – 4000 Hz equivalent bucket)
113
+ * - Band 3: high (4000 Hz+ equivalent bucket)
114
+ *
115
+ * For each band the following statistics are computed:
116
+ * - [0] RMS energy
117
+ * - [1] Spectral centroid (normalised to band range)
118
+ * - [2] Zero-crossing rate
119
+ * - [3] Delta energy (mean absolute frame-to-frame change)
120
+ *
121
+ * This is a placeholder implementation suitable for demos and testing.
122
+ * Replace with an ONNX x-vector model for production accuracy.
123
+ *
124
+ * @param chunk - Float32 PCM audio data.
125
+ * @returns 16-dimensional embedding vector.
126
+ */
127
+ extractSimpleEmbedding(chunk) {
128
+ const features = new Float32Array(FEATURE_DIMS);
129
+ const n = chunk.length;
130
+ if (n === 0)
131
+ return features;
132
+ // Split the chunk index range into OCTAVE_BANDS equal bands.
133
+ const bandSize = Math.ceil(n / OCTAVE_BANDS);
134
+ for (let band = 0; band < OCTAVE_BANDS; band++) {
135
+ const start = band * bandSize;
136
+ const end = Math.min(start + bandSize, n);
137
+ const bandLen = end - start;
138
+ if (bandLen === 0)
139
+ continue;
140
+ // --- RMS energy ---
141
+ let sumSq = 0;
142
+ for (let i = start; i < end; i++)
143
+ sumSq += (chunk[i]) ** 2;
144
+ const rms = Math.sqrt(sumSq / bandLen);
145
+ // --- Spectral centroid (weighted mean index, normalised to [0,1]) ---
146
+ let weightedIdx = 0;
147
+ let totalAbs = 0;
148
+ for (let i = start; i < end; i++) {
149
+ const abs = Math.abs(chunk[i]);
150
+ weightedIdx += (i - start) * abs;
151
+ totalAbs += abs;
152
+ }
153
+ const centroid = totalAbs > 0 ? weightedIdx / totalAbs / bandLen : 0;
154
+ // --- Zero-crossing rate ---
155
+ let zeroCrossings = 0;
156
+ for (let i = start + 1; i < end; i++) {
157
+ if ((chunk[i] >= 0) !== (chunk[i - 1] >= 0))
158
+ zeroCrossings++;
159
+ }
160
+ const zcr = zeroCrossings / (bandLen - 1);
161
+ // --- Delta energy (mean |diff| between consecutive samples) ---
162
+ let deltaSum = 0;
163
+ for (let i = start + 1; i < end; i++) {
164
+ deltaSum += Math.abs(chunk[i] - chunk[i - 1]);
165
+ }
166
+ const delta = deltaSum / (bandLen - 1);
167
+ // Write into the feature vector.
168
+ const base = band * DIMS_PER_BAND;
169
+ features[base + 0] = rms;
170
+ features[base + 1] = centroid;
171
+ features[base + 2] = zcr;
172
+ features[base + 3] = delta;
173
+ }
174
+ return features;
175
+ }
176
+ }
177
+ //# sourceMappingURL=LocalDiarizationBackend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalDiarizationBackend.js","sourceRoot":"","sources":["../src/LocalDiarizationBackend.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @file ProviderDiarizationBackend.ts
3
+ * @description Thin adapter that extracts speaker labels from STT provider
4
+ * transcript events containing word-level speaker annotations.
5
+ *
6
+ * Many cloud STT providers (Deepgram, AssemblyAI, …) support native
7
+ * diarization that produces a `speaker` label on each word in the transcript.
8
+ * This backend consumes those events and re-emits them as
9
+ * {@link DiarizedSegment} objects without any additional compute.
10
+ *
11
+ * @module diarization/ProviderDiarizationBackend
12
+ */
13
+ import type { TranscriptEvent, DiarizedSegment } from './types.js';
14
+ /**
15
+ * Extracts speaker labels from word-level STT results.
16
+ *
17
+ * Stateless — each call to {@link pushTranscriptEvent} is independent. The
18
+ * caller is responsible for any session-level bookkeeping.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * const backend = new ProviderDiarizationBackend();
23
+ * sttSession.on('transcript', (event) => {
24
+ * const segment = backend.pushTranscriptEvent(event);
25
+ * if (segment) console.log(segment.speakerId, segment.text);
26
+ * });
27
+ * ```
28
+ */
29
+ export declare class ProviderDiarizationBackend {
30
+ /**
31
+ * Process a transcript event and extract the dominant speaker label.
32
+ *
33
+ * The dominant speaker is determined by majority vote across the word-level
34
+ * `speaker` annotations. If no words carry speaker labels, `null` is
35
+ * returned.
36
+ *
37
+ * For final transcripts (`isFinal: true`) the returned segment covers the
38
+ * full utterance. For interim transcripts the segment is a best-effort
39
+ * approximation based on the current words.
40
+ *
41
+ * @param event - Transcript event from an STT provider session.
42
+ * @returns A {@link DiarizedSegment} if speaker information is available,
43
+ * or `null` if the event lacks word-level speaker labels.
44
+ */
45
+ pushTranscriptEvent(event: TranscriptEvent): DiarizedSegment | null;
46
+ /**
47
+ * Determine the most-frequently-occurring speaker label among a set of words.
48
+ *
49
+ * @param words - Words that all have a defined `speaker` property.
50
+ * @returns The dominant `speaker` label.
51
+ */
52
+ private dominantSpeaker;
53
+ }
54
+ //# sourceMappingURL=ProviderDiarizationBackend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProviderDiarizationBackend.d.ts","sourceRoot":"","sources":["../src/ProviderDiarizationBackend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAkB,MAAM,YAAY,CAAC;AAMnF;;;;;;;;;;;;;;GAcG;AACH,qBAAa,0BAA0B;IAKrC;;;;;;;;;;;;;;OAcG;IACH,mBAAmB,CAAC,KAAK,EAAE,eAAe,GAAG,eAAe,GAAG,IAAI;IAsCnE;;;;;OAKG;IACH,OAAO,CAAC,eAAe;CAoBxB"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * @file ProviderDiarizationBackend.ts
3
+ * @description Thin adapter that extracts speaker labels from STT provider
4
+ * transcript events containing word-level speaker annotations.
5
+ *
6
+ * Many cloud STT providers (Deepgram, AssemblyAI, …) support native
7
+ * diarization that produces a `speaker` label on each word in the transcript.
8
+ * This backend consumes those events and re-emits them as
9
+ * {@link DiarizedSegment} objects without any additional compute.
10
+ *
11
+ * @module diarization/ProviderDiarizationBackend
12
+ */
13
+ // ---------------------------------------------------------------------------
14
+ // Main class
15
+ // ---------------------------------------------------------------------------
16
+ /**
17
+ * Extracts speaker labels from word-level STT results.
18
+ *
19
+ * Stateless — each call to {@link pushTranscriptEvent} is independent. The
20
+ * caller is responsible for any session-level bookkeeping.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const backend = new ProviderDiarizationBackend();
25
+ * sttSession.on('transcript', (event) => {
26
+ * const segment = backend.pushTranscriptEvent(event);
27
+ * if (segment) console.log(segment.speakerId, segment.text);
28
+ * });
29
+ * ```
30
+ */
31
+ export class ProviderDiarizationBackend {
32
+ // -------------------------------------------------------------------------
33
+ // Public API
34
+ // -------------------------------------------------------------------------
35
+ /**
36
+ * Process a transcript event and extract the dominant speaker label.
37
+ *
38
+ * The dominant speaker is determined by majority vote across the word-level
39
+ * `speaker` annotations. If no words carry speaker labels, `null` is
40
+ * returned.
41
+ *
42
+ * For final transcripts (`isFinal: true`) the returned segment covers the
43
+ * full utterance. For interim transcripts the segment is a best-effort
44
+ * approximation based on the current words.
45
+ *
46
+ * @param event - Transcript event from an STT provider session.
47
+ * @returns A {@link DiarizedSegment} if speaker information is available,
48
+ * or `null` if the event lacks word-level speaker labels.
49
+ */
50
+ pushTranscriptEvent(event) {
51
+ const words = event.words;
52
+ if (!words || words.length === 0) {
53
+ return null;
54
+ }
55
+ // Collect all words that have a speaker label.
56
+ const labelledWords = words.filter((w) => w.speaker !== undefined && w.speaker !== '');
57
+ if (labelledWords.length === 0) {
58
+ return null;
59
+ }
60
+ // Majority-vote for the dominant speaker in this transcript event.
61
+ const speakerId = this.dominantSpeaker(labelledWords);
62
+ // Derive segment boundaries from word timestamps (seconds → milliseconds).
63
+ const firstWord = labelledWords[0];
64
+ const lastWord = labelledWords[labelledWords.length - 1];
65
+ // Average per-word confidence (default 1 if not present).
66
+ const avgConfidence = labelledWords.reduce((sum, w) => sum + (w.confidence ?? 1), 0) / labelledWords.length;
67
+ return {
68
+ speakerId,
69
+ text: event.text,
70
+ startMs: Math.round(firstWord.start * 1000),
71
+ endMs: Math.round(lastWord.end * 1000),
72
+ confidence: avgConfidence,
73
+ };
74
+ }
75
+ // -------------------------------------------------------------------------
76
+ // Internal helpers
77
+ // -------------------------------------------------------------------------
78
+ /**
79
+ * Determine the most-frequently-occurring speaker label among a set of words.
80
+ *
81
+ * @param words - Words that all have a defined `speaker` property.
82
+ * @returns The dominant `speaker` label.
83
+ */
84
+ dominantSpeaker(words) {
85
+ const counts = new Map();
86
+ for (const word of words) {
87
+ const label = word.speaker;
88
+ counts.set(label, (counts.get(label) ?? 0) + 1);
89
+ }
90
+ let best = words[0].speaker;
91
+ let bestCount = 0;
92
+ for (const [label, count] of counts) {
93
+ if (count > bestCount) {
94
+ bestCount = count;
95
+ best = label;
96
+ }
97
+ }
98
+ return best;
99
+ }
100
+ }
101
+ //# sourceMappingURL=ProviderDiarizationBackend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProviderDiarizationBackend.js","sourceRoot":"","sources":["../src/ProviderDiarizationBackend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,0BAA0B;IACrC,4EAA4E;IAC5E,aAAa;IACb,4EAA4E;IAE5E;;;;;;;;;;;;;;OAcG;IACH,mBAAmB,CAAC,KAAsB;QACxC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QAE1B,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjC,OAAO,IAAI,CAAC;QACd,CAAC;QAED,+CAA+C;QAC/C,MAAM,aAAa,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,SAAS,IAAI,CAAC,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC;QAEvF,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,OAAO,IAAI,CAAC;QACd,CAAC;QAED,mEAAmE;QACnE,MAAM,SAAS,GAAG,IAAI,CAAC,eAAe,CAAC,aAAa,CAAC,CAAC;QAEtD,2EAA2E;QAC3E,MAAM,SAAS,GAAG,aAAa,CAAC,CAAC,CAAE,CAAC;QACpC,MAAM,QAAQ,GAAG,aAAa,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC;QAE1D,0DAA0D;QAC1D,MAAM,aAAa,GACjB,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,aAAa,CAAC,MAAM,CAAC;QAExF,OAAO;YACL,SAAS;YACT,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,GAAG,IAAI,CAAC;YAC3C,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,GAAG,IAAI,CAAC;YACtC,UAAU,EAAE,aAAa;SAC1B,CAAC;IACJ,CAAC;IAED,4EAA4E;IAC5E,mBAAmB;IACnB,4EAA4E;IAE5E;;;;;OAKG;IACK,eAAe,CAAC,KAAuB;QAC7C,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;QAEzC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAQ,CAAC;YAC5B,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAClD,CAAC;QAED,IAAI,IAAI,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC,OAAQ,CAAC;QAC9B,IAAI,SAAS,GAAG,CAAC,CAAC;QAElB,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC;YACpC,IAAI,KAAK,GAAG,SAAS,EAAE,CAAC;gBACtB,SAAS,GAAG,KAAK,CAAC;gBAClB,IAAI,GAAG,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;CACF"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @file SlidingWindowExtractor.ts
3
+ * @description Ring buffer that accumulates audio frames into fixed-size chunks
4
+ * with configurable overlap, tuned for diarization (1.5 s chunks, 0.5 s overlap).
5
+ *
6
+ * Mirrors the design of `SlidingWindowBuffer` from the Whisper STT pack but
7
+ * operates on {@link AudioFrame} objects rather than raw sample arrays, and
8
+ * uses millisecond-based chunk/overlap sizes for a friendlier API.
9
+ *
10
+ * @module diarization/SlidingWindowExtractor
11
+ */
12
+ import { EventEmitter } from 'node:events';
13
+ import type { AudioFrame } from './types.js';
14
+ /** Event map for {@link SlidingWindowExtractor}. */
15
+ export interface SlidingWindowExtractorEvents {
16
+ /** Emitted when a complete chunk of audio samples is ready. */
17
+ chunk_ready: [chunk: Float32Array];
18
+ }
19
+ /**
20
+ * Sliding-window audio extractor for the diarization pipeline.
21
+ *
22
+ * Accepts {@link AudioFrame} objects pushed by the voice pipeline, accumulates
23
+ * samples in an internal ring buffer, and emits `'chunk_ready'` events when a
24
+ * full chunk is available. The last `overlapSamples` of each chunk are kept
25
+ * as context at the head of the next chunk.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const extractor = new SlidingWindowExtractor(1500, 500, 16000);
30
+ * extractor.on('chunk_ready', (chunk) => processChunk(chunk));
31
+ * microphone.on('frame', (f) => extractor.pushAudio(f));
32
+ * ```
33
+ */
34
+ export declare class SlidingWindowExtractor extends EventEmitter {
35
+ private readonly chunkSizeMs;
36
+ private readonly overlapMs;
37
+ private readonly sampleRate;
38
+ /** Internal sample store. Sized to {@link chunkSizeSamples}. */
39
+ private buffer;
40
+ /** Current write position within {@link buffer}. */
41
+ private writePos;
42
+ /** Number of samples per emitted chunk. */
43
+ private readonly chunkSizeSamples;
44
+ /** Number of overlap samples carried forward to the next chunk. */
45
+ private readonly overlapSamples;
46
+ /**
47
+ * @param chunkSizeMs - Duration of each emitted chunk in milliseconds.
48
+ * @defaultValue 1500
49
+ * @param overlapMs - Overlap between consecutive chunks in milliseconds.
50
+ * Must be less than `chunkSizeMs`.
51
+ * @defaultValue 500
52
+ * @param sampleRate - Expected audio sample rate in Hz.
53
+ * @defaultValue 16000
54
+ */
55
+ constructor(chunkSizeMs?: number, overlapMs?: number, sampleRate?: number);
56
+ /**
57
+ * Push an audio frame into the extractor.
58
+ *
59
+ * Samples are appended to the internal ring buffer. When the buffer fills
60
+ * to `chunkSizeSamples`, a `'chunk_ready'` event is emitted and the overlap
61
+ * region is copied to the head of the buffer for the next chunk.
62
+ *
63
+ * @param frame - Audio frame to append.
64
+ */
65
+ pushAudio(frame: AudioFrame): void;
66
+ /**
67
+ * Flush any remaining samples as a final partial chunk.
68
+ *
69
+ * If the buffer is empty this is a no-op.
70
+ */
71
+ flush(): void;
72
+ /**
73
+ * Clear the buffer without emitting a chunk.
74
+ */
75
+ reset(): void;
76
+ /** Number of samples currently held in the buffer. */
77
+ get bufferedSamples(): number;
78
+ /**
79
+ * Append raw PCM samples and emit chunks as they fill.
80
+ *
81
+ * @param samples - Float32 PCM data to append.
82
+ */
83
+ private pushSamples;
84
+ }
85
+ //# sourceMappingURL=SlidingWindowExtractor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SlidingWindowExtractor.d.ts","sourceRoot":"","sources":["../src/SlidingWindowExtractor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAM7C,oDAAoD;AACpD,MAAM,WAAW,4BAA4B;IAC3C,+DAA+D;IAC/D,WAAW,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;CACpC;AAMD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,sBAAuB,SAAQ,YAAY;IAmCpD,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAhC7B,iEAAiE;IACjE,OAAO,CAAC,MAAM,CAAe;IAE7B,oDAAoD;IACpD,OAAO,CAAC,QAAQ,CAAK;IAMrB,2CAA2C;IAC3C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAE1C,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IAMxC;;;;;;;;OAQG;gBAEgB,WAAW,GAAE,MAAa,EAC1B,SAAS,GAAE,MAAY,EACvB,UAAU,GAAE,MAAe;IAoB9C;;;;;;;;OAQG;IACH,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAIlC;;;;OAIG;IACH,KAAK,IAAI,IAAI;IAMb;;OAEG;IACH,KAAK,IAAI,IAAI;IASb,sDAAsD;IACtD,IAAI,eAAe,IAAI,MAAM,CAE5B;IAMD;;;;OAIG;IACH,OAAO,CAAC,WAAW;CAsBpB"}
@@ -0,0 +1,140 @@
1
+ /**
2
+ * @file SlidingWindowExtractor.ts
3
+ * @description Ring buffer that accumulates audio frames into fixed-size chunks
4
+ * with configurable overlap, tuned for diarization (1.5 s chunks, 0.5 s overlap).
5
+ *
6
+ * Mirrors the design of `SlidingWindowBuffer` from the Whisper STT pack but
7
+ * operates on {@link AudioFrame} objects rather than raw sample arrays, and
8
+ * uses millisecond-based chunk/overlap sizes for a friendlier API.
9
+ *
10
+ * @module diarization/SlidingWindowExtractor
11
+ */
12
+ import { EventEmitter } from 'node:events';
13
+ // ---------------------------------------------------------------------------
14
+ // Main class
15
+ // ---------------------------------------------------------------------------
16
+ /**
17
+ * Sliding-window audio extractor for the diarization pipeline.
18
+ *
19
+ * Accepts {@link AudioFrame} objects pushed by the voice pipeline, accumulates
20
+ * samples in an internal ring buffer, and emits `'chunk_ready'` events when a
21
+ * full chunk is available. The last `overlapSamples` of each chunk are kept
22
+ * as context at the head of the next chunk.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const extractor = new SlidingWindowExtractor(1500, 500, 16000);
27
+ * extractor.on('chunk_ready', (chunk) => processChunk(chunk));
28
+ * microphone.on('frame', (f) => extractor.pushAudio(f));
29
+ * ```
30
+ */
31
+ export class SlidingWindowExtractor extends EventEmitter {
32
+ chunkSizeMs;
33
+ overlapMs;
34
+ sampleRate;
35
+ // -------------------------------------------------------------------------
36
+ // Private state
37
+ // -------------------------------------------------------------------------
38
+ /** Internal sample store. Sized to {@link chunkSizeSamples}. */
39
+ buffer;
40
+ /** Current write position within {@link buffer}. */
41
+ writePos = 0;
42
+ // -------------------------------------------------------------------------
43
+ // Computed dimensions (from ms → samples)
44
+ // -------------------------------------------------------------------------
45
+ /** Number of samples per emitted chunk. */
46
+ chunkSizeSamples;
47
+ /** Number of overlap samples carried forward to the next chunk. */
48
+ overlapSamples;
49
+ // -------------------------------------------------------------------------
50
+ // Constructor
51
+ // -------------------------------------------------------------------------
52
+ /**
53
+ * @param chunkSizeMs - Duration of each emitted chunk in milliseconds.
54
+ * @defaultValue 1500
55
+ * @param overlapMs - Overlap between consecutive chunks in milliseconds.
56
+ * Must be less than `chunkSizeMs`.
57
+ * @defaultValue 500
58
+ * @param sampleRate - Expected audio sample rate in Hz.
59
+ * @defaultValue 16000
60
+ */
61
+ constructor(chunkSizeMs = 1500, overlapMs = 500, sampleRate = 16_000) {
62
+ super();
63
+ this.chunkSizeMs = chunkSizeMs;
64
+ this.overlapMs = overlapMs;
65
+ this.sampleRate = sampleRate;
66
+ this.chunkSizeSamples = Math.round((chunkSizeMs / 1000) * sampleRate);
67
+ this.overlapSamples = Math.round((overlapMs / 1000) * sampleRate);
68
+ if (this.overlapSamples >= this.chunkSizeSamples) {
69
+ throw new RangeError(`overlapMs (${overlapMs}) must result in fewer samples than chunkSizeMs (${chunkSizeMs})`);
70
+ }
71
+ this.buffer = new Float32Array(this.chunkSizeSamples);
72
+ }
73
+ // -------------------------------------------------------------------------
74
+ // Public API
75
+ // -------------------------------------------------------------------------
76
+ /**
77
+ * Push an audio frame into the extractor.
78
+ *
79
+ * Samples are appended to the internal ring buffer. When the buffer fills
80
+ * to `chunkSizeSamples`, a `'chunk_ready'` event is emitted and the overlap
81
+ * region is copied to the head of the buffer for the next chunk.
82
+ *
83
+ * @param frame - Audio frame to append.
84
+ */
85
+ pushAudio(frame) {
86
+ this.pushSamples(frame.samples);
87
+ }
88
+ /**
89
+ * Flush any remaining samples as a final partial chunk.
90
+ *
91
+ * If the buffer is empty this is a no-op.
92
+ */
93
+ flush() {
94
+ if (this.writePos === 0)
95
+ return;
96
+ this.emit('chunk_ready', this.buffer.slice(0, this.writePos));
97
+ this.reset();
98
+ }
99
+ /**
100
+ * Clear the buffer without emitting a chunk.
101
+ */
102
+ reset() {
103
+ this.buffer = new Float32Array(this.chunkSizeSamples);
104
+ this.writePos = 0;
105
+ }
106
+ // -------------------------------------------------------------------------
107
+ // Accessors
108
+ // -------------------------------------------------------------------------
109
+ /** Number of samples currently held in the buffer. */
110
+ get bufferedSamples() {
111
+ return this.writePos;
112
+ }
113
+ // -------------------------------------------------------------------------
114
+ // Internal helpers
115
+ // -------------------------------------------------------------------------
116
+ /**
117
+ * Append raw PCM samples and emit chunks as they fill.
118
+ *
119
+ * @param samples - Float32 PCM data to append.
120
+ */
121
+ pushSamples(samples) {
122
+ let srcOffset = 0;
123
+ while (srcOffset < samples.length) {
124
+ const spaceLeft = this.chunkSizeSamples - this.writePos;
125
+ const copyCount = Math.min(spaceLeft, samples.length - srcOffset);
126
+ this.buffer.set(samples.subarray(srcOffset, srcOffset + copyCount), this.writePos);
127
+ this.writePos += copyCount;
128
+ srcOffset += copyCount;
129
+ if (this.writePos >= this.chunkSizeSamples) {
130
+ // Emit a copy — the buffer is reused for the next chunk.
131
+ this.emit('chunk_ready', this.buffer.slice());
132
+ // Copy the overlap region to the start of the buffer.
133
+ const overlapStart = this.chunkSizeSamples - this.overlapSamples;
134
+ this.buffer.copyWithin(0, overlapStart, this.chunkSizeSamples);
135
+ this.writePos = this.overlapSamples;
136
+ }
137
+ }
138
+ }
139
+ }
140
+ //# sourceMappingURL=SlidingWindowExtractor.js.map