@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/angular/index.d.ts +1 -0
- package/dist/angular/index.js +139 -20
- package/dist/angular/voice-turn-latency.service.d.ts +12 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.js +182 -17
- package/dist/client/turnLatency.d.ts +19 -0
- package/dist/client/turnLatencyWidget.d.ts +30 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +278 -68
- package/dist/react/VoiceTurnLatency.d.ts +6 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.js +309 -50
- package/dist/react/useVoiceTurnLatency.d.ts +8 -0
- package/dist/svelte/createVoiceTurnLatency.d.ts +10 -0
- package/dist/svelte/index.d.ts +1 -0
- package/dist/svelte/index.js +187 -16
- package/dist/testing/index.js +57 -1
- package/dist/trace.d.ts +1 -1
- package/dist/turnLatency.d.ts +95 -0
- package/dist/vue/VoiceTurnLatency.d.ts +51 -0
- package/dist/vue/index.d.ts +2 -0
- package/dist/vue/index.js +303 -57
- package/dist/vue/useVoiceTurnLatency.d.ts +9 -0
- package/package.json +1 -1
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/
|
|
11229
|
+
// src/turnLatency.ts
|
|
11172
11230
|
import { Elysia as Elysia18 } from "elysia";
|
|
11173
|
-
var
|
|
11231
|
+
var DEFAULT_WARN_AFTER_MS = 1800;
|
|
11232
|
+
var DEFAULT_FAIL_AFTER_MS = 3200;
|
|
11174
11233
|
var escapeHtml19 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
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
|
|
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
|
|
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
|
|
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) =>
|
|
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 ${
|
|
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">${
|
|
11249
|
-
<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>${
|
|
11456
|
+
<strong>${escapeHtml20(turn.status)}</strong>
|
|
11252
11457
|
</div>
|
|
11253
11458
|
<dl>
|
|
11254
|
-
<div><dt>Source</dt><dd>${
|
|
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 (${
|
|
11257
|
-
<div><dt>Correction</dt><dd>${turn.correctionChanged ? `changed${turn.correctionProvider ? ` by ${
|
|
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>${
|
|
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
|
|
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
|
|
11289
|
-
var
|
|
11493
|
+
import { Elysia as Elysia20 } from "elysia";
|
|
11494
|
+
var escapeHtml21 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
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">${
|
|
11398
|
-
<h2>${
|
|
11399
|
-
${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>${
|
|
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>${
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
@@ -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
|
|
16446
|
+
var escapeHtml22 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
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>${
|
|
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>${
|
|
16290
|
-
<li><strong>Media stream:</strong> <code>${
|
|
16291
|
-
<li><strong>Status webhook:</strong> <code>${
|
|
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>${
|
|
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>${
|
|
16300
|
-
${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${
|
|
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("&", "&");
|
|
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>${
|
|
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>${
|
|
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>${
|
|
16323
|
-
<li><strong>Stream:</strong> <code>${
|
|
16324
|
-
<li><strong>Webhook:</strong> <code>${
|
|
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
|
|
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
|
|
17125
|
+
import { Elysia as Elysia24 } from "elysia";
|
|
16921
17126
|
var escapeXml3 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
16922
|
-
var
|
|
17127
|
+
var escapeHtml23 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
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>${
|
|
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>${
|
|
17127
|
-
<li><strong>Media stream:</strong> <code>${
|
|
17128
|
-
<li><strong>Status webhook:</strong> <code>${
|
|
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>${
|
|
17131
|
-
${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${
|
|
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>${
|
|
17340
|
+
<h1>${escapeHtml23(title)}</h1>
|
|
17136
17341
|
<p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
|
|
17137
|
-
<ul>${report.checks.map((check) => `<li><strong>${
|
|
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
|
|
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
|
|
17546
|
+
import { Elysia as Elysia25 } from "elysia";
|
|
17342
17547
|
var escapeXml4 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
17343
|
-
var
|
|
17548
|
+
var escapeHtml24 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
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>${
|
|
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>${
|
|
17598
|
-
<li><strong>Audio stream:</strong> <code>${
|
|
17599
|
-
<li><strong>Status webhook:</strong> <code>${
|
|
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>${
|
|
17602
|
-
${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${
|
|
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>${
|
|
17811
|
+
<h1>${escapeHtml24(title)}</h1>
|
|
17607
17812
|
<p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
|
|
17608
|
-
<ul>${report.checks.map((check) => `<li><strong>${
|
|
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
|
|
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,
|