@absolutejs/voice 0.0.22-beta.94 → 0.0.22-beta.96

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
@@ -3596,7 +3596,7 @@ var createVoiceSession = (options) => {
3596
3596
  } : undefined;
3597
3597
  const appendTrace = async (input) => {
3598
3598
  await options.trace?.append({
3599
- at: Date.now(),
3599
+ at: input.at ?? Date.now(),
3600
3600
  metadata: input.metadata,
3601
3601
  payload: input.payload,
3602
3602
  scenarioId: input.session?.scenarioId ?? options.scenarioId,
@@ -3605,6 +3605,13 @@ var createVoiceSession = (options) => {
3605
3605
  type: input.type
3606
3606
  });
3607
3607
  };
3608
+ const appendTurnLatencyStage = async (input) => appendTrace({
3609
+ at: input.at,
3610
+ payload: { stage: input.stage },
3611
+ session: input.session,
3612
+ turnId: input.turnId,
3613
+ type: "turn_latency.stage"
3614
+ });
3608
3615
  const phraseHints = options.phraseHints ?? [];
3609
3616
  const lexicon = options.lexicon ?? [];
3610
3617
  let socket = options.socket;
@@ -4555,6 +4562,13 @@ var createVoiceSession = (options) => {
4555
4562
  turnId: activeTTSTurnId,
4556
4563
  type: "audio"
4557
4564
  });
4565
+ if (activeTTSTurnId) {
4566
+ await appendTurnLatencyStage({
4567
+ at: receivedAt,
4568
+ stage: "assistant_audio_received",
4569
+ turnId: activeTTSTurnId
4570
+ });
4571
+ }
4558
4572
  });
4559
4573
  });
4560
4574
  openedSession.on("error", (event) => {
@@ -4613,6 +4627,7 @@ var createVoiceSession = (options) => {
4613
4627
  voicemail: committedOutput?.voicemail
4614
4628
  };
4615
4629
  if (output?.assistantText) {
4630
+ const assistantTextStartedAt = Date.now();
4616
4631
  await writeSession((currentSession) => {
4617
4632
  setTurnResult(currentSession, turn.id, {
4618
4633
  assistantText: output.assistantText
@@ -4623,6 +4638,12 @@ var createVoiceSession = (options) => {
4623
4638
  turnId: turn.id,
4624
4639
  type: "assistant"
4625
4640
  });
4641
+ await appendTurnLatencyStage({
4642
+ at: assistantTextStartedAt,
4643
+ session,
4644
+ stage: "assistant_text_started",
4645
+ turnId: turn.id
4646
+ });
4626
4647
  await appendTrace({
4627
4648
  payload: {
4628
4649
  text: output.assistantText,
@@ -4637,7 +4658,18 @@ var createVoiceSession = (options) => {
4637
4658
  if (activeTTSSession) {
4638
4659
  const ttsStartedAt = Date.now();
4639
4660
  activeTTSTurnId = turn.id;
4661
+ await appendTurnLatencyStage({
4662
+ at: ttsStartedAt,
4663
+ session,
4664
+ stage: "tts_send_started",
4665
+ turnId: turn.id
4666
+ });
4640
4667
  await activeTTSSession.send(output.assistantText);
4668
+ await appendTurnLatencyStage({
4669
+ session,
4670
+ stage: "tts_send_completed",
4671
+ turnId: turn.id
4672
+ });
4641
4673
  await appendTrace({
4642
4674
  payload: {
4643
4675
  elapsedMs: Date.now() - ttsStartedAt,
@@ -4834,6 +4866,30 @@ var createVoiceSession = (options) => {
4834
4866
  turnId: turn.id,
4835
4867
  type: "turn.cost"
4836
4868
  });
4869
+ const firstTranscriptAt = turn.transcripts.map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs).filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
4870
+ const finalTranscriptAt = turn.transcripts.filter((transcript) => transcript.isFinal).map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs).filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
4871
+ if (firstTranscriptAt !== undefined) {
4872
+ await appendTurnLatencyStage({
4873
+ at: firstTranscriptAt,
4874
+ session: updatedSession,
4875
+ stage: "speech_detected",
4876
+ turnId: turn.id
4877
+ });
4878
+ }
4879
+ if (finalTranscriptAt !== undefined) {
4880
+ await appendTurnLatencyStage({
4881
+ at: finalTranscriptAt,
4882
+ session: updatedSession,
4883
+ stage: "final_transcript",
4884
+ turnId: turn.id
4885
+ });
4886
+ }
4887
+ await appendTurnLatencyStage({
4888
+ at: turn.committedAt,
4889
+ session: updatedSession,
4890
+ stage: "turn_committed",
4891
+ turnId: turn.id
4892
+ });
4837
4893
  await send({
4838
4894
  turn,
4839
4895
  type: "turn"
@@ -9965,6 +10021,8 @@ var timelineLabel = (event) => {
9965
10021
  return `Error${getString9(event.payload.error) ? `: ${getString9(event.payload.error)}` : ""}`;
9966
10022
  case "turn.cost":
9967
10023
  return "Cost telemetry";
10024
+ case "turn_latency.stage":
10025
+ return `Latency ${getString9(event.payload.stage) ?? "stage"}`;
9968
10026
  case "workflow.contract":
9969
10027
  return `Workflow contract ${eventStatus(event) ?? ""}`.trim();
9970
10028
  default:
@@ -11168,10 +11226,157 @@ var createVoiceToolContractRoutes = (options) => {
11168
11226
  }
11169
11227
  return routes;
11170
11228
  };
11171
- // src/turnQuality.ts
11229
+ // src/turnLatency.ts
11172
11230
  import { Elysia as Elysia18 } from "elysia";
11173
- var DEFAULT_CONFIDENCE_WARN_THRESHOLD = 0.72;
11231
+ var DEFAULT_WARN_AFTER_MS = 1800;
11232
+ var DEFAULT_FAIL_AFTER_MS = 3200;
11174
11233
  var escapeHtml19 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11234
+ var firstNumber2 = (values) => values.filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
11235
+ var getString10 = (value) => typeof value === "string" && value.trim() ? value : undefined;
11236
+ var createTraceStageIndex = (events) => {
11237
+ const index = new Map;
11238
+ for (const event of events) {
11239
+ if (event.type !== "turn_latency.stage" || !event.turnId) {
11240
+ continue;
11241
+ }
11242
+ const stage = getString10(event.payload.stage);
11243
+ if (!stage) {
11244
+ continue;
11245
+ }
11246
+ const key = `${event.sessionId}:${event.turnId}`;
11247
+ const stages = index.get(key) ?? new Map;
11248
+ const previous = stages.get(stage);
11249
+ if (previous === undefined || event.at < previous) {
11250
+ stages.set(stage, event.at);
11251
+ }
11252
+ index.set(key, stages);
11253
+ }
11254
+ return index;
11255
+ };
11256
+ var summarizeTurn = (sessionId, turn, options) => {
11257
+ const traceStages = options.stageIndex?.get(`${sessionId}:${turn.id}`);
11258
+ const firstTranscriptAt = firstNumber2(turn.transcripts.map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs)) ?? traceStages?.get("speech_detected");
11259
+ const finalTranscriptAt = firstNumber2(turn.transcripts.filter((transcript) => transcript.isFinal).map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs)) ?? traceStages?.get("final_transcript");
11260
+ const committedAt = traceStages?.get("turn_committed") ?? turn.committedAt;
11261
+ const assistantTextStartedAt = traceStages?.get("assistant_text_started") ?? (turn.assistantText ? committedAt : undefined);
11262
+ const ttsSendStartedAt = traceStages?.get("tts_send_started");
11263
+ const ttsSendCompletedAt = traceStages?.get("tts_send_completed");
11264
+ const assistantAudioReceivedAt = traceStages?.get("assistant_audio_received");
11265
+ const commitAfterFirstMs = firstTranscriptAt === undefined ? undefined : Math.max(0, committedAt - firstTranscriptAt);
11266
+ const commitAfterFinalMs = finalTranscriptAt === undefined ? undefined : Math.max(0, committedAt - finalTranscriptAt);
11267
+ const totalEndAt = assistantAudioReceivedAt ?? assistantTextStartedAt ?? committedAt;
11268
+ const totalMs = firstTranscriptAt === undefined ? commitAfterFirstMs : Math.max(0, totalEndAt - firstTranscriptAt);
11269
+ const status = totalMs === undefined ? "warn" : totalMs > options.failAfterMs ? "fail" : totalMs > options.warnAfterMs ? "warn" : "pass";
11270
+ return {
11271
+ assistantTextStartedAt,
11272
+ committedAt,
11273
+ finalTranscriptAt,
11274
+ firstTranscriptAt,
11275
+ sessionId,
11276
+ stages: [
11277
+ { label: "Speech to commit", valueMs: commitAfterFirstMs },
11278
+ { label: "Final transcript to commit", valueMs: commitAfterFinalMs },
11279
+ {
11280
+ label: "Commit to assistant text",
11281
+ valueMs: assistantTextStartedAt === undefined ? undefined : Math.max(0, assistantTextStartedAt - committedAt)
11282
+ },
11283
+ {
11284
+ label: "Assistant text to TTS send",
11285
+ valueMs: ttsSendStartedAt === undefined || assistantTextStartedAt === undefined ? undefined : Math.max(0, ttsSendStartedAt - assistantTextStartedAt)
11286
+ },
11287
+ {
11288
+ label: "TTS send duration",
11289
+ valueMs: ttsSendCompletedAt === undefined || ttsSendStartedAt === undefined ? undefined : Math.max(0, ttsSendCompletedAt - ttsSendStartedAt)
11290
+ },
11291
+ {
11292
+ label: "TTS to first audio",
11293
+ valueMs: assistantAudioReceivedAt === undefined || ttsSendCompletedAt === undefined ? undefined : Math.max(0, assistantAudioReceivedAt - ttsSendCompletedAt)
11294
+ }
11295
+ ],
11296
+ status,
11297
+ text: turn.text,
11298
+ totalMs,
11299
+ turnId: turn.id
11300
+ };
11301
+ };
11302
+ var resolveSessions = async (options) => {
11303
+ if (options.sessions) {
11304
+ return options.sessions;
11305
+ }
11306
+ if (!options.store) {
11307
+ return [];
11308
+ }
11309
+ const summaries = await options.store.list();
11310
+ const ids = options.sessionIds ?? summaries.map((summary) => summary.id);
11311
+ const hydrated = await Promise.all(ids.slice(0, options.limit ?? 25).map((id) => options.store?.get(id)));
11312
+ const sessions = [];
11313
+ for (const session of hydrated) {
11314
+ if (session) {
11315
+ sessions.push(session);
11316
+ }
11317
+ }
11318
+ return sessions;
11319
+ };
11320
+ var summarizeVoiceTurnLatency = async (options) => {
11321
+ const sessions = await resolveSessions(options);
11322
+ const traceEvents = options.traceStore ? await options.traceStore.list({
11323
+ limit: 1000,
11324
+ type: "turn_latency.stage"
11325
+ }) : [];
11326
+ const stageIndex = createTraceStageIndex(traceEvents);
11327
+ const warnAfterMs = options.warnAfterMs ?? DEFAULT_WARN_AFTER_MS;
11328
+ const failAfterMs = options.failAfterMs ?? DEFAULT_FAIL_AFTER_MS;
11329
+ const turns = sessions.flatMap((session) => session.turns.map((turn) => summarizeTurn(session.id, turn, { failAfterMs, stageIndex, warnAfterMs }))).sort((left, right) => right.committedAt - left.committedAt);
11330
+ const totals = turns.map((turn) => turn.totalMs).filter((value) => typeof value === "number");
11331
+ const failed = turns.filter((turn) => turn.status === "fail").length;
11332
+ const warnings = turns.filter((turn) => turn.status === "warn").length;
11333
+ return {
11334
+ averageTotalMs: totals.length > 0 ? Math.round(totals.reduce((total, value) => total + value, 0) / totals.length) : undefined,
11335
+ checkedAt: Date.now(),
11336
+ failed,
11337
+ sessions: sessions.length,
11338
+ status: turns.length === 0 ? "empty" : failed > 0 ? "fail" : warnings > 0 ? "warn" : "pass",
11339
+ total: turns.length,
11340
+ turns,
11341
+ warnings
11342
+ };
11343
+ };
11344
+ var formatMs2 = (value) => typeof value === "number" ? `${Math.round(value)}ms` : "n/a";
11345
+ var renderVoiceTurnLatencyHTML = (report, options = {}) => {
11346
+ const title = options.title ?? "Voice Turn Latency";
11347
+ const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml19(turn.status)}">
11348
+ <header><div><p class="eyebrow">${escapeHtml19(turn.sessionId)} \xB7 ${escapeHtml19(turn.turnId)}</p><h2>${escapeHtml19(turn.text || "Empty turn")}</h2></div><strong>${escapeHtml19(turn.status)}</strong></header>
11349
+ <dl>${turn.stages.map((stage) => `<div><dt>${escapeHtml19(stage.label)}</dt><dd>${escapeHtml19(formatMs2(stage.valueMs))}</dd></div>`).join("")}</dl>
11350
+ </article>`).join("");
11351
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml19(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.turn{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(94,234,212,.16),rgba(251,191,36,.1))}.eyebrow{color:#5eead4;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}h2{margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.turn header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.warn,.empty{color:#fde68a}.fail{color:#fca5a5}.turn.fail{border-color:rgba(248,113,113,.45)}dl{display:grid;gap:8px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}dt{color:#a8b0b8;font-size:.8rem}dd{font-weight:900;margin:0}@media(max-width:800px){main{padding:18px}.turn header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">End-to-end responsiveness</p><h1>${escapeHtml19(title)}</h1><div class="summary"><span class="pill ${escapeHtml19(report.status)}">${escapeHtml19(report.status)}</span><span class="pill">${String(report.total)} turns</span><span class="pill">avg ${escapeHtml19(formatMs2(report.averageTotalMs))}</span><span class="pill">${String(report.warnings)} warnings</span><span class="pill">${String(report.failed)} failed</span></div></section>${turns || '<section class="turn"><p>No committed turns found.</p></section>'}</main></body></html>`;
11352
+ };
11353
+ var createVoiceTurnLatencyJSONHandler = (options) => async () => summarizeVoiceTurnLatency(options);
11354
+ var createVoiceTurnLatencyHTMLHandler = (options) => async () => {
11355
+ const report = await summarizeVoiceTurnLatency(options);
11356
+ const render = options.render ?? ((input) => renderVoiceTurnLatencyHTML(input, options));
11357
+ const body = await render(report);
11358
+ return new Response(body, {
11359
+ headers: {
11360
+ "Content-Type": "text/html; charset=utf-8",
11361
+ ...options.headers
11362
+ }
11363
+ });
11364
+ };
11365
+ var createVoiceTurnLatencyRoutes = (options) => {
11366
+ const path = options.path ?? "/api/turn-latency";
11367
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
11368
+ const routes = new Elysia18({
11369
+ name: options.name ?? "absolutejs-voice-turn-latency"
11370
+ }).get(path, createVoiceTurnLatencyJSONHandler(options));
11371
+ if (htmlPath) {
11372
+ routes.get(htmlPath, createVoiceTurnLatencyHTMLHandler(options));
11373
+ }
11374
+ return routes;
11375
+ };
11376
+ // src/turnQuality.ts
11377
+ import { Elysia as Elysia19 } from "elysia";
11378
+ var DEFAULT_CONFIDENCE_WARN_THRESHOLD = 0.72;
11379
+ var escapeHtml20 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11175
11380
  var getTurnLatencyMs = (turn) => {
11176
11381
  const firstTranscriptAt = turn.transcripts.map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs).filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
11177
11382
  if (firstTranscriptAt === undefined) {
@@ -11179,7 +11384,7 @@ var getTurnLatencyMs = (turn) => {
11179
11384
  }
11180
11385
  return Math.max(0, turn.committedAt - firstTranscriptAt);
11181
11386
  };
11182
- var summarizeTurn = (sessionId, turn, options) => {
11387
+ var summarizeTurn2 = (sessionId, turn, options) => {
11183
11388
  const quality = turn.quality;
11184
11389
  const correctionChanged = quality?.correction?.changed === true;
11185
11390
  const fallbackUsed = quality?.fallbackUsed === true;
@@ -11206,7 +11411,7 @@ var summarizeTurn = (sessionId, turn, options) => {
11206
11411
  turnId: turn.id
11207
11412
  };
11208
11413
  };
11209
- var resolveSessions = async (options) => {
11414
+ var resolveSessions2 = async (options) => {
11210
11415
  if (options.sessions) {
11211
11416
  return options.sessions;
11212
11417
  }
@@ -11225,9 +11430,9 @@ var resolveSessions = async (options) => {
11225
11430
  return sessions;
11226
11431
  };
11227
11432
  var summarizeVoiceTurnQuality = async (options) => {
11228
- const sessions = await resolveSessions(options);
11433
+ const sessions = await resolveSessions2(options);
11229
11434
  const confidenceWarnThreshold = options.confidenceWarnThreshold ?? DEFAULT_CONFIDENCE_WARN_THRESHOLD;
11230
- const turns = sessions.flatMap((session) => session.turns.map((turn) => summarizeTurn(session.id, turn, { confidenceWarnThreshold }))).sort((left, right) => right.committedAt - left.committedAt);
11435
+ const turns = sessions.flatMap((session) => session.turns.map((turn) => summarizeTurn2(session.id, turn, { confidenceWarnThreshold }))).sort((left, right) => right.committedAt - left.committedAt);
11231
11436
  const failed = turns.filter((turn) => turn.status === "fail").length;
11232
11437
  const warnings = turns.filter((turn) => turn.status === "warn").length;
11233
11438
  return {
@@ -11242,24 +11447,24 @@ var summarizeVoiceTurnQuality = async (options) => {
11242
11447
  };
11243
11448
  var renderVoiceTurnQualityHTML = (report, options = {}) => {
11244
11449
  const title = options.title ?? "Voice Turn Quality";
11245
- const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml19(turn.status)}">
11450
+ const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml20(turn.status)}">
11246
11451
  <div class="turn-header">
11247
11452
  <div>
11248
- <p class="eyebrow">${escapeHtml19(turn.sessionId)} \xB7 ${escapeHtml19(turn.turnId)}</p>
11249
- <h2>${escapeHtml19(turn.text || "Empty turn")}</h2>
11453
+ <p class="eyebrow">${escapeHtml20(turn.sessionId)} \xB7 ${escapeHtml20(turn.turnId)}</p>
11454
+ <h2>${escapeHtml20(turn.text || "Empty turn")}</h2>
11250
11455
  </div>
11251
- <strong>${escapeHtml19(turn.status)}</strong>
11456
+ <strong>${escapeHtml20(turn.status)}</strong>
11252
11457
  </div>
11253
11458
  <dl>
11254
- <div><dt>Source</dt><dd>${escapeHtml19(turn.source ?? "unknown")}</dd></div>
11459
+ <div><dt>Source</dt><dd>${escapeHtml20(turn.source ?? "unknown")}</dd></div>
11255
11460
  <div><dt>Confidence</dt><dd>${turn.averageConfidence === undefined ? "n/a" : `${Math.round(turn.averageConfidence * 100)}%`}</dd></div>
11256
- <div><dt>Fallback</dt><dd>${turn.fallbackUsed ? `yes (${escapeHtml19(turn.fallbackSelectionReason ?? "selected")})` : "no"}</dd></div>
11257
- <div><dt>Correction</dt><dd>${turn.correctionChanged ? `changed${turn.correctionProvider ? ` by ${escapeHtml19(turn.correctionProvider)}` : ""}` : "none"}</dd></div>
11461
+ <div><dt>Fallback</dt><dd>${turn.fallbackUsed ? `yes (${escapeHtml20(turn.fallbackSelectionReason ?? "selected")})` : "no"}</dd></div>
11462
+ <div><dt>Correction</dt><dd>${turn.correctionChanged ? `changed${turn.correctionProvider ? ` by ${escapeHtml20(turn.correctionProvider)}` : ""}` : "none"}</dd></div>
11258
11463
  <div><dt>Transcripts</dt><dd>${String(turn.selectedTranscriptCount)} selected \xB7 ${String(turn.finalTranscriptCount)} final \xB7 ${String(turn.partialTranscriptCount)} partial</dd></div>
11259
11464
  <div><dt>Cost</dt><dd>${turn.costUnits === undefined ? "n/a" : String(turn.costUnits)}</dd></div>
11260
11465
  </dl>
11261
11466
  </article>`).join("");
11262
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml19(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.turn{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(251,191,36,.16),rgba(34,197,94,.1))}.eyebrow{color:#fbbf24;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}h2{margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.turn-header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.warn,.unknown{color:#fde68a}.fail{color:#fca5a5}.turn.fail{border-color:rgba(248,113,113,.45)}dl{display:grid;gap:8px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}dt{color:#a8b0b8;font-size:.8rem}dd{margin:0}@media(max-width:800px){main{padding:18px}.turn-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Realtime STT Debugging</p><h1>${escapeHtml19(title)}</h1><div class="summary"><span class="pill ${escapeHtml19(report.status)}">${escapeHtml19(report.status)}</span><span class="pill">${String(report.total)} turns</span><span class="pill">${String(report.warnings)} warnings</span><span class="pill">${String(report.failed)} failed</span><span class="pill">${String(report.sessions)} sessions</span></div></section>${turns || '<section class="turn"><p>No committed turns found.</p></section>'}</main></body></html>`;
11467
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml20(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.turn{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(251,191,36,.16),rgba(34,197,94,.1))}.eyebrow{color:#fbbf24;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}h2{margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.turn-header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.warn,.unknown{color:#fde68a}.fail{color:#fca5a5}.turn.fail{border-color:rgba(248,113,113,.45)}dl{display:grid;gap:8px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}dt{color:#a8b0b8;font-size:.8rem}dd{margin:0}@media(max-width:800px){main{padding:18px}.turn-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Realtime STT Debugging</p><h1>${escapeHtml20(title)}</h1><div class="summary"><span class="pill ${escapeHtml20(report.status)}">${escapeHtml20(report.status)}</span><span class="pill">${String(report.total)} turns</span><span class="pill">${String(report.warnings)} warnings</span><span class="pill">${String(report.failed)} failed</span><span class="pill">${String(report.sessions)} sessions</span></div></section>${turns || '<section class="turn"><p>No committed turns found.</p></section>'}</main></body></html>`;
11263
11468
  };
11264
11469
  var createVoiceTurnQualityJSONHandler = (options) => async () => summarizeVoiceTurnQuality(options);
11265
11470
  var createVoiceTurnQualityHTMLHandler = (options) => async () => {
@@ -11276,7 +11481,7 @@ var createVoiceTurnQualityHTMLHandler = (options) => async () => {
11276
11481
  var createVoiceTurnQualityRoutes = (options) => {
11277
11482
  const path = options.path ?? "/api/turn-quality";
11278
11483
  const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
11279
- const routes = new Elysia18({
11484
+ const routes = new Elysia19({
11280
11485
  name: options.name ?? "absolutejs-voice-turn-quality"
11281
11486
  }).get(path, createVoiceTurnQualityJSONHandler(options));
11282
11487
  if (htmlPath) {
@@ -11285,8 +11490,8 @@ var createVoiceTurnQualityRoutes = (options) => {
11285
11490
  return routes;
11286
11491
  };
11287
11492
  // src/outcomeContract.ts
11288
- import { Elysia as Elysia19 } from "elysia";
11289
- var escapeHtml20 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11493
+ import { Elysia as Elysia20 } from "elysia";
11494
+ var escapeHtml21 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11290
11495
  var getPayloadString = (event, key) => typeof event.payload[key] === "string" ? event.payload[key] : undefined;
11291
11496
  var toList = async (input) => Array.isArray(input) ? input : await input?.list() ?? [];
11292
11497
  var hydrateSessions = async (input) => {
@@ -11394,9 +11599,9 @@ var renderVoiceOutcomeContractHTML = (report, options = {}) => {
11394
11599
  const contracts = report.contracts.map((contract) => `<section class="contract ${contract.pass ? "pass" : "fail"}">
11395
11600
  <div class="contract-header">
11396
11601
  <div>
11397
- <p class="eyebrow">${escapeHtml20(contract.contractId)}</p>
11398
- <h2>${escapeHtml20(contract.label ?? contract.contractId)}</h2>
11399
- ${contract.description ? `<p>${escapeHtml20(contract.description)}</p>` : ""}
11602
+ <p class="eyebrow">${escapeHtml21(contract.contractId)}</p>
11603
+ <h2>${escapeHtml21(contract.label ?? contract.contractId)}</h2>
11604
+ ${contract.description ? `<p>${escapeHtml21(contract.description)}</p>` : ""}
11400
11605
  </div>
11401
11606
  <strong>${contract.pass ? "pass" : "fail"}</strong>
11402
11607
  </div>
@@ -11407,9 +11612,9 @@ var renderVoiceOutcomeContractHTML = (report, options = {}) => {
11407
11612
  <span>handoffs ${String(contract.matched.handoffs)}</span>
11408
11613
  <span>events ${String(contract.matched.integrationEvents)}</span>
11409
11614
  </div>
11410
- ${contract.issues.length ? `<ul>${contract.issues.map((issue) => `<li>${escapeHtml20(issue.message)}</li>`).join("")}</ul>` : ""}
11615
+ ${contract.issues.length ? `<ul>${contract.issues.map((issue) => `<li>${escapeHtml21(issue.message)}</li>`).join("")}</ul>` : ""}
11411
11616
  </section>`).join("");
11412
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml20(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.contract{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(34,197,94,.14),rgba(14,165,233,.12))}.eyebrow{color:#7dd3fc;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}h2{margin:.2rem 0}.summary,.grid{display:flex;flex-wrap:wrap;gap:10px}.pill,.grid span{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.contract-header{display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.fail{color:#fca5a5}.contract.fail{border-color:rgba(248,113,113,.45)}li{margin:8px 0}@media(max-width:800px){main{padding:18px}.contract-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Business Outcome Verification</p><h1>${escapeHtml20(title)}</h1><div class="summary"><span class="pill ${report.status}">${report.status}</span><span class="pill">${String(report.passed)} passing</span><span class="pill">${String(report.failed)} failing</span><span class="pill">${String(report.total)} contracts</span></div></section>${contracts || '<section class="contract"><p>No outcome contracts configured.</p></section>'}</main></body></html>`;
11617
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml21(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.contract{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(34,197,94,.14),rgba(14,165,233,.12))}.eyebrow{color:#7dd3fc;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}h2{margin:.2rem 0}.summary,.grid{display:flex;flex-wrap:wrap;gap:10px}.pill,.grid span{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.contract-header{display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.fail{color:#fca5a5}.contract.fail{border-color:rgba(248,113,113,.45)}li{margin:8px 0}@media(max-width:800px){main{padding:18px}.contract-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Business Outcome Verification</p><h1>${escapeHtml21(title)}</h1><div class="summary"><span class="pill ${report.status}">${report.status}</span><span class="pill">${String(report.passed)} passing</span><span class="pill">${String(report.failed)} failing</span><span class="pill">${String(report.total)} contracts</span></div></section>${contracts || '<section class="contract"><p>No outcome contracts configured.</p></section>'}</main></body></html>`;
11413
11618
  };
11414
11619
  var createVoiceOutcomeContractJSONHandler = (options) => async () => runVoiceOutcomeContractSuite(options);
11415
11620
  var createVoiceOutcomeContractHTMLHandler = (options) => async () => {
@@ -11425,7 +11630,7 @@ var createVoiceOutcomeContractHTMLHandler = (options) => async () => {
11425
11630
  var createVoiceOutcomeContractRoutes = (options) => {
11426
11631
  const path = options.path ?? "/api/outcome-contracts";
11427
11632
  const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
11428
- const routes = new Elysia19({
11633
+ const routes = new Elysia20({
11429
11634
  name: options.name ?? "absolutejs-voice-outcome-contracts"
11430
11635
  }).get(path, createVoiceOutcomeContractJSONHandler(options));
11431
11636
  if (htmlPath) {
@@ -11434,7 +11639,7 @@ var createVoiceOutcomeContractRoutes = (options) => {
11434
11639
  return routes;
11435
11640
  };
11436
11641
  // src/telephonyOutcome.ts
11437
- import { Elysia as Elysia20 } from "elysia";
11642
+ import { Elysia as Elysia21 } from "elysia";
11438
11643
  var DEFAULT_COMPLETED_STATUSES = [
11439
11644
  "answered",
11440
11645
  "completed",
@@ -11505,7 +11710,7 @@ var firstString2 = (source, keys) => {
11505
11710
  }
11506
11711
  }
11507
11712
  };
11508
- var firstNumber2 = (source, keys) => {
11713
+ var firstNumber3 = (source, keys) => {
11509
11714
  for (const key of keys) {
11510
11715
  const value = source[key];
11511
11716
  if (typeof value === "number" && Number.isFinite(value)) {
@@ -11877,7 +12082,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
11877
12082
  "event_type",
11878
12083
  "type"
11879
12084
  ]);
11880
- const durationMs = firstNumber2(payload, ["durationMs", "duration_ms"]) ?? durationMsFromSeconds(firstNumber2(payload, [
12085
+ const durationMs = firstNumber3(payload, ["durationMs", "duration_ms"]) ?? durationMsFromSeconds(firstNumber3(payload, [
11881
12086
  "CallDuration",
11882
12087
  "call_duration",
11883
12088
  "callDuration",
@@ -11885,7 +12090,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
11885
12090
  "dial_call_duration",
11886
12091
  "duration"
11887
12092
  ]));
11888
- const sipCode = firstNumber2(payload, [
12093
+ const sipCode = firstNumber3(payload, [
11889
12094
  "SipResponseCode",
11890
12095
  "sip_response_code",
11891
12096
  "sipCode",
@@ -12081,7 +12286,7 @@ var createVoiceTelephonyWebhookHandler = (options = {}) => async (input) => {
12081
12286
  var createVoiceTelephonyWebhookRoutes = (options = {}) => {
12082
12287
  const path = options.path ?? "/api/voice/telephony/webhook";
12083
12288
  const handler = createVoiceTelephonyWebhookHandler(options);
12084
- return new Elysia20({
12289
+ return new Elysia21({
12085
12290
  name: options.name ?? "absolutejs-voice-telephony-webhooks"
12086
12291
  }).post(path, async ({ query, request }) => {
12087
12292
  try {
@@ -14349,7 +14554,7 @@ var createVoiceMemoryStore = () => {
14349
14554
  return { get, getOrCreate, list, remove, set };
14350
14555
  };
14351
14556
  // src/opsWebhook.ts
14352
- import { Elysia as Elysia21 } from "elysia";
14557
+ import { Elysia as Elysia22 } from "elysia";
14353
14558
  var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
14354
14559
  var signVoiceOpsWebhookBody = async (input) => {
14355
14560
  const encoder = new TextEncoder;
@@ -14479,7 +14684,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
14479
14684
  };
14480
14685
  var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
14481
14686
  const path = options.path ?? "/api/voice-ops/webhook";
14482
- return new Elysia21().post(path, async ({ body, request, set }) => {
14687
+ return new Elysia22().post(path, async ({ body, request, set }) => {
14483
14688
  const bodyText = typeof body === "string" ? body : JSON.stringify(body);
14484
14689
  if (options.signingSecret) {
14485
14690
  const verification = await verifyVoiceOpsWebhookSignature({
@@ -16208,7 +16413,7 @@ var createVoiceSTTRoutingCorrectionHandler = (mode = "generic") => {
16208
16413
  };
16209
16414
  // src/telephony/twilio.ts
16210
16415
  import { Buffer as Buffer3 } from "buffer";
16211
- import { Elysia as Elysia22 } from "elysia";
16416
+ import { Elysia as Elysia23 } from "elysia";
16212
16417
  var TWILIO_MULAW_SAMPLE_RATE = 8000;
16213
16418
  var VOICE_PCM_SAMPLE_RATE = 16000;
16214
16419
  var escapeXml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
@@ -16238,7 +16443,7 @@ var resolveTwilioStreamParameters = async (parameters, input) => {
16238
16443
  return parameters;
16239
16444
  };
16240
16445
  var joinUrlPath = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
16241
- var escapeHtml21 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
16446
+ var escapeHtml22 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
16242
16447
  var getWebhookVerificationUrl = (webhook, input) => {
16243
16448
  if (!webhook?.verificationUrl) {
16244
16449
  return;
@@ -16281,23 +16486,23 @@ var buildTwilioVoiceSetupStatus = async (options, input) => {
16281
16486
  };
16282
16487
  var renderTwilioVoiceSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
16283
16488
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio setup</p>
16284
- <h1>${escapeHtml21(title)}</h1>
16489
+ <h1>${escapeHtml22(title)}</h1>
16285
16490
  <p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
16286
16491
  <section>
16287
16492
  <h2>URLs</h2>
16288
16493
  <ul>
16289
- <li><strong>TwiML:</strong> <code>${escapeHtml21(status.urls.twiml)}</code></li>
16290
- <li><strong>Media stream:</strong> <code>${escapeHtml21(status.urls.stream)}</code></li>
16291
- <li><strong>Status webhook:</strong> <code>${escapeHtml21(status.urls.webhook)}</code></li>
16494
+ <li><strong>TwiML:</strong> <code>${escapeHtml22(status.urls.twiml)}</code></li>
16495
+ <li><strong>Media stream:</strong> <code>${escapeHtml22(status.urls.stream)}</code></li>
16496
+ <li><strong>Status webhook:</strong> <code>${escapeHtml22(status.urls.webhook)}</code></li>
16292
16497
  </ul>
16293
16498
  </section>
16294
16499
  <section>
16295
16500
  <h2>Signing</h2>
16296
16501
  <p>Mode: <code>${status.signing.mode}</code></p>
16297
- ${status.signing.verificationUrl ? `<p>Verification URL: <code>${escapeHtml21(status.signing.verificationUrl)}</code></p>` : ""}
16502
+ ${status.signing.verificationUrl ? `<p>Verification URL: <code>${escapeHtml22(status.signing.verificationUrl)}</code></p>` : ""}
16298
16503
  </section>
16299
- ${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml21(name)}</code></li>`).join("")}</ul></section>` : ""}
16300
- ${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml21(warning)}</li>`).join("")}</ul></section>` : ""}
16504
+ ${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml22(name)}</code></li>`).join("")}</ul></section>` : ""}
16505
+ ${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml22(warning)}</li>`).join("")}</ul></section>` : ""}
16301
16506
  </main>`;
16302
16507
  var extractTwilioStreamUrl = (twiml) => twiml.match(/<Stream\b[^>]*\surl="([^"]+)"/i)?.[1]?.replaceAll("&amp;", "&");
16303
16508
  var createSmokeCheck = (name, status, message, details) => ({
@@ -16308,20 +16513,20 @@ var createSmokeCheck = (name, status, message, details) => ({
16308
16513
  });
16309
16514
  var renderTwilioVoiceSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
16310
16515
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio smoke test</p>
16311
- <h1>${escapeHtml21(title)}</h1>
16516
+ <h1>${escapeHtml22(title)}</h1>
16312
16517
  <p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
16313
16518
  <section>
16314
16519
  <h2>Checks</h2>
16315
16520
  <ul>
16316
- ${report.checks.map((check) => `<li><strong>${escapeHtml21(check.name)}</strong>: ${escapeHtml21(check.status)}${check.message ? ` - ${escapeHtml21(check.message)}` : ""}</li>`).join("")}
16521
+ ${report.checks.map((check) => `<li><strong>${escapeHtml22(check.name)}</strong>: ${escapeHtml22(check.status)}${check.message ? ` - ${escapeHtml22(check.message)}` : ""}</li>`).join("")}
16317
16522
  </ul>
16318
16523
  </section>
16319
16524
  <section>
16320
16525
  <h2>Observed URLs</h2>
16321
16526
  <ul>
16322
- <li><strong>TwiML:</strong> <code>${escapeHtml21(report.setup.urls.twiml)}</code></li>
16323
- <li><strong>Stream:</strong> <code>${escapeHtml21(report.twiml?.streamUrl ?? report.setup.urls.stream)}</code></li>
16324
- <li><strong>Webhook:</strong> <code>${escapeHtml21(report.setup.urls.webhook)}</code></li>
16527
+ <li><strong>TwiML:</strong> <code>${escapeHtml22(report.setup.urls.twiml)}</code></li>
16528
+ <li><strong>Stream:</strong> <code>${escapeHtml22(report.twiml?.streamUrl ?? report.setup.urls.stream)}</code></li>
16529
+ <li><strong>Webhook:</strong> <code>${escapeHtml22(report.setup.urls.webhook)}</code></li>
16325
16530
  </ul>
16326
16531
  </section>
16327
16532
  </main>`;
@@ -16781,7 +16986,7 @@ var createTwilioVoiceRoutes = (options) => {
16781
16986
  const smokePath = options.smoke?.path === false ? false : options.smoke?.path ?? "/api/voice/twilio/smoke";
16782
16987
  const bridges = new WeakMap;
16783
16988
  const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
16784
- const app = new Elysia22({
16989
+ const app = new Elysia23({
16785
16990
  name: options.name ?? "absolutejs-voice-twilio"
16786
16991
  }).get(twimlPath, async ({ query, request }) => {
16787
16992
  const streamUrl = await resolveTwilioStreamUrl(options, {
@@ -16917,9 +17122,9 @@ var createTwilioVoiceRoutes = (options) => {
16917
17122
  };
16918
17123
  // src/telephony/telnyx.ts
16919
17124
  import { Buffer as Buffer4 } from "buffer";
16920
- import { Elysia as Elysia23 } from "elysia";
17125
+ import { Elysia as Elysia24 } from "elysia";
16921
17126
  var escapeXml3 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
16922
- var escapeHtml22 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
17127
+ var escapeHtml23 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
16923
17128
  var joinUrlPath2 = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
16924
17129
  var resolveRequestOrigin2 = (request) => {
16925
17130
  const url = new URL(request.url);
@@ -17120,21 +17325,21 @@ var buildTelnyxVoiceSetupStatus = async (options, input) => {
17120
17325
  };
17121
17326
  var renderTelnyxSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
17122
17327
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Telnyx setup</p>
17123
- <h1>${escapeHtml22(title)}</h1>
17328
+ <h1>${escapeHtml23(title)}</h1>
17124
17329
  <p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
17125
17330
  <ul>
17126
- <li><strong>TeXML:</strong> <code>${escapeHtml22(status.urls.texml)}</code></li>
17127
- <li><strong>Media stream:</strong> <code>${escapeHtml22(status.urls.stream)}</code></li>
17128
- <li><strong>Status webhook:</strong> <code>${escapeHtml22(status.urls.webhook)}</code></li>
17331
+ <li><strong>TeXML:</strong> <code>${escapeHtml23(status.urls.texml)}</code></li>
17332
+ <li><strong>Media stream:</strong> <code>${escapeHtml23(status.urls.stream)}</code></li>
17333
+ <li><strong>Status webhook:</strong> <code>${escapeHtml23(status.urls.webhook)}</code></li>
17129
17334
  </ul>
17130
- ${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml22(name)}</code></li>`).join("")}</ul>` : ""}
17131
- ${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml22(warning)}</li>`).join("")}</ul>` : ""}
17335
+ ${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml23(name)}</code></li>`).join("")}</ul>` : ""}
17336
+ ${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml23(warning)}</li>`).join("")}</ul>` : ""}
17132
17337
  </main>`;
17133
17338
  var renderTelnyxSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
17134
17339
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Telnyx smoke test</p>
17135
- <h1>${escapeHtml22(title)}</h1>
17340
+ <h1>${escapeHtml23(title)}</h1>
17136
17341
  <p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
17137
- <ul>${report.checks.map((check) => `<li><strong>${escapeHtml22(check.name)}</strong>: ${escapeHtml22(check.status)}${check.message ? ` - ${escapeHtml22(check.message)}` : ""}</li>`).join("")}</ul>
17342
+ <ul>${report.checks.map((check) => `<li><strong>${escapeHtml23(check.name)}</strong>: ${escapeHtml23(check.status)}${check.message ? ` - ${escapeHtml23(check.message)}` : ""}</li>`).join("")}</ul>
17138
17343
  </main>`;
17139
17344
  var runTelnyxSmokeTest = async (input) => {
17140
17345
  const setup = await buildTelnyxVoiceSetupStatus(input.options, input);
@@ -17228,7 +17433,7 @@ var createTelnyxVoiceRoutes = (options = {}) => {
17228
17433
  publicKey: options.webhook?.publicKey,
17229
17434
  toleranceSeconds: options.webhook?.toleranceSeconds
17230
17435
  }) : undefined);
17231
- const app = new Elysia23({
17436
+ const app = new Elysia24({
17232
17437
  name: options.name ?? "absolutejs-voice-telnyx"
17233
17438
  }).get(texmlPath, async ({ query, request }) => {
17234
17439
  const streamUrl = await resolveTelnyxStreamUrl(options, {
@@ -17338,9 +17543,9 @@ var createTelnyxVoiceRoutes = (options = {}) => {
17338
17543
  };
17339
17544
  // src/telephony/plivo.ts
17340
17545
  import { Buffer as Buffer5 } from "buffer";
17341
- import { Elysia as Elysia24 } from "elysia";
17546
+ import { Elysia as Elysia25 } from "elysia";
17342
17547
  var escapeXml4 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
17343
- var escapeHtml23 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
17548
+ var escapeHtml24 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
17344
17549
  var joinUrlPath3 = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
17345
17550
  var resolveRequestOrigin3 = (request) => {
17346
17551
  const url = new URL(request.url);
@@ -17591,21 +17796,21 @@ var buildPlivoVoiceSetupStatus = async (options, input) => {
17591
17796
  };
17592
17797
  var renderPlivoSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
17593
17798
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Plivo setup</p>
17594
- <h1>${escapeHtml23(title)}</h1>
17799
+ <h1>${escapeHtml24(title)}</h1>
17595
17800
  <p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
17596
17801
  <ul>
17597
- <li><strong>Answer XML:</strong> <code>${escapeHtml23(status.urls.answer)}</code></li>
17598
- <li><strong>Audio stream:</strong> <code>${escapeHtml23(status.urls.stream)}</code></li>
17599
- <li><strong>Status webhook:</strong> <code>${escapeHtml23(status.urls.webhook)}</code></li>
17802
+ <li><strong>Answer XML:</strong> <code>${escapeHtml24(status.urls.answer)}</code></li>
17803
+ <li><strong>Audio stream:</strong> <code>${escapeHtml24(status.urls.stream)}</code></li>
17804
+ <li><strong>Status webhook:</strong> <code>${escapeHtml24(status.urls.webhook)}</code></li>
17600
17805
  </ul>
17601
- ${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml23(name)}</code></li>`).join("")}</ul>` : ""}
17602
- ${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml23(warning)}</li>`).join("")}</ul>` : ""}
17806
+ ${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml24(name)}</code></li>`).join("")}</ul>` : ""}
17807
+ ${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml24(warning)}</li>`).join("")}</ul>` : ""}
17603
17808
  </main>`;
17604
17809
  var renderPlivoSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
17605
17810
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Plivo smoke test</p>
17606
- <h1>${escapeHtml23(title)}</h1>
17811
+ <h1>${escapeHtml24(title)}</h1>
17607
17812
  <p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
17608
- <ul>${report.checks.map((check) => `<li><strong>${escapeHtml23(check.name)}</strong>: ${escapeHtml23(check.status)}${check.message ? ` - ${escapeHtml23(check.message)}` : ""}</li>`).join("")}</ul>
17813
+ <ul>${report.checks.map((check) => `<li><strong>${escapeHtml24(check.name)}</strong>: ${escapeHtml24(check.status)}${check.message ? ` - ${escapeHtml24(check.message)}` : ""}</li>`).join("")}</ul>
17609
17814
  </main>`;
17610
17815
  var runPlivoSmokeTest = async (input) => {
17611
17816
  const setup = await buildPlivoVoiceSetupStatus(input.options, input);
@@ -17700,7 +17905,7 @@ var createPlivoVoiceRoutes = (options = {}) => {
17700
17905
  request: input.request
17701
17906
  }) : verificationUrl ?? input.request.url
17702
17907
  }) : undefined);
17703
- const app = new Elysia24({
17908
+ const app = new Elysia25({
17704
17909
  name: options.name ?? "absolutejs-voice-plivo"
17705
17910
  }).get(answerPath, async ({ query, request }) => {
17706
17911
  const streamUrl = await resolvePlivoStreamUrl(options, {
@@ -17873,6 +18078,7 @@ export {
17873
18078
  transcodeTwilioInboundPayloadToPCM16,
17874
18079
  transcodePCMToTwilioOutboundPayload,
17875
18080
  summarizeVoiceTurnQuality,
18081
+ summarizeVoiceTurnLatency,
17876
18082
  summarizeVoiceTraceTimeline,
17877
18083
  summarizeVoiceTraceSinkDeliveries,
17878
18084
  summarizeVoiceTrace,
@@ -17920,6 +18126,7 @@ export {
17920
18126
  requeueVoiceOpsTask,
17921
18127
  reopenVoiceOpsTask,
17922
18128
  renderVoiceTurnQualityHTML,
18129
+ renderVoiceTurnLatencyHTML,
17923
18130
  renderVoiceTraceTimelineSessionHTML,
17924
18131
  renderVoiceTraceTimelineHTML,
17925
18132
  renderVoiceTraceMarkdown,
@@ -17985,6 +18192,9 @@ export {
17985
18192
  createVoiceTurnQualityRoutes,
17986
18193
  createVoiceTurnQualityJSONHandler,
17987
18194
  createVoiceTurnQualityHTMLHandler,
18195
+ createVoiceTurnLatencyRoutes,
18196
+ createVoiceTurnLatencyJSONHandler,
18197
+ createVoiceTurnLatencyHTMLHandler,
17988
18198
  createVoiceTraceTimelineRoutes,
17989
18199
  createVoiceTraceSinkStore,
17990
18200
  createVoiceTraceSinkDeliveryWorkerLoop,