vocal-stack 1.0.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,1026 @@
1
+ 'use strict';
2
+
3
+ // src/errors.ts
4
+ var VocalStackError = class extends Error {
5
+ constructor(message, code, context) {
6
+ super(message);
7
+ this.code = code;
8
+ this.context = context;
9
+ this.name = "VocalStackError";
10
+ Error.captureStackTrace(this, this.constructor);
11
+ }
12
+ };
13
+ var SanitizerError = class extends VocalStackError {
14
+ constructor(message, context) {
15
+ super(message, "SANITIZER_ERROR", context);
16
+ this.name = "SanitizerError";
17
+ }
18
+ };
19
+ var FlowControlError = class extends VocalStackError {
20
+ constructor(message, context) {
21
+ super(message, "FLOW_CONTROL_ERROR", context);
22
+ this.name = "FlowControlError";
23
+ }
24
+ };
25
+ var MonitorError = class extends VocalStackError {
26
+ constructor(message, context) {
27
+ super(message, "MONITOR_ERROR", context);
28
+ this.name = "MonitorError";
29
+ }
30
+ };
31
+
32
+ // src/sanitizer/rules/code-blocks.ts
33
+ function codeBlocksRule(text, _config) {
34
+ let result = text;
35
+ result = result.replace(/```[\s\S]*?```/g, "");
36
+ result = result.replace(/^(?: {4}|\t).+$/gm, "");
37
+ return result;
38
+ }
39
+
40
+ // src/sanitizer/rules/markdown.ts
41
+ function markdownRule(text, _config) {
42
+ let result = text;
43
+ result = result.replace(/^#{1,6}\s+/gm, "");
44
+ result = result.replace(/(\*\*|__)(.*?)\1/g, "$2");
45
+ result = result.replace(/(\*|_)(.*?)\1/g, "$2");
46
+ result = result.replace(/`([^`]+)`/g, "$1");
47
+ result = result.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
48
+ result = result.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1");
49
+ result = result.replace(/^>\s+/gm, "");
50
+ result = result.replace(/^[\-*_]{3,}$/gm, "");
51
+ result = result.replace(/^[\s]*[-*+]\s+/gm, "");
52
+ result = result.replace(/^[\s]*\d+\.\s+/gm, "");
53
+ result = result.replace(/~~(.*?)~~/g, "$1");
54
+ result = result.replace(/^[\s]*-\s+\[[ xX]\]\s+/gm, "");
55
+ return result;
56
+ }
57
+
58
+ // src/sanitizer/rules/punctuation.ts
59
+ function punctuationRule(text, _config) {
60
+ let result = text;
61
+ result = result.replace(/!{2,}/g, "!");
62
+ result = result.replace(/\?{2,}/g, "?");
63
+ result = result.replace(/\.{3,}/g, ".");
64
+ result = result.replace(/-{2,}/g, " ");
65
+ result = result.replace(/[—–]/g, " ");
66
+ result = result.replace(/[()[\]{}]/g, "");
67
+ result = result.replace(/,{2,}/g, ",");
68
+ result = result.replace(/[;:]/g, ",");
69
+ result = result.replace(/["'"'`]/g, "");
70
+ result = result.replace(/[*_]/g, "");
71
+ result = result.replace(/[/\\]/g, " ");
72
+ result = result.replace(/[|&]/g, " ");
73
+ result = result.replace(/[@#$%]/g, "");
74
+ return result;
75
+ }
76
+
77
+ // src/sanitizer/rules/urls.ts
78
+ function urlsRule(text, _config) {
79
+ let result = text;
80
+ const urlWithProtocol = /\b(https?:\/\/|ftp:\/\/|www\.)[^\s<>"{}|\\^`[\]]+/gi;
81
+ const emailPattern = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g;
82
+ result = result.replace(urlWithProtocol, "");
83
+ result = result.replace(emailPattern, "");
84
+ result = result.replace(/\b[a-z0-9-]+\.[a-z]{2,}\b/gi, "");
85
+ return result;
86
+ }
87
+
88
+ // src/sanitizer/rules/index.ts
89
+ var ruleRegistry = /* @__PURE__ */ new Map([
90
+ ["markdown", markdownRule],
91
+ ["urls", urlsRule],
92
+ ["code-blocks", codeBlocksRule],
93
+ ["punctuation", punctuationRule]
94
+ ]);
95
+
96
+ // src/sanitizer/sanitizer.ts
97
+ var SpeechSanitizer = class {
98
+ config;
99
+ plugins;
100
+ constructor(config = {}) {
101
+ this.config = {
102
+ rules: config.rules ?? ["markdown", "urls", "code-blocks", "punctuation"],
103
+ plugins: config.plugins ?? [],
104
+ preserveLineBreaks: config.preserveLineBreaks ?? false,
105
+ customReplacements: config.customReplacements ?? /* @__PURE__ */ new Map()
106
+ };
107
+ this.plugins = [...this.config.plugins].sort((a, b) => a.priority - b.priority);
108
+ }
109
+ /**
110
+ * Sanitize a string synchronously
111
+ */
112
+ sanitize(text) {
113
+ if (!text || text.trim().length === 0) {
114
+ return "";
115
+ }
116
+ let result = text;
117
+ try {
118
+ for (const rule of this.config.rules) {
119
+ const ruleFunction = ruleRegistry.get(rule);
120
+ if (ruleFunction) {
121
+ result = ruleFunction(result, this.config);
122
+ }
123
+ }
124
+ for (const plugin of this.plugins) {
125
+ const transformed = plugin.transform(result);
126
+ if (transformed instanceof Promise) {
127
+ throw new SanitizerError(
128
+ `Plugin ${plugin.name} returned a Promise in sync sanitize(). Use sanitizeAsync() instead.`
129
+ );
130
+ }
131
+ result = transformed;
132
+ }
133
+ for (const [pattern, replacement] of this.config.customReplacements) {
134
+ if (typeof pattern === "string") {
135
+ result = result.replaceAll(pattern, replacement);
136
+ } else {
137
+ result = result.replace(pattern, replacement);
138
+ }
139
+ }
140
+ if (this.config.preserveLineBreaks) {
141
+ result = result.replace(/ +/g, " ").replace(/^\s+|\s+$/gm, "");
142
+ } else {
143
+ result = result.replace(/\s+/g, " ").trim();
144
+ }
145
+ return result;
146
+ } catch (error) {
147
+ if (error instanceof SanitizerError) {
148
+ throw error;
149
+ }
150
+ throw new SanitizerError("Failed to sanitize text", {
151
+ originalText: text.substring(0, 100),
152
+ error
153
+ });
154
+ }
155
+ }
156
+ /**
157
+ * Sanitize with detailed result metadata
158
+ */
159
+ sanitizeWithMetadata(text) {
160
+ const original = text;
161
+ const sanitized = this.sanitize(text);
162
+ return {
163
+ original,
164
+ sanitized,
165
+ appliedRules: this.config.rules,
166
+ metadata: {
167
+ removedCount: original.length - sanitized.length,
168
+ transformedCount: this.config.rules.length + this.plugins.length
169
+ }
170
+ };
171
+ }
172
+ /**
173
+ * Sanitize a stream of text chunks (AsyncIterable)
174
+ */
175
+ async *sanitizeStream(input) {
176
+ let buffer = "";
177
+ const sentenceBoundary = /[.!?]\s+/;
178
+ for await (const chunk of input) {
179
+ buffer += chunk;
180
+ const sentences = buffer.split(sentenceBoundary);
181
+ buffer = sentences.pop() ?? "";
182
+ for (const sentence of sentences) {
183
+ if (sentence.trim()) {
184
+ yield `${this.sanitize(sentence)} `;
185
+ }
186
+ }
187
+ }
188
+ if (buffer.trim()) {
189
+ yield this.sanitize(buffer);
190
+ }
191
+ }
192
+ };
193
+ function sanitizeForSpeech(text, config) {
194
+ const sanitizer = new SpeechSanitizer(config);
195
+ return sanitizer.sanitize(text);
196
+ }
197
+
198
+ // src/monitor/exporters/csv.ts
199
+ function exportToCsv(metrics) {
200
+ const headers = [
201
+ "id",
202
+ "timestamp",
203
+ "completed",
204
+ "timeToFirstToken",
205
+ "totalDuration",
206
+ "tokenCount",
207
+ "averageTokenLatency",
208
+ "tags"
209
+ ];
210
+ const rows = metrics.map((m) => [
211
+ m.id,
212
+ m.timestamp.toString(),
213
+ m.completed.toString(),
214
+ m.metrics.timeToFirstToken?.toString() ?? "",
215
+ m.metrics.totalDuration?.toString() ?? "",
216
+ m.metrics.tokenCount.toString(),
217
+ m.metrics.averageTokenLatency?.toString() ?? "",
218
+ JSON.stringify(m.tags)
219
+ ]);
220
+ const csvLines = [
221
+ headers.join(","),
222
+ ...rows.map(
223
+ (row) => row.map((cell) => {
224
+ if (cell.includes(",") || cell.includes('"') || cell.includes("\n")) {
225
+ return `"${cell.replace(/"/g, '""')}"`;
226
+ }
227
+ return cell;
228
+ }).join(",")
229
+ )
230
+ ];
231
+ return csvLines.join("\n");
232
+ }
233
+
234
+ // src/monitor/exporters/json.ts
235
+ function exportToJson(metrics) {
236
+ return JSON.stringify(
237
+ {
238
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
239
+ count: metrics.length,
240
+ metrics: metrics.map((m) => ({
241
+ id: m.id,
242
+ timestamp: m.timestamp,
243
+ completed: m.completed,
244
+ timeToFirstToken: m.metrics.timeToFirstToken,
245
+ totalDuration: m.metrics.totalDuration,
246
+ tokenCount: m.metrics.tokenCount,
247
+ averageTokenLatency: m.metrics.averageTokenLatency,
248
+ tags: m.tags
249
+ }))
250
+ },
251
+ null,
252
+ 2
253
+ );
254
+ }
255
+
256
+ // src/monitor/metrics-collector.ts
257
+ var MetricsCollector = class {
258
+ metrics = [];
259
+ addMetric(metric) {
260
+ this.metrics.push(metric);
261
+ }
262
+ getMetrics() {
263
+ return [...this.metrics];
264
+ }
265
+ getSummary() {
266
+ if (this.metrics.length === 0) {
267
+ return {
268
+ count: 0,
269
+ avgTimeToFirstToken: 0,
270
+ avgTotalDuration: 0,
271
+ p50TimeToFirstToken: 0,
272
+ p95TimeToFirstToken: 0,
273
+ p99TimeToFirstToken: 0,
274
+ minTimeToFirstToken: 0,
275
+ maxTimeToFirstToken: 0
276
+ };
277
+ }
278
+ const ttfts = this.metrics.map((m) => m.metrics.timeToFirstToken).filter((t) => t !== null).sort((a, b) => a - b);
279
+ const durations = this.metrics.map((m) => m.metrics.totalDuration).filter((d) => d !== null);
280
+ const percentile = (arr, p) => {
281
+ if (arr.length === 0) return 0;
282
+ const index = Math.ceil(arr.length * p) - 1;
283
+ return arr[index] ?? 0;
284
+ };
285
+ return {
286
+ count: this.metrics.length,
287
+ avgTimeToFirstToken: ttfts.reduce((a, b) => a + b, 0) / ttfts.length || 0,
288
+ avgTotalDuration: durations.reduce((a, b) => a + b, 0) / durations.length || 0,
289
+ p50TimeToFirstToken: percentile(ttfts, 0.5),
290
+ p95TimeToFirstToken: percentile(ttfts, 0.95),
291
+ p99TimeToFirstToken: percentile(ttfts, 0.99),
292
+ minTimeToFirstToken: ttfts.length > 0 ? Math.min(...ttfts) : 0,
293
+ maxTimeToFirstToken: ttfts.length > 0 ? Math.max(...ttfts) : 0
294
+ };
295
+ }
296
+ clear() {
297
+ this.metrics = [];
298
+ }
299
+ };
300
+
301
+ // src/monitor/voice-auditor.ts
302
+ var VoiceAuditor = class {
303
+ config;
304
+ collector;
305
+ activeMetrics = /* @__PURE__ */ new Map();
306
+ constructor(config = {}) {
307
+ this.config = {
308
+ enableRealtime: config.enableRealtime ?? false,
309
+ onMetric: config.onMetric ?? (() => {
310
+ }),
311
+ tags: config.tags ?? {}
312
+ };
313
+ this.collector = new MetricsCollector();
314
+ }
315
+ /**
316
+ * Start tracking a new voice interaction
317
+ */
318
+ startTracking(id, tags) {
319
+ if (this.activeMetrics.has(id)) {
320
+ throw new MonitorError(`Metric with id ${id} is already being tracked`);
321
+ }
322
+ const metric = {
323
+ id,
324
+ timestamp: Date.now(),
325
+ startTime: Date.now(),
326
+ firstTokenReceivedTime: null,
327
+ lastTokenReceivedTime: null,
328
+ completed: false,
329
+ metrics: {
330
+ timeToFirstToken: null,
331
+ totalDuration: null,
332
+ tokenCount: 0,
333
+ averageTokenLatency: null
334
+ },
335
+ tags: { ...this.config.tags, ...tags }
336
+ };
337
+ this.activeMetrics.set(id, metric);
338
+ return metric;
339
+ }
340
+ /**
341
+ * Record first token received
342
+ */
343
+ recordFirstToken(id) {
344
+ const metric = this.activeMetrics.get(id);
345
+ if (!metric) {
346
+ throw new MonitorError(`No active metric found for id ${id}`);
347
+ }
348
+ if (metric.firstTokenReceivedTime === null) {
349
+ const updated = {
350
+ ...metric,
351
+ firstTokenReceivedTime: Date.now(),
352
+ metrics: {
353
+ ...metric.metrics,
354
+ timeToFirstToken: Date.now() - metric.startTime,
355
+ tokenCount: 1
356
+ }
357
+ };
358
+ this.activeMetrics.set(id, updated);
359
+ if (this.config.enableRealtime) {
360
+ this.config.onMetric(updated);
361
+ }
362
+ }
363
+ }
364
+ /**
365
+ * Record token received
366
+ */
367
+ recordToken(id) {
368
+ const metric = this.activeMetrics.get(id);
369
+ if (!metric) {
370
+ throw new MonitorError(`No active metric found for id ${id}`);
371
+ }
372
+ const updated = {
373
+ ...metric,
374
+ lastTokenReceivedTime: Date.now(),
375
+ metrics: {
376
+ ...metric.metrics,
377
+ tokenCount: metric.metrics.tokenCount + 1
378
+ }
379
+ };
380
+ this.activeMetrics.set(id, updated);
381
+ }
382
+ /**
383
+ * Complete tracking for a voice interaction
384
+ */
385
+ completeTracking(id) {
386
+ const metric = this.activeMetrics.get(id);
387
+ if (!metric) {
388
+ throw new MonitorError(`No active metric found for id ${id}`);
389
+ }
390
+ const lastTime = metric.lastTokenReceivedTime ?? Date.now();
391
+ const totalDuration = lastTime - metric.startTime;
392
+ const avgLatency = metric.metrics.tokenCount > 0 ? totalDuration / metric.metrics.tokenCount : null;
393
+ const completed = {
394
+ ...metric,
395
+ completed: true,
396
+ metrics: {
397
+ ...metric.metrics,
398
+ totalDuration,
399
+ averageTokenLatency: avgLatency
400
+ }
401
+ };
402
+ this.activeMetrics.delete(id);
403
+ this.collector.addMetric(completed);
404
+ if (this.config.enableRealtime) {
405
+ this.config.onMetric(completed);
406
+ }
407
+ return completed;
408
+ }
409
+ /**
410
+ * Wrap an async iterable with automatic tracking
411
+ */
412
+ async *track(id, input, tags) {
413
+ this.startTracking(id, tags);
414
+ let firstToken = true;
415
+ try {
416
+ for await (const chunk of input) {
417
+ if (firstToken) {
418
+ this.recordFirstToken(id);
419
+ firstToken = false;
420
+ } else {
421
+ this.recordToken(id);
422
+ }
423
+ yield chunk;
424
+ }
425
+ } finally {
426
+ this.completeTracking(id);
427
+ }
428
+ }
429
+ /**
430
+ * Get all collected metrics
431
+ */
432
+ getMetrics() {
433
+ return this.collector.getMetrics();
434
+ }
435
+ /**
436
+ * Get summary statistics
437
+ */
438
+ getSummary() {
439
+ return this.collector.getSummary();
440
+ }
441
+ /**
442
+ * Export metrics in specified format
443
+ */
444
+ export(format) {
445
+ const metrics = this.collector.getMetrics();
446
+ switch (format) {
447
+ case "json":
448
+ return exportToJson(metrics);
449
+ case "csv":
450
+ return exportToCsv(metrics);
451
+ default:
452
+ throw new MonitorError(`Unsupported export format: ${format}`);
453
+ }
454
+ }
455
+ /**
456
+ * Clear all collected metrics
457
+ */
458
+ clear() {
459
+ this.activeMetrics.clear();
460
+ this.collector.clear();
461
+ }
462
+ };
463
+
464
+ // src/flow/buffer-manager.ts
465
+ var BufferManager = class {
466
+ buffer = [];
467
+ maxSize;
468
+ head = 0;
469
+ size = 0;
470
+ constructor(maxSize = 10) {
471
+ if (maxSize <= 0) {
472
+ throw new Error("Buffer size must be positive");
473
+ }
474
+ this.maxSize = maxSize;
475
+ }
476
+ /**
477
+ * Add chunk to buffer
478
+ */
479
+ add(chunk) {
480
+ if (this.size < this.maxSize) {
481
+ this.buffer.push(chunk);
482
+ this.size++;
483
+ } else {
484
+ this.buffer[this.head] = chunk;
485
+ this.head = (this.head + 1) % this.maxSize;
486
+ }
487
+ }
488
+ /**
489
+ * Get all buffered chunks in order
490
+ */
491
+ getAll() {
492
+ if (this.size < this.maxSize) {
493
+ return [...this.buffer];
494
+ }
495
+ return [...this.buffer.slice(this.head), ...this.buffer.slice(0, this.head)];
496
+ }
497
+ /**
498
+ * Clear all buffered chunks
499
+ */
500
+ clear() {
501
+ this.buffer = [];
502
+ this.head = 0;
503
+ this.size = 0;
504
+ }
505
+ /**
506
+ * Get current buffer size
507
+ */
508
+ getSize() {
509
+ return this.size;
510
+ }
511
+ /**
512
+ * Check if buffer is empty
513
+ */
514
+ isEmpty() {
515
+ return this.size === 0;
516
+ }
517
+ /**
518
+ * Check if buffer is full
519
+ */
520
+ isFull() {
521
+ return this.size === this.maxSize;
522
+ }
523
+ };
524
+
525
+ // src/flow/constants.ts
526
+ var DEFAULT_STALL_THRESHOLD_MS = 700;
527
+ var DEFAULT_FILLER_PHRASES = ["um", "let me think", "hmm"];
528
+ var DEFAULT_MAX_FILLERS_PER_RESPONSE = 3;
529
+
530
+ // src/flow/filler-injector.ts
531
+ var FillerInjector = class {
532
+ phrases;
533
+ maxFillers;
534
+ fillersUsed = 0;
535
+ lastFillerIndex = -1;
536
+ constructor(phrases, maxFillers) {
537
+ this.phrases = phrases;
538
+ this.maxFillers = maxFillers;
539
+ }
540
+ /**
541
+ * Get next filler phrase (returns null if limit reached)
542
+ */
543
+ getFiller() {
544
+ if (this.fillersUsed >= this.maxFillers) {
545
+ return null;
546
+ }
547
+ this.lastFillerIndex = (this.lastFillerIndex + 1) % this.phrases.length;
548
+ this.fillersUsed++;
549
+ return this.phrases[this.lastFillerIndex] ?? null;
550
+ }
551
+ /**
552
+ * Reset filler state
553
+ */
554
+ reset() {
555
+ this.fillersUsed = 0;
556
+ this.lastFillerIndex = -1;
557
+ }
558
+ /**
559
+ * Check if more fillers can be injected
560
+ */
561
+ canInjectMore() {
562
+ return this.fillersUsed < this.maxFillers;
563
+ }
564
+ /**
565
+ * Get count of fillers used
566
+ */
567
+ getUsedCount() {
568
+ return this.fillersUsed;
569
+ }
570
+ };
571
+
572
+ // src/flow/stall-detector.ts
573
+ var StallDetector = class {
574
+ lastChunkTime = null;
575
+ stallTimer = null;
576
+ thresholdMs;
577
+ onStall;
578
+ constructor(thresholdMs, onStall) {
579
+ this.thresholdMs = thresholdMs;
580
+ this.onStall = onStall;
581
+ }
582
+ /**
583
+ * Notify detector that a chunk was received
584
+ */
585
+ notifyChunk() {
586
+ this.lastChunkTime = Date.now();
587
+ this.clearTimer();
588
+ this.scheduleStallCheck();
589
+ }
590
+ /**
591
+ * Start monitoring for stalls
592
+ */
593
+ start() {
594
+ this.lastChunkTime = Date.now();
595
+ this.scheduleStallCheck();
596
+ }
597
+ /**
598
+ * Stop monitoring
599
+ */
600
+ stop() {
601
+ this.clearTimer();
602
+ this.lastChunkTime = null;
603
+ }
604
+ scheduleStallCheck() {
605
+ this.clearTimer();
606
+ this.stallTimer = setTimeout(() => {
607
+ if (this.lastChunkTime !== null) {
608
+ const elapsed = Date.now() - this.lastChunkTime;
609
+ if (elapsed >= this.thresholdMs) {
610
+ this.onStall(elapsed);
611
+ this.scheduleStallCheck();
612
+ }
613
+ }
614
+ }, this.thresholdMs);
615
+ }
616
+ clearTimer() {
617
+ if (this.stallTimer) {
618
+ clearTimeout(this.stallTimer);
619
+ this.stallTimer = null;
620
+ }
621
+ }
622
+ };
623
+
624
+ // src/flow/types.ts
625
+ var ConversationState = /* @__PURE__ */ ((ConversationState2) => {
626
+ ConversationState2["IDLE"] = "idle";
627
+ ConversationState2["WAITING"] = "waiting";
628
+ ConversationState2["SPEAKING"] = "speaking";
629
+ ConversationState2["INTERRUPTED"] = "interrupted";
630
+ return ConversationState2;
631
+ })(ConversationState || {});
632
+
633
+ // src/flow/state-machine.ts
634
+ var VALID_TRANSITIONS = /* @__PURE__ */ new Map([
635
+ ["idle" /* IDLE */, ["speaking" /* SPEAKING */, "waiting" /* WAITING */]],
636
+ [
637
+ "waiting" /* WAITING */,
638
+ ["speaking" /* SPEAKING */, "idle" /* IDLE */, "interrupted" /* INTERRUPTED */]
639
+ ],
640
+ ["speaking" /* SPEAKING */, ["interrupted" /* INTERRUPTED */, "idle" /* IDLE */]],
641
+ ["interrupted" /* INTERRUPTED */, ["idle" /* IDLE */, "waiting" /* WAITING */]]
642
+ ]);
643
+ var ConversationStateMachine = class {
644
+ currentState = "idle" /* IDLE */;
645
+ listeners = /* @__PURE__ */ new Set();
646
+ /**
647
+ * Get current state
648
+ */
649
+ getState() {
650
+ return this.currentState;
651
+ }
652
+ /**
653
+ * Attempt to transition to new state
654
+ */
655
+ transition(to) {
656
+ const validTransitions = VALID_TRANSITIONS.get(this.currentState);
657
+ if (!validTransitions?.includes(to)) {
658
+ throw new FlowControlError(`Invalid state transition: ${this.currentState} -> ${to}`, {
659
+ from: this.currentState,
660
+ to
661
+ });
662
+ }
663
+ const from = this.currentState;
664
+ this.currentState = to;
665
+ for (const listener of this.listeners) {
666
+ listener(from, to);
667
+ }
668
+ return true;
669
+ }
670
+ /**
671
+ * Add state change listener
672
+ */
673
+ onStateChange(listener) {
674
+ this.listeners.add(listener);
675
+ return () => this.listeners.delete(listener);
676
+ }
677
+ /**
678
+ * Reset to IDLE
679
+ */
680
+ reset() {
681
+ this.currentState = "idle" /* IDLE */;
682
+ }
683
+ };
684
+
685
+ // src/flow/flow-controller.ts
686
+ var FlowController = class {
687
+ config;
688
+ stateMachine;
689
+ stallDetector;
690
+ fillerInjector;
691
+ bufferManager;
692
+ firstChunkEmitted = false;
693
+ stats = {
694
+ fillersInjected: 0,
695
+ stallsDetected: 0,
696
+ chunksProcessed: 0,
697
+ firstChunkTime: null,
698
+ totalDurationMs: 0
699
+ };
700
+ startTime = null;
701
+ constructor(config = {}) {
702
+ this.config = {
703
+ stallThresholdMs: config.stallThresholdMs ?? DEFAULT_STALL_THRESHOLD_MS,
704
+ fillerPhrases: config.fillerPhrases ?? DEFAULT_FILLER_PHRASES,
705
+ enableFillers: config.enableFillers ?? true,
706
+ maxFillersPerResponse: config.maxFillersPerResponse ?? DEFAULT_MAX_FILLERS_PER_RESPONSE,
707
+ onFillerInjected: config.onFillerInjected ?? (() => {
708
+ }),
709
+ onStallDetected: config.onStallDetected ?? (() => {
710
+ }),
711
+ onFirstChunk: config.onFirstChunk ?? (() => {
712
+ })
713
+ };
714
+ this.stateMachine = new ConversationStateMachine();
715
+ this.fillerInjector = new FillerInjector(
716
+ this.config.fillerPhrases,
717
+ this.config.maxFillersPerResponse
718
+ );
719
+ this.bufferManager = new BufferManager(10);
720
+ this.stallDetector = new StallDetector(
721
+ this.config.stallThresholdMs,
722
+ this.handleStall.bind(this)
723
+ );
724
+ }
725
+ /**
726
+ * Wrap an async iterable with flow control
727
+ */
728
+ async *wrap(input) {
729
+ this.reset();
730
+ this.startTime = Date.now();
731
+ this.stateMachine.transition("waiting" /* WAITING */);
732
+ this.stallDetector.start();
733
+ try {
734
+ for await (const chunk of input) {
735
+ if (this.stateMachine.getState() === "interrupted" /* INTERRUPTED */) {
736
+ break;
737
+ }
738
+ this.stallDetector.notifyChunk();
739
+ this.stats.chunksProcessed++;
740
+ this.bufferManager.add(chunk);
741
+ if (!this.firstChunkEmitted) {
742
+ this.firstChunkEmitted = true;
743
+ this.stats.firstChunkTime = Date.now() - (this.startTime ?? Date.now());
744
+ this.stateMachine.transition("speaking" /* SPEAKING */);
745
+ this.config.onFirstChunk();
746
+ }
747
+ yield chunk;
748
+ }
749
+ if (this.stateMachine.getState() !== "interrupted" /* INTERRUPTED */) {
750
+ this.stateMachine.transition("idle" /* IDLE */);
751
+ }
752
+ } catch (error) {
753
+ throw new FlowControlError("Flow control error during stream processing", { error });
754
+ } finally {
755
+ this.stallDetector.stop();
756
+ this.stats.totalDurationMs = Date.now() - (this.startTime ?? Date.now());
757
+ }
758
+ }
759
+ /**
760
+ * Interrupt the current flow (for barge-in)
761
+ */
762
+ interrupt() {
763
+ const currentState = this.stateMachine.getState();
764
+ if (currentState === "speaking" /* SPEAKING */ || currentState === "waiting" /* WAITING */) {
765
+ this.stateMachine.transition("interrupted" /* INTERRUPTED */);
766
+ this.stallDetector.stop();
767
+ this.fillerInjector.reset();
768
+ this.bufferManager.clear();
769
+ }
770
+ }
771
+ /**
772
+ * Get current conversation state
773
+ */
774
+ getState() {
775
+ return this.stateMachine.getState();
776
+ }
777
+ /**
778
+ * Get flow statistics
779
+ */
780
+ getStats() {
781
+ return { ...this.stats };
782
+ }
783
+ /**
784
+ * Get buffered chunks (for advanced barge-in scenarios)
785
+ */
786
+ getBufferedChunks() {
787
+ return this.bufferManager.getAll();
788
+ }
789
+ handleStall(durationMs) {
790
+ if (this.config.enableFillers && !this.firstChunkEmitted && this.stateMachine.getState() !== "interrupted" /* INTERRUPTED */) {
791
+ const filler = this.fillerInjector.getFiller();
792
+ if (filler) {
793
+ this.stats.fillersInjected++;
794
+ this.config.onFillerInjected(filler);
795
+ }
796
+ }
797
+ this.stats.stallsDetected++;
798
+ this.config.onStallDetected(durationMs);
799
+ }
800
+ reset() {
801
+ this.firstChunkEmitted = false;
802
+ this.fillerInjector.reset();
803
+ this.bufferManager.clear();
804
+ this.stats = {
805
+ fillersInjected: 0,
806
+ stallsDetected: 0,
807
+ chunksProcessed: 0,
808
+ firstChunkTime: null,
809
+ totalDurationMs: 0
810
+ };
811
+ }
812
+ };
813
+ function withFlowControl(input, config) {
814
+ const controller = new FlowController(config);
815
+ return controller.wrap(input);
816
+ }
817
+
818
+ // src/flow/flow-manager.ts
819
+ var FlowManager = class {
820
+ config;
821
+ stateMachine;
822
+ stallDetector;
823
+ fillerInjector;
824
+ bufferManager;
825
+ listeners = /* @__PURE__ */ new Set();
826
+ firstChunkEmitted = false;
827
+ stats = {
828
+ fillersInjected: 0,
829
+ stallsDetected: 0,
830
+ chunksProcessed: 0,
831
+ firstChunkTime: null,
832
+ totalDurationMs: 0
833
+ };
834
+ startTime = null;
835
+ stateChangeUnsubscribe = null;
836
+ constructor(config = {}) {
837
+ this.config = {
838
+ stallThresholdMs: config.stallThresholdMs ?? DEFAULT_STALL_THRESHOLD_MS,
839
+ fillerPhrases: config.fillerPhrases ?? DEFAULT_FILLER_PHRASES,
840
+ enableFillers: config.enableFillers ?? true,
841
+ maxFillersPerResponse: config.maxFillersPerResponse ?? DEFAULT_MAX_FILLERS_PER_RESPONSE,
842
+ bufferSize: config.bufferSize ?? 10
843
+ };
844
+ this.stateMachine = new ConversationStateMachine();
845
+ this.fillerInjector = new FillerInjector(
846
+ this.config.fillerPhrases,
847
+ this.config.maxFillersPerResponse
848
+ );
849
+ this.bufferManager = new BufferManager(this.config.bufferSize);
850
+ this.stallDetector = new StallDetector(
851
+ this.config.stallThresholdMs,
852
+ this.handleStall.bind(this)
853
+ );
854
+ }
855
+ /**
856
+ * Add event listener
857
+ */
858
+ on(listener) {
859
+ this.listeners.add(listener);
860
+ return () => this.listeners.delete(listener);
861
+ }
862
+ /**
863
+ * Start flow tracking
864
+ */
865
+ start() {
866
+ if (this.startTime !== null) {
867
+ throw new FlowControlError("FlowManager already started");
868
+ }
869
+ this.reset();
870
+ this.startTime = Date.now();
871
+ this.stateMachine.transition("waiting" /* WAITING */);
872
+ this.stallDetector.start();
873
+ this.stateChangeUnsubscribe = this.stateMachine.onStateChange((from, to) => {
874
+ this.emit({
875
+ type: "state-change",
876
+ from,
877
+ to
878
+ });
879
+ });
880
+ }
881
+ /**
882
+ * Process a chunk from the stream
883
+ */
884
+ processChunk(chunk) {
885
+ if (this.startTime === null) {
886
+ throw new FlowControlError("FlowManager not started. Call start() first.");
887
+ }
888
+ if (this.stateMachine.getState() === "interrupted" /* INTERRUPTED */) {
889
+ return;
890
+ }
891
+ this.stallDetector.notifyChunk();
892
+ this.stats.chunksProcessed++;
893
+ this.bufferManager.add(chunk);
894
+ if (!this.firstChunkEmitted) {
895
+ this.firstChunkEmitted = true;
896
+ this.stats.firstChunkTime = Date.now() - this.startTime;
897
+ this.stateMachine.transition("speaking" /* SPEAKING */);
898
+ this.emit({
899
+ type: "first-chunk",
900
+ chunk
901
+ });
902
+ }
903
+ this.emit({
904
+ type: "chunk-processed",
905
+ chunk
906
+ });
907
+ }
908
+ /**
909
+ * Complete the flow
910
+ */
911
+ complete() {
912
+ if (this.startTime === null) {
913
+ throw new FlowControlError("FlowManager not started");
914
+ }
915
+ this.stallDetector.stop();
916
+ this.stats.totalDurationMs = Date.now() - this.startTime;
917
+ if (this.stateMachine.getState() !== "interrupted" /* INTERRUPTED */) {
918
+ this.stateMachine.transition("idle" /* IDLE */);
919
+ }
920
+ this.emit({
921
+ type: "completed",
922
+ stats: this.getStats()
923
+ });
924
+ if (this.stateChangeUnsubscribe) {
925
+ this.stateChangeUnsubscribe();
926
+ this.stateChangeUnsubscribe = null;
927
+ }
928
+ this.startTime = null;
929
+ }
930
+ /**
931
+ * Interrupt the flow (for barge-in)
932
+ */
933
+ interrupt() {
934
+ const currentState = this.stateMachine.getState();
935
+ if (currentState === "speaking" /* SPEAKING */ || currentState === "waiting" /* WAITING */) {
936
+ this.stateMachine.transition("interrupted" /* INTERRUPTED */);
937
+ this.stallDetector.stop();
938
+ this.fillerInjector.reset();
939
+ this.bufferManager.clear();
940
+ this.emit({
941
+ type: "interrupted"
942
+ });
943
+ }
944
+ }
945
+ /**
946
+ * Get current conversation state
947
+ */
948
+ getState() {
949
+ return this.stateMachine.getState();
950
+ }
951
+ /**
952
+ * Get flow statistics
953
+ */
954
+ getStats() {
955
+ return { ...this.stats };
956
+ }
957
+ /**
958
+ * Get buffered chunks
959
+ */
960
+ getBufferedChunks() {
961
+ return this.bufferManager.getAll();
962
+ }
963
+ handleStall(durationMs) {
964
+ this.stats.stallsDetected++;
965
+ this.emit({
966
+ type: "stall-detected",
967
+ durationMs
968
+ });
969
+ if (this.config.enableFillers && !this.firstChunkEmitted && this.stateMachine.getState() !== "interrupted" /* INTERRUPTED */) {
970
+ const filler = this.fillerInjector.getFiller();
971
+ if (filler) {
972
+ this.stats.fillersInjected++;
973
+ this.emit({
974
+ type: "filler-injected",
975
+ filler
976
+ });
977
+ }
978
+ }
979
+ }
980
+ emit(event) {
981
+ for (const listener of this.listeners) {
982
+ try {
983
+ listener(event);
984
+ } catch (error) {
985
+ console.error("Error in FlowManager event listener:", error);
986
+ }
987
+ }
988
+ }
989
+ reset() {
990
+ this.firstChunkEmitted = false;
991
+ this.fillerInjector.reset();
992
+ this.bufferManager.clear();
993
+ this.stats = {
994
+ fillersInjected: 0,
995
+ stallsDetected: 0,
996
+ chunksProcessed: 0,
997
+ firstChunkTime: null,
998
+ totalDurationMs: 0
999
+ };
1000
+ }
1001
+ };
1002
+
1003
+ exports.BufferManager = BufferManager;
1004
+ exports.ConversationState = ConversationState;
1005
+ exports.ConversationStateMachine = ConversationStateMachine;
1006
+ exports.DEFAULT_FILLER_PHRASES = DEFAULT_FILLER_PHRASES;
1007
+ exports.DEFAULT_MAX_FILLERS_PER_RESPONSE = DEFAULT_MAX_FILLERS_PER_RESPONSE;
1008
+ exports.DEFAULT_STALL_THRESHOLD_MS = DEFAULT_STALL_THRESHOLD_MS;
1009
+ exports.FillerInjector = FillerInjector;
1010
+ exports.FlowControlError = FlowControlError;
1011
+ exports.FlowController = FlowController;
1012
+ exports.FlowManager = FlowManager;
1013
+ exports.MetricsCollector = MetricsCollector;
1014
+ exports.MonitorError = MonitorError;
1015
+ exports.SanitizerError = SanitizerError;
1016
+ exports.SpeechSanitizer = SpeechSanitizer;
1017
+ exports.StallDetector = StallDetector;
1018
+ exports.VocalStackError = VocalStackError;
1019
+ exports.VoiceAuditor = VoiceAuditor;
1020
+ exports.exportToCsv = exportToCsv;
1021
+ exports.exportToJson = exportToJson;
1022
+ exports.ruleRegistry = ruleRegistry;
1023
+ exports.sanitizeForSpeech = sanitizeForSpeech;
1024
+ exports.withFlowControl = withFlowControl;
1025
+ //# sourceMappingURL=index.cjs.map
1026
+ //# sourceMappingURL=index.cjs.map