@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,119 @@
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
+ import type { TranscriptEvent, DiarizedSegment, TranscriptWord } from './types.js';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Main class
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /**
21
+ * Extracts speaker labels from word-level STT results.
22
+ *
23
+ * Stateless — each call to {@link pushTranscriptEvent} is independent. The
24
+ * caller is responsible for any session-level bookkeeping.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const backend = new ProviderDiarizationBackend();
29
+ * sttSession.on('transcript', (event) => {
30
+ * const segment = backend.pushTranscriptEvent(event);
31
+ * if (segment) console.log(segment.speakerId, segment.text);
32
+ * });
33
+ * ```
34
+ */
35
+ export class ProviderDiarizationBackend {
36
+ // -------------------------------------------------------------------------
37
+ // Public API
38
+ // -------------------------------------------------------------------------
39
+
40
+ /**
41
+ * Process a transcript event and extract the dominant speaker label.
42
+ *
43
+ * The dominant speaker is determined by majority vote across the word-level
44
+ * `speaker` annotations. If no words carry speaker labels, `null` is
45
+ * returned.
46
+ *
47
+ * For final transcripts (`isFinal: true`) the returned segment covers the
48
+ * full utterance. For interim transcripts the segment is a best-effort
49
+ * approximation based on the current words.
50
+ *
51
+ * @param event - Transcript event from an STT provider session.
52
+ * @returns A {@link DiarizedSegment} if speaker information is available,
53
+ * or `null` if the event lacks word-level speaker labels.
54
+ */
55
+ pushTranscriptEvent(event: TranscriptEvent): DiarizedSegment | null {
56
+ const words = event.words;
57
+
58
+ if (!words || words.length === 0) {
59
+ return null;
60
+ }
61
+
62
+ // Collect all words that have a speaker label.
63
+ const labelledWords = words.filter((w) => w.speaker !== undefined && w.speaker !== '');
64
+
65
+ if (labelledWords.length === 0) {
66
+ return null;
67
+ }
68
+
69
+ // Majority-vote for the dominant speaker in this transcript event.
70
+ const speakerId = this.dominantSpeaker(labelledWords);
71
+
72
+ // Derive segment boundaries from word timestamps (seconds → milliseconds).
73
+ const firstWord = labelledWords[0]!;
74
+ const lastWord = labelledWords[labelledWords.length - 1]!;
75
+
76
+ // Average per-word confidence (default 1 if not present).
77
+ const avgConfidence =
78
+ labelledWords.reduce((sum, w) => sum + (w.confidence ?? 1), 0) / labelledWords.length;
79
+
80
+ return {
81
+ speakerId,
82
+ text: event.text,
83
+ startMs: Math.round(firstWord.start * 1000),
84
+ endMs: Math.round(lastWord.end * 1000),
85
+ confidence: avgConfidence,
86
+ };
87
+ }
88
+
89
+ // -------------------------------------------------------------------------
90
+ // Internal helpers
91
+ // -------------------------------------------------------------------------
92
+
93
+ /**
94
+ * Determine the most-frequently-occurring speaker label among a set of words.
95
+ *
96
+ * @param words - Words that all have a defined `speaker` property.
97
+ * @returns The dominant `speaker` label.
98
+ */
99
+ private dominantSpeaker(words: TranscriptWord[]): string {
100
+ const counts = new Map<string, number>();
101
+
102
+ for (const word of words) {
103
+ const label = word.speaker!;
104
+ counts.set(label, (counts.get(label) ?? 0) + 1);
105
+ }
106
+
107
+ let best = words[0]!.speaker!;
108
+ let bestCount = 0;
109
+
110
+ for (const [label, count] of counts) {
111
+ if (count > bestCount) {
112
+ bestCount = count;
113
+ best = label;
114
+ }
115
+ }
116
+
117
+ return best;
118
+ }
119
+ }
@@ -0,0 +1,174 @@
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
+
13
+ import { EventEmitter } from 'node:events';
14
+ import type { AudioFrame } from './types.js';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Events interface
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /** Event map for {@link SlidingWindowExtractor}. */
21
+ export interface SlidingWindowExtractorEvents {
22
+ /** Emitted when a complete chunk of audio samples is ready. */
23
+ chunk_ready: [chunk: Float32Array];
24
+ }
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Main class
28
+ // ---------------------------------------------------------------------------
29
+
30
+ /**
31
+ * Sliding-window audio extractor for the diarization pipeline.
32
+ *
33
+ * Accepts {@link AudioFrame} objects pushed by the voice pipeline, accumulates
34
+ * samples in an internal ring buffer, and emits `'chunk_ready'` events when a
35
+ * full chunk is available. The last `overlapSamples` of each chunk are kept
36
+ * as context at the head of the next chunk.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * const extractor = new SlidingWindowExtractor(1500, 500, 16000);
41
+ * extractor.on('chunk_ready', (chunk) => processChunk(chunk));
42
+ * microphone.on('frame', (f) => extractor.pushAudio(f));
43
+ * ```
44
+ */
45
+ export class SlidingWindowExtractor extends EventEmitter {
46
+ // -------------------------------------------------------------------------
47
+ // Private state
48
+ // -------------------------------------------------------------------------
49
+
50
+ /** Internal sample store. Sized to {@link chunkSizeSamples}. */
51
+ private buffer: Float32Array;
52
+
53
+ /** Current write position within {@link buffer}. */
54
+ private writePos = 0;
55
+
56
+ // -------------------------------------------------------------------------
57
+ // Computed dimensions (from ms → samples)
58
+ // -------------------------------------------------------------------------
59
+
60
+ /** Number of samples per emitted chunk. */
61
+ private readonly chunkSizeSamples: number;
62
+
63
+ /** Number of overlap samples carried forward to the next chunk. */
64
+ private readonly overlapSamples: number;
65
+
66
+ // -------------------------------------------------------------------------
67
+ // Constructor
68
+ // -------------------------------------------------------------------------
69
+
70
+ /**
71
+ * @param chunkSizeMs - Duration of each emitted chunk in milliseconds.
72
+ * @defaultValue 1500
73
+ * @param overlapMs - Overlap between consecutive chunks in milliseconds.
74
+ * Must be less than `chunkSizeMs`.
75
+ * @defaultValue 500
76
+ * @param sampleRate - Expected audio sample rate in Hz.
77
+ * @defaultValue 16000
78
+ */
79
+ constructor(
80
+ private readonly chunkSizeMs: number = 1500,
81
+ private readonly overlapMs: number = 500,
82
+ private readonly sampleRate: number = 16_000,
83
+ ) {
84
+ super();
85
+
86
+ this.chunkSizeSamples = Math.round((chunkSizeMs / 1000) * sampleRate);
87
+ this.overlapSamples = Math.round((overlapMs / 1000) * sampleRate);
88
+
89
+ if (this.overlapSamples >= this.chunkSizeSamples) {
90
+ throw new RangeError(
91
+ `overlapMs (${overlapMs}) must result in fewer samples than chunkSizeMs (${chunkSizeMs})`,
92
+ );
93
+ }
94
+
95
+ this.buffer = new Float32Array(this.chunkSizeSamples);
96
+ }
97
+
98
+ // -------------------------------------------------------------------------
99
+ // Public API
100
+ // -------------------------------------------------------------------------
101
+
102
+ /**
103
+ * Push an audio frame into the extractor.
104
+ *
105
+ * Samples are appended to the internal ring buffer. When the buffer fills
106
+ * to `chunkSizeSamples`, a `'chunk_ready'` event is emitted and the overlap
107
+ * region is copied to the head of the buffer for the next chunk.
108
+ *
109
+ * @param frame - Audio frame to append.
110
+ */
111
+ pushAudio(frame: AudioFrame): void {
112
+ this.pushSamples(frame.samples);
113
+ }
114
+
115
+ /**
116
+ * Flush any remaining samples as a final partial chunk.
117
+ *
118
+ * If the buffer is empty this is a no-op.
119
+ */
120
+ flush(): void {
121
+ if (this.writePos === 0) return;
122
+ this.emit('chunk_ready', this.buffer.slice(0, this.writePos));
123
+ this.reset();
124
+ }
125
+
126
+ /**
127
+ * Clear the buffer without emitting a chunk.
128
+ */
129
+ reset(): void {
130
+ this.buffer = new Float32Array(this.chunkSizeSamples);
131
+ this.writePos = 0;
132
+ }
133
+
134
+ // -------------------------------------------------------------------------
135
+ // Accessors
136
+ // -------------------------------------------------------------------------
137
+
138
+ /** Number of samples currently held in the buffer. */
139
+ get bufferedSamples(): number {
140
+ return this.writePos;
141
+ }
142
+
143
+ // -------------------------------------------------------------------------
144
+ // Internal helpers
145
+ // -------------------------------------------------------------------------
146
+
147
+ /**
148
+ * Append raw PCM samples and emit chunks as they fill.
149
+ *
150
+ * @param samples - Float32 PCM data to append.
151
+ */
152
+ private pushSamples(samples: Float32Array): void {
153
+ let srcOffset = 0;
154
+
155
+ while (srcOffset < samples.length) {
156
+ const spaceLeft = this.chunkSizeSamples - this.writePos;
157
+ const copyCount = Math.min(spaceLeft, samples.length - srcOffset);
158
+
159
+ this.buffer.set(samples.subarray(srcOffset, srcOffset + copyCount), this.writePos);
160
+ this.writePos += copyCount;
161
+ srcOffset += copyCount;
162
+
163
+ if (this.writePos >= this.chunkSizeSamples) {
164
+ // Emit a copy — the buffer is reused for the next chunk.
165
+ this.emit('chunk_ready', this.buffer.slice());
166
+
167
+ // Copy the overlap region to the start of the buffer.
168
+ const overlapStart = this.chunkSizeSamples - this.overlapSamples;
169
+ this.buffer.copyWithin(0, overlapStart, this.chunkSizeSamples);
170
+ this.writePos = this.overlapSamples;
171
+ }
172
+ }
173
+ }
174
+ }
@@ -0,0 +1,228 @@
1
+ /**
2
+ * @file SpeakerEmbeddingCache.ts
3
+ * @description Stores and matches speaker voiceprints using cosine similarity.
4
+ *
5
+ * Each speaker is represented as a running centroid — the mean of all
6
+ * embeddings observed so far. New audio embeddings are matched against
7
+ * existing centroids; if the best match exceeds the similarity threshold the
8
+ * frame is attributed to that speaker and the centroid is updated. Otherwise
9
+ * a new `Speaker_N` identity is created.
10
+ *
11
+ * @module diarization/SpeakerEmbeddingCache
12
+ */
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Internal types
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /** Running centroid entry stored per speaker. */
19
+ interface CentroidEntry {
20
+ /** Current mean embedding vector. */
21
+ embedding: Float32Array;
22
+ /** Number of frames that have contributed to this centroid. */
23
+ count: number;
24
+ }
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Helpers
28
+ // ---------------------------------------------------------------------------
29
+
30
+ /**
31
+ * Compute the cosine similarity between two embedding vectors.
32
+ *
33
+ * Returns a value in the range `[-1, 1]`. A small epsilon is added to the
34
+ * denominator to prevent division-by-zero on silent/zero frames.
35
+ *
36
+ * @param a - First embedding vector.
37
+ * @param b - Second embedding vector.
38
+ * @returns Cosine similarity.
39
+ */
40
+ export function cosineSimilarity(a: Float32Array, b: Float32Array): number {
41
+ let dot = 0;
42
+ let magA = 0;
43
+ let magB = 0;
44
+
45
+ for (let i = 0; i < a.length; i++) {
46
+ dot += a[i]! * b[i]!;
47
+ magA += (a[i]!) ** 2;
48
+ magB += (b[i]!) ** 2;
49
+ }
50
+
51
+ return dot / (Math.sqrt(magA) * Math.sqrt(magB) + 1e-8);
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Main class
56
+ // ---------------------------------------------------------------------------
57
+
58
+ /**
59
+ * In-memory cache of speaker voiceprint centroids.
60
+ *
61
+ * Centroids are updated with an online running-average as new embeddings
62
+ * arrive, so memory usage is O(speakers × embedding dimensions) regardless of
63
+ * the number of frames observed.
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * const cache = new SpeakerEmbeddingCache(0.7);
68
+ * const { speakerId, isNew } = cache.getOrCreateSpeaker(embedding);
69
+ * ```
70
+ */
71
+ export class SpeakerEmbeddingCache {
72
+ // -------------------------------------------------------------------------
73
+ // Private state
74
+ // -------------------------------------------------------------------------
75
+
76
+ /**
77
+ * Centroid map: `speakerId → { embedding, count }`.
78
+ *
79
+ * Access via {@link centroids} for testing/introspection.
80
+ */
81
+ private readonly _centroids = new Map<string, CentroidEntry>();
82
+
83
+ // -------------------------------------------------------------------------
84
+ // Constructor
85
+ // -------------------------------------------------------------------------
86
+
87
+ /**
88
+ * @param threshold - Minimum cosine similarity required to attribute a new
89
+ * embedding to an existing speaker. Embeddings below this threshold
90
+ * create a new speaker identity.
91
+ * @defaultValue 0.7
92
+ */
93
+ constructor(private readonly threshold: number = 0.7) {}
94
+
95
+ // -------------------------------------------------------------------------
96
+ // Public API
97
+ // -------------------------------------------------------------------------
98
+
99
+ /**
100
+ * Update the running centroid for `speakerId` with a new embedding.
101
+ *
102
+ * The centroid is recalculated as an online running average:
103
+ * `new_mean = old_mean + (x - old_mean) / (count + 1)`.
104
+ *
105
+ * @param speakerId - Speaker identity to update.
106
+ * @param embedding - New embedding vector to incorporate.
107
+ */
108
+ addEmbedding(speakerId: string, embedding: Float32Array): void {
109
+ const existing = this._centroids.get(speakerId);
110
+
111
+ if (!existing) {
112
+ // First frame — store a copy of the embedding directly.
113
+ this._centroids.set(speakerId, {
114
+ embedding: embedding.slice(),
115
+ count: 1,
116
+ });
117
+ return;
118
+ }
119
+
120
+ // Online running average: mean_n = mean_{n-1} + (x - mean_{n-1}) / n
121
+ const newCount = existing.count + 1;
122
+ const newEmbedding = new Float32Array(existing.embedding.length);
123
+
124
+ for (let i = 0; i < existing.embedding.length; i++) {
125
+ newEmbedding[i] = existing.embedding[i]! + (embedding[i]! - existing.embedding[i]!) / newCount;
126
+ }
127
+
128
+ this._centroids.set(speakerId, { embedding: newEmbedding, count: newCount });
129
+ }
130
+
131
+ /**
132
+ * Find the existing speaker whose centroid is most similar to `embedding`.
133
+ *
134
+ * Returns `null` if no speaker exceeds the similarity threshold or the cache
135
+ * is empty.
136
+ *
137
+ * @param embedding - Query embedding vector.
138
+ * @returns Best match above threshold, or `null`.
139
+ */
140
+ findClosestSpeaker(embedding: Float32Array): { speakerId: string; similarity: number } | null {
141
+ let bestId: string | null = null;
142
+ let bestSim = -Infinity;
143
+
144
+ for (const [id, entry] of this._centroids) {
145
+ const sim = cosineSimilarity(embedding, entry.embedding);
146
+ if (sim > bestSim) {
147
+ bestSim = sim;
148
+ bestId = id;
149
+ }
150
+ }
151
+
152
+ if (bestId === null || bestSim < this.threshold) {
153
+ return null;
154
+ }
155
+
156
+ return { speakerId: bestId, similarity: bestSim };
157
+ }
158
+
159
+ /**
160
+ * Look up or create a speaker identity for `embedding`.
161
+ *
162
+ * If a match above the threshold is found, the centroid is updated and the
163
+ * existing ID is returned. Otherwise a new `Speaker_N` identity is
164
+ * registered.
165
+ *
166
+ * @param embedding - Audio embedding to resolve.
167
+ * @returns Speaker ID and whether a new identity was created.
168
+ */
169
+ getOrCreateSpeaker(embedding: Float32Array): { speakerId: string; isNew: boolean } {
170
+ const match = this.findClosestSpeaker(embedding);
171
+
172
+ if (match) {
173
+ this.addEmbedding(match.speakerId, embedding);
174
+ return { speakerId: match.speakerId, isNew: false };
175
+ }
176
+
177
+ const newId = `Speaker_${this._centroids.size}`;
178
+ this.addEmbedding(newId, embedding);
179
+ return { speakerId: newId, isNew: true };
180
+ }
181
+
182
+ /**
183
+ * Pre-register a known speaker voiceprint, overwriting any existing centroid
184
+ * for this ID.
185
+ *
186
+ * Use this to seed the cache with enrolment recordings before a session
187
+ * begins so that known participants are matched by name rather than the
188
+ * auto-generated `Speaker_N` label.
189
+ *
190
+ * @param id - Human-readable speaker identifier.
191
+ * @param voiceprint - Reference embedding for this speaker.
192
+ */
193
+ enrollSpeaker(id: string, voiceprint: Float32Array): void {
194
+ this._centroids.set(id, { embedding: voiceprint.slice(), count: 1 });
195
+ }
196
+
197
+ // -------------------------------------------------------------------------
198
+ // Accessors (useful for testing and ClusteringStrategy)
199
+ // -------------------------------------------------------------------------
200
+
201
+ /**
202
+ * Read-only view of the internal centroid map.
203
+ *
204
+ * Returns a snapshot — modifications to the returned map do not affect the
205
+ * cache.
206
+ */
207
+ get centroids(): Map<string, Float32Array> {
208
+ const result = new Map<string, Float32Array>();
209
+ for (const [id, entry] of this._centroids) {
210
+ result.set(id, entry.embedding.slice());
211
+ }
212
+ return result;
213
+ }
214
+
215
+ /** Number of distinct speaker identities currently tracked. */
216
+ get size(): number {
217
+ return this._centroids.size;
218
+ }
219
+
220
+ /**
221
+ * Remove all centroids from the cache.
222
+ *
223
+ * Useful for resetting state between test cases.
224
+ */
225
+ clear(): void {
226
+ this._centroids.clear();
227
+ }
228
+ }
package/src/index.ts ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * @file index.ts
3
+ * @description Pack factory for the Diarization extension pack.
4
+ *
5
+ * ### Usage (direct)
6
+ * ```ts
7
+ * import { createDiarizationEngine } from '@framers/agentos-ext-diarization';
8
+ *
9
+ * const engine = createDiarizationEngine();
10
+ * const session = engine.startSession({ backend: 'local' });
11
+ * session.on('speaker_identified', ({ speakerId }) => console.log(speakerId));
12
+ * ```
13
+ *
14
+ * ### Usage (manifest-driven)
15
+ * ```json
16
+ * { "packs": [{ "module": "@framers/agentos-ext-diarization" }] }
17
+ * ```
18
+ *
19
+ * @module diarization
20
+ */
21
+
22
+ import { DiarizationEngine } from './DiarizationEngine.js';
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Local types (avoid hard runtime dep on @framers/agentos)
26
+ // ---------------------------------------------------------------------------
27
+
28
+ /** Subset of ExtensionDescriptor required by this pack. */
29
+ interface ExtensionDescriptor {
30
+ id: string;
31
+ kind: string;
32
+ payload: unknown;
33
+ enableByDefault?: boolean;
34
+ metadata?: Record<string, unknown>;
35
+ }
36
+
37
+ /** Subset of ExtensionPack required by this pack. */
38
+ interface ExtensionPack {
39
+ id: string;
40
+ descriptors: ExtensionDescriptor[];
41
+ }
42
+
43
+ /** Subset of ExtensionPackContext required by this pack. */
44
+ interface ExtensionPackContext {
45
+ getSecret?: (id: string) => string | undefined;
46
+ options?: Record<string, unknown>;
47
+ }
48
+
49
+ /** Kind constant matching packages/agentos/src/extensions/types.ts. */
50
+ const EXTENSION_KIND_DIARIZATION = 'diarization-provider';
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Factory
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /**
57
+ * Create a standalone {@link DiarizationEngine} instance.
58
+ *
59
+ * Use this when composing the engine programmatically outside of the
60
+ * AgentOS extension system.
61
+ *
62
+ * @returns A configured {@link DiarizationEngine}.
63
+ */
64
+ export function createDiarizationEngine(): DiarizationEngine {
65
+ return new DiarizationEngine();
66
+ }
67
+
68
+ /**
69
+ * AgentOS manifest factory function.
70
+ *
71
+ * Returns an {@link ExtensionPack} containing a single `diarization-provider`
72
+ * descriptor backed by {@link DiarizationEngine}.
73
+ *
74
+ * @param _context - Pack context supplied by the extension manager.
75
+ * @returns A fully configured {@link ExtensionPack}.
76
+ */
77
+ export function createExtensionPack(_context: ExtensionPackContext): ExtensionPack {
78
+ const engine = new DiarizationEngine();
79
+
80
+ return {
81
+ id: 'diarization',
82
+ descriptors: [
83
+ {
84
+ id: 'diarization-engine',
85
+ kind: EXTENSION_KIND_DIARIZATION,
86
+ payload: engine,
87
+ enableByDefault: true,
88
+ metadata: { providerId: 'diarization-local' },
89
+ },
90
+ ],
91
+ };
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Re-exports
96
+ // ---------------------------------------------------------------------------
97
+
98
+ export * from './types.js';
99
+ export { DiarizationEngine } from './DiarizationEngine.js';
100
+ export { DiarizationSession } from './DiarizationSession.js';
101
+ export { SpeakerEmbeddingCache, cosineSimilarity } from './SpeakerEmbeddingCache.js';
102
+ export { ClusteringStrategy } from './ClusteringStrategy.js';
103
+ export { SlidingWindowExtractor } from './SlidingWindowExtractor.js';
104
+ export { ProviderDiarizationBackend } from './ProviderDiarizationBackend.js';
105
+ export { LocalDiarizationBackend } from './LocalDiarizationBackend.js';