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.
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Configuration for voice auditor
3
+ */
4
+ interface AuditorConfig {
5
+ /**
6
+ * Whether to enable real-time monitoring
7
+ * @default false
8
+ */
9
+ readonly enableRealtime?: boolean;
10
+ /**
11
+ * Callback for real-time metrics
12
+ */
13
+ readonly onMetric?: (metric: VoiceMetric) => void;
14
+ /**
15
+ * Custom tags to attach to metrics
16
+ */
17
+ readonly tags?: Record<string, string>;
18
+ }
19
+ /**
20
+ * Voice latency metric
21
+ */
22
+ interface VoiceMetric {
23
+ readonly id: string;
24
+ readonly timestamp: number;
25
+ readonly startTime: number;
26
+ readonly firstTokenReceivedTime: number | null;
27
+ readonly lastTokenReceivedTime: number | null;
28
+ readonly completed: boolean;
29
+ readonly metrics: {
30
+ readonly timeToFirstToken: number | null;
31
+ readonly totalDuration: number | null;
32
+ readonly tokenCount: number;
33
+ readonly averageTokenLatency: number | null;
34
+ };
35
+ readonly tags: Record<string, string>;
36
+ }
37
+ /**
38
+ * Summary statistics
39
+ */
40
+ interface MetricsSummary {
41
+ readonly count: number;
42
+ readonly avgTimeToFirstToken: number;
43
+ readonly avgTotalDuration: number;
44
+ readonly p50TimeToFirstToken: number;
45
+ readonly p95TimeToFirstToken: number;
46
+ readonly p99TimeToFirstToken: number;
47
+ readonly minTimeToFirstToken: number;
48
+ readonly maxTimeToFirstToken: number;
49
+ }
50
+ /**
51
+ * Export format options
52
+ */
53
+ type ExportFormat = 'json' | 'csv';
54
+
55
+ /**
56
+ * Voice latency auditor and profiler
57
+ */
58
+ declare class VoiceAuditor {
59
+ private readonly config;
60
+ private readonly collector;
61
+ private activeMetrics;
62
+ constructor(config?: AuditorConfig);
63
+ /**
64
+ * Start tracking a new voice interaction
65
+ */
66
+ startTracking(id: string, tags?: Record<string, string>): VoiceMetric;
67
+ /**
68
+ * Record first token received
69
+ */
70
+ recordFirstToken(id: string): void;
71
+ /**
72
+ * Record token received
73
+ */
74
+ recordToken(id: string): void;
75
+ /**
76
+ * Complete tracking for a voice interaction
77
+ */
78
+ completeTracking(id: string): VoiceMetric;
79
+ /**
80
+ * Wrap an async iterable with automatic tracking
81
+ */
82
+ track(id: string, input: AsyncIterable<string>, tags?: Record<string, string>): AsyncIterable<string>;
83
+ /**
84
+ * Get all collected metrics
85
+ */
86
+ getMetrics(): readonly VoiceMetric[];
87
+ /**
88
+ * Get summary statistics
89
+ */
90
+ getSummary(): MetricsSummary;
91
+ /**
92
+ * Export metrics in specified format
93
+ */
94
+ export(format: ExportFormat): string;
95
+ /**
96
+ * Clear all collected metrics
97
+ */
98
+ clear(): void;
99
+ }
100
+
101
+ /**
102
+ * Export metrics to CSV format
103
+ */
104
+ declare function exportToCsv(metrics: readonly VoiceMetric[]): string;
105
+
106
+ /**
107
+ * Export metrics to JSON format
108
+ */
109
+ declare function exportToJson(metrics: readonly VoiceMetric[]): string;
110
+
111
+ /**
112
+ * Collects and aggregates metrics
113
+ */
114
+ declare class MetricsCollector {
115
+ private metrics;
116
+ addMetric(metric: VoiceMetric): void;
117
+ getMetrics(): readonly VoiceMetric[];
118
+ getSummary(): MetricsSummary;
119
+ clear(): void;
120
+ }
121
+
122
+ export { type AuditorConfig, type ExportFormat, MetricsCollector, type MetricsSummary, VoiceAuditor, type VoiceMetric, exportToCsv, exportToJson };
@@ -0,0 +1,286 @@
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 MonitorError = class extends VocalStackError {
12
+ constructor(message, context) {
13
+ super(message, "MONITOR_ERROR", context);
14
+ this.name = "MonitorError";
15
+ }
16
+ };
17
+
18
+ // src/monitor/exporters/csv.ts
19
+ function exportToCsv(metrics) {
20
+ const headers = [
21
+ "id",
22
+ "timestamp",
23
+ "completed",
24
+ "timeToFirstToken",
25
+ "totalDuration",
26
+ "tokenCount",
27
+ "averageTokenLatency",
28
+ "tags"
29
+ ];
30
+ const rows = metrics.map((m) => [
31
+ m.id,
32
+ m.timestamp.toString(),
33
+ m.completed.toString(),
34
+ m.metrics.timeToFirstToken?.toString() ?? "",
35
+ m.metrics.totalDuration?.toString() ?? "",
36
+ m.metrics.tokenCount.toString(),
37
+ m.metrics.averageTokenLatency?.toString() ?? "",
38
+ JSON.stringify(m.tags)
39
+ ]);
40
+ const csvLines = [
41
+ headers.join(","),
42
+ ...rows.map(
43
+ (row) => row.map((cell) => {
44
+ if (cell.includes(",") || cell.includes('"') || cell.includes("\n")) {
45
+ return `"${cell.replace(/"/g, '""')}"`;
46
+ }
47
+ return cell;
48
+ }).join(",")
49
+ )
50
+ ];
51
+ return csvLines.join("\n");
52
+ }
53
+
54
+ // src/monitor/exporters/json.ts
55
+ function exportToJson(metrics) {
56
+ return JSON.stringify(
57
+ {
58
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
59
+ count: metrics.length,
60
+ metrics: metrics.map((m) => ({
61
+ id: m.id,
62
+ timestamp: m.timestamp,
63
+ completed: m.completed,
64
+ timeToFirstToken: m.metrics.timeToFirstToken,
65
+ totalDuration: m.metrics.totalDuration,
66
+ tokenCount: m.metrics.tokenCount,
67
+ averageTokenLatency: m.metrics.averageTokenLatency,
68
+ tags: m.tags
69
+ }))
70
+ },
71
+ null,
72
+ 2
73
+ );
74
+ }
75
+
76
+ // src/monitor/metrics-collector.ts
77
+ var MetricsCollector = class {
78
+ metrics = [];
79
+ addMetric(metric) {
80
+ this.metrics.push(metric);
81
+ }
82
+ getMetrics() {
83
+ return [...this.metrics];
84
+ }
85
+ getSummary() {
86
+ if (this.metrics.length === 0) {
87
+ return {
88
+ count: 0,
89
+ avgTimeToFirstToken: 0,
90
+ avgTotalDuration: 0,
91
+ p50TimeToFirstToken: 0,
92
+ p95TimeToFirstToken: 0,
93
+ p99TimeToFirstToken: 0,
94
+ minTimeToFirstToken: 0,
95
+ maxTimeToFirstToken: 0
96
+ };
97
+ }
98
+ const ttfts = this.metrics.map((m) => m.metrics.timeToFirstToken).filter((t) => t !== null).sort((a, b) => a - b);
99
+ const durations = this.metrics.map((m) => m.metrics.totalDuration).filter((d) => d !== null);
100
+ const percentile = (arr, p) => {
101
+ if (arr.length === 0) return 0;
102
+ const index = Math.ceil(arr.length * p) - 1;
103
+ return arr[index] ?? 0;
104
+ };
105
+ return {
106
+ count: this.metrics.length,
107
+ avgTimeToFirstToken: ttfts.reduce((a, b) => a + b, 0) / ttfts.length || 0,
108
+ avgTotalDuration: durations.reduce((a, b) => a + b, 0) / durations.length || 0,
109
+ p50TimeToFirstToken: percentile(ttfts, 0.5),
110
+ p95TimeToFirstToken: percentile(ttfts, 0.95),
111
+ p99TimeToFirstToken: percentile(ttfts, 0.99),
112
+ minTimeToFirstToken: ttfts.length > 0 ? Math.min(...ttfts) : 0,
113
+ maxTimeToFirstToken: ttfts.length > 0 ? Math.max(...ttfts) : 0
114
+ };
115
+ }
116
+ clear() {
117
+ this.metrics = [];
118
+ }
119
+ };
120
+
121
+ // src/monitor/voice-auditor.ts
122
+ var VoiceAuditor = class {
123
+ config;
124
+ collector;
125
+ activeMetrics = /* @__PURE__ */ new Map();
126
+ constructor(config = {}) {
127
+ this.config = {
128
+ enableRealtime: config.enableRealtime ?? false,
129
+ onMetric: config.onMetric ?? (() => {
130
+ }),
131
+ tags: config.tags ?? {}
132
+ };
133
+ this.collector = new MetricsCollector();
134
+ }
135
+ /**
136
+ * Start tracking a new voice interaction
137
+ */
138
+ startTracking(id, tags) {
139
+ if (this.activeMetrics.has(id)) {
140
+ throw new MonitorError(`Metric with id ${id} is already being tracked`);
141
+ }
142
+ const metric = {
143
+ id,
144
+ timestamp: Date.now(),
145
+ startTime: Date.now(),
146
+ firstTokenReceivedTime: null,
147
+ lastTokenReceivedTime: null,
148
+ completed: false,
149
+ metrics: {
150
+ timeToFirstToken: null,
151
+ totalDuration: null,
152
+ tokenCount: 0,
153
+ averageTokenLatency: null
154
+ },
155
+ tags: { ...this.config.tags, ...tags }
156
+ };
157
+ this.activeMetrics.set(id, metric);
158
+ return metric;
159
+ }
160
+ /**
161
+ * Record first token received
162
+ */
163
+ recordFirstToken(id) {
164
+ const metric = this.activeMetrics.get(id);
165
+ if (!metric) {
166
+ throw new MonitorError(`No active metric found for id ${id}`);
167
+ }
168
+ if (metric.firstTokenReceivedTime === null) {
169
+ const updated = {
170
+ ...metric,
171
+ firstTokenReceivedTime: Date.now(),
172
+ metrics: {
173
+ ...metric.metrics,
174
+ timeToFirstToken: Date.now() - metric.startTime,
175
+ tokenCount: 1
176
+ }
177
+ };
178
+ this.activeMetrics.set(id, updated);
179
+ if (this.config.enableRealtime) {
180
+ this.config.onMetric(updated);
181
+ }
182
+ }
183
+ }
184
+ /**
185
+ * Record token received
186
+ */
187
+ recordToken(id) {
188
+ const metric = this.activeMetrics.get(id);
189
+ if (!metric) {
190
+ throw new MonitorError(`No active metric found for id ${id}`);
191
+ }
192
+ const updated = {
193
+ ...metric,
194
+ lastTokenReceivedTime: Date.now(),
195
+ metrics: {
196
+ ...metric.metrics,
197
+ tokenCount: metric.metrics.tokenCount + 1
198
+ }
199
+ };
200
+ this.activeMetrics.set(id, updated);
201
+ }
202
+ /**
203
+ * Complete tracking for a voice interaction
204
+ */
205
+ completeTracking(id) {
206
+ const metric = this.activeMetrics.get(id);
207
+ if (!metric) {
208
+ throw new MonitorError(`No active metric found for id ${id}`);
209
+ }
210
+ const lastTime = metric.lastTokenReceivedTime ?? Date.now();
211
+ const totalDuration = lastTime - metric.startTime;
212
+ const avgLatency = metric.metrics.tokenCount > 0 ? totalDuration / metric.metrics.tokenCount : null;
213
+ const completed = {
214
+ ...metric,
215
+ completed: true,
216
+ metrics: {
217
+ ...metric.metrics,
218
+ totalDuration,
219
+ averageTokenLatency: avgLatency
220
+ }
221
+ };
222
+ this.activeMetrics.delete(id);
223
+ this.collector.addMetric(completed);
224
+ if (this.config.enableRealtime) {
225
+ this.config.onMetric(completed);
226
+ }
227
+ return completed;
228
+ }
229
+ /**
230
+ * Wrap an async iterable with automatic tracking
231
+ */
232
+ async *track(id, input, tags) {
233
+ this.startTracking(id, tags);
234
+ let firstToken = true;
235
+ try {
236
+ for await (const chunk of input) {
237
+ if (firstToken) {
238
+ this.recordFirstToken(id);
239
+ firstToken = false;
240
+ } else {
241
+ this.recordToken(id);
242
+ }
243
+ yield chunk;
244
+ }
245
+ } finally {
246
+ this.completeTracking(id);
247
+ }
248
+ }
249
+ /**
250
+ * Get all collected metrics
251
+ */
252
+ getMetrics() {
253
+ return this.collector.getMetrics();
254
+ }
255
+ /**
256
+ * Get summary statistics
257
+ */
258
+ getSummary() {
259
+ return this.collector.getSummary();
260
+ }
261
+ /**
262
+ * Export metrics in specified format
263
+ */
264
+ export(format) {
265
+ const metrics = this.collector.getMetrics();
266
+ switch (format) {
267
+ case "json":
268
+ return exportToJson(metrics);
269
+ case "csv":
270
+ return exportToCsv(metrics);
271
+ default:
272
+ throw new MonitorError(`Unsupported export format: ${format}`);
273
+ }
274
+ }
275
+ /**
276
+ * Clear all collected metrics
277
+ */
278
+ clear() {
279
+ this.activeMetrics.clear();
280
+ this.collector.clear();
281
+ }
282
+ };
283
+
284
+ export { MetricsCollector, VoiceAuditor, exportToCsv, exportToJson };
285
+ //# sourceMappingURL=index.js.map
286
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/errors.ts","../../src/monitor/exporters/csv.ts","../../src/monitor/exporters/json.ts","../../src/monitor/metrics-collector.ts","../../src/monitor/voice-auditor.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Base error class for all vocal-stack errors\n */\nexport class VocalStackError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n public readonly context?: Record<string, unknown>\n ) {\n super(message);\n this.name = 'VocalStackError';\n Error.captureStackTrace(this, this.constructor);\n }\n}\n\n/**\n * Error thrown during text sanitization\n */\nexport class SanitizerError extends VocalStackError {\n constructor(message: string, context?: Record<string, unknown>) {\n super(message, 'SANITIZER_ERROR', context);\n this.name = 'SanitizerError';\n }\n}\n\n/**\n * Error thrown during flow control operations\n */\nexport class FlowControlError extends VocalStackError {\n constructor(message: string, context?: Record<string, unknown>) {\n super(message, 'FLOW_CONTROL_ERROR', context);\n this.name = 'FlowControlError';\n }\n}\n\n/**\n * Error thrown during monitoring operations\n */\nexport class MonitorError extends VocalStackError {\n constructor(message: string, context?: Record<string, unknown>) {\n super(message, 'MONITOR_ERROR', context);\n this.name = 'MonitorError';\n }\n}\n","import type { VoiceMetric } from '../types';\n\n/**\n * Export metrics to CSV format\n */\nexport function exportToCsv(metrics: readonly VoiceMetric[]): string {\n const headers = [\n 'id',\n 'timestamp',\n 'completed',\n 'timeToFirstToken',\n 'totalDuration',\n 'tokenCount',\n 'averageTokenLatency',\n 'tags',\n ];\n\n const rows = metrics.map((m) => [\n m.id,\n m.timestamp.toString(),\n m.completed.toString(),\n m.metrics.timeToFirstToken?.toString() ?? '',\n m.metrics.totalDuration?.toString() ?? '',\n m.metrics.tokenCount.toString(),\n m.metrics.averageTokenLatency?.toString() ?? '',\n JSON.stringify(m.tags),\n ]);\n\n const csvLines = [\n headers.join(','),\n ...rows.map((row) =>\n row\n .map((cell) => {\n // Escape commas and quotes in cell values\n if (cell.includes(',') || cell.includes('\"') || cell.includes('\\n')) {\n return `\"${cell.replace(/\"/g, '\"\"')}\"`;\n }\n return cell;\n })\n .join(',')\n ),\n ];\n\n return csvLines.join('\\n');\n}\n","import type { VoiceMetric } from '../types';\n\n/**\n * Export metrics to JSON format\n */\nexport function exportToJson(metrics: readonly VoiceMetric[]): string {\n return JSON.stringify(\n {\n exportedAt: new Date().toISOString(),\n count: metrics.length,\n metrics: metrics.map((m) => ({\n id: m.id,\n timestamp: m.timestamp,\n completed: m.completed,\n timeToFirstToken: m.metrics.timeToFirstToken,\n totalDuration: m.metrics.totalDuration,\n tokenCount: m.metrics.tokenCount,\n averageTokenLatency: m.metrics.averageTokenLatency,\n tags: m.tags,\n })),\n },\n null,\n 2\n );\n}\n","import type { MetricsSummary, VoiceMetric } from './types';\n\n/**\n * Collects and aggregates metrics\n */\nexport class MetricsCollector {\n private metrics: VoiceMetric[] = [];\n\n addMetric(metric: VoiceMetric): void {\n this.metrics.push(metric);\n }\n\n getMetrics(): readonly VoiceMetric[] {\n return [...this.metrics];\n }\n\n getSummary(): MetricsSummary {\n if (this.metrics.length === 0) {\n return {\n count: 0,\n avgTimeToFirstToken: 0,\n avgTotalDuration: 0,\n p50TimeToFirstToken: 0,\n p95TimeToFirstToken: 0,\n p99TimeToFirstToken: 0,\n minTimeToFirstToken: 0,\n maxTimeToFirstToken: 0,\n };\n }\n\n const ttfts = this.metrics\n .map((m) => m.metrics.timeToFirstToken)\n .filter((t): t is number => t !== null)\n .sort((a, b) => a - b);\n\n const durations = this.metrics\n .map((m) => m.metrics.totalDuration)\n .filter((d): d is number => d !== null);\n\n const percentile = (arr: number[], p: number): number => {\n if (arr.length === 0) return 0;\n const index = Math.ceil(arr.length * p) - 1;\n return arr[index] ?? 0;\n };\n\n return {\n count: this.metrics.length,\n avgTimeToFirstToken: ttfts.reduce((a, b) => a + b, 0) / ttfts.length || 0,\n avgTotalDuration: durations.reduce((a, b) => a + b, 0) / durations.length || 0,\n p50TimeToFirstToken: percentile(ttfts, 0.5),\n p95TimeToFirstToken: percentile(ttfts, 0.95),\n p99TimeToFirstToken: percentile(ttfts, 0.99),\n minTimeToFirstToken: ttfts.length > 0 ? Math.min(...ttfts) : 0,\n maxTimeToFirstToken: ttfts.length > 0 ? Math.max(...ttfts) : 0,\n };\n }\n\n clear(): void {\n this.metrics = [];\n }\n}\n","import { MonitorError } from '../errors';\nimport { exportToCsv, exportToJson } from './exporters';\nimport { MetricsCollector } from './metrics-collector';\nimport type { AuditorConfig, ExportFormat, MetricsSummary, VoiceMetric } from './types';\n\n/**\n * Voice latency auditor and profiler\n */\nexport class VoiceAuditor {\n private readonly config: Required<AuditorConfig>;\n private readonly collector: MetricsCollector;\n private activeMetrics = new Map<string, VoiceMetric>();\n\n constructor(config: AuditorConfig = {}) {\n this.config = {\n enableRealtime: config.enableRealtime ?? false,\n onMetric: config.onMetric ?? (() => {}),\n tags: config.tags ?? {},\n };\n this.collector = new MetricsCollector();\n }\n\n /**\n * Start tracking a new voice interaction\n */\n startTracking(id: string, tags?: Record<string, string>): VoiceMetric {\n if (this.activeMetrics.has(id)) {\n throw new MonitorError(`Metric with id ${id} is already being tracked`);\n }\n\n const metric: VoiceMetric = {\n id,\n timestamp: Date.now(),\n startTime: Date.now(),\n firstTokenReceivedTime: null,\n lastTokenReceivedTime: null,\n completed: false,\n metrics: {\n timeToFirstToken: null,\n totalDuration: null,\n tokenCount: 0,\n averageTokenLatency: null,\n },\n tags: { ...this.config.tags, ...tags },\n };\n\n this.activeMetrics.set(id, metric);\n return metric;\n }\n\n /**\n * Record first token received\n */\n recordFirstToken(id: string): void {\n const metric = this.activeMetrics.get(id);\n if (!metric) {\n throw new MonitorError(`No active metric found for id ${id}`);\n }\n\n if (metric.firstTokenReceivedTime === null) {\n const updated: VoiceMetric = {\n ...metric,\n firstTokenReceivedTime: Date.now(),\n metrics: {\n ...metric.metrics,\n timeToFirstToken: Date.now() - metric.startTime,\n tokenCount: 1,\n },\n };\n this.activeMetrics.set(id, updated);\n\n if (this.config.enableRealtime) {\n this.config.onMetric(updated);\n }\n }\n }\n\n /**\n * Record token received\n */\n recordToken(id: string): void {\n const metric = this.activeMetrics.get(id);\n if (!metric) {\n throw new MonitorError(`No active metric found for id ${id}`);\n }\n\n const updated: VoiceMetric = {\n ...metric,\n lastTokenReceivedTime: Date.now(),\n metrics: {\n ...metric.metrics,\n tokenCount: metric.metrics.tokenCount + 1,\n },\n };\n this.activeMetrics.set(id, updated);\n }\n\n /**\n * Complete tracking for a voice interaction\n */\n completeTracking(id: string): VoiceMetric {\n const metric = this.activeMetrics.get(id);\n if (!metric) {\n throw new MonitorError(`No active metric found for id ${id}`);\n }\n\n const lastTime = metric.lastTokenReceivedTime ?? Date.now();\n const totalDuration = lastTime - metric.startTime;\n const avgLatency =\n metric.metrics.tokenCount > 0 ? totalDuration / metric.metrics.tokenCount : null;\n\n const completed: VoiceMetric = {\n ...metric,\n completed: true,\n metrics: {\n ...metric.metrics,\n totalDuration,\n averageTokenLatency: avgLatency,\n },\n };\n\n this.activeMetrics.delete(id);\n this.collector.addMetric(completed);\n\n if (this.config.enableRealtime) {\n this.config.onMetric(completed);\n }\n\n return completed;\n }\n\n /**\n * Wrap an async iterable with automatic tracking\n */\n async *track(\n id: string,\n input: AsyncIterable<string>,\n tags?: Record<string, string>\n ): AsyncIterable<string> {\n this.startTracking(id, tags);\n\n let firstToken = true;\n try {\n for await (const chunk of input) {\n if (firstToken) {\n this.recordFirstToken(id);\n firstToken = false;\n } else {\n this.recordToken(id);\n }\n yield chunk;\n }\n } finally {\n this.completeTracking(id);\n }\n }\n\n /**\n * Get all collected metrics\n */\n getMetrics(): readonly VoiceMetric[] {\n return this.collector.getMetrics();\n }\n\n /**\n * Get summary statistics\n */\n getSummary(): MetricsSummary {\n return this.collector.getSummary();\n }\n\n /**\n * Export metrics in specified format\n */\n export(format: ExportFormat): string {\n const metrics = this.collector.getMetrics();\n\n switch (format) {\n case 'json':\n return exportToJson(metrics);\n case 'csv':\n return exportToCsv(metrics);\n default:\n throw new MonitorError(`Unsupported export format: ${format}`);\n }\n }\n\n /**\n * Clear all collected metrics\n */\n clear(): void {\n this.activeMetrics.clear();\n this.collector.clear();\n }\n}\n"]}
@@ -0,0 +1,190 @@
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
+
20
+ // src/sanitizer/rules/code-blocks.ts
21
+ function codeBlocksRule(text, _config) {
22
+ let result = text;
23
+ result = result.replace(/```[\s\S]*?```/g, "");
24
+ result = result.replace(/^(?: {4}|\t).+$/gm, "");
25
+ return result;
26
+ }
27
+
28
+ // src/sanitizer/rules/markdown.ts
29
+ function markdownRule(text, _config) {
30
+ let result = text;
31
+ result = result.replace(/^#{1,6}\s+/gm, "");
32
+ result = result.replace(/(\*\*|__)(.*?)\1/g, "$2");
33
+ result = result.replace(/(\*|_)(.*?)\1/g, "$2");
34
+ result = result.replace(/`([^`]+)`/g, "$1");
35
+ result = result.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
36
+ result = result.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1");
37
+ result = result.replace(/^>\s+/gm, "");
38
+ result = result.replace(/^[\-*_]{3,}$/gm, "");
39
+ result = result.replace(/^[\s]*[-*+]\s+/gm, "");
40
+ result = result.replace(/^[\s]*\d+\.\s+/gm, "");
41
+ result = result.replace(/~~(.*?)~~/g, "$1");
42
+ result = result.replace(/^[\s]*-\s+\[[ xX]\]\s+/gm, "");
43
+ return result;
44
+ }
45
+
46
+ // src/sanitizer/rules/punctuation.ts
47
+ function punctuationRule(text, _config) {
48
+ let result = text;
49
+ result = result.replace(/!{2,}/g, "!");
50
+ result = result.replace(/\?{2,}/g, "?");
51
+ result = result.replace(/\.{3,}/g, ".");
52
+ result = result.replace(/-{2,}/g, " ");
53
+ result = result.replace(/[—–]/g, " ");
54
+ result = result.replace(/[()[\]{}]/g, "");
55
+ result = result.replace(/,{2,}/g, ",");
56
+ result = result.replace(/[;:]/g, ",");
57
+ result = result.replace(/["'"'`]/g, "");
58
+ result = result.replace(/[*_]/g, "");
59
+ result = result.replace(/[/\\]/g, " ");
60
+ result = result.replace(/[|&]/g, " ");
61
+ result = result.replace(/[@#$%]/g, "");
62
+ return result;
63
+ }
64
+
65
+ // src/sanitizer/rules/urls.ts
66
+ function urlsRule(text, _config) {
67
+ let result = text;
68
+ const urlWithProtocol = /\b(https?:\/\/|ftp:\/\/|www\.)[^\s<>"{}|\\^`[\]]+/gi;
69
+ const emailPattern = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g;
70
+ result = result.replace(urlWithProtocol, "");
71
+ result = result.replace(emailPattern, "");
72
+ result = result.replace(/\b[a-z0-9-]+\.[a-z]{2,}\b/gi, "");
73
+ return result;
74
+ }
75
+
76
+ // src/sanitizer/rules/index.ts
77
+ var ruleRegistry = /* @__PURE__ */ new Map([
78
+ ["markdown", markdownRule],
79
+ ["urls", urlsRule],
80
+ ["code-blocks", codeBlocksRule],
81
+ ["punctuation", punctuationRule]
82
+ ]);
83
+
84
+ // src/sanitizer/sanitizer.ts
85
+ var SpeechSanitizer = class {
86
+ config;
87
+ plugins;
88
+ constructor(config = {}) {
89
+ this.config = {
90
+ rules: config.rules ?? ["markdown", "urls", "code-blocks", "punctuation"],
91
+ plugins: config.plugins ?? [],
92
+ preserveLineBreaks: config.preserveLineBreaks ?? false,
93
+ customReplacements: config.customReplacements ?? /* @__PURE__ */ new Map()
94
+ };
95
+ this.plugins = [...this.config.plugins].sort((a, b) => a.priority - b.priority);
96
+ }
97
+ /**
98
+ * Sanitize a string synchronously
99
+ */
100
+ sanitize(text) {
101
+ if (!text || text.trim().length === 0) {
102
+ return "";
103
+ }
104
+ let result = text;
105
+ try {
106
+ for (const rule of this.config.rules) {
107
+ const ruleFunction = ruleRegistry.get(rule);
108
+ if (ruleFunction) {
109
+ result = ruleFunction(result, this.config);
110
+ }
111
+ }
112
+ for (const plugin of this.plugins) {
113
+ const transformed = plugin.transform(result);
114
+ if (transformed instanceof Promise) {
115
+ throw new SanitizerError(
116
+ `Plugin ${plugin.name} returned a Promise in sync sanitize(). Use sanitizeAsync() instead.`
117
+ );
118
+ }
119
+ result = transformed;
120
+ }
121
+ for (const [pattern, replacement] of this.config.customReplacements) {
122
+ if (typeof pattern === "string") {
123
+ result = result.replaceAll(pattern, replacement);
124
+ } else {
125
+ result = result.replace(pattern, replacement);
126
+ }
127
+ }
128
+ if (this.config.preserveLineBreaks) {
129
+ result = result.replace(/ +/g, " ").replace(/^\s+|\s+$/gm, "");
130
+ } else {
131
+ result = result.replace(/\s+/g, " ").trim();
132
+ }
133
+ return result;
134
+ } catch (error) {
135
+ if (error instanceof SanitizerError) {
136
+ throw error;
137
+ }
138
+ throw new SanitizerError("Failed to sanitize text", {
139
+ originalText: text.substring(0, 100),
140
+ error
141
+ });
142
+ }
143
+ }
144
+ /**
145
+ * Sanitize with detailed result metadata
146
+ */
147
+ sanitizeWithMetadata(text) {
148
+ const original = text;
149
+ const sanitized = this.sanitize(text);
150
+ return {
151
+ original,
152
+ sanitized,
153
+ appliedRules: this.config.rules,
154
+ metadata: {
155
+ removedCount: original.length - sanitized.length,
156
+ transformedCount: this.config.rules.length + this.plugins.length
157
+ }
158
+ };
159
+ }
160
+ /**
161
+ * Sanitize a stream of text chunks (AsyncIterable)
162
+ */
163
+ async *sanitizeStream(input) {
164
+ let buffer = "";
165
+ const sentenceBoundary = /[.!?]\s+/;
166
+ for await (const chunk of input) {
167
+ buffer += chunk;
168
+ const sentences = buffer.split(sentenceBoundary);
169
+ buffer = sentences.pop() ?? "";
170
+ for (const sentence of sentences) {
171
+ if (sentence.trim()) {
172
+ yield `${this.sanitize(sentence)} `;
173
+ }
174
+ }
175
+ }
176
+ if (buffer.trim()) {
177
+ yield this.sanitize(buffer);
178
+ }
179
+ }
180
+ };
181
+ function sanitizeForSpeech(text, config) {
182
+ const sanitizer = new SpeechSanitizer(config);
183
+ return sanitizer.sanitize(text);
184
+ }
185
+
186
+ exports.SpeechSanitizer = SpeechSanitizer;
187
+ exports.ruleRegistry = ruleRegistry;
188
+ exports.sanitizeForSpeech = sanitizeForSpeech;
189
+ //# sourceMappingURL=index.cjs.map
190
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/errors.ts","../../src/sanitizer/rules/code-blocks.ts","../../src/sanitizer/rules/markdown.ts","../../src/sanitizer/rules/punctuation.ts","../../src/sanitizer/rules/urls.ts","../../src/sanitizer/rules/index.ts","../../src/sanitizer/sanitizer.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Base error class for all vocal-stack errors\n */\nexport class VocalStackError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n public readonly context?: Record<string, unknown>\n ) {\n super(message);\n this.name = 'VocalStackError';\n Error.captureStackTrace(this, this.constructor);\n }\n}\n\n/**\n * Error thrown during text sanitization\n */\nexport class SanitizerError extends VocalStackError {\n constructor(message: string, context?: Record<string, unknown>) {\n super(message, 'SANITIZER_ERROR', context);\n this.name = 'SanitizerError';\n }\n}\n\n/**\n * Error thrown during flow control operations\n */\nexport class FlowControlError extends VocalStackError {\n constructor(message: string, context?: Record<string, unknown>) {\n super(message, 'FLOW_CONTROL_ERROR', context);\n this.name = 'FlowControlError';\n }\n}\n\n/**\n * Error thrown during monitoring operations\n */\nexport class MonitorError extends VocalStackError {\n constructor(message: string, context?: Record<string, unknown>) {\n super(message, 'MONITOR_ERROR', context);\n this.name = 'MonitorError';\n }\n}\n","import type { SanitizerConfig } from '../types';\n\n/**\n * Remove code blocks and inline code to make text speakable\n */\nexport function codeBlocksRule(text: string, _config: Required<SanitizerConfig>): string {\n let result = text;\n\n // Remove fenced code blocks (```code```)\n // Match both with and without language specifier\n result = result.replace(/```[\\s\\S]*?```/g, '');\n\n // Remove indented code blocks (4 spaces or 1 tab at line start)\n result = result.replace(/^(?: {4}|\\t).+$/gm, '');\n\n // Note: Inline code (`code`) is handled by markdown rule\n\n return result;\n}\n","import type { SanitizerConfig } from '../types';\n\n/**\n * Strip markdown syntax to make text speakable\n */\nexport function markdownRule(text: string, _config: Required<SanitizerConfig>): string {\n let result = text;\n\n // Remove headers (##, ###, etc.)\n result = result.replace(/^#{1,6}\\s+/gm, '');\n\n // Remove bold/italic (**text**, *text*, __text__, _text_)\n result = result.replace(/(\\*\\*|__)(.*?)\\1/g, '$2');\n result = result.replace(/(\\*|_)(.*?)\\1/g, '$2');\n\n // Remove inline code (`code`)\n result = result.replace(/`([^`]+)`/g, '$1');\n\n // Remove links but keep text [text](url) -> text\n result = result.replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, '$1');\n\n // Remove images ![alt](url) -> alt or empty\n result = result.replace(/!\\[([^\\]]*)\\]\\([^)]+\\)/g, '$1');\n\n // Remove blockquotes (>)\n result = result.replace(/^>\\s+/gm, '');\n\n // Remove horizontal rules (---, ***, ___)\n result = result.replace(/^[\\-*_]{3,}$/gm, '');\n\n // Remove list markers (-, *, 1., etc.)\n result = result.replace(/^[\\s]*[-*+]\\s+/gm, '');\n result = result.replace(/^[\\s]*\\d+\\.\\s+/gm, '');\n\n // Remove strikethrough (~~text~~)\n result = result.replace(/~~(.*?)~~/g, '$1');\n\n // Remove task list markers (- [ ], - [x])\n result = result.replace(/^[\\s]*-\\s+\\[[ xX]\\]\\s+/gm, '');\n\n return result;\n}\n","import type { SanitizerConfig } from '../types';\n\n/**\n * Normalize punctuation for better TTS handling\n */\nexport function punctuationRule(text: string, _config: Required<SanitizerConfig>): string {\n let result = text;\n\n // Replace multiple exclamation marks with single\n result = result.replace(/!{2,}/g, '!');\n\n // Replace multiple question marks with single\n result = result.replace(/\\?{2,}/g, '?');\n\n // Replace ellipsis variations with single space or period\n result = result.replace(/\\.{3,}/g, '.');\n\n // Remove multiple dashes/hyphens\n result = result.replace(/-{2,}/g, ' ');\n\n // Replace em dash and en dash with space\n result = result.replace(/[—–]/g, ' ');\n\n // Remove parentheses and brackets but keep content\n result = result.replace(/[()[\\]{}]/g, '');\n\n // Replace multiple commas with single\n result = result.replace(/,{2,}/g, ',');\n\n // Remove semicolons and colons (can be disruptive in speech)\n result = result.replace(/[;:]/g, ',');\n\n // Remove quotation marks\n result = result.replace(/[\"'\"'`]/g, '');\n\n // Remove asterisks and underscores (from markdown remnants)\n result = result.replace(/[*_]/g, '');\n\n // Remove forward slashes and backslashes\n result = result.replace(/[/\\\\]/g, ' ');\n\n // Remove pipes and ampersands\n result = result.replace(/[|&]/g, ' ');\n\n // Remove @ # $ % symbols\n result = result.replace(/[@#$%]/g, '');\n\n return result;\n}\n","import type { SanitizerConfig } from '../types';\n\n/**\n * Remove or replace URLs to make text speakable\n */\nexport function urlsRule(text: string, _config: Required<SanitizerConfig>): string {\n let result = text;\n\n // Match URLs with protocol (http://, https://, ftp://, etc.)\n const urlWithProtocol = /\\b(https?:\\/\\/|ftp:\\/\\/|www\\.)[^\\s<>\"{}|\\\\^`[\\]]+/gi;\n\n // Match email addresses\n const emailPattern = /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b/g;\n\n // Remove URLs with protocol\n result = result.replace(urlWithProtocol, '');\n\n // Remove email addresses\n result = result.replace(emailPattern, '');\n\n // Remove standalone domain-like patterns (example.com)\n result = result.replace(/\\b[a-z0-9-]+\\.[a-z]{2,}\\b/gi, '');\n\n return result;\n}\n","import type { RuleFunction } from '../types';\nimport { codeBlocksRule } from './code-blocks';\nimport { markdownRule } from './markdown';\nimport { punctuationRule } from './punctuation';\nimport { urlsRule } from './urls';\n\n/**\n * Registry of built-in sanitization rules\n */\nexport const ruleRegistry = new Map<string, RuleFunction>([\n ['markdown', markdownRule],\n ['urls', urlsRule],\n ['code-blocks', codeBlocksRule],\n ['punctuation', punctuationRule],\n]);\n","import { SanitizerError } from '../errors';\nimport { ruleRegistry } from './rules';\nimport type { SanitizationResult, SanitizerConfig, SanitizerPlugin } from './types';\n\n/**\n * Speech sanitizer that transforms text for TTS optimization\n */\nexport class SpeechSanitizer {\n private readonly config: Required<SanitizerConfig>;\n private readonly plugins: readonly SanitizerPlugin[];\n\n constructor(config: SanitizerConfig = {}) {\n this.config = {\n rules: config.rules ?? ['markdown', 'urls', 'code-blocks', 'punctuation'],\n plugins: config.plugins ?? [],\n preserveLineBreaks: config.preserveLineBreaks ?? false,\n customReplacements: config.customReplacements ?? new Map(),\n };\n this.plugins = [...this.config.plugins].sort((a, b) => a.priority - b.priority);\n }\n\n /**\n * Sanitize a string synchronously\n */\n sanitize(text: string): string {\n if (!text || text.trim().length === 0) {\n return '';\n }\n\n let result = text;\n\n try {\n // Apply built-in rules\n for (const rule of this.config.rules) {\n const ruleFunction = ruleRegistry.get(rule);\n if (ruleFunction) {\n result = ruleFunction(result, this.config);\n }\n }\n\n // Apply plugins (sync only in this version)\n for (const plugin of this.plugins) {\n const transformed = plugin.transform(result);\n if (transformed instanceof Promise) {\n throw new SanitizerError(\n `Plugin ${plugin.name} returned a Promise in sync sanitize(). Use sanitizeAsync() instead.`\n );\n }\n result = transformed;\n }\n\n // Apply custom replacements\n for (const [pattern, replacement] of this.config.customReplacements) {\n if (typeof pattern === 'string') {\n result = result.replaceAll(pattern, replacement);\n } else {\n result = result.replace(pattern, replacement);\n }\n }\n\n // Clean up whitespace\n if (this.config.preserveLineBreaks) {\n // Collapse multiple spaces on same line but keep line breaks\n result = result.replace(/ +/g, ' ').replace(/^\\s+|\\s+$/gm, '');\n } else {\n // Collapse all whitespace including line breaks\n result = result.replace(/\\s+/g, ' ').trim();\n }\n\n return result;\n } catch (error) {\n if (error instanceof SanitizerError) {\n throw error;\n }\n throw new SanitizerError('Failed to sanitize text', {\n originalText: text.substring(0, 100),\n error,\n });\n }\n }\n\n /**\n * Sanitize with detailed result metadata\n */\n sanitizeWithMetadata(text: string): SanitizationResult {\n const original = text;\n const sanitized = this.sanitize(text);\n\n return {\n original,\n sanitized,\n appliedRules: this.config.rules as string[],\n metadata: {\n removedCount: original.length - sanitized.length,\n transformedCount: this.config.rules.length + this.plugins.length,\n },\n };\n }\n\n /**\n * Sanitize a stream of text chunks (AsyncIterable)\n */\n async *sanitizeStream(input: AsyncIterable<string>): AsyncIterable<string> {\n let buffer = '';\n const sentenceBoundary = /[.!?]\\s+/;\n\n for await (const chunk of input) {\n buffer += chunk;\n\n // Process complete sentences\n const sentences = buffer.split(sentenceBoundary);\n buffer = sentences.pop() ?? ''; // Keep incomplete sentence\n\n for (const sentence of sentences) {\n if (sentence.trim()) {\n yield `${this.sanitize(sentence)} `;\n }\n }\n }\n\n // Process remaining buffer\n if (buffer.trim()) {\n yield this.sanitize(buffer);\n }\n }\n}\n\n/**\n * Convenience function for one-off sanitization\n */\nexport function sanitizeForSpeech(text: string, config?: SanitizerConfig): string {\n const sanitizer = new SpeechSanitizer(config);\n return sanitizer.sanitize(text);\n}\n"]}