@omote/core 0.10.5 → 0.10.6

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 CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,6 +30,220 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
33
+ // src/telemetry/exporters/otlp.ts
34
+ var otlp_exports = {};
35
+ __export(otlp_exports, {
36
+ OTLPExporter: () => OTLPExporter
37
+ });
38
+ function toOTLPAttributeValue(value) {
39
+ if (typeof value === "string") return { stringValue: value };
40
+ if (typeof value === "number") return Number.isInteger(value) ? { intValue: value } : { doubleValue: value };
41
+ return { boolValue: value };
42
+ }
43
+ function toOTLPAttributes(attrs) {
44
+ return Object.entries(attrs).filter(([, v]) => v !== void 0).map(([key, value]) => ({ key, value: toOTLPAttributeValue(value) }));
45
+ }
46
+ function spanToOTLPSpan(span) {
47
+ return {
48
+ traceId: span.traceId,
49
+ spanId: span.spanId,
50
+ parentSpanId: span.parentSpanId || "",
51
+ name: span.name,
52
+ kind: 1,
53
+ // INTERNAL
54
+ startTimeUnixNano: String(span.epochMs * 1e6),
55
+ endTimeUnixNano: String(span.endEpochMs * 1e6),
56
+ attributes: toOTLPAttributes(span.attributes),
57
+ status: {
58
+ code: span.status === "ok" ? StatusCode.OK : StatusCode.ERROR,
59
+ message: span.error?.message || ""
60
+ }
61
+ };
62
+ }
63
+ function metricToOTLPMetric(metric) {
64
+ const attributes = toOTLPAttributes(metric.attributes);
65
+ const timeUnixNano = String(metric.timestamp * 1e6);
66
+ if (metric.type === "counter") {
67
+ return {
68
+ name: metric.name,
69
+ sum: {
70
+ dataPoints: [{
71
+ attributes,
72
+ timeUnixNano,
73
+ asInt: metric.value
74
+ }],
75
+ aggregationTemporality: 1,
76
+ // DELTA
77
+ isMonotonic: true
78
+ }
79
+ };
80
+ }
81
+ if (metric.histogramData) {
82
+ return {
83
+ name: metric.name,
84
+ histogram: {
85
+ dataPoints: [{
86
+ attributes,
87
+ timeUnixNano,
88
+ count: metric.histogramData.count,
89
+ sum: metric.histogramData.sum,
90
+ min: metric.histogramData.min,
91
+ max: metric.histogramData.max,
92
+ explicitBounds: metric.histogramData.bucketBoundaries,
93
+ bucketCounts: metric.histogramData.bucketCounts
94
+ }],
95
+ aggregationTemporality: 1
96
+ // DELTA
97
+ }
98
+ };
99
+ }
100
+ return {
101
+ name: metric.name,
102
+ gauge: {
103
+ dataPoints: [{
104
+ attributes,
105
+ timeUnixNano,
106
+ asDouble: metric.value
107
+ }]
108
+ }
109
+ };
110
+ }
111
+ var StatusCode, OTLPExporter;
112
+ var init_otlp = __esm({
113
+ "src/telemetry/exporters/otlp.ts"() {
114
+ "use strict";
115
+ StatusCode = {
116
+ UNSET: 0,
117
+ OK: 1,
118
+ ERROR: 2
119
+ };
120
+ OTLPExporter = class {
121
+ constructor(config, serviceName = "omote-sdk", serviceVersion = "0.1.0") {
122
+ this.spanBuffer = [];
123
+ this.metricBuffer = [];
124
+ this.flushIntervalId = null;
125
+ this.BUFFER_SIZE = 100;
126
+ this.FLUSH_INTERVAL_MS = 5e3;
127
+ this.isShutdown = false;
128
+ const parsed = new URL(config.endpoint);
129
+ if (parsed.protocol !== "https:") {
130
+ const isLocalhost = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]";
131
+ if (!isLocalhost) {
132
+ throw new Error("OTLP endpoint must use HTTPS (or localhost for development)");
133
+ }
134
+ }
135
+ this.config = {
136
+ endpoint: config.endpoint,
137
+ timeoutMs: config.timeoutMs ?? 1e4,
138
+ headers: config.headers ? { ...config.headers } : {}
139
+ };
140
+ this.serviceName = serviceName;
141
+ this.serviceVersion = serviceVersion;
142
+ this.flushIntervalId = setInterval(() => {
143
+ this.flush().catch(console.error);
144
+ }, this.FLUSH_INTERVAL_MS);
145
+ }
146
+ exportSpan(span) {
147
+ if (this.isShutdown) return;
148
+ this.spanBuffer.push(span);
149
+ if (this.spanBuffer.length >= this.BUFFER_SIZE) {
150
+ this.flush().catch(console.error);
151
+ }
152
+ }
153
+ exportMetric(metric) {
154
+ if (this.isShutdown) return;
155
+ this.metricBuffer.push(metric);
156
+ if (this.metricBuffer.length >= this.BUFFER_SIZE) {
157
+ this.flush().catch(console.error);
158
+ }
159
+ }
160
+ async flush() {
161
+ if (this.isShutdown) return;
162
+ const spans = this.spanBuffer.splice(0);
163
+ const metrics = this.metricBuffer.splice(0);
164
+ const promises = [];
165
+ if (spans.length > 0) {
166
+ promises.push(this.exportSpans(spans));
167
+ }
168
+ if (metrics.length > 0) {
169
+ promises.push(this.exportMetrics(metrics));
170
+ }
171
+ await Promise.all(promises);
172
+ }
173
+ async shutdown() {
174
+ if (this.flushIntervalId) {
175
+ clearInterval(this.flushIntervalId);
176
+ this.flushIntervalId = null;
177
+ }
178
+ await this.flush();
179
+ this.isShutdown = true;
180
+ }
181
+ async exportSpans(spans) {
182
+ const resourceAttrs = [
183
+ { key: "service.name", value: { stringValue: this.serviceName } },
184
+ { key: "service.version", value: { stringValue: this.serviceVersion } },
185
+ { key: "telemetry.sdk.name", value: { stringValue: "omote-sdk" } },
186
+ { key: "telemetry.sdk.language", value: { stringValue: "javascript" } }
187
+ ];
188
+ const body = {
189
+ resourceSpans: [{
190
+ resource: { attributes: resourceAttrs },
191
+ scopeSpans: [{
192
+ scope: { name: "omote-sdk", version: this.serviceVersion },
193
+ spans: spans.map(spanToOTLPSpan)
194
+ }]
195
+ }]
196
+ };
197
+ const endpoint = this.config.endpoint.replace(/\/$/, "") + "/v1/traces";
198
+ await this.sendRequest(endpoint, body);
199
+ }
200
+ async exportMetrics(metrics) {
201
+ const resourceAttrs = [
202
+ { key: "service.name", value: { stringValue: this.serviceName } },
203
+ { key: "service.version", value: { stringValue: this.serviceVersion } }
204
+ ];
205
+ const body = {
206
+ resourceMetrics: [{
207
+ resource: { attributes: resourceAttrs },
208
+ scopeMetrics: [{
209
+ scope: { name: "omote-sdk", version: this.serviceVersion },
210
+ metrics: metrics.map(metricToOTLPMetric)
211
+ }]
212
+ }]
213
+ };
214
+ const endpoint = this.config.endpoint.replace(/\/$/, "") + "/v1/metrics";
215
+ await this.sendRequest(endpoint, body);
216
+ }
217
+ async sendRequest(endpoint, body) {
218
+ const controller = new AbortController();
219
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeoutMs);
220
+ try {
221
+ const response = await fetch(endpoint, {
222
+ method: "POST",
223
+ headers: {
224
+ "Content-Type": "application/json",
225
+ ...this.config.headers
226
+ },
227
+ body: JSON.stringify(body),
228
+ signal: controller.signal
229
+ });
230
+ if (!response.ok) {
231
+ console.warn(`[OTLP] Export failed: ${response.status} ${response.statusText}`);
232
+ }
233
+ } catch (error) {
234
+ if (error.name === "AbortError") {
235
+ console.warn("[OTLP] Export timed out");
236
+ } else {
237
+ console.warn("[OTLP] Export error:", error);
238
+ }
239
+ } finally {
240
+ clearTimeout(timeoutId);
241
+ }
242
+ }
243
+ };
244
+ }
245
+ });
246
+
30
247
  // src/index.ts
31
248
  var index_exports = {};
32
249
  __export(index_exports, {
@@ -57,7 +274,6 @@ __export(index_exports, {
57
274
  EmotionResolver: () => EmotionResolver,
58
275
  EmphasisDetector: () => EmphasisDetector,
59
276
  ErrorCodes: () => ErrorCodes,
60
- ErrorTypes: () => ErrorTypes,
61
277
  EventEmitter: () => EventEmitter,
62
278
  FaceCompositor: () => FaceCompositor,
63
279
  HF_CDN_URLS: () => HF_CDN_URLS,
@@ -329,6 +545,8 @@ var jsonFormatter = (entry) => {
329
545
  if (entry.data && Object.keys(entry.data).length > 0) {
330
546
  output.data = entry.data;
331
547
  }
548
+ if (entry.traceId) output.traceId = entry.traceId;
549
+ if (entry.spanId) output.spanId = entry.spanId;
332
550
  if (entry.error) {
333
551
  output.error = {
334
552
  name: entry.error.name,
@@ -350,6 +568,9 @@ var prettyFormatter = (entry) => {
350
568
  const color = LEVEL_COLORS[entry.level];
351
569
  output = `${COLORS.gray}${time}${COLORS.reset} ${color}${level}${COLORS.reset} ${COLORS.cyan}[${module2}]${COLORS.reset} ${message}`;
352
570
  }
571
+ if (entry.traceId) {
572
+ output += ` [trace:${entry.traceId.slice(0, 8)}]`;
573
+ }
353
574
  if (entry.data && Object.keys(entry.data).length > 0) {
354
575
  const dataStr = safeStringify(entry.data);
355
576
  if (dataStr.length > 80) {
@@ -454,198 +675,6 @@ var ConsoleExporter = class {
454
675
  }
455
676
  };
456
677
 
457
- // src/telemetry/exporters/otlp.ts
458
- var StatusCode = {
459
- UNSET: 0,
460
- OK: 1,
461
- ERROR: 2
462
- };
463
- function spanToOTLP(span, serviceName, serviceVersion) {
464
- const attributes = Object.entries(span.attributes).filter(([, v]) => v !== void 0).map(([key, value]) => ({
465
- key,
466
- value: typeof value === "string" ? { stringValue: value } : typeof value === "number" ? Number.isInteger(value) ? { intValue: value } : { doubleValue: value } : { boolValue: value }
467
- }));
468
- return {
469
- resourceSpans: [{
470
- resource: {
471
- attributes: [
472
- { key: "service.name", value: { stringValue: serviceName } },
473
- { key: "service.version", value: { stringValue: serviceVersion } },
474
- { key: "telemetry.sdk.name", value: { stringValue: "omote-sdk" } },
475
- { key: "telemetry.sdk.language", value: { stringValue: "javascript" } }
476
- ]
477
- },
478
- scopeSpans: [{
479
- scope: {
480
- name: "omote-sdk",
481
- version: serviceVersion
482
- },
483
- spans: [{
484
- traceId: span.traceId,
485
- spanId: span.spanId,
486
- parentSpanId: span.parentSpanId || "",
487
- name: span.name,
488
- kind: 1,
489
- // INTERNAL
490
- startTimeUnixNano: String(span.startTime * 1e6),
491
- endTimeUnixNano: String(span.endTime * 1e6),
492
- attributes,
493
- status: {
494
- code: span.status === "ok" ? StatusCode.OK : StatusCode.ERROR,
495
- message: span.error?.message || ""
496
- }
497
- }]
498
- }]
499
- }]
500
- };
501
- }
502
- function metricToOTLP(metric, serviceName, serviceVersion) {
503
- const attributes = Object.entries(metric.attributes).filter(([, v]) => v !== void 0).map(([key, value]) => ({
504
- key,
505
- value: typeof value === "string" ? { stringValue: value } : typeof value === "number" ? Number.isInteger(value) ? { intValue: value } : { doubleValue: value } : { boolValue: value }
506
- }));
507
- const dataPoint = {
508
- attributes,
509
- timeUnixNano: String(metric.timestamp * 1e6),
510
- ...metric.type === "counter" ? { asInt: metric.value } : { asDouble: metric.value }
511
- };
512
- return {
513
- resourceMetrics: [{
514
- resource: {
515
- attributes: [
516
- { key: "service.name", value: { stringValue: serviceName } },
517
- { key: "service.version", value: { stringValue: serviceVersion } }
518
- ]
519
- },
520
- scopeMetrics: [{
521
- scope: {
522
- name: "omote-sdk",
523
- version: serviceVersion
524
- },
525
- metrics: [{
526
- name: metric.name,
527
- ...metric.type === "counter" ? {
528
- sum: {
529
- dataPoints: [dataPoint],
530
- aggregationTemporality: 2,
531
- // CUMULATIVE
532
- isMonotonic: true
533
- }
534
- } : {
535
- gauge: {
536
- dataPoints: [dataPoint]
537
- }
538
- }
539
- }]
540
- }]
541
- }]
542
- };
543
- }
544
- var OTLPExporter = class {
545
- constructor(config, serviceName = "omote-sdk", serviceVersion = "0.1.0") {
546
- this.spanBuffer = [];
547
- this.metricBuffer = [];
548
- this.flushIntervalId = null;
549
- this.BUFFER_SIZE = 100;
550
- this.FLUSH_INTERVAL_MS = 5e3;
551
- this.isShutdown = false;
552
- const parsed = new URL(config.endpoint);
553
- if (parsed.protocol !== "https:") {
554
- const isLocalhost = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]";
555
- if (!isLocalhost) {
556
- throw new Error("OTLP endpoint must use HTTPS (or localhost for development)");
557
- }
558
- }
559
- this.config = {
560
- endpoint: config.endpoint,
561
- timeoutMs: config.timeoutMs ?? 1e4,
562
- headers: config.headers ? { ...config.headers } : {}
563
- };
564
- this.serviceName = serviceName;
565
- this.serviceVersion = serviceVersion;
566
- this.flushIntervalId = setInterval(() => {
567
- this.flush().catch(console.error);
568
- }, this.FLUSH_INTERVAL_MS);
569
- }
570
- exportSpan(span) {
571
- if (this.isShutdown) return;
572
- this.spanBuffer.push(span);
573
- if (this.spanBuffer.length >= this.BUFFER_SIZE) {
574
- this.flush().catch(console.error);
575
- }
576
- }
577
- exportMetric(metric) {
578
- if (this.isShutdown) return;
579
- this.metricBuffer.push(metric);
580
- if (this.metricBuffer.length >= this.BUFFER_SIZE) {
581
- this.flush().catch(console.error);
582
- }
583
- }
584
- async flush() {
585
- if (this.isShutdown) return;
586
- const spans = this.spanBuffer.splice(0);
587
- const metrics = this.metricBuffer.splice(0);
588
- const promises = [];
589
- if (spans.length > 0) {
590
- promises.push(this.exportSpans(spans));
591
- }
592
- if (metrics.length > 0) {
593
- promises.push(this.exportMetrics(metrics));
594
- }
595
- await Promise.all(promises);
596
- }
597
- async shutdown() {
598
- if (this.flushIntervalId) {
599
- clearInterval(this.flushIntervalId);
600
- this.flushIntervalId = null;
601
- }
602
- await this.flush();
603
- this.isShutdown = true;
604
- }
605
- async exportSpans(spans) {
606
- const resourceSpans = spans.map(
607
- (span) => spanToOTLP(span, this.serviceName, this.serviceVersion).resourceSpans[0]
608
- );
609
- const body = { resourceSpans };
610
- const endpoint = this.config.endpoint.replace(/\/$/, "") + "/v1/traces";
611
- await this.sendRequest(endpoint, body);
612
- }
613
- async exportMetrics(metrics) {
614
- const resourceMetrics = metrics.map(
615
- (metric) => metricToOTLP(metric, this.serviceName, this.serviceVersion).resourceMetrics[0]
616
- );
617
- const body = { resourceMetrics };
618
- const endpoint = this.config.endpoint.replace(/\/$/, "") + "/v1/metrics";
619
- await this.sendRequest(endpoint, body);
620
- }
621
- async sendRequest(endpoint, body) {
622
- const controller = new AbortController();
623
- const timeoutId = setTimeout(() => controller.abort(), this.config.timeoutMs);
624
- try {
625
- const response = await fetch(endpoint, {
626
- method: "POST",
627
- headers: {
628
- "Content-Type": "application/json",
629
- ...this.config.headers
630
- },
631
- body: JSON.stringify(body),
632
- signal: controller.signal
633
- });
634
- if (!response.ok) {
635
- console.warn(`[OTLP] Export failed: ${response.status} ${response.statusText}`);
636
- }
637
- } catch (error) {
638
- if (error.name === "AbortError") {
639
- console.warn("[OTLP] Export timed out");
640
- } else {
641
- console.warn("[OTLP] Export error:", error);
642
- }
643
- } finally {
644
- clearTimeout(timeoutId);
645
- }
646
- }
647
- };
648
-
649
678
  // src/logging/Clock.ts
650
679
  var defaultClock = {
651
680
  now: () => performance.now(),
@@ -660,6 +689,7 @@ function getClock() {
660
689
  }
661
690
 
662
691
  // src/telemetry/OmoteTelemetry.ts
692
+ var DEFAULT_HISTOGRAM_BUCKETS = [1, 5, 10, 25, 50, 100, 250, 500, 1e3, 2500, 5e3, 1e4, 3e4, 6e4];
663
693
  function generateId(length = 16) {
664
694
  const bytes = new Uint8Array(length);
665
695
  crypto.getRandomValues(bytes);
@@ -679,6 +709,7 @@ function getTelemetry() {
679
709
  var OmoteTelemetry = class {
680
710
  constructor(config) {
681
711
  this.exporter = null;
712
+ this.exporterReady = null;
682
713
  this.activeTraceId = null;
683
714
  this.metricsIntervalId = null;
684
715
  // Span stack for log-to-span correlation
@@ -697,14 +728,18 @@ var OmoteTelemetry = class {
697
728
  metricsIntervalMs: config.metricsIntervalMs ?? 6e4
698
729
  };
699
730
  if (this.config.enabled) {
700
- this.initExporter();
701
- this.startMetricsCollection();
731
+ if (config.customExporter) {
732
+ this.exporter = config.customExporter;
733
+ this.startMetricsCollection();
734
+ } else {
735
+ this.exporterReady = this.initExporter();
736
+ }
702
737
  }
703
738
  }
704
739
  /**
705
740
  * Initialize the configured exporter
706
741
  */
707
- initExporter() {
742
+ async initExporter() {
708
743
  switch (this.config.exporter) {
709
744
  case "console":
710
745
  this.exporter = new ConsoleExporter({ enabled: true });
@@ -714,16 +749,20 @@ var OmoteTelemetry = class {
714
749
  console.warn("[Telemetry] OTLP exporter requires exporterConfig with endpoint");
715
750
  return;
716
751
  }
717
- this.exporter = new OTLPExporter(
718
- this.config.exporterConfig,
719
- this.config.serviceName,
720
- this.config.serviceVersion
721
- );
752
+ {
753
+ const { OTLPExporter: OTLPExporter2 } = await Promise.resolve().then(() => (init_otlp(), otlp_exports));
754
+ this.exporter = new OTLPExporter2(
755
+ this.config.exporterConfig,
756
+ this.config.serviceName,
757
+ this.config.serviceVersion
758
+ );
759
+ }
722
760
  break;
723
761
  case "none":
724
762
  default:
725
763
  this.exporter = null;
726
764
  }
765
+ this.startMetricsCollection();
727
766
  }
728
767
  /**
729
768
  * Start periodic metrics collection
@@ -768,6 +807,7 @@ var OmoteTelemetry = class {
768
807
  const spanId = generateId(8);
769
808
  const parentSpanId = parentContext?.spanId;
770
809
  const startTime = getClock().now();
810
+ const epochMs = Date.now();
771
811
  if (!parentContext && !this.activeTraceId) {
772
812
  this.activeTraceId = traceId;
773
813
  }
@@ -783,6 +823,7 @@ var OmoteTelemetry = class {
783
823
  if (idx !== -1) this.spanStack.splice(idx, 1);
784
824
  const endTime = getClock().now();
785
825
  const durationMs = endTime - startTime;
826
+ const endEpochMs = epochMs + (endTime - startTime);
786
827
  if (status === "error" && !sampled) {
787
828
  sampled = this.shouldSample(true);
788
829
  }
@@ -795,6 +836,8 @@ var OmoteTelemetry = class {
795
836
  startTime,
796
837
  endTime,
797
838
  durationMs,
839
+ epochMs,
840
+ endEpochMs,
798
841
  status,
799
842
  attributes: spanAttributes,
800
843
  error
@@ -869,7 +912,7 @@ var OmoteTelemetry = class {
869
912
  * });
870
913
  * ```
871
914
  */
872
- recordHistogram(name, value, attributes = {}) {
915
+ recordHistogram(name, value, attributes = {}, bucketBoundaries = DEFAULT_HISTOGRAM_BUCKETS) {
873
916
  if (!this.config.enabled || !this.config.metricsEnabled) return;
874
917
  const key = this.getMetricKey(name, attributes);
875
918
  const existing = this.histograms.get(key);
@@ -878,8 +921,27 @@ var OmoteTelemetry = class {
878
921
  existing.sum += value;
879
922
  if (value < existing.min) existing.min = value;
880
923
  if (value > existing.max) existing.max = value;
924
+ let placed = false;
925
+ for (let i = 0; i < existing.bucketBoundaries.length; i++) {
926
+ if (value <= existing.bucketBoundaries[i]) {
927
+ existing.bucketCounts[i]++;
928
+ placed = true;
929
+ break;
930
+ }
931
+ }
932
+ if (!placed) existing.bucketCounts[existing.bucketCounts.length - 1]++;
881
933
  } else {
882
- this.histograms.set(key, { count: 1, sum: value, min: value, max: value, attributes });
934
+ const bucketCounts = new Array(bucketBoundaries.length + 1).fill(0);
935
+ let placed = false;
936
+ for (let i = 0; i < bucketBoundaries.length; i++) {
937
+ if (value <= bucketBoundaries[i]) {
938
+ bucketCounts[i]++;
939
+ placed = true;
940
+ break;
941
+ }
942
+ }
943
+ if (!placed) bucketCounts[bucketCounts.length - 1]++;
944
+ this.histograms.set(key, { count: 1, sum: value, min: value, max: value, bucketBoundaries, bucketCounts, attributes });
883
945
  }
884
946
  }
885
947
  /**
@@ -896,7 +958,7 @@ var OmoteTelemetry = class {
896
958
  */
897
959
  flushMetrics() {
898
960
  if (!this.exporter) return;
899
- const timestamp = getClock().now();
961
+ const timestamp = Date.now();
900
962
  for (const [key, data] of this.counters) {
901
963
  if (data.value === 0) continue;
902
964
  const name = key.split("|")[0];
@@ -925,19 +987,29 @@ var OmoteTelemetry = class {
925
987
  min: data.min,
926
988
  max: data.max
927
989
  },
928
- timestamp
990
+ timestamp,
991
+ histogramData: {
992
+ count: data.count,
993
+ sum: data.sum,
994
+ min: data.min,
995
+ max: data.max,
996
+ bucketBoundaries: [...data.bucketBoundaries],
997
+ bucketCounts: [...data.bucketCounts]
998
+ }
929
999
  };
930
1000
  this.exporter.exportMetric(metric);
931
1001
  data.count = 0;
932
1002
  data.sum = 0;
933
1003
  data.min = Infinity;
934
1004
  data.max = -Infinity;
1005
+ data.bucketCounts.fill(0);
935
1006
  }
936
1007
  }
937
1008
  /**
938
1009
  * Force flush all pending data
939
1010
  */
940
1011
  async flush() {
1012
+ if (this.exporterReady) await this.exporterReady;
941
1013
  this.flushMetrics();
942
1014
  await this.exporter?.flush();
943
1015
  }
@@ -1059,12 +1131,12 @@ var Logger = class _Logger {
1059
1131
  };
1060
1132
  var loggerCache = /* @__PURE__ */ new Map();
1061
1133
  function createLogger(module2) {
1062
- let logger36 = loggerCache.get(module2);
1063
- if (!logger36) {
1064
- logger36 = new Logger(module2);
1065
- loggerCache.set(module2, logger36);
1134
+ let logger37 = loggerCache.get(module2);
1135
+ if (!logger37) {
1136
+ logger37 = new Logger(module2);
1137
+ loggerCache.set(module2, logger37);
1066
1138
  }
1067
- return logger36;
1139
+ return logger37;
1068
1140
  }
1069
1141
  var noopLogger = {
1070
1142
  module: "noop",
@@ -1140,6 +1212,67 @@ var ErrorCodes = {
1140
1212
  NET_WEBSOCKET_ERROR: "OMOTE_NET_003"
1141
1213
  };
1142
1214
 
1215
+ // src/telemetry/types.ts
1216
+ var MetricNames = {
1217
+ // --- Inference ---
1218
+ /** Histogram: Inference latency in ms */
1219
+ INFERENCE_LATENCY: "omote.inference.latency",
1220
+ /** Histogram: Model load time in ms */
1221
+ MODEL_LOAD_TIME: "omote.model.load_time",
1222
+ /** Counter: Total inference operations */
1223
+ INFERENCE_TOTAL: "omote.inference.total",
1224
+ /** Counter: Total errors */
1225
+ ERRORS_TOTAL: "omote.errors.total",
1226
+ /** Counter: Cache hits */
1227
+ CACHE_HITS: "omote.cache.hits",
1228
+ /** Counter: Cache misses */
1229
+ CACHE_MISSES: "omote.cache.misses",
1230
+ /** Counter: Cache stale (version/etag mismatch) */
1231
+ CACHE_STALE: "omote.cache.stale",
1232
+ /** Counter: Cache quota warning (>90% used) */
1233
+ CACHE_QUOTA_WARNING: "omote.cache.quota_warning",
1234
+ /** Counter: Cache eviction (LRU) */
1235
+ CACHE_EVICTION: "omote.cache.eviction",
1236
+ // --- Pipeline ---
1237
+ /** Histogram: Voice turn latency (speech end → transcript ready, excludes playback) */
1238
+ VOICE_TURN_LATENCY: "omote.voice.turn.latency",
1239
+ /** Histogram: ASR transcription latency in ms */
1240
+ VOICE_TRANSCRIPTION_LATENCY: "omote.voice.transcription.latency",
1241
+ /** Histogram: Response handler latency in ms */
1242
+ VOICE_RESPONSE_LATENCY: "omote.voice.response.latency",
1243
+ /** Counter: Total transcriptions */
1244
+ VOICE_TRANSCRIPTIONS: "omote.voice.transcriptions",
1245
+ /** Counter: Total interruptions */
1246
+ VOICE_INTERRUPTIONS: "omote.voice.interruptions",
1247
+ // --- Playback ---
1248
+ /** Histogram: PlaybackPipeline session duration in ms */
1249
+ PLAYBACK_SESSION_DURATION: "omote.playback.session.duration",
1250
+ /** Histogram: Audio chunk processing latency in ms */
1251
+ PLAYBACK_CHUNK_LATENCY: "omote.playback.chunk.latency",
1252
+ // --- TTS ---
1253
+ /** Histogram: TTSSpeaker.connect() latency in ms */
1254
+ TTS_CONNECT_LATENCY: "omote.tts.connect.latency",
1255
+ /** Histogram: TTSSpeaker.speak() latency in ms */
1256
+ TTS_SPEAK_LATENCY: "omote.tts.speak.latency",
1257
+ /** Counter: TTSSpeaker.stop() aborted speak calls */
1258
+ TTS_SPEAK_ABORTED: "omote.tts.speak.aborted",
1259
+ // --- Mic ---
1260
+ /** Counter: MicLipSync sessions started */
1261
+ MIC_SESSIONS: "omote.mic.sessions",
1262
+ // --- Frame budget ---
1263
+ /** Histogram: CharacterController.update() latency in µs */
1264
+ AVATAR_FRAME_LATENCY: "omote.avatar.frame.latency_us",
1265
+ /** Histogram: FaceCompositor.compose() latency in µs */
1266
+ COMPOSITOR_COMPOSE_LATENCY: "omote.compositor.compose.latency_us",
1267
+ /** Counter: Frames exceeding budget threshold */
1268
+ AVATAR_FRAME_DROPS: "omote.avatar.frame.drops",
1269
+ // --- Audio scheduling ---
1270
+ /** Counter: Audio scheduling gaps (playback fell behind) */
1271
+ AUDIO_SCHEDULE_GAP: "omote.audio.schedule_gap"
1272
+ };
1273
+ var INFERENCE_LATENCY_BUCKETS = [1, 5, 10, 25, 50, 100, 250, 500, 1e3, 2500, 5e3];
1274
+ var MODEL_LOAD_TIME_BUCKETS = [100, 500, 1e3, 2500, 5e3, 1e4, 3e4, 6e4];
1275
+
1143
1276
  // src/audio/MicrophoneCapture.ts
1144
1277
  var logger = createLogger("MicrophoneCapture");
1145
1278
  var MicrophoneCapture = class {
@@ -1176,6 +1309,7 @@ var MicrophoneCapture = class {
1176
1309
  return;
1177
1310
  }
1178
1311
  if (this._isRecording) return;
1312
+ const span = getTelemetry()?.startSpan("MicrophoneCapture.start");
1179
1313
  try {
1180
1314
  this.stream = await navigator.mediaDevices.getUserMedia({
1181
1315
  audio: {
@@ -1249,6 +1383,8 @@ var MicrophoneCapture = class {
1249
1383
  source.connect(this.processor);
1250
1384
  this.processor.connect(this.context.destination);
1251
1385
  this._isRecording = true;
1386
+ getTelemetry()?.incrementCounter(MetricNames.MIC_SESSIONS);
1387
+ span?.end();
1252
1388
  logger.info("Started recording", {
1253
1389
  contextState: this.context.state,
1254
1390
  sampleRate: this.config.sampleRate,
@@ -1269,6 +1405,7 @@ var MicrophoneCapture = class {
1269
1405
  message: err.message,
1270
1406
  details: err
1271
1407
  });
1408
+ span?.endWithError(err instanceof Error ? err : new Error(String(err)));
1272
1409
  }
1273
1410
  }
1274
1411
  stop() {
@@ -1470,6 +1607,7 @@ var AudioScheduler = class {
1470
1607
  if (scheduleTime < ctx.currentTime) {
1471
1608
  const gap = ctx.currentTime - scheduleTime;
1472
1609
  const gapMs = gap * 1e3;
1610
+ getTelemetry()?.incrementCounter(MetricNames.AUDIO_SCHEDULE_GAP, 1, { gap_ms: Math.round(gapMs) });
1473
1611
  if (gap > 0.5) {
1474
1612
  logger2.error("Critical audio scheduling gap", {
1475
1613
  code: ErrorCodes.AUD_SCHEDULE_GAP,
@@ -2047,6 +2185,8 @@ var _A2EProcessor = class _A2EProcessor {
2047
2185
  const t0 = getClock().now();
2048
2186
  const result = await this.backend.infer(chunk, this.identityIndex);
2049
2187
  const inferMs = Math.round(getClock().now() - t0);
2188
+ getTelemetry()?.recordHistogram(MetricNames.INFERENCE_LATENCY, inferMs);
2189
+ getTelemetry()?.incrementCounter(MetricNames.INFERENCE_TOTAL);
2050
2190
  const effectiveSamples = actualSamples ?? chunk.length;
2051
2191
  const actualDuration = effectiveSamples / this.sampleRate;
2052
2192
  const actualFrameCount = Math.ceil(actualDuration * FRAME_RATE);
@@ -2093,79 +2233,13 @@ var _A2EProcessor = class _A2EProcessor {
2093
2233
  error: error.message,
2094
2234
  code
2095
2235
  });
2236
+ getTelemetry()?.incrementCounter(MetricNames.ERRORS_TOTAL, 1, { source: "A2EProcessor", code });
2096
2237
  this.onError?.(error);
2097
2238
  }
2098
2239
  };
2099
2240
  _A2EProcessor.MAX_PENDING_CHUNKS = 10;
2100
2241
  var A2EProcessor = _A2EProcessor;
2101
2242
 
2102
- // src/telemetry/types.ts
2103
- var MetricNames = {
2104
- // --- Inference ---
2105
- /** Histogram: Inference latency in ms */
2106
- INFERENCE_LATENCY: "omote.inference.latency",
2107
- /** Histogram: Model load time in ms */
2108
- MODEL_LOAD_TIME: "omote.model.load_time",
2109
- /** Counter: Total inference operations */
2110
- INFERENCE_TOTAL: "omote.inference.total",
2111
- /** Counter: Total errors */
2112
- ERRORS_TOTAL: "omote.errors.total",
2113
- /** Counter: Cache hits */
2114
- CACHE_HITS: "omote.cache.hits",
2115
- /** Counter: Cache misses */
2116
- CACHE_MISSES: "omote.cache.misses",
2117
- /** Counter: Cache stale (version/etag mismatch) */
2118
- CACHE_STALE: "omote.cache.stale",
2119
- /** Counter: Cache quota warning (>90% used) */
2120
- CACHE_QUOTA_WARNING: "omote.cache.quota_warning",
2121
- /** Counter: Cache eviction (LRU) */
2122
- CACHE_EVICTION: "omote.cache.eviction",
2123
- // --- Pipeline ---
2124
- /** Histogram: Voice turn latency (speech end → transcript ready, excludes playback) */
2125
- VOICE_TURN_LATENCY: "omote.voice.turn.latency",
2126
- /** Histogram: ASR transcription latency in ms */
2127
- VOICE_TRANSCRIPTION_LATENCY: "omote.voice.transcription.latency",
2128
- /** Histogram: Response handler latency in ms */
2129
- VOICE_RESPONSE_LATENCY: "omote.voice.response.latency",
2130
- /** Counter: Total transcriptions */
2131
- VOICE_TRANSCRIPTIONS: "omote.voice.transcriptions",
2132
- /** Counter: Total interruptions */
2133
- VOICE_INTERRUPTIONS: "omote.voice.interruptions",
2134
- // --- Playback ---
2135
- /** Histogram: PlaybackPipeline session duration in ms */
2136
- PLAYBACK_SESSION_DURATION: "omote.playback.session.duration",
2137
- /** Histogram: Audio chunk processing latency in ms */
2138
- PLAYBACK_CHUNK_LATENCY: "omote.playback.chunk.latency",
2139
- // --- TTS ---
2140
- /** Histogram: TTSSpeaker.connect() latency in ms */
2141
- TTS_CONNECT_LATENCY: "omote.tts.connect.latency",
2142
- /** Histogram: TTSSpeaker.speak() latency in ms */
2143
- TTS_SPEAK_LATENCY: "omote.tts.speak.latency",
2144
- /** Counter: TTSSpeaker.stop() aborted speak calls */
2145
- TTS_SPEAK_ABORTED: "omote.tts.speak.aborted",
2146
- // --- Mic ---
2147
- /** Counter: MicLipSync sessions started */
2148
- MIC_SESSIONS: "omote.mic.sessions",
2149
- // --- Frame budget ---
2150
- /** Histogram: CharacterController.update() latency in µs */
2151
- AVATAR_FRAME_LATENCY: "omote.avatar.frame.latency_us",
2152
- /** Histogram: FaceCompositor.compose() latency in µs */
2153
- COMPOSITOR_COMPOSE_LATENCY: "omote.compositor.compose.latency_us",
2154
- /** Counter: Frames exceeding budget threshold */
2155
- AVATAR_FRAME_DROPS: "omote.avatar.frame.drops"
2156
- };
2157
- var ErrorTypes = {
2158
- INFERENCE: "inference_error",
2159
- NETWORK: "network_error",
2160
- TIMEOUT: "timeout",
2161
- USER: "user_error",
2162
- RUNTIME: "runtime_error",
2163
- MEDIA: "media_error",
2164
- MODEL: "model_error"
2165
- };
2166
- var INFERENCE_LATENCY_BUCKETS = [1, 5, 10, 25, 50, 100, 250, 500, 1e3, 2500, 5e3];
2167
- var MODEL_LOAD_TIME_BUCKETS = [100, 500, 1e3, 2500, 5e3, 1e4, 3e4, 6e4];
2168
-
2169
2243
  // src/inference/blendshapeUtils.ts
2170
2244
  var ARKIT_BLENDSHAPES = [
2171
2245
  "browDownLeft",
@@ -2901,6 +2975,9 @@ function resetModelUrls() {
2901
2975
  }
2902
2976
  var HF_CDN_URLS = HF_MODEL_URLS;
2903
2977
 
2978
+ // src/telemetry/index.ts
2979
+ init_otlp();
2980
+
2904
2981
  // src/utils/runtime.ts
2905
2982
  var logger8 = createLogger("Runtime");
2906
2983
  function isIOSSafari() {
@@ -4365,7 +4442,7 @@ var SenseVoiceUnifiedAdapter = class {
4365
4442
  });
4366
4443
  span?.setAttributes({ "model.backend": "wasm", "model.load_time_ms": result.loadTimeMs });
4367
4444
  span?.end();
4368
- telemetry?.recordHistogram("omote.model.load_time", result.loadTimeMs, {
4445
+ telemetry?.recordHistogram(MetricNames.MODEL_LOAD_TIME, result.loadTimeMs, {
4369
4446
  model: "sensevoice-unified",
4370
4447
  backend: "wasm"
4371
4448
  });
@@ -4389,11 +4466,11 @@ var SenseVoiceUnifiedAdapter = class {
4389
4466
  try {
4390
4467
  const result = await this.worker.transcribe(audio);
4391
4468
  const latencyMs = getClock().now() - startTime;
4392
- telemetry?.recordHistogram("omote.inference.latency", latencyMs, {
4469
+ telemetry?.recordHistogram(MetricNames.INFERENCE_LATENCY, latencyMs, {
4393
4470
  model: "sensevoice-unified",
4394
4471
  backend: "wasm"
4395
4472
  });
4396
- telemetry?.incrementCounter("omote.inference.total", 1, {
4473
+ telemetry?.incrementCounter(MetricNames.INFERENCE_TOTAL, 1, {
4397
4474
  model: "sensevoice-unified",
4398
4475
  backend: "wasm",
4399
4476
  status: "success"
@@ -4402,7 +4479,7 @@ var SenseVoiceUnifiedAdapter = class {
4402
4479
  span?.end();
4403
4480
  resolve(result);
4404
4481
  } catch (err) {
4405
- telemetry?.incrementCounter("omote.inference.total", 1, {
4482
+ telemetry?.incrementCounter(MetricNames.INFERENCE_TOTAL, 1, {
4406
4483
  model: "sensevoice-unified",
4407
4484
  backend: "wasm",
4408
4485
  status: "error"
@@ -4470,7 +4547,7 @@ var A2EUnifiedAdapter = class {
4470
4547
  });
4471
4548
  span?.setAttributes({ "model.backend": result.backend, "model.load_time_ms": result.loadTimeMs });
4472
4549
  span?.end();
4473
- telemetry?.recordHistogram("omote.model.load_time", result.loadTimeMs, {
4550
+ telemetry?.recordHistogram(MetricNames.MODEL_LOAD_TIME, result.loadTimeMs, {
4474
4551
  model: "a2e-unified",
4475
4552
  backend: result.backend
4476
4553
  });
@@ -5839,14 +5916,14 @@ var KokoroTTSUnifiedAdapter = class {
5839
5916
  });
5840
5917
  span?.setAttributes({ "model.backend": this._backend, "model.load_time_ms": loadTimeMs });
5841
5918
  span?.end();
5842
- telemetry?.recordHistogram("omote.model.load_time", loadTimeMs, {
5919
+ telemetry?.recordHistogram(MetricNames.MODEL_LOAD_TIME, loadTimeMs, {
5843
5920
  model: "kokoro-tts-unified",
5844
5921
  backend: this._backend
5845
5922
  });
5846
5923
  return { backend: this._backend, loadTimeMs, defaultVoice: this.config.defaultVoice };
5847
5924
  } catch (error) {
5848
5925
  span?.endWithError(error instanceof Error ? error : new Error(String(error)));
5849
- getTelemetry()?.incrementCounter("omote.errors.total", 1, {
5926
+ getTelemetry()?.incrementCounter(MetricNames.ERRORS_TOTAL, 1, {
5850
5927
  model: "kokoro-tts-unified",
5851
5928
  error_type: "load_failed"
5852
5929
  });
@@ -5911,22 +5988,27 @@ var KokoroTTSUnifiedAdapter = class {
5911
5988
  try {
5912
5989
  const result = await this.worker.inferKokoro(tokens, style, speed);
5913
5990
  const latencyMs = getClock().now() - startTime;
5914
- telemetry?.recordHistogram("omote.inference.latency", latencyMs, {
5991
+ telemetry?.recordHistogram(MetricNames.INFERENCE_LATENCY, latencyMs, {
5915
5992
  model: "kokoro-tts-unified",
5916
5993
  backend: this._backend
5917
5994
  });
5918
- telemetry?.incrementCounter("omote.inference.total", 1, {
5995
+ telemetry?.incrementCounter(MetricNames.INFERENCE_TOTAL, 1, {
5919
5996
  model: "kokoro-tts-unified",
5920
5997
  backend: this._backend,
5921
5998
  status: "success"
5922
5999
  });
5923
6000
  resolve(result.audio);
5924
6001
  } catch (err) {
5925
- telemetry?.incrementCounter("omote.inference.total", 1, {
6002
+ telemetry?.incrementCounter(MetricNames.INFERENCE_TOTAL, 1, {
5926
6003
  model: "kokoro-tts-unified",
5927
6004
  backend: this._backend,
5928
6005
  status: "error"
5929
6006
  });
6007
+ const span = telemetry?.startSpan("KokoroTTSUnifiedAdapter.inferError", {
6008
+ "model.name": "kokoro-tts-unified",
6009
+ "model.backend": this._backend
6010
+ });
6011
+ span?.endWithError(err instanceof Error ? err : new Error(String(err)));
5930
6012
  reject(err);
5931
6013
  }
5932
6014
  });
@@ -6004,7 +6086,7 @@ var SileroVADUnifiedAdapter = class {
6004
6086
  });
6005
6087
  span?.setAttributes({ "model.backend": "wasm", "model.load_time_ms": result.loadTimeMs });
6006
6088
  span?.end();
6007
- telemetry?.recordHistogram("omote.model.load_time", result.loadTimeMs, {
6089
+ telemetry?.recordHistogram(MetricNames.MODEL_LOAD_TIME, result.loadTimeMs, {
6008
6090
  model: "silero-vad-unified",
6009
6091
  backend: "wasm"
6010
6092
  });
@@ -6615,6 +6697,7 @@ function createKokoroTTS(config = {}) {
6615
6697
  }
6616
6698
 
6617
6699
  // src/audio/createTTSPlayer.ts
6700
+ var logger22 = createLogger("TTSPlayer");
6618
6701
  function createTTSPlayer(config) {
6619
6702
  return new TTSPlayer(config);
6620
6703
  }
@@ -6628,19 +6711,27 @@ var TTSPlayer = class extends TTSSpeaker {
6628
6711
  }
6629
6712
  /** Load TTS model and connect in audio-only mode. */
6630
6713
  async load() {
6631
- let worker = this.ttsConfig.unifiedWorker;
6632
- if (!worker) {
6633
- worker = await acquireSharedWorker();
6634
- this.ttsPlayerUsesSharedWorker = true;
6635
- }
6636
- this.backend = createKokoroTTS({
6637
- defaultVoice: this.ttsConfig.voice,
6638
- modelUrl: this.ttsConfig.modelUrl,
6639
- voiceBaseUrl: this.ttsConfig.voiceBaseUrl,
6640
- unifiedWorker: worker
6641
- });
6642
- await this.backend.load();
6643
- await this.connect(this.backend, { audioOnly: true });
6714
+ const span = getTelemetry()?.startSpan("TTSPlayer.load");
6715
+ try {
6716
+ let worker = this.ttsConfig.unifiedWorker;
6717
+ if (!worker) {
6718
+ worker = await acquireSharedWorker();
6719
+ this.ttsPlayerUsesSharedWorker = true;
6720
+ }
6721
+ this.backend = createKokoroTTS({
6722
+ defaultVoice: this.ttsConfig.voice,
6723
+ modelUrl: this.ttsConfig.modelUrl,
6724
+ voiceBaseUrl: this.ttsConfig.voiceBaseUrl,
6725
+ unifiedWorker: worker
6726
+ });
6727
+ await this.backend.load();
6728
+ await this.connect(this.backend, { audioOnly: true });
6729
+ logger22.info("TTSPlayer loaded");
6730
+ span?.end();
6731
+ } catch (err) {
6732
+ span?.endWithError(err instanceof Error ? err : new Error(String(err)));
6733
+ throw err;
6734
+ }
6644
6735
  }
6645
6736
  /** Whether the TTS model is loaded and ready. */
6646
6737
  get isLoaded() {
@@ -6659,7 +6750,7 @@ var TTSPlayer = class extends TTSSpeaker {
6659
6750
  };
6660
6751
 
6661
6752
  // src/inference/createSenseVoice.ts
6662
- var logger22 = createLogger("createSenseVoice");
6753
+ var logger23 = createLogger("createSenseVoice");
6663
6754
  var LazySenseVoice = class {
6664
6755
  constructor(config) {
6665
6756
  this.inner = null;
@@ -6707,7 +6798,7 @@ var LazySenseVoice = class {
6707
6798
  function createSenseVoice(config = {}) {
6708
6799
  const modelUrl = config.modelUrl ?? DEFAULT_MODEL_URLS.senseVoice;
6709
6800
  if (config.unifiedWorker) {
6710
- logger22.info("Creating SenseVoiceUnifiedAdapter (shared unified worker)");
6801
+ logger23.info("Creating SenseVoiceUnifiedAdapter (shared unified worker)");
6711
6802
  return new SenseVoiceUnifiedAdapter(config.unifiedWorker, {
6712
6803
  modelUrl,
6713
6804
  tokensUrl: config.tokensUrl,
@@ -6715,12 +6806,12 @@ function createSenseVoice(config = {}) {
6715
6806
  textNorm: config.textNorm
6716
6807
  });
6717
6808
  }
6718
- logger22.info("Creating SenseVoiceUnifiedAdapter (dedicated worker, lazy init)");
6809
+ logger23.info("Creating SenseVoiceUnifiedAdapter (dedicated worker, lazy init)");
6719
6810
  return new LazySenseVoice(config);
6720
6811
  }
6721
6812
 
6722
6813
  // src/inference/createSileroVAD.ts
6723
- var logger23 = createLogger("createSileroVAD");
6814
+ var logger24 = createLogger("createSileroVAD");
6724
6815
  var LazySileroVAD = class {
6725
6816
  constructor(config) {
6726
6817
  this.inner = null;
@@ -6781,15 +6872,15 @@ function createSileroVAD(config = {}) {
6781
6872
  const modelUrl = config.modelUrl ?? DEFAULT_MODEL_URLS.sileroVad;
6782
6873
  const resolvedConfig = { ...config, modelUrl };
6783
6874
  if (config.unifiedWorker) {
6784
- logger23.info("Creating SileroVADUnifiedAdapter (shared unified worker)");
6875
+ logger24.info("Creating SileroVADUnifiedAdapter (shared unified worker)");
6785
6876
  return new SileroVADUnifiedAdapter(config.unifiedWorker, resolvedConfig);
6786
6877
  }
6787
- logger23.info("Creating SileroVADUnifiedAdapter (dedicated worker, lazy init)");
6878
+ logger24.info("Creating SileroVADUnifiedAdapter (dedicated worker, lazy init)");
6788
6879
  return new LazySileroVAD(config);
6789
6880
  }
6790
6881
 
6791
6882
  // src/audio/SpeechListener.ts
6792
- var logger24 = createLogger("SpeechListener");
6883
+ var logger25 = createLogger("SpeechListener");
6793
6884
  var _SpeechListener = class _SpeechListener extends EventEmitter {
6794
6885
  constructor(config) {
6795
6886
  super();
@@ -6913,11 +7004,11 @@ var _SpeechListener = class _SpeechListener extends EventEmitter {
6913
7004
  }
6914
7005
  span?.end();
6915
7006
  this.setState("ready");
6916
- logger24.info("SpeechListener models loaded");
7007
+ logger25.info("SpeechListener models loaded");
6917
7008
  } catch (error) {
6918
7009
  const err = error instanceof Error ? error : new Error(String(error));
6919
7010
  span?.endWithError(err);
6920
- logger24.error("Model loading failed", { message: err.message });
7011
+ logger25.error("Model loading failed", { message: err.message });
6921
7012
  this.emit("error", err);
6922
7013
  this.setState("idle");
6923
7014
  throw err;
@@ -6950,7 +7041,7 @@ var _SpeechListener = class _SpeechListener extends EventEmitter {
6950
7041
  });
6951
7042
  await this.mic.start();
6952
7043
  this.setState("listening");
6953
- logger24.info("Listening started");
7044
+ logger25.info("Listening started");
6954
7045
  }
6955
7046
  /** Stop listening — deactivates mic, clears buffers. */
6956
7047
  stop() {
@@ -6970,7 +7061,7 @@ var _SpeechListener = class _SpeechListener extends EventEmitter {
6970
7061
  if (this._state !== "idle") {
6971
7062
  this.setState("ready");
6972
7063
  }
6973
- logger24.info("Listening stopped");
7064
+ logger25.info("Listening stopped");
6974
7065
  }
6975
7066
  /** Pause VAD/ASR but keep mic active for audio:chunk events (for interruption detection). */
6976
7067
  pause() {
@@ -6991,7 +7082,7 @@ var _SpeechListener = class _SpeechListener extends EventEmitter {
6991
7082
  }
6992
7083
  /** Dispose all resources. */
6993
7084
  async dispose() {
6994
- logger24.debug("Disposing SpeechListener");
7085
+ logger25.debug("Disposing SpeechListener");
6995
7086
  this.stop();
6996
7087
  this.epoch++;
6997
7088
  await Promise.allSettled([
@@ -7026,14 +7117,14 @@ var _SpeechListener = class _SpeechListener extends EventEmitter {
7026
7117
  this.audioBufferSamples = 0;
7027
7118
  this.lastProgressiveResult = null;
7028
7119
  this.lastProgressiveSamples = 0;
7029
- logger24.debug("Speech start");
7120
+ logger25.debug("Speech start");
7030
7121
  this.emit("speech:start");
7031
7122
  this.startProgressiveTranscription();
7032
7123
  }
7033
7124
  this.audioBuffer.push(new Float32Array(samples));
7034
7125
  this.audioBufferSamples += samples.length;
7035
7126
  if (this.audioBufferSamples >= _SpeechListener.MAX_AUDIO_BUFFER_SAMPLES) {
7036
- logger24.warn("Audio buffer exceeded max, forcing transcription flush");
7127
+ logger25.warn("Audio buffer exceeded max, forcing transcription flush");
7037
7128
  this.onSilenceDetected();
7038
7129
  return;
7039
7130
  }
@@ -7049,7 +7140,7 @@ var _SpeechListener = class _SpeechListener extends EventEmitter {
7049
7140
  }
7050
7141
  }
7051
7142
  } catch (err) {
7052
- logger24.warn("VAD error", { error: String(err) });
7143
+ logger25.warn("VAD error", { error: String(err) });
7053
7144
  }
7054
7145
  }
7055
7146
  // ---------------------------------------------------------------------------
@@ -7067,11 +7158,11 @@ var _SpeechListener = class _SpeechListener extends EventEmitter {
7067
7158
  const capturedEpoch = this.epoch;
7068
7159
  this.isSpeechActive = false;
7069
7160
  const durationMs = getClock().now() - this.speechStartTime;
7070
- logger24.debug("Speech end", { durationMs: Math.round(durationMs) });
7161
+ logger25.debug("Speech end", { durationMs: Math.round(durationMs) });
7071
7162
  this.emit("speech:end", { durationMs });
7072
7163
  this.clearSilenceTimer();
7073
7164
  this.processEndOfSpeech(capturedEpoch).catch((err) => {
7074
- logger24.error("End of speech processing failed", { error: String(err) });
7165
+ logger25.error("End of speech processing failed", { error: String(err) });
7075
7166
  if (this.epoch === capturedEpoch) {
7076
7167
  this.emit("error", err instanceof Error ? err : new Error(String(err)));
7077
7168
  this.setState("listening");
@@ -7103,7 +7194,7 @@ var _SpeechListener = class _SpeechListener extends EventEmitter {
7103
7194
  const minEnergy = this.config.minAudioEnergy ?? 0.02;
7104
7195
  const durationSec = totalSamples / 16e3;
7105
7196
  if (durationSec < minDuration) {
7106
- logger24.info("Audio too short, discarding", { durationSec });
7197
+ logger25.info("Audio too short, discarding", { durationSec });
7107
7198
  this.setState("listening");
7108
7199
  return;
7109
7200
  }
@@ -7113,7 +7204,7 @@ var _SpeechListener = class _SpeechListener extends EventEmitter {
7113
7204
  }
7114
7205
  rms = Math.sqrt(rms / fullAudio.length);
7115
7206
  if (rms < minEnergy) {
7116
- logger24.info("Audio too quiet, discarding", { rms });
7207
+ logger25.info("Audio too quiet, discarding", { rms });
7117
7208
  this.setState("listening");
7118
7209
  return;
7119
7210
  }
@@ -7130,7 +7221,7 @@ var _SpeechListener = class _SpeechListener extends EventEmitter {
7130
7221
  }
7131
7222
  if (this.epoch !== capturedEpoch) return;
7132
7223
  if (!transcript || !transcript.text.trim()) {
7133
- logger24.info("No transcript, resuming listening");
7224
+ logger25.info("No transcript, resuming listening");
7134
7225
  this.setState("listening");
7135
7226
  return;
7136
7227
  }
@@ -7166,7 +7257,7 @@ var _SpeechListener = class _SpeechListener extends EventEmitter {
7166
7257
  } catch (err) {
7167
7258
  this.progressiveErrorCount = (this.progressiveErrorCount ?? 0) + 1;
7168
7259
  if (this.progressiveErrorCount % 10 === 1) {
7169
- logger24.warn("Progressive transcription error", {
7260
+ logger25.warn("Progressive transcription error", {
7170
7261
  code: ErrorCodes.SPH_ASR_ERROR,
7171
7262
  error: String(err),
7172
7263
  count: this.progressiveErrorCount
@@ -7218,9 +7309,9 @@ var _SpeechListener = class _SpeechListener extends EventEmitter {
7218
7309
  } catch (error) {
7219
7310
  span?.endWithError(error instanceof Error ? error : new Error(String(error)));
7220
7311
  this.asrErrorCount++;
7221
- logger24.warn("Transcription failed", { attempt: this.asrErrorCount, error: String(error) });
7312
+ logger25.warn("Transcription failed", { attempt: this.asrErrorCount, error: String(error) });
7222
7313
  if (this.asrErrorCount >= 3 && this.config.models) {
7223
- logger24.warn("3 consecutive ASR errors, recreating session");
7314
+ logger25.warn("3 consecutive ASR errors, recreating session");
7224
7315
  try {
7225
7316
  await this.asr.dispose();
7226
7317
  this.asr = createSenseVoice({
@@ -7233,7 +7324,7 @@ var _SpeechListener = class _SpeechListener extends EventEmitter {
7233
7324
  await this.asr.load();
7234
7325
  this.asrErrorCount = 0;
7235
7326
  } catch (recreateErr) {
7236
- logger24.error("ASR session recreation failed", { error: String(recreateErr) });
7327
+ logger25.error("ASR session recreation failed", { error: String(recreateErr) });
7237
7328
  }
7238
7329
  }
7239
7330
  return null;
@@ -7262,7 +7353,7 @@ var _SpeechListener = class _SpeechListener extends EventEmitter {
7262
7353
  // ---------------------------------------------------------------------------
7263
7354
  setState(state) {
7264
7355
  if (this._state === state) return;
7265
- logger24.debug("State transition", { from: this._state, to: state });
7356
+ logger25.debug("State transition", { from: this._state, to: state });
7266
7357
  this._state = state;
7267
7358
  this.emit("state", state);
7268
7359
  }
@@ -7281,7 +7372,7 @@ _SpeechListener.MAX_AUDIO_BUFFER_SAMPLES = 16e3 * 30;
7281
7372
  var SpeechListener = _SpeechListener;
7282
7373
 
7283
7374
  // src/audio/InterruptionHandler.ts
7284
- var logger25 = createLogger("InterruptionHandler");
7375
+ var logger26 = createLogger("InterruptionHandler");
7285
7376
  var InterruptionHandler = class extends EventEmitter {
7286
7377
  constructor(config = {}) {
7287
7378
  super();
@@ -7302,7 +7393,7 @@ var InterruptionHandler = class extends EventEmitter {
7302
7393
  enabled: true,
7303
7394
  ...config
7304
7395
  };
7305
- logger25.debug("Constructed with config", {
7396
+ logger26.debug("Constructed with config", {
7306
7397
  vadThreshold: this.config.vadThreshold,
7307
7398
  minSpeechDurationMs: this.config.minSpeechDurationMs,
7308
7399
  silenceTimeoutMs: this.config.silenceTimeoutMs,
@@ -7333,7 +7424,7 @@ var InterruptionHandler = class extends EventEmitter {
7333
7424
  processVADResult(vadProbability, audioEnergy = 0) {
7334
7425
  if (!this.config.enabled) return;
7335
7426
  if (this.aiIsSpeaking) {
7336
- logger25.trace("VAD during AI speech", {
7427
+ logger26.trace("VAD during AI speech", {
7337
7428
  vadProbability,
7338
7429
  audioEnergy,
7339
7430
  threshold: this.config.vadThreshold
@@ -7347,12 +7438,12 @@ var InterruptionHandler = class extends EventEmitter {
7347
7438
  }
7348
7439
  /** Notify that AI started/stopped speaking */
7349
7440
  setAISpeaking(speaking) {
7350
- logger25.debug("AI speaking state changed", { speaking });
7441
+ logger26.debug("AI speaking state changed", { speaking });
7351
7442
  this.aiIsSpeaking = speaking;
7352
7443
  }
7353
7444
  /** Enable/disable interruption detection */
7354
7445
  setEnabled(enabled) {
7355
- logger25.debug("Enabled state changed", { enabled });
7446
+ logger26.debug("Enabled state changed", { enabled });
7356
7447
  this.config.enabled = enabled;
7357
7448
  if (!enabled) {
7358
7449
  this.reset();
@@ -7396,7 +7487,8 @@ var InterruptionHandler = class extends EventEmitter {
7396
7487
  const speechDuration = now - this.speechStartTime;
7397
7488
  if (speechDuration >= this.config.minSpeechDurationMs) {
7398
7489
  this.interruptionTriggeredThisSession = true;
7399
- logger25.debug("Interruption triggered", { rms, durationMs: speechDuration });
7490
+ logger26.debug("Interruption triggered", { rms, durationMs: speechDuration });
7491
+ getTelemetry()?.incrementCounter(MetricNames.VOICE_INTERRUPTIONS, 1, { source: "detector" });
7400
7492
  this.emit("interruption.triggered", { rms, durationMs: speechDuration });
7401
7493
  }
7402
7494
  }
@@ -7416,7 +7508,7 @@ var InterruptionHandler = class extends EventEmitter {
7416
7508
  };
7417
7509
 
7418
7510
  // src/inference/SafariSpeechRecognition.ts
7419
- var logger26 = createLogger("SafariSpeech");
7511
+ var logger27 = createLogger("SafariSpeech");
7420
7512
  var SafariSpeechRecognition = class _SafariSpeechRecognition {
7421
7513
  constructor(config = {}) {
7422
7514
  this.recognition = null;
@@ -7435,7 +7527,7 @@ var SafariSpeechRecognition = class _SafariSpeechRecognition {
7435
7527
  interimResults: config.interimResults ?? true,
7436
7528
  maxAlternatives: config.maxAlternatives ?? 1
7437
7529
  };
7438
- logger26.debug("SafariSpeechRecognition created", {
7530
+ logger27.debug("SafariSpeechRecognition created", {
7439
7531
  language: this.config.language,
7440
7532
  continuous: this.config.continuous
7441
7533
  });
@@ -7496,7 +7588,7 @@ var SafariSpeechRecognition = class _SafariSpeechRecognition {
7496
7588
  */
7497
7589
  async start() {
7498
7590
  if (this.isListening) {
7499
- logger26.warn("Already listening");
7591
+ logger27.warn("Already listening");
7500
7592
  return;
7501
7593
  }
7502
7594
  if (!_SafariSpeechRecognition.isAvailable()) {
@@ -7526,7 +7618,7 @@ var SafariSpeechRecognition = class _SafariSpeechRecognition {
7526
7618
  this.isListening = true;
7527
7619
  this.startTime = getClock().now();
7528
7620
  this.accumulatedText = "";
7529
- logger26.info("Speech recognition started", {
7621
+ logger27.info("Speech recognition started", {
7530
7622
  language: this.config.language
7531
7623
  });
7532
7624
  span?.end();
@@ -7541,7 +7633,7 @@ var SafariSpeechRecognition = class _SafariSpeechRecognition {
7541
7633
  */
7542
7634
  async stop() {
7543
7635
  if (!this.isListening || !this.recognition) {
7544
- logger26.warn("Not currently listening");
7636
+ logger27.warn("Not currently listening");
7545
7637
  return {
7546
7638
  text: this.accumulatedText,
7547
7639
  language: this.config.language,
@@ -7570,7 +7662,7 @@ var SafariSpeechRecognition = class _SafariSpeechRecognition {
7570
7662
  if (this.recognition && this.isListening) {
7571
7663
  this.recognition.abort();
7572
7664
  this.isListening = false;
7573
- logger26.info("Speech recognition aborted");
7665
+ logger27.info("Speech recognition aborted");
7574
7666
  }
7575
7667
  }
7576
7668
  /**
@@ -7592,7 +7684,7 @@ var SafariSpeechRecognition = class _SafariSpeechRecognition {
7592
7684
  * Dispose of recognition resources
7593
7685
  */
7594
7686
  dispose() {
7595
- logger26.debug("Disposed");
7687
+ logger27.debug("Disposed");
7596
7688
  if (this.recognition) {
7597
7689
  if (this.isListening) {
7598
7690
  this.recognition.abort();
@@ -7602,7 +7694,7 @@ var SafariSpeechRecognition = class _SafariSpeechRecognition {
7602
7694
  this.isListening = false;
7603
7695
  this.resultCallbacks = [];
7604
7696
  this.errorCallbacks = [];
7605
- logger26.debug("SafariSpeechRecognition disposed");
7697
+ logger27.debug("SafariSpeechRecognition disposed");
7606
7698
  }
7607
7699
  /**
7608
7700
  * Set up event handlers for the recognition instance
@@ -7630,7 +7722,7 @@ var SafariSpeechRecognition = class _SafariSpeechRecognition {
7630
7722
  confidence: alternative.confidence
7631
7723
  };
7632
7724
  this.emitResult(speechResult);
7633
- logger26.trace("Speech result", {
7725
+ logger27.trace("Speech result", {
7634
7726
  text: text.substring(0, 50),
7635
7727
  isFinal,
7636
7728
  confidence: alternative.confidence
@@ -7640,12 +7732,12 @@ var SafariSpeechRecognition = class _SafariSpeechRecognition {
7640
7732
  span?.end();
7641
7733
  } catch (error) {
7642
7734
  span?.endWithError(error instanceof Error ? error : new Error(String(error)));
7643
- logger26.error("Error processing speech result", { error });
7735
+ logger27.error("Error processing speech result", { error });
7644
7736
  }
7645
7737
  };
7646
7738
  this.recognition.onerror = (event) => {
7647
7739
  const error = new Error(`Speech recognition error: ${event.error} - ${event.message}`);
7648
- logger26.error("Speech recognition error", { error: event.error, message: event.message });
7740
+ logger27.error("Speech recognition error", { error: event.error, message: event.message });
7649
7741
  this.emitError(error);
7650
7742
  if (this.stopRejecter) {
7651
7743
  this.stopRejecter(error);
@@ -7655,7 +7747,7 @@ var SafariSpeechRecognition = class _SafariSpeechRecognition {
7655
7747
  };
7656
7748
  this.recognition.onend = () => {
7657
7749
  this.isListening = false;
7658
- logger26.info("Speech recognition ended", {
7750
+ logger27.info("Speech recognition ended", {
7659
7751
  totalText: this.accumulatedText.length,
7660
7752
  durationMs: getClock().now() - this.startTime
7661
7753
  });
@@ -7672,13 +7764,13 @@ var SafariSpeechRecognition = class _SafariSpeechRecognition {
7672
7764
  }
7673
7765
  };
7674
7766
  this.recognition.onstart = () => {
7675
- logger26.debug("Speech recognition started by browser");
7767
+ logger27.debug("Speech recognition started by browser");
7676
7768
  };
7677
7769
  this.recognition.onspeechstart = () => {
7678
- logger26.debug("Speech detected");
7770
+ logger27.debug("Speech detected");
7679
7771
  };
7680
7772
  this.recognition.onspeechend = () => {
7681
- logger26.debug("Speech ended");
7773
+ logger27.debug("Speech ended");
7682
7774
  };
7683
7775
  }
7684
7776
  /**
@@ -7689,7 +7781,7 @@ var SafariSpeechRecognition = class _SafariSpeechRecognition {
7689
7781
  try {
7690
7782
  callback(result);
7691
7783
  } catch (error) {
7692
- logger26.error("Error in result callback", { error });
7784
+ logger27.error("Error in result callback", { error });
7693
7785
  }
7694
7786
  }
7695
7787
  }
@@ -7701,14 +7793,14 @@ var SafariSpeechRecognition = class _SafariSpeechRecognition {
7701
7793
  try {
7702
7794
  callback(error);
7703
7795
  } catch (callbackError) {
7704
- logger26.error("Error in error callback", { error: callbackError });
7796
+ logger27.error("Error in error callback", { error: callbackError });
7705
7797
  }
7706
7798
  }
7707
7799
  }
7708
7800
  };
7709
7801
 
7710
7802
  // src/inference/ElevenLabsTTSBackend.ts
7711
- var logger27 = createLogger("ElevenLabsTTS");
7803
+ var logger28 = createLogger("ElevenLabsTTS");
7712
7804
  var DEFAULT_MODEL = "eleven_multilingual_v2";
7713
7805
  var DEFAULT_OUTPUT_FORMAT = "pcm_16000";
7714
7806
  var DEFAULT_STABILITY = 0.5;
@@ -7753,7 +7845,7 @@ var ElevenLabsTTSBackend = class {
7753
7845
  */
7754
7846
  async load() {
7755
7847
  this._isLoaded = true;
7756
- logger27.info("ElevenLabs TTS ready", { voiceId: this.voiceId, model: this.model });
7848
+ logger28.info("ElevenLabs TTS ready", { voiceId: this.voiceId, model: this.model });
7757
7849
  }
7758
7850
  // ─── Stream ─────────────────────────────────────────────────────────────
7759
7851
  /**
@@ -7799,7 +7891,7 @@ var ElevenLabsTTSBackend = class {
7799
7891
  if (!response.ok) {
7800
7892
  const errorText = await response.text().catch(() => "unknown");
7801
7893
  const msg = `ElevenLabsTTS: HTTP ${response.status} \u2014 ${this.getHttpErrorMessage(response.status, errorText)}`;
7802
- logger27.error(msg);
7894
+ logger28.error(msg);
7803
7895
  throw new Error(msg);
7804
7896
  }
7805
7897
  if (!response.body) {
@@ -7809,7 +7901,7 @@ var ElevenLabsTTSBackend = class {
7809
7901
  const latency2 = getClock().now() - startTime;
7810
7902
  span?.setAttributes({ "tts.duration_s": duration, "tts.latency_ms": latency2 });
7811
7903
  span?.end();
7812
- telemetry?.recordHistogram("omote.inference.latency", latency2, {
7904
+ telemetry?.recordHistogram(MetricNames.INFERENCE_LATENCY, latency2, {
7813
7905
  model: "elevenlabs-tts",
7814
7906
  backend: "cloud"
7815
7907
  });
@@ -7822,7 +7914,7 @@ var ElevenLabsTTSBackend = class {
7822
7914
  while (true) {
7823
7915
  if (options?.signal?.aborted) {
7824
7916
  reader.cancel();
7825
- logger27.debug("Stream aborted by signal");
7917
+ logger28.debug("Stream aborted by signal");
7826
7918
  return;
7827
7919
  }
7828
7920
  const { done, value } = await reader.read();
@@ -7841,32 +7933,32 @@ var ElevenLabsTTSBackend = class {
7841
7933
  }
7842
7934
  const latency = getClock().now() - startTime;
7843
7935
  const totalDuration = totalSamples / this._sampleRate;
7844
- logger27.debug("Stream complete", {
7936
+ logger28.debug("Stream complete", {
7845
7937
  totalDuration: `${totalDuration.toFixed(2)}s`,
7846
7938
  latencyMs: Math.round(latency),
7847
7939
  totalSamples
7848
7940
  });
7849
7941
  span?.setAttributes({ "tts.duration_s": totalDuration, "tts.latency_ms": latency });
7850
7942
  span?.end();
7851
- telemetry?.recordHistogram("omote.inference.latency", latency, {
7943
+ telemetry?.recordHistogram(MetricNames.INFERENCE_LATENCY, latency, {
7852
7944
  model: "elevenlabs-tts",
7853
7945
  backend: "cloud"
7854
7946
  });
7855
- telemetry?.incrementCounter("omote.inference.total", 1, {
7947
+ telemetry?.incrementCounter(MetricNames.INFERENCE_TOTAL, 1, {
7856
7948
  model: "elevenlabs-tts",
7857
7949
  backend: "cloud",
7858
7950
  status: "success"
7859
7951
  });
7860
7952
  } catch (err) {
7861
7953
  if (err instanceof DOMException && err.name === "AbortError") {
7862
- logger27.debug("Stream aborted");
7954
+ logger28.debug("Stream aborted");
7863
7955
  span?.end();
7864
7956
  return;
7865
7957
  }
7866
7958
  const errMsg = err instanceof Error ? err.message : String(err);
7867
- logger27.error("Stream failed", { error: errMsg });
7959
+ logger28.error("Stream failed", { error: errMsg });
7868
7960
  span?.endWithError(err instanceof Error ? err : new Error(String(err)));
7869
- telemetry?.incrementCounter("omote.inference.total", 1, {
7961
+ telemetry?.incrementCounter(MetricNames.INFERENCE_TOTAL, 1, {
7870
7962
  model: "elevenlabs-tts",
7871
7963
  backend: "cloud",
7872
7964
  status: "error"
@@ -7877,7 +7969,7 @@ var ElevenLabsTTSBackend = class {
7877
7969
  // ─── Dispose ────────────────────────────────────────────────────────────
7878
7970
  async dispose() {
7879
7971
  this._isLoaded = false;
7880
- logger27.info("ElevenLabs TTS disposed");
7972
+ logger28.info("ElevenLabs TTS disposed");
7881
7973
  }
7882
7974
  // ─── Private ────────────────────────────────────────────────────────────
7883
7975
  getHttpErrorMessage(status, body) {
@@ -7897,7 +7989,7 @@ var ElevenLabsTTSBackend = class {
7897
7989
  };
7898
7990
 
7899
7991
  // src/emotion/Emotion.ts
7900
- var logger28 = createLogger("EmotionController");
7992
+ var logger29 = createLogger("EmotionController");
7901
7993
  var EMOTION_NAMES = [
7902
7994
  "amazement",
7903
7995
  "anger",
@@ -7919,7 +8011,7 @@ function createEmotionVector(weights = {}) {
7919
8011
  if (idx >= 0) {
7920
8012
  vector[idx] = Math.max(0, Math.min(1, value));
7921
8013
  } else {
7922
- logger28.warn(`Invalid emotion name in createEmotionVector: "${name}"`);
8014
+ logger29.warn(`Invalid emotion name in createEmotionVector: "${name}"`);
7923
8015
  }
7924
8016
  }
7925
8017
  return vector;
@@ -8002,7 +8094,7 @@ var EmotionController = class {
8002
8094
  this.targetEmotion.set(newEmotion);
8003
8095
  this.currentEmotion.set(newEmotion);
8004
8096
  this.transitionProgress = 1;
8005
- logger28.debug("set", { weights });
8097
+ logger29.debug("set", { weights });
8006
8098
  }
8007
8099
  /**
8008
8100
  * Set emotion from preset immediately
@@ -8012,7 +8104,7 @@ var EmotionController = class {
8012
8104
  this.targetEmotion.set(newEmotion);
8013
8105
  this.currentEmotion.set(newEmotion);
8014
8106
  this.transitionProgress = 1;
8015
- logger28.debug("setPreset", { preset });
8107
+ logger29.debug("setPreset", { preset });
8016
8108
  }
8017
8109
  /**
8018
8110
  * Transition to new emotion over time
@@ -8026,7 +8118,7 @@ var EmotionController = class {
8026
8118
  this.transitionDuration = durationMs;
8027
8119
  this.transitionStartTime = getClock().now();
8028
8120
  this.transitionProgress = 0;
8029
- logger28.debug("transitionTo", { weights, durationMs });
8121
+ logger29.debug("transitionTo", { weights, durationMs });
8030
8122
  }
8031
8123
  /**
8032
8124
  * Transition to preset over time
@@ -8059,7 +8151,7 @@ var EmotionController = class {
8059
8151
  this.currentEmotion.fill(0);
8060
8152
  this.targetEmotion.fill(0);
8061
8153
  this.transitionProgress = 1;
8062
- logger28.debug("reset");
8154
+ logger29.debug("reset");
8063
8155
  }
8064
8156
  };
8065
8157
 
@@ -8140,7 +8232,7 @@ var DEFAULT_ANIMATION_CONFIG = {
8140
8232
  };
8141
8233
 
8142
8234
  // src/animation/AnimationGraph.ts
8143
- var logger29 = createLogger("AnimationGraph");
8235
+ var logger30 = createLogger("AnimationGraph");
8144
8236
  var AnimationGraph = class extends EventEmitter {
8145
8237
  constructor(config = {}) {
8146
8238
  super();
@@ -8173,7 +8265,7 @@ var AnimationGraph = class extends EventEmitter {
8173
8265
  this.stateEnterTime = Date.now();
8174
8266
  this.lastUpdateTime = Date.now();
8175
8267
  this.cachedOutput = this.computeOutput();
8176
- logger29.info("constructor", {
8268
+ logger30.info("constructor", {
8177
8269
  initialState: this.config.initialState,
8178
8270
  stateCount: this.config.states.length,
8179
8271
  transitionCount: this.config.transitions.length
@@ -8244,7 +8336,7 @@ var AnimationGraph = class extends EventEmitter {
8244
8336
  setState(stateName, blendDuration = 300) {
8245
8337
  const targetState = this.config.states.find((s) => s.name === stateName);
8246
8338
  if (!targetState) {
8247
- logger29.warn(`State '${stateName}' not found`);
8339
+ logger30.warn(`State '${stateName}' not found`);
8248
8340
  return;
8249
8341
  }
8250
8342
  if (targetState.name === this.currentState.name && !this.isTransitioning) {
@@ -8322,7 +8414,7 @@ var AnimationGraph = class extends EventEmitter {
8322
8414
  (s) => s.name === transition.to
8323
8415
  );
8324
8416
  if (!targetState) {
8325
- logger29.warn(`Target state '${transition.to}' not found`);
8417
+ logger30.warn(`Target state '${transition.to}' not found`);
8326
8418
  return;
8327
8419
  }
8328
8420
  const fromState = this.currentState.name;
@@ -8336,7 +8428,7 @@ var AnimationGraph = class extends EventEmitter {
8336
8428
  if (!this.currentState.emotionBlendEnabled) {
8337
8429
  this.targetEmotionWeight = 0;
8338
8430
  }
8339
- logger29.debug("state transition", {
8431
+ logger30.debug("state transition", {
8340
8432
  from: fromState,
8341
8433
  to: targetState.name,
8342
8434
  trigger: event,
@@ -8373,7 +8465,7 @@ var AnimationGraph = class extends EventEmitter {
8373
8465
  if (this.currentState.timeout <= 0) return;
8374
8466
  const elapsed = now - this.stateEnterTime;
8375
8467
  if (elapsed >= this.currentState.timeout) {
8376
- logger29.debug("timeout transition", {
8468
+ logger30.debug("timeout transition", {
8377
8469
  state: this.currentState.name,
8378
8470
  elapsed,
8379
8471
  timeout: this.currentState.timeout
@@ -8603,7 +8695,7 @@ var EmphasisDetector = class {
8603
8695
 
8604
8696
  // src/animation/ProceduralLifeLayer.ts
8605
8697
  var import_simplex_noise = require("simplex-noise");
8606
- var logger30 = createLogger("ProceduralLifeLayer");
8698
+ var logger31 = createLogger("ProceduralLifeLayer");
8607
8699
  var simplex2d = (0, import_simplex_noise.createNoise2D)();
8608
8700
  var LIFE_BS_INDEX = /* @__PURE__ */ new Map();
8609
8701
  for (let i = 0; i < ARKIT_BLENDSHAPES.length; i++) {
@@ -8709,7 +8801,7 @@ var ProceduralLifeLayer = class {
8709
8801
  }
8710
8802
  this.blinkInterval = this.nextBlinkInterval();
8711
8803
  this.gazeBreakInterval = randomRange(...this.gazeBreakIntervalRange);
8712
- logger30.debug("constructor", {
8804
+ logger31.debug("constructor", {
8713
8805
  blinkIntervalRange: this.blinkIntervalRange,
8714
8806
  useLogNormalBlinks: this.useLogNormalBlinks,
8715
8807
  gazeBreakIntervalRange: this.gazeBreakIntervalRange,
@@ -8813,7 +8905,7 @@ var ProceduralLifeLayer = class {
8813
8905
  * Reset all internal state to initial values.
8814
8906
  */
8815
8907
  reset() {
8816
- logger30.debug("reset");
8908
+ logger31.debug("reset");
8817
8909
  this.blinkTimer = 0;
8818
8910
  this.blinkInterval = this.nextBlinkInterval();
8819
8911
  this.blinkPhase = PHASE_OPEN;
@@ -8865,7 +8957,7 @@ var ProceduralLifeLayer = class {
8865
8957
  this.blinkTimer = 0;
8866
8958
  this.blinkInterval = this.nextBlinkInterval();
8867
8959
  this.asymmetryRight = 0.95 + Math.random() * 0.08;
8868
- logger30.trace("blink", { nextInterval: this.blinkInterval });
8960
+ logger31.trace("blink", { nextInterval: this.blinkInterval });
8869
8961
  }
8870
8962
  if (this.blinkPhase > PHASE_OPEN) {
8871
8963
  this.blinkProgress += delta;
@@ -8946,7 +9038,7 @@ var ProceduralLifeLayer = class {
8946
9038
  this.gazeBreakTargetX = (Math.random() - 0.5) * 2 * amp;
8947
9039
  this.gazeBreakTargetY = (Math.random() - 0.5) * amp * 0.4;
8948
9040
  this.gazeBreakInterval = randomRange(...params.interval);
8949
- logger30.trace("gaze break", {
9041
+ logger31.trace("gaze break", {
8950
9042
  targetX: this.gazeBreakTargetX.toFixed(3),
8951
9043
  targetY: this.gazeBreakTargetY.toFixed(3),
8952
9044
  nextInterval: this.gazeBreakInterval.toFixed(2),
@@ -9189,7 +9281,7 @@ var ALL_AUS = [...new Set(
9189
9281
  )];
9190
9282
 
9191
9283
  // src/face/EmotionResolver.ts
9192
- var logger31 = createLogger("EmotionResolver");
9284
+ var logger32 = createLogger("EmotionResolver");
9193
9285
  var BS_INDEX = /* @__PURE__ */ new Map();
9194
9286
  for (let i = 0; i < ARKIT_BLENDSHAPES.length; i++) {
9195
9287
  BS_INDEX.set(ARKIT_BLENDSHAPES[i], i);
@@ -9216,7 +9308,7 @@ var EmotionResolver = class {
9216
9308
  if (!emotionWeight || emotionWeight < 0.01) continue;
9217
9309
  const auActivations = EMOTION_TO_AU[emotionName];
9218
9310
  if (!auActivations) {
9219
- logger31.warn(`Unknown emotion name with no AU mapping: "${emotionName}"`);
9311
+ logger32.warn(`Unknown emotion name with no AU mapping: "${emotionName}"`);
9220
9312
  continue;
9221
9313
  }
9222
9314
  for (const activation of auActivations) {
@@ -9241,7 +9333,7 @@ var EmotionResolver = class {
9241
9333
  };
9242
9334
 
9243
9335
  // src/face/FaceCompositor.ts
9244
- var logger32 = createLogger("FaceCompositor");
9336
+ var logger33 = createLogger("FaceCompositor");
9245
9337
  function smoothstep(t) {
9246
9338
  return t * t * (3 - 2 * t);
9247
9339
  }
@@ -9272,7 +9364,7 @@ var FaceCompositor = class {
9272
9364
  if (config?.profile) {
9273
9365
  this.applyProfileArrays(config.profile);
9274
9366
  }
9275
- logger32.debug("constructor", {
9367
+ logger33.debug("constructor", {
9276
9368
  emotionSmoothing: this.emotionSmoothing,
9277
9369
  hasProfile: !!config?.profile,
9278
9370
  hasLifeLayer: !!config?.lifeLayer
@@ -9345,7 +9437,7 @@ var FaceCompositor = class {
9345
9437
  */
9346
9438
  setEmotion(weights) {
9347
9439
  this.stickyEmotion = weights;
9348
- logger32.debug("setEmotion", { weights });
9440
+ logger33.debug("setEmotion", { weights });
9349
9441
  }
9350
9442
  /**
9351
9443
  * Update character profile at runtime.
@@ -9354,7 +9446,7 @@ var FaceCompositor = class {
9354
9446
  this.multiplier.fill(1);
9355
9447
  this.offset.fill(0);
9356
9448
  this.applyProfileArrays(profile);
9357
- logger32.debug("setProfile", {
9449
+ logger33.debug("setProfile", {
9358
9450
  multiplierKeys: profile.multiplier ? Object.keys(profile.multiplier).length : 0,
9359
9451
  offsetKeys: profile.offset ? Object.keys(profile.offset).length : 0
9360
9452
  });
@@ -9368,7 +9460,7 @@ var FaceCompositor = class {
9368
9460
  this.lifeBuffer.fill(0);
9369
9461
  this.stickyEmotion = void 0;
9370
9462
  this.lifeLayer.reset();
9371
- logger32.debug("reset");
9463
+ logger33.debug("reset");
9372
9464
  }
9373
9465
  /** Expand partial profile maps into dense Float32Arrays */
9374
9466
  applyProfileArrays(profile) {
@@ -9453,7 +9545,7 @@ function parseEmotionTags(text) {
9453
9545
  }
9454
9546
 
9455
9547
  // src/character/CharacterController.ts
9456
- var logger33 = createLogger("CharacterController");
9548
+ var logger34 = createLogger("CharacterController");
9457
9549
  var FRAME_BUDGET_US = 33e3;
9458
9550
  var EMOTION_MAP = {
9459
9551
  // Synced with EmotionPresets (packages/core/src/emotion/Emotion.ts)
@@ -9523,7 +9615,7 @@ var CharacterController = class {
9523
9615
  this.gazeYawInfluence = config?.gaze?.yawInfluence ?? 0.4;
9524
9616
  this.gazePitchInfluence = config?.gaze?.pitchInfluence ?? 0.3;
9525
9617
  this.gazeSmoothing = config?.gaze?.smoothing ?? 5;
9526
- logger33.debug("constructor", {
9618
+ logger34.debug("constructor", {
9527
9619
  gazeEnabled: this.gazeEnabled,
9528
9620
  gazeYawInfluence: this.gazeYawInfluence,
9529
9621
  gazePitchInfluence: this.gazePitchInfluence,
@@ -9587,13 +9679,13 @@ var CharacterController = class {
9587
9679
  const resolved = resolveEmotion(emotion);
9588
9680
  if (resolved) {
9589
9681
  this._compositor.setEmotion(resolved);
9590
- logger33.debug("setEmotion", { emotion, resolved });
9682
+ logger34.debug("setEmotion", { emotion, resolved });
9591
9683
  }
9592
9684
  }
9593
9685
  /** Update character profile at runtime. */
9594
9686
  setProfile(profile) {
9595
9687
  this._compositor.setProfile(profile);
9596
- logger33.debug("setProfile", {
9688
+ logger34.debug("setProfile", {
9597
9689
  multiplierKeys: profile.multiplier ? Object.keys(profile.multiplier).length : 0,
9598
9690
  offsetKeys: profile.offset ? Object.keys(profile.offset).length : 0
9599
9691
  });
@@ -9628,11 +9720,11 @@ var CharacterController = class {
9628
9720
  this._compositor.reset();
9629
9721
  this.gazeHeadYaw = 0;
9630
9722
  this.gazeHeadPitch = -0.1;
9631
- logger33.debug("reset");
9723
+ logger34.debug("reset");
9632
9724
  }
9633
9725
  dispose() {
9634
9726
  this.reset();
9635
- logger33.debug("dispose");
9727
+ logger34.debug("dispose");
9636
9728
  }
9637
9729
  // ---------------------------------------------------------------------------
9638
9730
  // Eye angle math (extracted from r3f useGazeTracking.computeEyeTargets)
@@ -9714,7 +9806,7 @@ var CharacterController = class {
9714
9806
  };
9715
9807
 
9716
9808
  // src/orchestration/MicLipSync.ts
9717
- var logger34 = createLogger("MicLipSync");
9809
+ var logger35 = createLogger("MicLipSync");
9718
9810
  var MicLipSync = class extends EventEmitter {
9719
9811
  constructor(config) {
9720
9812
  super();
@@ -9733,7 +9825,7 @@ var MicLipSync = class extends EventEmitter {
9733
9825
  this.vadChunkSize = 0;
9734
9826
  this.vadBuffer = null;
9735
9827
  this.vadBufferOffset = 0;
9736
- logger34.info("MicLipSync created", {
9828
+ logger35.info("MicLipSync created", {
9737
9829
  sampleRate: config.sampleRate ?? 16e3,
9738
9830
  micChunkSize: config.micChunkSize ?? 512,
9739
9831
  hasVAD: !!config.vad,
@@ -9755,12 +9847,12 @@ var MicLipSync = class extends EventEmitter {
9755
9847
  this._currentFrame = scaled;
9756
9848
  if (!this._firstFrameEmitted) {
9757
9849
  this._firstFrameEmitted = true;
9758
- logger34.trace("First blendshape frame emitted");
9850
+ logger35.trace("First blendshape frame emitted");
9759
9851
  }
9760
9852
  this.emit("frame", { blendshapes: scaled, rawBlendshapes: raw });
9761
9853
  },
9762
9854
  onError: (error) => {
9763
- logger34.error("A2E inference error", { message: error.message });
9855
+ logger35.error("A2E inference error", { message: error.message });
9764
9856
  this.emit("error", error);
9765
9857
  }
9766
9858
  });
@@ -9769,7 +9861,7 @@ var MicLipSync = class extends EventEmitter {
9769
9861
  this.processor.pushAudio(float32);
9770
9862
  if (this.vad) {
9771
9863
  this.vadQueue = this.vadQueue.then(() => this.processVAD(float32)).catch((err) => {
9772
- logger34.warn("VAD processing error", { error: String(err), code: ErrorCodes.SPH_VAD_ERROR });
9864
+ logger35.warn("VAD processing error", { error: String(err), code: ErrorCodes.SPH_VAD_ERROR });
9773
9865
  this.emit("error", err instanceof Error ? err : new Error(String(err)));
9774
9866
  });
9775
9867
  }
@@ -9805,7 +9897,7 @@ var MicLipSync = class extends EventEmitter {
9805
9897
  /** Start microphone capture and inference loop */
9806
9898
  async start() {
9807
9899
  if (this._state === "active") return;
9808
- logger34.info("Starting MicLipSync");
9900
+ logger35.info("Starting MicLipSync");
9809
9901
  getTelemetry()?.incrementCounter(MetricNames.MIC_SESSIONS);
9810
9902
  await this.mic.start();
9811
9903
  this.processor.startDrip();
@@ -9815,7 +9907,7 @@ var MicLipSync = class extends EventEmitter {
9815
9907
  /** Stop microphone and inference */
9816
9908
  stop() {
9817
9909
  if (this._state === "idle") return;
9818
- logger34.info("Stopping MicLipSync");
9910
+ logger35.info("Stopping MicLipSync");
9819
9911
  this.processor.stopDrip();
9820
9912
  this.mic.stop();
9821
9913
  this._isSpeaking = false;
@@ -9864,7 +9956,7 @@ var MicLipSync = class extends EventEmitter {
9864
9956
  this.emit("speech:end", { durationMs });
9865
9957
  }
9866
9958
  } catch (err) {
9867
- logger34.warn("VAD process error", { error: String(err), code: ErrorCodes.SPH_VAD_ERROR });
9959
+ logger35.warn("VAD process error", { error: String(err), code: ErrorCodes.SPH_VAD_ERROR });
9868
9960
  this.emit("error", err instanceof Error ? err : new Error(String(err)));
9869
9961
  }
9870
9962
  this.vadBufferOffset = 0;
@@ -9882,7 +9974,7 @@ var MicLipSync = class extends EventEmitter {
9882
9974
  };
9883
9975
 
9884
9976
  // src/orchestration/VoiceOrchestrator.ts
9885
- var logger35 = createLogger("VoiceOrchestrator");
9977
+ var logger36 = createLogger("VoiceOrchestrator");
9886
9978
  var VoiceOrchestrator = class extends EventEmitter {
9887
9979
  constructor() {
9888
9980
  super(...arguments);
@@ -9934,12 +10026,16 @@ var VoiceOrchestrator = class extends EventEmitter {
9934
10026
  const epoch = ++this.connectEpoch;
9935
10027
  this._mode = config.mode ?? "local";
9936
10028
  this._sessionId = crypto.randomUUID();
10029
+ const span = getTelemetry()?.startSpan("VoiceOrchestrator.connect", {
10030
+ "mode": this._mode,
10031
+ "session.id": this._sessionId
10032
+ });
9937
10033
  if (config.onStateChange) this.on("state", config.onStateChange);
9938
10034
  if (config.onLoadingProgress) this.on("loading:progress", config.onLoadingProgress);
9939
10035
  if (config.onError) this.on("error", config.onError);
9940
10036
  if (config.onTranscriptEvent) this.on("transcript", config.onTranscriptEvent);
9941
10037
  if (config.onInterruption) this.on("interruption", config.onInterruption);
9942
- logger35.info("Connecting voice orchestrator", { mode: this._mode });
10038
+ logger36.info("Connecting voice orchestrator", { mode: this._mode });
9943
10039
  try {
9944
10040
  if (this._mode === "local") {
9945
10041
  const localCfg = config;
@@ -10020,9 +10116,11 @@ var VoiceOrchestrator = class extends EventEmitter {
10020
10116
  } else {
10021
10117
  this.wireCloudTranscript(config);
10022
10118
  }
10023
- logger35.info("Voice orchestrator connected", { mode: this._mode });
10119
+ logger36.info("Voice orchestrator connected", { mode: this._mode });
10120
+ span?.end();
10024
10121
  } catch (err) {
10025
- logger35.error("Voice orchestrator connect failed, cleaning up", { error: String(err) });
10122
+ logger36.error("Voice orchestrator connect failed, cleaning up", { error: String(err) });
10123
+ span?.endWithError(err instanceof Error ? err : new Error(String(err)));
10026
10124
  await this.disconnect();
10027
10125
  throw err;
10028
10126
  }
@@ -10131,6 +10229,7 @@ var VoiceOrchestrator = class extends EventEmitter {
10131
10229
  const handler = async (result) => {
10132
10230
  this.emit("transcript", result);
10133
10231
  if (!result.isFinal || !result.text.trim()) return;
10232
+ const turnStart = getClock().now();
10134
10233
  this.setState("thinking");
10135
10234
  this.speechListener?.pause();
10136
10235
  this.interruption?.setAISpeaking(true);
@@ -10147,10 +10246,11 @@ var VoiceOrchestrator = class extends EventEmitter {
10147
10246
  await this.speak(text);
10148
10247
  }
10149
10248
  } catch (e) {
10150
- logger35.error("Voice transcript handler error", { error: String(e) });
10249
+ logger36.error("Voice transcript handler error", { error: String(e) });
10151
10250
  } finally {
10152
10251
  this.interruption?.setAISpeaking(false);
10153
10252
  this.speechListener?.resume();
10253
+ getTelemetry()?.recordHistogram(MetricNames.VOICE_TURN_LATENCY, getClock().now() - turnStart);
10154
10254
  this.setState("listening");
10155
10255
  }
10156
10256
  };
@@ -10161,6 +10261,8 @@ var VoiceOrchestrator = class extends EventEmitter {
10161
10261
  const handler = async (result) => {
10162
10262
  this.emit("transcript", result);
10163
10263
  if (!result.isFinal || !result.text.trim()) return;
10264
+ const turnStart = getClock().now();
10265
+ let firstChunkSent = false;
10164
10266
  this.setState("thinking");
10165
10267
  this.speechListener?.pause();
10166
10268
  this.interruption?.setAISpeaking(true);
@@ -10177,10 +10279,15 @@ var VoiceOrchestrator = class extends EventEmitter {
10177
10279
  setEmotion: (emotion) => this.playbackPipeline?.setEmotion(emotion),
10178
10280
  send: async (chunk) => {
10179
10281
  if (abortController.signal.aborted) return;
10282
+ if (!firstChunkSent) {
10283
+ firstChunkSent = true;
10284
+ getTelemetry()?.recordHistogram(MetricNames.VOICE_RESPONSE_LATENCY, getClock().now() - turnStart);
10285
+ }
10180
10286
  await this.playbackPipeline.onAudioChunk(chunk);
10181
10287
  },
10182
10288
  done: async () => {
10183
10289
  if (abortController.signal.aborted) return;
10290
+ getTelemetry()?.recordHistogram(MetricNames.VOICE_TURN_LATENCY, getClock().now() - turnStart);
10184
10291
  await this.playbackPipeline.end();
10185
10292
  },
10186
10293
  signal: abortController.signal,
@@ -10188,7 +10295,7 @@ var VoiceOrchestrator = class extends EventEmitter {
10188
10295
  });
10189
10296
  } catch (e) {
10190
10297
  if (!abortController.signal.aborted) {
10191
- logger35.error("Cloud response handler error", { error: String(e) });
10298
+ logger36.error("Cloud response handler error", { error: String(e) });
10192
10299
  }
10193
10300
  } finally {
10194
10301
  this.responseAbortController = null;
@@ -10202,9 +10309,10 @@ var VoiceOrchestrator = class extends EventEmitter {
10202
10309
  // -------------------------------------------------------------------------
10203
10310
  handleInterruption() {
10204
10311
  if (this._state !== "speaking") return;
10205
- logger35.info("Interruption triggered");
10312
+ logger36.info("Interruption triggered");
10206
10313
  this.stopSpeaking();
10207
10314
  this.emit("interruption");
10315
+ getTelemetry()?.incrementCounter(MetricNames.VOICE_INTERRUPTIONS, 1, { source: "orchestrator" });
10208
10316
  this.speechListener?.resume();
10209
10317
  this.setState("listening");
10210
10318
  }