@absolutejs/voice 0.0.22-beta.98 → 0.0.22-beta.99

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.
@@ -6,6 +6,7 @@ export { bindVoiceBargeIn, createVoiceDuplexController } from './duplex';
6
6
  export { bindVoiceHTMX } from './htmx';
7
7
  export { createMicrophoneCapture } from './microphone';
8
8
  export { createVoiceBargeInMonitor } from './bargeInMonitor';
9
+ export { createVoiceLiveTurnLatencyMonitor } from './liveTurnLatency';
9
10
  export { createVoiceAppKitStatusStore, fetchVoiceAppKitStatus } from './appKitStatus';
10
11
  export { createVoiceOpsStatusViewModel, defineVoiceOpsStatusElement, getVoiceOpsStatusCSS, getVoiceOpsStatusLabel, mountVoiceOpsStatus, renderVoiceOpsStatusHTML } from './opsStatusWidget';
11
12
  export { createVoiceRoutingStatusStore, fetchVoiceRoutingStatus } from './routingStatus';
@@ -25,6 +26,7 @@ export { createVoiceTraceTimelineViewModel, defineVoiceTraceTimelineElement, get
25
26
  export { createVoiceWorkflowStatusStore, fetchVoiceWorkflowStatus } from './workflowStatus';
26
27
  export type { VoiceAppKitStatusClientOptions, VoiceAppKitStatusSnapshot } from './appKitStatus';
27
28
  export type { VoiceBargeInMonitorOptions } from './bargeInMonitor';
29
+ export type { VoiceLiveTurnLatencyEvent, VoiceLiveTurnLatencyMonitorOptions, VoiceLiveTurnLatencySnapshot, VoiceLiveTurnLatencyStatus } from './liveTurnLatency';
28
30
  export type { VoiceOpsStatusSurfaceView, VoiceOpsStatusViewModel, VoiceOpsStatusWidgetOptions } from './opsStatusWidget';
29
31
  export type { VoiceRoutingStatusClientOptions, VoiceRoutingStatusSnapshot } from './routingStatus';
30
32
  export type { VoiceRoutingStatusViewModel, VoiceRoutingStatusWidgetOptions } from './routingStatusWidget';
@@ -1724,6 +1724,116 @@ var createVoiceBargeInMonitor = (options = {}) => {
1724
1724
  }
1725
1725
  };
1726
1726
  };
1727
+ // src/client/liveTurnLatency.ts
1728
+ var getAudioLevel = (audio) => {
1729
+ const bytes = audio instanceof Uint8Array ? audio : new Uint8Array(audio);
1730
+ if (bytes.byteLength < 2) {
1731
+ return 0;
1732
+ }
1733
+ const samples = new Int16Array(bytes.buffer, bytes.byteOffset, Math.floor(bytes.byteLength / 2));
1734
+ if (samples.length === 0) {
1735
+ return 0;
1736
+ }
1737
+ let sumSquares = 0;
1738
+ for (const sample of samples) {
1739
+ const normalized = sample / 32768;
1740
+ sumSquares += normalized * normalized;
1741
+ }
1742
+ return Math.min(1, Math.max(0, Math.sqrt(sumSquares / samples.length) * 5.5));
1743
+ };
1744
+ var createVoiceLiveTurnLatencyMonitor = (options = {}) => {
1745
+ const listeners = new Set;
1746
+ const clock = options.clock ?? (() => Date.now());
1747
+ const failAfterMs = options.failAfterMs ?? 3200;
1748
+ const maxEvents = options.maxEvents ?? 20;
1749
+ const speechThreshold = options.speechThreshold ?? 0.04;
1750
+ const warnAfterMs = options.warnAfterMs ?? 1800;
1751
+ let events = [];
1752
+ let pending;
1753
+ let lastAudioCount = 0;
1754
+ let lastTextCount = 0;
1755
+ let lastSessionId;
1756
+ const emit = () => {
1757
+ for (const listener of listeners) {
1758
+ listener();
1759
+ }
1760
+ };
1761
+ const completePending = (input) => {
1762
+ if (!pending) {
1763
+ return;
1764
+ }
1765
+ const completedAt = input.assistantAudioAt ?? input.assistantTextAt ?? clock();
1766
+ const latencyMs = Math.max(0, completedAt - pending.startedAt);
1767
+ const status = latencyMs > failAfterMs ? "fail" : latencyMs > warnAfterMs ? "warn" : "pass";
1768
+ pending = {
1769
+ ...pending,
1770
+ ...input,
1771
+ completedAt,
1772
+ latencyMs,
1773
+ status
1774
+ };
1775
+ events = [pending, ...events].slice(0, maxEvents);
1776
+ pending = undefined;
1777
+ emit();
1778
+ };
1779
+ const observe = (state) => {
1780
+ const now = clock();
1781
+ if (pending) {
1782
+ if (state.assistantAudio.length > lastAudioCount) {
1783
+ completePending({ assistantAudioAt: now });
1784
+ } else if (state.assistantTexts.length > lastTextCount) {
1785
+ completePending({ assistantTextAt: now });
1786
+ }
1787
+ }
1788
+ lastAudioCount = state.assistantAudio.length;
1789
+ lastTextCount = state.assistantTexts.length;
1790
+ lastSessionId = state.sessionId;
1791
+ };
1792
+ const recordAudio = (audio) => {
1793
+ if (pending || getAudioLevel(audio) < speechThreshold) {
1794
+ return pending;
1795
+ }
1796
+ pending = {
1797
+ id: `live-turn-${crypto.randomUUID()}`,
1798
+ sessionId: lastSessionId ?? null,
1799
+ startedAt: clock(),
1800
+ status: "pending",
1801
+ thresholdMs: failAfterMs
1802
+ };
1803
+ emit();
1804
+ return pending;
1805
+ };
1806
+ const getSnapshot = () => {
1807
+ const completed = events.filter((event) => typeof event.latencyMs === "number");
1808
+ const latencies = completed.map((event) => event.latencyMs);
1809
+ const failed = events.filter((event) => event.status === "fail").length;
1810
+ const warnings = events.filter((event) => event.status === "warn").length;
1811
+ const passed = events.filter((event) => event.status === "pass").length;
1812
+ return {
1813
+ averageLatencyMs: latencies.length ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
1814
+ events,
1815
+ failed,
1816
+ lastEvent: events[0],
1817
+ passed,
1818
+ pending,
1819
+ status: pending ? "pending" : events.length === 0 ? "empty" : failed > 0 ? "fail" : warnings > 0 ? "warn" : "pass",
1820
+ thresholdMs: failAfterMs,
1821
+ total: events.length,
1822
+ warnings
1823
+ };
1824
+ };
1825
+ return {
1826
+ getSnapshot,
1827
+ observe,
1828
+ recordAudio,
1829
+ subscribe: (listener) => {
1830
+ listeners.add(listener);
1831
+ return () => {
1832
+ listeners.delete(listener);
1833
+ };
1834
+ }
1835
+ };
1836
+ };
1727
1837
  // src/client/appKitStatus.ts
1728
1838
  var fetchVoiceAppKitStatus = async (path = "/app-kit/status", options = {}) => {
1729
1839
  const fetchImpl = options.fetch ?? globalThis.fetch;
@@ -3352,6 +3462,7 @@ export {
3352
3462
  createVoiceProviderCapabilitiesViewModel,
3353
3463
  createVoiceProviderCapabilitiesStore,
3354
3464
  createVoiceOpsStatusViewModel,
3465
+ createVoiceLiveTurnLatencyMonitor,
3355
3466
  createVoiceDuplexController,
3356
3467
  createVoiceController,
3357
3468
  createVoiceConnection,
@@ -0,0 +1,38 @@
1
+ import type { VoiceStreamState } from '../types';
2
+ export type VoiceLiveTurnLatencyStatus = 'empty' | 'pending' | 'pass' | 'warn' | 'fail';
3
+ export type VoiceLiveTurnLatencyEvent = {
4
+ assistantAudioAt?: number;
5
+ assistantTextAt?: number;
6
+ completedAt?: number;
7
+ id: string;
8
+ latencyMs?: number;
9
+ sessionId?: string | null;
10
+ startedAt: number;
11
+ status: Exclude<VoiceLiveTurnLatencyStatus, 'empty'>;
12
+ thresholdMs: number;
13
+ };
14
+ export type VoiceLiveTurnLatencySnapshot = {
15
+ averageLatencyMs?: number;
16
+ events: VoiceLiveTurnLatencyEvent[];
17
+ failed: number;
18
+ lastEvent?: VoiceLiveTurnLatencyEvent;
19
+ passed: number;
20
+ pending?: VoiceLiveTurnLatencyEvent;
21
+ status: VoiceLiveTurnLatencyStatus;
22
+ thresholdMs: number;
23
+ total: number;
24
+ warnings: number;
25
+ };
26
+ export type VoiceLiveTurnLatencyMonitorOptions = {
27
+ clock?: () => number;
28
+ failAfterMs?: number;
29
+ maxEvents?: number;
30
+ speechThreshold?: number;
31
+ warnAfterMs?: number;
32
+ };
33
+ export declare const createVoiceLiveTurnLatencyMonitor: (options?: VoiceLiveTurnLatencyMonitorOptions) => {
34
+ getSnapshot: () => VoiceLiveTurnLatencySnapshot;
35
+ observe: <TResult = unknown>(state: VoiceStreamState<TResult>) => void;
36
+ recordAudio: (audio: Uint8Array | ArrayBuffer) => VoiceLiveTurnLatencyEvent | undefined;
37
+ subscribe: (listener: () => void) => () => void;
38
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.98",
3
+ "version": "0.0.22-beta.99",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",