@absolutejs/voice 0.0.22-beta.66 → 0.0.22-beta.67
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 +133 -10
- package/dist/angular/voice-turn-quality.service.d.ts +12 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.js +196 -0
- package/dist/client/turnQuality.d.ts +19 -0
- package/dist/client/turnQualityWidget.d.ts +32 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +123 -2
- package/dist/react/VoiceTurnQuality.d.ts +6 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.js +304 -12
- package/dist/react/useVoiceTurnQuality.d.ts +8 -0
- package/dist/svelte/createVoiceTurnQuality.d.ts +10 -0
- package/dist/svelte/index.d.ts +1 -0
- package/dist/svelte/index.js +201 -0
- package/dist/turnQuality.d.ts +94 -0
- package/dist/vue/VoiceTurnQuality.d.ts +51 -0
- package/dist/vue/index.d.ts +2 -0
- package/dist/vue/index.js +291 -14
- package/dist/vue/useVoiceTurnQuality.d.ts +9 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10294,6 +10294,122 @@ var createVoiceToolContractRoutes = (options) => {
|
|
|
10294
10294
|
}
|
|
10295
10295
|
return routes;
|
|
10296
10296
|
};
|
|
10297
|
+
// src/turnQuality.ts
|
|
10298
|
+
import { Elysia as Elysia14 } from "elysia";
|
|
10299
|
+
var DEFAULT_CONFIDENCE_WARN_THRESHOLD = 0.72;
|
|
10300
|
+
var escapeHtml15 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
10301
|
+
var getTurnLatencyMs = (turn) => {
|
|
10302
|
+
const firstTranscriptAt = turn.transcripts.map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs).filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
|
|
10303
|
+
if (firstTranscriptAt === undefined) {
|
|
10304
|
+
return;
|
|
10305
|
+
}
|
|
10306
|
+
return Math.max(0, turn.committedAt - firstTranscriptAt);
|
|
10307
|
+
};
|
|
10308
|
+
var summarizeTurn = (sessionId, turn, options) => {
|
|
10309
|
+
const quality = turn.quality;
|
|
10310
|
+
const correctionChanged = quality?.correction?.changed === true;
|
|
10311
|
+
const fallbackUsed = quality?.fallbackUsed === true;
|
|
10312
|
+
const lowConfidence = typeof quality?.averageConfidence === "number" && quality.averageConfidence < options.confidenceWarnThreshold;
|
|
10313
|
+
const hasNoQuality = !quality;
|
|
10314
|
+
const status = hasNoQuality ? "unknown" : quality.selectedTranscriptCount === 0 || turn.text.trim().length === 0 ? "fail" : fallbackUsed || correctionChanged || lowConfidence ? "warn" : "pass";
|
|
10315
|
+
return {
|
|
10316
|
+
averageConfidence: quality?.averageConfidence,
|
|
10317
|
+
committedAt: turn.committedAt,
|
|
10318
|
+
correctionChanged,
|
|
10319
|
+
correctionProvider: quality?.correction?.provider,
|
|
10320
|
+
correctionReason: quality?.correction?.reason,
|
|
10321
|
+
costUnits: quality?.cost?.estimatedRelativeCostUnits,
|
|
10322
|
+
fallbackSelectionReason: quality?.fallback?.selectionReason,
|
|
10323
|
+
fallbackUsed,
|
|
10324
|
+
finalTranscriptCount: quality?.finalTranscriptCount ?? 0,
|
|
10325
|
+
latencyMs: getTurnLatencyMs(turn),
|
|
10326
|
+
partialTranscriptCount: quality?.partialTranscriptCount ?? 0,
|
|
10327
|
+
selectedTranscriptCount: quality?.selectedTranscriptCount ?? 0,
|
|
10328
|
+
sessionId,
|
|
10329
|
+
source: quality?.source,
|
|
10330
|
+
status,
|
|
10331
|
+
text: turn.text,
|
|
10332
|
+
turnId: turn.id
|
|
10333
|
+
};
|
|
10334
|
+
};
|
|
10335
|
+
var resolveSessions = async (options) => {
|
|
10336
|
+
if (options.sessions) {
|
|
10337
|
+
return options.sessions;
|
|
10338
|
+
}
|
|
10339
|
+
if (!options.store) {
|
|
10340
|
+
return [];
|
|
10341
|
+
}
|
|
10342
|
+
const summaries = await options.store.list();
|
|
10343
|
+
const ids = options.sessionIds ?? summaries.map((summary) => summary.id);
|
|
10344
|
+
const hydrated = await Promise.all(ids.slice(0, options.limit ?? 25).map((id) => options.store?.get(id)));
|
|
10345
|
+
const sessions = [];
|
|
10346
|
+
for (const session of hydrated) {
|
|
10347
|
+
if (session) {
|
|
10348
|
+
sessions.push(session);
|
|
10349
|
+
}
|
|
10350
|
+
}
|
|
10351
|
+
return sessions;
|
|
10352
|
+
};
|
|
10353
|
+
var summarizeVoiceTurnQuality = async (options) => {
|
|
10354
|
+
const sessions = await resolveSessions(options);
|
|
10355
|
+
const confidenceWarnThreshold = options.confidenceWarnThreshold ?? DEFAULT_CONFIDENCE_WARN_THRESHOLD;
|
|
10356
|
+
const turns = sessions.flatMap((session) => session.turns.map((turn) => summarizeTurn(session.id, turn, { confidenceWarnThreshold }))).sort((left, right) => right.committedAt - left.committedAt);
|
|
10357
|
+
const failed = turns.filter((turn) => turn.status === "fail").length;
|
|
10358
|
+
const warnings = turns.filter((turn) => turn.status === "warn").length;
|
|
10359
|
+
return {
|
|
10360
|
+
checkedAt: Date.now(),
|
|
10361
|
+
failed,
|
|
10362
|
+
sessions: sessions.length,
|
|
10363
|
+
status: failed > 0 ? "fail" : warnings > 0 ? "warn" : "pass",
|
|
10364
|
+
total: turns.length,
|
|
10365
|
+
turns,
|
|
10366
|
+
warnings
|
|
10367
|
+
};
|
|
10368
|
+
};
|
|
10369
|
+
var renderVoiceTurnQualityHTML = (report, options = {}) => {
|
|
10370
|
+
const title = options.title ?? "Voice Turn Quality";
|
|
10371
|
+
const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml15(turn.status)}">
|
|
10372
|
+
<div class="turn-header">
|
|
10373
|
+
<div>
|
|
10374
|
+
<p class="eyebrow">${escapeHtml15(turn.sessionId)} \xB7 ${escapeHtml15(turn.turnId)}</p>
|
|
10375
|
+
<h2>${escapeHtml15(turn.text || "Empty turn")}</h2>
|
|
10376
|
+
</div>
|
|
10377
|
+
<strong>${escapeHtml15(turn.status)}</strong>
|
|
10378
|
+
</div>
|
|
10379
|
+
<dl>
|
|
10380
|
+
<div><dt>Source</dt><dd>${escapeHtml15(turn.source ?? "unknown")}</dd></div>
|
|
10381
|
+
<div><dt>Confidence</dt><dd>${turn.averageConfidence === undefined ? "n/a" : `${Math.round(turn.averageConfidence * 100)}%`}</dd></div>
|
|
10382
|
+
<div><dt>Fallback</dt><dd>${turn.fallbackUsed ? `yes (${escapeHtml15(turn.fallbackSelectionReason ?? "selected")})` : "no"}</dd></div>
|
|
10383
|
+
<div><dt>Correction</dt><dd>${turn.correctionChanged ? `changed${turn.correctionProvider ? ` by ${escapeHtml15(turn.correctionProvider)}` : ""}` : "none"}</dd></div>
|
|
10384
|
+
<div><dt>Transcripts</dt><dd>${String(turn.selectedTranscriptCount)} selected \xB7 ${String(turn.finalTranscriptCount)} final \xB7 ${String(turn.partialTranscriptCount)} partial</dd></div>
|
|
10385
|
+
<div><dt>Cost</dt><dd>${turn.costUnits === undefined ? "n/a" : String(turn.costUnits)}</dd></div>
|
|
10386
|
+
</dl>
|
|
10387
|
+
</article>`).join("");
|
|
10388
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml15(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>${escapeHtml15(title)}</h1><div class="summary"><span class="pill ${escapeHtml15(report.status)}">${escapeHtml15(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>`;
|
|
10389
|
+
};
|
|
10390
|
+
var createVoiceTurnQualityJSONHandler = (options) => async () => summarizeVoiceTurnQuality(options);
|
|
10391
|
+
var createVoiceTurnQualityHTMLHandler = (options) => async () => {
|
|
10392
|
+
const report = await summarizeVoiceTurnQuality(options);
|
|
10393
|
+
const render = options.render ?? ((input) => renderVoiceTurnQualityHTML(input, options));
|
|
10394
|
+
const body = await render(report);
|
|
10395
|
+
return new Response(body, {
|
|
10396
|
+
headers: {
|
|
10397
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
10398
|
+
...options.headers
|
|
10399
|
+
}
|
|
10400
|
+
});
|
|
10401
|
+
};
|
|
10402
|
+
var createVoiceTurnQualityRoutes = (options) => {
|
|
10403
|
+
const path = options.path ?? "/api/turn-quality";
|
|
10404
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
10405
|
+
const routes = new Elysia14({
|
|
10406
|
+
name: options.name ?? "absolutejs-voice-turn-quality"
|
|
10407
|
+
}).get(path, createVoiceTurnQualityJSONHandler(options));
|
|
10408
|
+
if (htmlPath) {
|
|
10409
|
+
routes.get(htmlPath, createVoiceTurnQualityHTMLHandler(options));
|
|
10410
|
+
}
|
|
10411
|
+
return routes;
|
|
10412
|
+
};
|
|
10297
10413
|
// src/fileStore.ts
|
|
10298
10414
|
import { mkdir as mkdir2, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
10299
10415
|
import { join } from "path";
|
|
@@ -12387,7 +12503,7 @@ var createVoiceMemoryStore = () => {
|
|
|
12387
12503
|
return { get, getOrCreate, list, remove, set };
|
|
12388
12504
|
};
|
|
12389
12505
|
// src/opsWebhook.ts
|
|
12390
|
-
import { Elysia as
|
|
12506
|
+
import { Elysia as Elysia15 } from "elysia";
|
|
12391
12507
|
var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
12392
12508
|
var signVoiceOpsWebhookBody = async (input) => {
|
|
12393
12509
|
const encoder = new TextEncoder;
|
|
@@ -12517,7 +12633,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
|
|
|
12517
12633
|
};
|
|
12518
12634
|
var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
|
|
12519
12635
|
const path = options.path ?? "/api/voice-ops/webhook";
|
|
12520
|
-
return new
|
|
12636
|
+
return new Elysia15().post(path, async ({ body, request, set }) => {
|
|
12521
12637
|
const bodyText = typeof body === "string" ? body : JSON.stringify(body);
|
|
12522
12638
|
if (options.signingSecret) {
|
|
12523
12639
|
const verification = await verifyVoiceOpsWebhookSignature({
|
|
@@ -14664,6 +14780,7 @@ export {
|
|
|
14664
14780
|
validateVoiceWorkflowRouteResult,
|
|
14665
14781
|
transcodeTwilioInboundPayloadToPCM16,
|
|
14666
14782
|
transcodePCMToTwilioOutboundPayload,
|
|
14783
|
+
summarizeVoiceTurnQuality,
|
|
14667
14784
|
summarizeVoiceTraceSinkDeliveries,
|
|
14668
14785
|
summarizeVoiceTrace,
|
|
14669
14786
|
summarizeVoiceSessions,
|
|
@@ -14703,6 +14820,7 @@ export {
|
|
|
14703
14820
|
resolveAudioConditioningConfig,
|
|
14704
14821
|
requeueVoiceOpsTask,
|
|
14705
14822
|
reopenVoiceOpsTask,
|
|
14823
|
+
renderVoiceTurnQualityHTML,
|
|
14706
14824
|
renderVoiceTraceMarkdown,
|
|
14707
14825
|
renderVoiceTraceHTML,
|
|
14708
14826
|
renderVoiceToolContractHTML,
|
|
@@ -14757,6 +14875,9 @@ export {
|
|
|
14757
14875
|
createVoiceWebhookDeliveryWorkerLoop,
|
|
14758
14876
|
createVoiceWebhookDeliveryWorker,
|
|
14759
14877
|
createVoiceTwilioRedirectHandoffAdapter,
|
|
14878
|
+
createVoiceTurnQualityRoutes,
|
|
14879
|
+
createVoiceTurnQualityJSONHandler,
|
|
14880
|
+
createVoiceTurnQualityHTMLHandler,
|
|
14760
14881
|
createVoiceTraceSinkStore,
|
|
14761
14882
|
createVoiceTraceSinkDeliveryWorkerLoop,
|
|
14762
14883
|
createVoiceTraceSinkDeliveryWorker,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type VoiceTurnQualityWidgetOptions } from '../client/turnQualityWidget';
|
|
2
|
+
export type VoiceTurnQualityProps = VoiceTurnQualityWidgetOptions & {
|
|
3
|
+
className?: string;
|
|
4
|
+
path?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare const VoiceTurnQuality: ({ className, path, ...options }: VoiceTurnQualityProps) => import("react/jsx-runtime").JSX.Element;
|
package/dist/react/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { VoiceProviderSimulationControls } from './VoiceProviderSimulationContro
|
|
|
3
3
|
export { VoiceProviderCapabilities } from './VoiceProviderCapabilities';
|
|
4
4
|
export { VoiceProviderStatus } from './VoiceProviderStatus';
|
|
5
5
|
export { VoiceRoutingStatus } from './VoiceRoutingStatus';
|
|
6
|
+
export { VoiceTurnQuality } from './VoiceTurnQuality';
|
|
6
7
|
export { useVoiceAppKitStatus } from './useVoiceAppKitStatus';
|
|
7
8
|
export { useVoiceStream } from './useVoiceStream';
|
|
8
9
|
export { useVoiceController } from './useVoiceController';
|
|
@@ -10,4 +11,5 @@ export { useVoiceProviderStatus } from './useVoiceProviderStatus';
|
|
|
10
11
|
export { useVoiceProviderCapabilities } from './useVoiceProviderCapabilities';
|
|
11
12
|
export { useVoiceProviderSimulationControls } from './useVoiceProviderSimulationControls';
|
|
12
13
|
export { useVoiceRoutingStatus } from './useVoiceRoutingStatus';
|
|
14
|
+
export { useVoiceTurnQuality } from './useVoiceTurnQuality';
|
|
13
15
|
export { useVoiceWorkflowStatus } from './useVoiceWorkflowStatus';
|
package/dist/react/index.js
CHANGED
|
@@ -1495,9 +1495,299 @@ var VoiceRoutingStatus = ({
|
|
|
1495
1495
|
]
|
|
1496
1496
|
}, undefined, true, undefined, this);
|
|
1497
1497
|
};
|
|
1498
|
-
// src/react/
|
|
1498
|
+
// src/react/useVoiceTurnQuality.tsx
|
|
1499
1499
|
import { useEffect as useEffect6, useRef as useRef6, useSyncExternalStore as useSyncExternalStore6 } from "react";
|
|
1500
1500
|
|
|
1501
|
+
// src/client/turnQuality.ts
|
|
1502
|
+
var fetchVoiceTurnQuality = async (path = "/api/turn-quality", options = {}) => {
|
|
1503
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1504
|
+
const response = await fetchImpl(path);
|
|
1505
|
+
if (!response.ok) {
|
|
1506
|
+
throw new Error(`Voice turn quality failed: HTTP ${response.status}`);
|
|
1507
|
+
}
|
|
1508
|
+
return await response.json();
|
|
1509
|
+
};
|
|
1510
|
+
var createVoiceTurnQualityStore = (path = "/api/turn-quality", options = {}) => {
|
|
1511
|
+
const listeners = new Set;
|
|
1512
|
+
let closed = false;
|
|
1513
|
+
let timer;
|
|
1514
|
+
let snapshot = {
|
|
1515
|
+
error: null,
|
|
1516
|
+
isLoading: false
|
|
1517
|
+
};
|
|
1518
|
+
const emit = () => {
|
|
1519
|
+
for (const listener of listeners) {
|
|
1520
|
+
listener();
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
const refresh = async () => {
|
|
1524
|
+
if (closed) {
|
|
1525
|
+
return snapshot.report;
|
|
1526
|
+
}
|
|
1527
|
+
snapshot = {
|
|
1528
|
+
...snapshot,
|
|
1529
|
+
error: null,
|
|
1530
|
+
isLoading: true
|
|
1531
|
+
};
|
|
1532
|
+
emit();
|
|
1533
|
+
try {
|
|
1534
|
+
const report = await fetchVoiceTurnQuality(path, options);
|
|
1535
|
+
snapshot = {
|
|
1536
|
+
error: null,
|
|
1537
|
+
isLoading: false,
|
|
1538
|
+
report,
|
|
1539
|
+
updatedAt: Date.now()
|
|
1540
|
+
};
|
|
1541
|
+
emit();
|
|
1542
|
+
return report;
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
snapshot = {
|
|
1545
|
+
...snapshot,
|
|
1546
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1547
|
+
isLoading: false
|
|
1548
|
+
};
|
|
1549
|
+
emit();
|
|
1550
|
+
throw error;
|
|
1551
|
+
}
|
|
1552
|
+
};
|
|
1553
|
+
const close = () => {
|
|
1554
|
+
closed = true;
|
|
1555
|
+
if (timer) {
|
|
1556
|
+
clearInterval(timer);
|
|
1557
|
+
timer = undefined;
|
|
1558
|
+
}
|
|
1559
|
+
listeners.clear();
|
|
1560
|
+
};
|
|
1561
|
+
if (options.intervalMs && options.intervalMs > 0) {
|
|
1562
|
+
timer = setInterval(() => {
|
|
1563
|
+
refresh().catch(() => {});
|
|
1564
|
+
}, options.intervalMs);
|
|
1565
|
+
}
|
|
1566
|
+
return {
|
|
1567
|
+
close,
|
|
1568
|
+
getServerSnapshot: () => snapshot,
|
|
1569
|
+
getSnapshot: () => snapshot,
|
|
1570
|
+
refresh,
|
|
1571
|
+
subscribe: (listener) => {
|
|
1572
|
+
listeners.add(listener);
|
|
1573
|
+
return () => {
|
|
1574
|
+
listeners.delete(listener);
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
};
|
|
1579
|
+
|
|
1580
|
+
// src/react/useVoiceTurnQuality.tsx
|
|
1581
|
+
var useVoiceTurnQuality = (path = "/api/turn-quality", options = {}) => {
|
|
1582
|
+
const storeRef = useRef6(null);
|
|
1583
|
+
if (!storeRef.current) {
|
|
1584
|
+
storeRef.current = createVoiceTurnQualityStore(path, options);
|
|
1585
|
+
}
|
|
1586
|
+
const store = storeRef.current;
|
|
1587
|
+
useEffect6(() => {
|
|
1588
|
+
store.refresh().catch(() => {});
|
|
1589
|
+
return () => store.close();
|
|
1590
|
+
}, [store]);
|
|
1591
|
+
return {
|
|
1592
|
+
...useSyncExternalStore6(store.subscribe, store.getSnapshot, store.getServerSnapshot),
|
|
1593
|
+
refresh: store.refresh
|
|
1594
|
+
};
|
|
1595
|
+
};
|
|
1596
|
+
|
|
1597
|
+
// src/client/turnQualityWidget.ts
|
|
1598
|
+
var DEFAULT_TITLE5 = "Turn Quality";
|
|
1599
|
+
var DEFAULT_DESCRIPTION5 = "Per-turn STT confidence, fallback selection, corrections, and transcript coverage.";
|
|
1600
|
+
var escapeHtml6 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1601
|
+
var formatConfidence = (value) => typeof value === "number" ? `${Math.round(value * 100)}%` : "n/a";
|
|
1602
|
+
var formatMaybe = (value) => value === undefined || value === "" ? "n/a" : String(value);
|
|
1603
|
+
var getTurnDetail = (turn) => {
|
|
1604
|
+
if (turn.status === "fail") {
|
|
1605
|
+
return "Empty or unusable committed turn; inspect transcripts and adapter events.";
|
|
1606
|
+
}
|
|
1607
|
+
if (turn.fallbackUsed) {
|
|
1608
|
+
return `Fallback STT selected${turn.fallbackSelectionReason ? ` by ${turn.fallbackSelectionReason}` : ""}.`;
|
|
1609
|
+
}
|
|
1610
|
+
if (turn.correctionChanged) {
|
|
1611
|
+
return `Correction changed the turn${turn.correctionProvider ? ` via ${turn.correctionProvider}` : ""}.`;
|
|
1612
|
+
}
|
|
1613
|
+
if (turn.status === "warn") {
|
|
1614
|
+
return "Turn completed with quality warnings.";
|
|
1615
|
+
}
|
|
1616
|
+
if (turn.status === "unknown") {
|
|
1617
|
+
return "No quality diagnostics were recorded for this turn.";
|
|
1618
|
+
}
|
|
1619
|
+
return "Turn quality looks healthy.";
|
|
1620
|
+
};
|
|
1621
|
+
var createVoiceTurnQualityViewModel = (snapshot, options = {}) => {
|
|
1622
|
+
const turns = (snapshot.report?.turns ?? []).map((turn) => ({
|
|
1623
|
+
...turn,
|
|
1624
|
+
detail: getTurnDetail(turn),
|
|
1625
|
+
label: turn.text || "Empty turn",
|
|
1626
|
+
rows: [
|
|
1627
|
+
{ label: "Source", value: turn.source ?? "unknown" },
|
|
1628
|
+
{ label: "Confidence", value: formatConfidence(turn.averageConfidence) },
|
|
1629
|
+
{ label: "Fallback", value: turn.fallbackUsed ? "Yes" : "No" },
|
|
1630
|
+
{ label: "Correction", value: turn.correctionChanged ? "Changed" : "None" },
|
|
1631
|
+
{ label: "Transcripts", value: `${turn.selectedTranscriptCount} selected` },
|
|
1632
|
+
{ label: "Cost", value: formatMaybe(turn.costUnits) }
|
|
1633
|
+
]
|
|
1634
|
+
}));
|
|
1635
|
+
const warningCount = snapshot.report?.warnings ?? turns.filter((turn) => turn.status === "warn").length;
|
|
1636
|
+
const failedCount = snapshot.report?.failed ?? turns.filter((turn) => turn.status === "fail").length;
|
|
1637
|
+
return {
|
|
1638
|
+
description: options.description ?? DEFAULT_DESCRIPTION5,
|
|
1639
|
+
error: snapshot.error,
|
|
1640
|
+
isLoading: snapshot.isLoading,
|
|
1641
|
+
label: snapshot.error ? "Unavailable" : turns.length ? failedCount > 0 ? `${failedCount} failed` : warningCount > 0 ? `${warningCount} warnings` : `${turns.length} healthy` : snapshot.isLoading ? "Checking" : "No turns",
|
|
1642
|
+
status: snapshot.error ? "error" : turns.length ? failedCount > 0 || warningCount > 0 ? "warning" : "ready" : snapshot.isLoading ? "loading" : "empty",
|
|
1643
|
+
title: options.title ?? DEFAULT_TITLE5,
|
|
1644
|
+
turns,
|
|
1645
|
+
updatedAt: snapshot.updatedAt
|
|
1646
|
+
};
|
|
1647
|
+
};
|
|
1648
|
+
var renderVoiceTurnQualityHTML = (snapshot, options = {}) => {
|
|
1649
|
+
const model = createVoiceTurnQualityViewModel(snapshot, options);
|
|
1650
|
+
const turns = model.turns.length ? `<div class="absolute-voice-turn-quality__turns">${model.turns.map((turn) => `<article class="absolute-voice-turn-quality__turn absolute-voice-turn-quality__turn--${escapeHtml6(turn.status)}">
|
|
1651
|
+
<header>
|
|
1652
|
+
<strong>${escapeHtml6(turn.label)}</strong>
|
|
1653
|
+
<span>${escapeHtml6(turn.status)}</span>
|
|
1654
|
+
</header>
|
|
1655
|
+
<p>${escapeHtml6(turn.detail)}</p>
|
|
1656
|
+
<dl>${turn.rows.map((row) => `<div>
|
|
1657
|
+
<dt>${escapeHtml6(row.label)}</dt>
|
|
1658
|
+
<dd>${escapeHtml6(row.value)}</dd>
|
|
1659
|
+
</div>`).join("")}</dl>
|
|
1660
|
+
</article>`).join("")}</div>` : '<p class="absolute-voice-turn-quality__empty">Complete a voice turn to see STT quality diagnostics.</p>';
|
|
1661
|
+
return `<section class="absolute-voice-turn-quality absolute-voice-turn-quality--${escapeHtml6(model.status)}">
|
|
1662
|
+
<header class="absolute-voice-turn-quality__header">
|
|
1663
|
+
<span class="absolute-voice-turn-quality__eyebrow">${escapeHtml6(model.title)}</span>
|
|
1664
|
+
<strong class="absolute-voice-turn-quality__label">${escapeHtml6(model.label)}</strong>
|
|
1665
|
+
</header>
|
|
1666
|
+
<p class="absolute-voice-turn-quality__description">${escapeHtml6(model.description)}</p>
|
|
1667
|
+
${turns}
|
|
1668
|
+
${model.error ? `<p class="absolute-voice-turn-quality__error">${escapeHtml6(model.error)}</p>` : ""}
|
|
1669
|
+
</section>`;
|
|
1670
|
+
};
|
|
1671
|
+
var getVoiceTurnQualityCSS = () => `.absolute-voice-turn-quality{border:1px solid #e4d1a3;border-radius:20px;background:#fff9eb;color:#17120a;padding:18px;box-shadow:0 18px 40px rgba(73,48,14,.12);font-family:inherit}.absolute-voice-turn-quality--error,.absolute-voice-turn-quality--warning{border-color:#f2a7a7;background:#fff5f3}.absolute-voice-turn-quality__header,.absolute-voice-turn-quality__turn header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-turn-quality__eyebrow{color:#8a5a0a;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-turn-quality__label{font-size:24px;line-height:1}.absolute-voice-turn-quality__description,.absolute-voice-turn-quality__turn p,.absolute-voice-turn-quality__turn dt,.absolute-voice-turn-quality__empty{color:#5a4930}.absolute-voice-turn-quality__turns{display:grid;gap:12px;margin-top:14px}.absolute-voice-turn-quality__turn{background:#fff;border:1px solid #f0dfba;border-radius:16px;padding:14px}.absolute-voice-turn-quality__turn--pass{border-color:#86efac}.absolute-voice-turn-quality__turn--warn,.absolute-voice-turn-quality__turn--unknown{border-color:#fbbf24}.absolute-voice-turn-quality__turn--fail{border-color:#f2a7a7}.absolute-voice-turn-quality__turn p{margin:10px 0}.absolute-voice-turn-quality__turn dl{display:grid;gap:8px;grid-template-columns:repeat(2,minmax(0,1fr));margin:0}.absolute-voice-turn-quality__turn div{background:#fff9eb;border:1px solid #f0dfba;border-radius:12px;padding:8px}.absolute-voice-turn-quality__turn dt{font-size:12px}.absolute-voice-turn-quality__turn dd{font-weight:800;margin:4px 0 0}.absolute-voice-turn-quality__empty{margin:14px 0 0}.absolute-voice-turn-quality__error{color:#9f1239;font-weight:700}`;
|
|
1672
|
+
var mountVoiceTurnQuality = (element, path = "/api/turn-quality", options = {}) => {
|
|
1673
|
+
const store = createVoiceTurnQualityStore(path, options);
|
|
1674
|
+
const render = () => {
|
|
1675
|
+
element.innerHTML = renderVoiceTurnQualityHTML(store.getSnapshot(), options);
|
|
1676
|
+
};
|
|
1677
|
+
const unsubscribe = store.subscribe(render);
|
|
1678
|
+
render();
|
|
1679
|
+
store.refresh().catch(() => {});
|
|
1680
|
+
return {
|
|
1681
|
+
close: () => {
|
|
1682
|
+
unsubscribe();
|
|
1683
|
+
store.close();
|
|
1684
|
+
},
|
|
1685
|
+
refresh: store.refresh
|
|
1686
|
+
};
|
|
1687
|
+
};
|
|
1688
|
+
var defineVoiceTurnQualityElement = (tagName = "absolute-voice-turn-quality") => {
|
|
1689
|
+
if (typeof window === "undefined" || typeof customElements === "undefined" || customElements.get(tagName)) {
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
customElements.define(tagName, class AbsoluteVoiceTurnQualityElement extends HTMLElement {
|
|
1693
|
+
mounted;
|
|
1694
|
+
connectedCallback() {
|
|
1695
|
+
const intervalMs = Number(this.getAttribute("interval-ms") ?? 5000);
|
|
1696
|
+
this.mounted = mountVoiceTurnQuality(this, this.getAttribute("path") ?? "/api/turn-quality", {
|
|
1697
|
+
description: this.getAttribute("description") ?? undefined,
|
|
1698
|
+
intervalMs: Number.isFinite(intervalMs) ? intervalMs : 5000,
|
|
1699
|
+
title: this.getAttribute("title") ?? undefined
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
disconnectedCallback() {
|
|
1703
|
+
this.mounted?.close();
|
|
1704
|
+
this.mounted = undefined;
|
|
1705
|
+
}
|
|
1706
|
+
});
|
|
1707
|
+
};
|
|
1708
|
+
|
|
1709
|
+
// src/react/VoiceTurnQuality.tsx
|
|
1710
|
+
import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
|
|
1711
|
+
var VoiceTurnQuality = ({
|
|
1712
|
+
className,
|
|
1713
|
+
path = "/api/turn-quality",
|
|
1714
|
+
...options
|
|
1715
|
+
}) => {
|
|
1716
|
+
const snapshot = useVoiceTurnQuality(path, options);
|
|
1717
|
+
const model = createVoiceTurnQualityViewModel(snapshot, options);
|
|
1718
|
+
return /* @__PURE__ */ jsxDEV6("section", {
|
|
1719
|
+
className: [
|
|
1720
|
+
"absolute-voice-turn-quality",
|
|
1721
|
+
`absolute-voice-turn-quality--${model.status}`,
|
|
1722
|
+
className
|
|
1723
|
+
].filter(Boolean).join(" "),
|
|
1724
|
+
children: [
|
|
1725
|
+
/* @__PURE__ */ jsxDEV6("header", {
|
|
1726
|
+
className: "absolute-voice-turn-quality__header",
|
|
1727
|
+
children: [
|
|
1728
|
+
/* @__PURE__ */ jsxDEV6("span", {
|
|
1729
|
+
className: "absolute-voice-turn-quality__eyebrow",
|
|
1730
|
+
children: model.title
|
|
1731
|
+
}, undefined, false, undefined, this),
|
|
1732
|
+
/* @__PURE__ */ jsxDEV6("strong", {
|
|
1733
|
+
className: "absolute-voice-turn-quality__label",
|
|
1734
|
+
children: model.label
|
|
1735
|
+
}, undefined, false, undefined, this)
|
|
1736
|
+
]
|
|
1737
|
+
}, undefined, true, undefined, this),
|
|
1738
|
+
/* @__PURE__ */ jsxDEV6("p", {
|
|
1739
|
+
className: "absolute-voice-turn-quality__description",
|
|
1740
|
+
children: model.description
|
|
1741
|
+
}, undefined, false, undefined, this),
|
|
1742
|
+
model.turns.length ? /* @__PURE__ */ jsxDEV6("div", {
|
|
1743
|
+
className: "absolute-voice-turn-quality__turns",
|
|
1744
|
+
children: model.turns.map((turn) => /* @__PURE__ */ jsxDEV6("article", {
|
|
1745
|
+
className: [
|
|
1746
|
+
"absolute-voice-turn-quality__turn",
|
|
1747
|
+
`absolute-voice-turn-quality__turn--${turn.status}`
|
|
1748
|
+
].join(" "),
|
|
1749
|
+
children: [
|
|
1750
|
+
/* @__PURE__ */ jsxDEV6("header", {
|
|
1751
|
+
children: [
|
|
1752
|
+
/* @__PURE__ */ jsxDEV6("strong", {
|
|
1753
|
+
children: turn.label
|
|
1754
|
+
}, undefined, false, undefined, this),
|
|
1755
|
+
/* @__PURE__ */ jsxDEV6("span", {
|
|
1756
|
+
children: turn.status
|
|
1757
|
+
}, undefined, false, undefined, this)
|
|
1758
|
+
]
|
|
1759
|
+
}, undefined, true, undefined, this),
|
|
1760
|
+
/* @__PURE__ */ jsxDEV6("p", {
|
|
1761
|
+
children: turn.detail
|
|
1762
|
+
}, undefined, false, undefined, this),
|
|
1763
|
+
/* @__PURE__ */ jsxDEV6("dl", {
|
|
1764
|
+
children: turn.rows.map((row) => /* @__PURE__ */ jsxDEV6("div", {
|
|
1765
|
+
children: [
|
|
1766
|
+
/* @__PURE__ */ jsxDEV6("dt", {
|
|
1767
|
+
children: row.label
|
|
1768
|
+
}, undefined, false, undefined, this),
|
|
1769
|
+
/* @__PURE__ */ jsxDEV6("dd", {
|
|
1770
|
+
children: row.value
|
|
1771
|
+
}, undefined, false, undefined, this)
|
|
1772
|
+
]
|
|
1773
|
+
}, row.label, true, undefined, this))
|
|
1774
|
+
}, undefined, false, undefined, this)
|
|
1775
|
+
]
|
|
1776
|
+
}, `${turn.sessionId}:${turn.turnId}`, true, undefined, this))
|
|
1777
|
+
}, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV6("p", {
|
|
1778
|
+
className: "absolute-voice-turn-quality__empty",
|
|
1779
|
+
children: "Complete a voice turn to see STT quality diagnostics."
|
|
1780
|
+
}, undefined, false, undefined, this),
|
|
1781
|
+
model.error ? /* @__PURE__ */ jsxDEV6("p", {
|
|
1782
|
+
className: "absolute-voice-turn-quality__error",
|
|
1783
|
+
children: model.error
|
|
1784
|
+
}, undefined, false, undefined, this) : null
|
|
1785
|
+
]
|
|
1786
|
+
}, undefined, true, undefined, this);
|
|
1787
|
+
};
|
|
1788
|
+
// src/react/useVoiceStream.tsx
|
|
1789
|
+
import { useEffect as useEffect7, useRef as useRef7, useSyncExternalStore as useSyncExternalStore7 } from "react";
|
|
1790
|
+
|
|
1501
1791
|
// src/client/actions.ts
|
|
1502
1792
|
var normalizeErrorMessage = (value) => {
|
|
1503
1793
|
if (typeof value === "string" && value.trim()) {
|
|
@@ -2024,13 +2314,13 @@ var EMPTY_SNAPSHOT = {
|
|
|
2024
2314
|
turns: []
|
|
2025
2315
|
};
|
|
2026
2316
|
var useVoiceStream = (path, options = {}) => {
|
|
2027
|
-
const streamRef =
|
|
2317
|
+
const streamRef = useRef7(null);
|
|
2028
2318
|
if (!streamRef.current) {
|
|
2029
2319
|
streamRef.current = createVoiceStream(path, options);
|
|
2030
2320
|
}
|
|
2031
2321
|
const stream = streamRef.current;
|
|
2032
|
-
|
|
2033
|
-
const snapshot =
|
|
2322
|
+
useEffect7(() => () => stream.close(), [stream]);
|
|
2323
|
+
const snapshot = useSyncExternalStore7(stream.subscribe, stream.getSnapshot, stream.getServerSnapshot) ?? EMPTY_SNAPSHOT;
|
|
2034
2324
|
return {
|
|
2035
2325
|
...snapshot,
|
|
2036
2326
|
callControl: (message) => stream.callControl(message),
|
|
@@ -2040,7 +2330,7 @@ var useVoiceStream = (path, options = {}) => {
|
|
|
2040
2330
|
};
|
|
2041
2331
|
};
|
|
2042
2332
|
// src/react/useVoiceController.tsx
|
|
2043
|
-
import { useEffect as
|
|
2333
|
+
import { useEffect as useEffect8, useRef as useRef8, useSyncExternalStore as useSyncExternalStore8 } from "react";
|
|
2044
2334
|
|
|
2045
2335
|
// src/client/htmx.ts
|
|
2046
2336
|
var DEFAULT_EVENT_NAME = "voice-refresh";
|
|
@@ -2687,13 +2977,13 @@ var EMPTY_SNAPSHOT2 = {
|
|
|
2687
2977
|
turns: []
|
|
2688
2978
|
};
|
|
2689
2979
|
var useVoiceController = (path, options = {}) => {
|
|
2690
|
-
const controllerRef =
|
|
2980
|
+
const controllerRef = useRef8(null);
|
|
2691
2981
|
if (!controllerRef.current) {
|
|
2692
2982
|
controllerRef.current = createVoiceController(path, options);
|
|
2693
2983
|
}
|
|
2694
2984
|
const controller = controllerRef.current;
|
|
2695
|
-
|
|
2696
|
-
const snapshot =
|
|
2985
|
+
useEffect8(() => () => controller.close(), [controller]);
|
|
2986
|
+
const snapshot = useSyncExternalStore8(controller.subscribe, controller.getSnapshot, controller.getServerSnapshot) ?? EMPTY_SNAPSHOT2;
|
|
2697
2987
|
return {
|
|
2698
2988
|
...snapshot,
|
|
2699
2989
|
bindHTMX: controller.bindHTMX,
|
|
@@ -2707,7 +2997,7 @@ var useVoiceController = (path, options = {}) => {
|
|
|
2707
2997
|
};
|
|
2708
2998
|
};
|
|
2709
2999
|
// src/react/useVoiceWorkflowStatus.tsx
|
|
2710
|
-
import { useEffect as
|
|
3000
|
+
import { useEffect as useEffect9, useRef as useRef9, useSyncExternalStore as useSyncExternalStore9 } from "react";
|
|
2711
3001
|
|
|
2712
3002
|
// src/client/workflowStatus.ts
|
|
2713
3003
|
var fetchVoiceWorkflowStatus = async (path = "/evals/scenarios/json", options = {}) => {
|
|
@@ -2790,22 +3080,23 @@ var createVoiceWorkflowStatusStore = (path = "/evals/scenarios/json", options =
|
|
|
2790
3080
|
|
|
2791
3081
|
// src/react/useVoiceWorkflowStatus.tsx
|
|
2792
3082
|
var useVoiceWorkflowStatus = (path = "/evals/scenarios/json", options = {}) => {
|
|
2793
|
-
const storeRef =
|
|
3083
|
+
const storeRef = useRef9(null);
|
|
2794
3084
|
if (!storeRef.current) {
|
|
2795
3085
|
storeRef.current = createVoiceWorkflowStatusStore(path, options);
|
|
2796
3086
|
}
|
|
2797
3087
|
const store = storeRef.current;
|
|
2798
|
-
|
|
3088
|
+
useEffect9(() => {
|
|
2799
3089
|
store.refresh().catch(() => {});
|
|
2800
3090
|
return () => store.close();
|
|
2801
3091
|
}, [store]);
|
|
2802
3092
|
return {
|
|
2803
|
-
...
|
|
3093
|
+
...useSyncExternalStore9(store.subscribe, store.getSnapshot, store.getServerSnapshot),
|
|
2804
3094
|
refresh: store.refresh
|
|
2805
3095
|
};
|
|
2806
3096
|
};
|
|
2807
3097
|
export {
|
|
2808
3098
|
useVoiceWorkflowStatus,
|
|
3099
|
+
useVoiceTurnQuality,
|
|
2809
3100
|
useVoiceStream,
|
|
2810
3101
|
useVoiceRoutingStatus,
|
|
2811
3102
|
useVoiceProviderStatus,
|
|
@@ -2813,6 +3104,7 @@ export {
|
|
|
2813
3104
|
useVoiceProviderCapabilities,
|
|
2814
3105
|
useVoiceController,
|
|
2815
3106
|
useVoiceAppKitStatus,
|
|
3107
|
+
VoiceTurnQuality,
|
|
2816
3108
|
VoiceRoutingStatus,
|
|
2817
3109
|
VoiceProviderStatus,
|
|
2818
3110
|
VoiceProviderSimulationControls,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type VoiceTurnQualityClientOptions } from '../client/turnQuality';
|
|
2
|
+
export declare const useVoiceTurnQuality: (path?: string, options?: VoiceTurnQualityClientOptions) => {
|
|
3
|
+
refresh: () => Promise<import("..").VoiceTurnQualityReport | undefined>;
|
|
4
|
+
error: string | null;
|
|
5
|
+
isLoading: boolean;
|
|
6
|
+
report?: import("..").VoiceTurnQualityReport;
|
|
7
|
+
updatedAt?: number;
|
|
8
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type VoiceTurnQualityWidgetOptions } from '../client/turnQualityWidget';
|
|
2
|
+
export declare const createVoiceTurnQuality: (path?: string, options?: VoiceTurnQualityWidgetOptions) => {
|
|
3
|
+
getHTML: () => string;
|
|
4
|
+
getViewModel: () => import("../client").VoiceTurnQualityViewModel;
|
|
5
|
+
close: () => void;
|
|
6
|
+
getServerSnapshot: () => import("../client").VoiceTurnQualitySnapshot;
|
|
7
|
+
getSnapshot: () => import("../client").VoiceTurnQualitySnapshot;
|
|
8
|
+
refresh: () => Promise<import("..").VoiceTurnQualityReport | undefined>;
|
|
9
|
+
subscribe: (listener: () => void) => () => void;
|
|
10
|
+
};
|
package/dist/svelte/index.d.ts
CHANGED
|
@@ -5,5 +5,6 @@ export { createVoiceProviderCapabilities } from './createVoiceProviderCapabiliti
|
|
|
5
5
|
export { createVoiceStream } from './createVoiceStream';
|
|
6
6
|
export { createVoiceProviderStatus } from './createVoiceProviderStatus';
|
|
7
7
|
export { createVoiceRoutingStatus } from './createVoiceRoutingStatus';
|
|
8
|
+
export { createVoiceTurnQuality } from './createVoiceTurnQuality';
|
|
8
9
|
export { createVoiceWorkflowStatus } from './createVoiceWorkflowStatus';
|
|
9
10
|
export { createVoiceController } from '../client/controller';
|