@absolutejs/voice 0.0.22-beta.93 → 0.0.22-beta.95

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.
@@ -1757,6 +1757,176 @@ var createVoiceTraceTimeline = (path = "/api/voice-traces", options = {}) => {
1757
1757
  getViewModel: () => createVoiceTraceTimelineViewModel(store.getSnapshot(), options)
1758
1758
  };
1759
1759
  };
1760
+ // src/client/turnLatency.ts
1761
+ var fetchVoiceTurnLatency = async (path = "/api/turn-latency", options = {}) => {
1762
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1763
+ const response = await fetchImpl(path);
1764
+ if (!response.ok) {
1765
+ throw new Error(`Voice turn latency failed: HTTP ${response.status}`);
1766
+ }
1767
+ return await response.json();
1768
+ };
1769
+ var createVoiceTurnLatencyStore = (path = "/api/turn-latency", options = {}) => {
1770
+ const listeners = new Set;
1771
+ let closed = false;
1772
+ let timer;
1773
+ let snapshot = {
1774
+ error: null,
1775
+ isLoading: false
1776
+ };
1777
+ const emit = () => {
1778
+ for (const listener of listeners) {
1779
+ listener();
1780
+ }
1781
+ };
1782
+ const refresh = async () => {
1783
+ if (closed) {
1784
+ return snapshot.report;
1785
+ }
1786
+ snapshot = { ...snapshot, error: null, isLoading: true };
1787
+ emit();
1788
+ try {
1789
+ const report = await fetchVoiceTurnLatency(path, options);
1790
+ snapshot = {
1791
+ error: null,
1792
+ isLoading: false,
1793
+ report,
1794
+ updatedAt: Date.now()
1795
+ };
1796
+ emit();
1797
+ return report;
1798
+ } catch (error) {
1799
+ snapshot = {
1800
+ ...snapshot,
1801
+ error: error instanceof Error ? error.message : String(error),
1802
+ isLoading: false
1803
+ };
1804
+ emit();
1805
+ throw error;
1806
+ }
1807
+ };
1808
+ const close = () => {
1809
+ closed = true;
1810
+ if (timer) {
1811
+ clearInterval(timer);
1812
+ timer = undefined;
1813
+ }
1814
+ listeners.clear();
1815
+ };
1816
+ if (options.intervalMs && options.intervalMs > 0) {
1817
+ timer = setInterval(() => {
1818
+ refresh().catch(() => {});
1819
+ }, options.intervalMs);
1820
+ }
1821
+ return {
1822
+ close,
1823
+ getServerSnapshot: () => snapshot,
1824
+ getSnapshot: () => snapshot,
1825
+ refresh,
1826
+ subscribe: (listener) => {
1827
+ listeners.add(listener);
1828
+ return () => {
1829
+ listeners.delete(listener);
1830
+ };
1831
+ }
1832
+ };
1833
+ };
1834
+
1835
+ // src/client/turnLatencyWidget.ts
1836
+ var DEFAULT_TITLE6 = "Turn Latency";
1837
+ var DEFAULT_DESCRIPTION6 = "Per-turn timing from first transcript to commit and assistant response start.";
1838
+ var escapeHtml7 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
1839
+ var formatMs2 = (value) => typeof value === "number" ? `${Math.round(value)}ms` : "n/a";
1840
+ var createVoiceTurnLatencyViewModel = (snapshot, options = {}) => {
1841
+ const turns = (snapshot.report?.turns ?? []).map((turn) => ({
1842
+ ...turn,
1843
+ label: turn.text || "Empty turn",
1844
+ rows: turn.stages.map((stage) => ({
1845
+ label: stage.label,
1846
+ value: formatMs2(stage.valueMs)
1847
+ }))
1848
+ }));
1849
+ const warningCount = snapshot.report?.warnings ?? turns.filter((turn) => turn.status === "warn").length;
1850
+ const failedCount = snapshot.report?.failed ?? turns.filter((turn) => turn.status === "fail").length;
1851
+ return {
1852
+ description: options.description ?? DEFAULT_DESCRIPTION6,
1853
+ error: snapshot.error,
1854
+ isLoading: snapshot.isLoading,
1855
+ label: snapshot.error ? "Unavailable" : turns.length ? failedCount > 0 ? `${failedCount} slow` : warningCount > 0 ? `${warningCount} warnings` : `avg ${formatMs2(snapshot.report?.averageTotalMs)}` : snapshot.isLoading ? "Checking" : "No turns",
1856
+ status: snapshot.error ? "error" : turns.length ? failedCount > 0 || warningCount > 0 ? "warning" : "ready" : snapshot.isLoading ? "loading" : "empty",
1857
+ title: options.title ?? DEFAULT_TITLE6,
1858
+ turns,
1859
+ updatedAt: snapshot.updatedAt
1860
+ };
1861
+ };
1862
+ var renderVoiceTurnLatencyHTML = (snapshot, options = {}) => {
1863
+ const model = createVoiceTurnLatencyViewModel(snapshot, options);
1864
+ const turns = model.turns.length ? `<div class="absolute-voice-turn-latency__turns">${model.turns.map((turn) => `<article class="absolute-voice-turn-latency__turn absolute-voice-turn-latency__turn--${escapeHtml7(turn.status)}">
1865
+ <header>
1866
+ <strong>${escapeHtml7(turn.label)}</strong>
1867
+ <span>${escapeHtml7(turn.status)}</span>
1868
+ </header>
1869
+ <dl>${turn.rows.map((row) => `<div>
1870
+ <dt>${escapeHtml7(row.label)}</dt>
1871
+ <dd>${escapeHtml7(row.value)}</dd>
1872
+ </div>`).join("")}</dl>
1873
+ </article>`).join("")}</div>` : '<p class="absolute-voice-turn-latency__empty">Complete a voice turn to see latency diagnostics.</p>';
1874
+ return `<section class="absolute-voice-turn-latency absolute-voice-turn-latency--${escapeHtml7(model.status)}">
1875
+ <header class="absolute-voice-turn-latency__header">
1876
+ <span class="absolute-voice-turn-latency__eyebrow">${escapeHtml7(model.title)}</span>
1877
+ <strong class="absolute-voice-turn-latency__label">${escapeHtml7(model.label)}</strong>
1878
+ </header>
1879
+ <p class="absolute-voice-turn-latency__description">${escapeHtml7(model.description)}</p>
1880
+ ${turns}
1881
+ ${model.error ? `<p class="absolute-voice-turn-latency__error">${escapeHtml7(model.error)}</p>` : ""}
1882
+ </section>`;
1883
+ };
1884
+ var mountVoiceTurnLatency = (element, path = "/api/turn-latency", options = {}) => {
1885
+ const store = createVoiceTurnLatencyStore(path, options);
1886
+ const render = () => {
1887
+ element.innerHTML = renderVoiceTurnLatencyHTML(store.getSnapshot(), options);
1888
+ };
1889
+ const unsubscribe = store.subscribe(render);
1890
+ render();
1891
+ store.refresh().catch(() => {});
1892
+ return {
1893
+ close: () => {
1894
+ unsubscribe();
1895
+ store.close();
1896
+ },
1897
+ refresh: store.refresh
1898
+ };
1899
+ };
1900
+ var defineVoiceTurnLatencyElement = (tagName = "absolute-voice-turn-latency") => {
1901
+ if (typeof window === "undefined" || typeof customElements === "undefined" || customElements.get(tagName)) {
1902
+ return;
1903
+ }
1904
+ customElements.define(tagName, class AbsoluteVoiceTurnLatencyElement extends HTMLElement {
1905
+ mounted;
1906
+ connectedCallback() {
1907
+ const intervalMs = Number(this.getAttribute("interval-ms") ?? 5000);
1908
+ this.mounted = mountVoiceTurnLatency(this, this.getAttribute("path") ?? "/api/turn-latency", {
1909
+ description: this.getAttribute("description") ?? undefined,
1910
+ intervalMs: Number.isFinite(intervalMs) ? intervalMs : 5000,
1911
+ title: this.getAttribute("title") ?? undefined
1912
+ });
1913
+ }
1914
+ disconnectedCallback() {
1915
+ this.mounted?.close();
1916
+ this.mounted = undefined;
1917
+ }
1918
+ });
1919
+ };
1920
+
1921
+ // src/svelte/createVoiceTurnLatency.ts
1922
+ var createVoiceTurnLatency = (path = "/api/turn-latency", options = {}) => {
1923
+ const store = createVoiceTurnLatencyStore(path, options);
1924
+ return {
1925
+ ...store,
1926
+ getHTML: () => renderVoiceTurnLatencyHTML(store.getSnapshot(), options),
1927
+ getViewModel: () => createVoiceTurnLatencyViewModel(store.getSnapshot(), options)
1928
+ };
1929
+ };
1760
1930
  // src/client/turnQuality.ts
1761
1931
  var fetchVoiceTurnQuality = async (path = "/api/turn-quality", options = {}) => {
1762
1932
  const fetchImpl = options.fetch ?? globalThis.fetch;
@@ -1837,9 +2007,9 @@ var createVoiceTurnQualityStore = (path = "/api/turn-quality", options = {}) =>
1837
2007
  };
1838
2008
 
1839
2009
  // src/client/turnQualityWidget.ts
1840
- var DEFAULT_TITLE6 = "Turn Quality";
1841
- var DEFAULT_DESCRIPTION6 = "Per-turn STT confidence, fallback selection, corrections, and transcript coverage.";
1842
- var escapeHtml7 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
2010
+ var DEFAULT_TITLE7 = "Turn Quality";
2011
+ var DEFAULT_DESCRIPTION7 = "Per-turn STT confidence, fallback selection, corrections, and transcript coverage.";
2012
+ var escapeHtml8 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
1843
2013
  var formatConfidence = (value) => typeof value === "number" ? `${Math.round(value * 100)}%` : "n/a";
1844
2014
  var formatMaybe = (value) => value === undefined || value === "" ? "n/a" : String(value);
1845
2015
  var getTurnDetail = (turn) => {
@@ -1877,37 +2047,37 @@ var createVoiceTurnQualityViewModel = (snapshot, options = {}) => {
1877
2047
  const warningCount = snapshot.report?.warnings ?? turns.filter((turn) => turn.status === "warn").length;
1878
2048
  const failedCount = snapshot.report?.failed ?? turns.filter((turn) => turn.status === "fail").length;
1879
2049
  return {
1880
- description: options.description ?? DEFAULT_DESCRIPTION6,
2050
+ description: options.description ?? DEFAULT_DESCRIPTION7,
1881
2051
  error: snapshot.error,
1882
2052
  isLoading: snapshot.isLoading,
1883
2053
  label: snapshot.error ? "Unavailable" : turns.length ? failedCount > 0 ? `${failedCount} failed` : warningCount > 0 ? `${warningCount} warnings` : `${turns.length} healthy` : snapshot.isLoading ? "Checking" : "No turns",
1884
2054
  status: snapshot.error ? "error" : turns.length ? failedCount > 0 || warningCount > 0 ? "warning" : "ready" : snapshot.isLoading ? "loading" : "empty",
1885
- title: options.title ?? DEFAULT_TITLE6,
2055
+ title: options.title ?? DEFAULT_TITLE7,
1886
2056
  turns,
1887
2057
  updatedAt: snapshot.updatedAt
1888
2058
  };
1889
2059
  };
1890
2060
  var renderVoiceTurnQualityHTML = (snapshot, options = {}) => {
1891
2061
  const model = createVoiceTurnQualityViewModel(snapshot, options);
1892
- 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--${escapeHtml7(turn.status)}">
2062
+ 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--${escapeHtml8(turn.status)}">
1893
2063
  <header>
1894
- <strong>${escapeHtml7(turn.label)}</strong>
1895
- <span>${escapeHtml7(turn.status)}</span>
2064
+ <strong>${escapeHtml8(turn.label)}</strong>
2065
+ <span>${escapeHtml8(turn.status)}</span>
1896
2066
  </header>
1897
- <p>${escapeHtml7(turn.detail)}</p>
2067
+ <p>${escapeHtml8(turn.detail)}</p>
1898
2068
  <dl>${turn.rows.map((row) => `<div>
1899
- <dt>${escapeHtml7(row.label)}</dt>
1900
- <dd>${escapeHtml7(row.value)}</dd>
2069
+ <dt>${escapeHtml8(row.label)}</dt>
2070
+ <dd>${escapeHtml8(row.value)}</dd>
1901
2071
  </div>`).join("")}</dl>
1902
2072
  </article>`).join("")}</div>` : '<p class="absolute-voice-turn-quality__empty">Complete a voice turn to see STT quality diagnostics.</p>';
1903
- return `<section class="absolute-voice-turn-quality absolute-voice-turn-quality--${escapeHtml7(model.status)}">
2073
+ return `<section class="absolute-voice-turn-quality absolute-voice-turn-quality--${escapeHtml8(model.status)}">
1904
2074
  <header class="absolute-voice-turn-quality__header">
1905
- <span class="absolute-voice-turn-quality__eyebrow">${escapeHtml7(model.title)}</span>
1906
- <strong class="absolute-voice-turn-quality__label">${escapeHtml7(model.label)}</strong>
2075
+ <span class="absolute-voice-turn-quality__eyebrow">${escapeHtml8(model.title)}</span>
2076
+ <strong class="absolute-voice-turn-quality__label">${escapeHtml8(model.label)}</strong>
1907
2077
  </header>
1908
- <p class="absolute-voice-turn-quality__description">${escapeHtml7(model.description)}</p>
2078
+ <p class="absolute-voice-turn-quality__description">${escapeHtml8(model.description)}</p>
1909
2079
  ${turns}
1910
- ${model.error ? `<p class="absolute-voice-turn-quality__error">${escapeHtml7(model.error)}</p>` : ""}
2080
+ ${model.error ? `<p class="absolute-voice-turn-quality__error">${escapeHtml8(model.error)}</p>` : ""}
1911
2081
  </section>`;
1912
2082
  };
1913
2083
  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}`;
@@ -2676,6 +2846,7 @@ var createVoiceController = (path, options = {}) => {
2676
2846
  export {
2677
2847
  createVoiceWorkflowStatus,
2678
2848
  createVoiceTurnQuality,
2849
+ createVoiceTurnLatency,
2679
2850
  createVoiceTraceTimeline,
2680
2851
  createVoiceStream2 as createVoiceStream,
2681
2852
  createVoiceRoutingStatus,
@@ -0,0 +1,93 @@
1
+ import { Elysia } from 'elysia';
2
+ import type { VoiceSessionRecord, VoiceSessionStore } from './types';
3
+ export type VoiceTurnLatencyStatus = 'pass' | 'warn' | 'fail' | 'empty';
4
+ export type VoiceTurnLatencyStage = {
5
+ label: string;
6
+ valueMs?: number;
7
+ };
8
+ export type VoiceTurnLatencyItem = {
9
+ assistantTextStartedAt?: number;
10
+ committedAt: number;
11
+ finalTranscriptAt?: number;
12
+ firstTranscriptAt?: number;
13
+ sessionId: string;
14
+ stages: VoiceTurnLatencyStage[];
15
+ status: VoiceTurnLatencyStatus;
16
+ text: string;
17
+ totalMs?: number;
18
+ turnId: string;
19
+ };
20
+ export type VoiceTurnLatencyReport = {
21
+ averageTotalMs?: number;
22
+ checkedAt: number;
23
+ failed: number;
24
+ sessions: number;
25
+ status: VoiceTurnLatencyStatus;
26
+ total: number;
27
+ turns: VoiceTurnLatencyItem[];
28
+ warnings: number;
29
+ };
30
+ export type VoiceTurnLatencyOptions<TSession extends VoiceSessionRecord = VoiceSessionRecord> = {
31
+ limit?: number;
32
+ sessionIds?: string[];
33
+ sessions?: TSession[];
34
+ store?: VoiceSessionStore<TSession>;
35
+ warnAfterMs?: number;
36
+ failAfterMs?: number;
37
+ };
38
+ export type VoiceTurnLatencyHTMLHandlerOptions<TSession extends VoiceSessionRecord = VoiceSessionRecord> = VoiceTurnLatencyOptions<TSession> & {
39
+ headers?: HeadersInit;
40
+ render?: (report: VoiceTurnLatencyReport) => string | Promise<string>;
41
+ title?: string;
42
+ };
43
+ export type VoiceTurnLatencyRoutesOptions<TSession extends VoiceSessionRecord = VoiceSessionRecord> = VoiceTurnLatencyHTMLHandlerOptions<TSession> & {
44
+ htmlPath?: false | string;
45
+ name?: string;
46
+ path?: string;
47
+ };
48
+ export declare const summarizeVoiceTurnLatency: <TSession extends VoiceSessionRecord = VoiceSessionRecord>(options: VoiceTurnLatencyOptions<TSession>) => Promise<VoiceTurnLatencyReport>;
49
+ export declare const renderVoiceTurnLatencyHTML: (report: VoiceTurnLatencyReport, options?: {
50
+ title?: string;
51
+ }) => string;
52
+ export declare const createVoiceTurnLatencyJSONHandler: <TSession extends VoiceSessionRecord = VoiceSessionRecord>(options: VoiceTurnLatencyOptions<TSession>) => () => Promise<VoiceTurnLatencyReport>;
53
+ export declare const createVoiceTurnLatencyHTMLHandler: <TSession extends VoiceSessionRecord = VoiceSessionRecord>(options: VoiceTurnLatencyHTMLHandlerOptions<TSession>) => () => Promise<Response>;
54
+ export declare const createVoiceTurnLatencyRoutes: <TSession extends VoiceSessionRecord = VoiceSessionRecord>(options: VoiceTurnLatencyRoutesOptions<TSession>) => Elysia<"", {
55
+ decorator: {};
56
+ store: {};
57
+ derive: {};
58
+ resolve: {};
59
+ }, {
60
+ typebox: {};
61
+ error: {};
62
+ }, {
63
+ schema: {};
64
+ standaloneSchema: {};
65
+ macro: {};
66
+ macroFn: {};
67
+ parser: {};
68
+ response: {};
69
+ }, {
70
+ [x: string]: {
71
+ get: {
72
+ body: unknown;
73
+ params: {};
74
+ query: unknown;
75
+ headers: unknown;
76
+ response: {
77
+ 200: VoiceTurnLatencyReport;
78
+ };
79
+ };
80
+ };
81
+ }, {
82
+ derive: {};
83
+ resolve: {};
84
+ schema: {};
85
+ standaloneSchema: {};
86
+ response: {};
87
+ }, {
88
+ derive: {};
89
+ resolve: {};
90
+ schema: {};
91
+ standaloneSchema: {};
92
+ response: {};
93
+ }>;
@@ -0,0 +1,51 @@
1
+ export declare const VoiceTurnLatency: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
2
+ class: {
3
+ default: string;
4
+ type: StringConstructor;
5
+ };
6
+ description: {
7
+ default: undefined;
8
+ type: StringConstructor;
9
+ };
10
+ intervalMs: {
11
+ default: number;
12
+ type: NumberConstructor;
13
+ };
14
+ path: {
15
+ default: string;
16
+ type: StringConstructor;
17
+ };
18
+ title: {
19
+ default: undefined;
20
+ type: StringConstructor;
21
+ };
22
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
23
+ [key: string]: any;
24
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
25
+ class: {
26
+ default: string;
27
+ type: StringConstructor;
28
+ };
29
+ description: {
30
+ default: undefined;
31
+ type: StringConstructor;
32
+ };
33
+ intervalMs: {
34
+ default: number;
35
+ type: NumberConstructor;
36
+ };
37
+ path: {
38
+ default: string;
39
+ type: StringConstructor;
40
+ };
41
+ title: {
42
+ default: undefined;
43
+ type: StringConstructor;
44
+ };
45
+ }>> & Readonly<{}>, {
46
+ description: string;
47
+ title: string;
48
+ path: string;
49
+ intervalMs: number;
50
+ class: string;
51
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -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 { VoiceTurnLatency } from './VoiceTurnLatency';
6
7
  export { VoiceTurnQuality } from './VoiceTurnQuality';
7
8
  export { useVoiceAppKitStatus } from './useVoiceAppKitStatus';
8
9
  export { useVoiceStream } from './useVoiceStream';
@@ -12,5 +13,6 @@ export { useVoiceProviderCapabilities } from './useVoiceProviderCapabilities';
12
13
  export { useVoiceProviderSimulationControls } from './useVoiceProviderSimulationControls';
13
14
  export { useVoiceRoutingStatus } from './useVoiceRoutingStatus';
14
15
  export { useVoiceTraceTimeline } from './useVoiceTraceTimeline';
16
+ export { useVoiceTurnLatency } from './useVoiceTurnLatency';
15
17
  export { useVoiceTurnQuality } from './useVoiceTurnQuality';
16
18
  export { useVoiceWorkflowStatus } from './useVoiceWorkflowStatus';