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