ttp-agent-sdk 2.48.7 → 2.48.9

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.
@@ -526,6 +526,12 @@
526
526
  }
527
527
  };
528
528
 
529
+ // A/B test hook (local testing): ?hop=0 disables the MediaStream→<audio> playback hop
530
+ if (new URLSearchParams(window.location.search).get('hop') === '0') {
531
+ config.htmlAudioPlayback = false;
532
+ console.log('🧪 TEST MODE: htmlAudioPlayback DISABLED — playing directly to AudioContext.destination');
533
+ }
534
+
529
535
  // Add optional configs
530
536
  if (getSessionUrl) {
531
537
  config.getSessionUrl = getSessionUrl;
@@ -0,0 +1,166 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Audio Wobble Diagnostics</title>
6
+ <style>
7
+ body { font-family: system-ui, sans-serif; max-width: 900px; margin: 24px auto; padding: 0 16px; background: #0f172a; color: #e2e8f0; }
8
+ h1 { font-size: 20px; } h2 { font-size: 16px; margin-top: 28px; }
9
+ .card { background: #1e293b; border-radius: 10px; padding: 16px; margin: 12px 0; }
10
+ button { background: #3b82f6; color: #fff; border: 0; border-radius: 8px; padding: 10px 18px; font-size: 14px; cursor: pointer; margin-right: 8px; }
11
+ button:hover { background: #2563eb; }
12
+ audio { width: 100%; margin-top: 8px; }
13
+ #log { background: #020617; border-radius: 8px; padding: 12px; font-family: ui-monospace, monospace; font-size: 12px; height: 320px; overflow-y: auto; white-space: pre-wrap; }
14
+ .verdict { font-weight: 700; }
15
+ .ok { color: #4ade80; } .bad { color: #f87171; } .info { color: #93c5fd; }
16
+ </style>
17
+ </head>
18
+ <body>
19
+ <h1>Audio Wobble Diagnostics</h1>
20
+ <p>This page plays a <b>known-clean</b> greeting recording (captured straight from the server) three ways.
21
+ The greeting is 2.75s of speech at 1.4x voice speed — that speed is intentional.</p>
22
+
23
+ <div class="card">
24
+ <h2>Test A — plain &lt;audio&gt; element (no Web Audio, no mic)</h2>
25
+ <p>Play this. If it wobbles, the machine's audio output path itself is the problem.</p>
26
+ <audio controls src="greeting-capture.wav"></audio>
27
+ </div>
28
+
29
+ <div class="card">
30
+ <h2>Test A3 — OCADO greeting (Cartesia path, captured from server)</h2>
31
+ <p>"Hello! How can I help you today?" — the exact bytes the server sends for the Ocado agent. Does this sound clean?</p>
32
+ <audio controls src="ocado-greeting.wav"></audio>
33
+ <p>Ocado response to "Hi, how are you?" — 3 sentences:</p>
34
+ <p>"Hey," (0.6s):</p>
35
+ <audio controls src="ocado-segment-3.wav"></audio>
36
+ <p>"doing well thanks for asking!" (1.7s):</p>
37
+ <audio controls src="ocado-segment-2.wav"></audio>
38
+ <p>"What can I help you shop for today?" (1.7s):</p>
39
+ <audio controls src="ocado-segment-4.wav"></audio>
40
+ </div>
41
+
42
+ <div class="card">
43
+ <h2>Test A2 — captured server RESPONSES (streaming path, plain &lt;audio&gt;)</h2>
44
+ <p>These are the exact bytes the server streamed for a real response ("Hi, how are you?"). Listen to each — especially the long one.</p>
45
+ <p>Seg 5 — "Hello!" (0.7s):</p>
46
+ <audio controls src="segment-5.wav"></audio>
47
+ <p>Seg 2 — "I'm doing great, thank you for asking." (1.6s):</p>
48
+ <audio controls src="segment-2.wav"></audio>
49
+ <p>Seg 3 — "How can I assist you today?" (1.4s):</p>
50
+ <audio controls src="segment-3.wav"></audio>
51
+ <p>Seg 4 — "Are you interested in booking a room with us at Daniel Hotel" (5.1s):</p>
52
+ <audio controls src="segment-4.wav"></audio>
53
+ </div>
54
+
55
+ <div class="card">
56
+ <h2>Test B — AudioContext drift by sample rate (no mic)</h2>
57
+ <p>Run each. The rate whose drift stays flat at +0.00% is the one your machine's audio path actually likes.</p>
58
+ <button data-rate="native">Native rate</button>
59
+ <button data-rate="44100">44100 Hz</button>
60
+ <button data-rate="48000">48000 Hz</button>
61
+ <button data-rate="24000">24000 Hz</button>
62
+ </div>
63
+
64
+ <div class="card">
65
+ <h2>Test C — AudioContext @48kHz WITH mic open (real call conditions)</h2>
66
+ <p>Opens the mic with echo cancellation first (like a call), then plays. Watch for the drift ratio jumping when playback starts.</p>
67
+ <button id="runC">Run Test C</button>
68
+ </div>
69
+
70
+ <div class="card">
71
+ <h2>Log</h2>
72
+ <div id="log"></div>
73
+ <button id="copy">Copy log</button>
74
+ </div>
75
+
76
+ <script>
77
+ const logEl = document.getElementById('log');
78
+ function log(msg, cls) {
79
+ const line = document.createElement('div');
80
+ if (cls) line.className = cls;
81
+ line.textContent = msg;
82
+ logEl.appendChild(line);
83
+ logEl.scrollTop = logEl.scrollHeight;
84
+ console.log(msg);
85
+ }
86
+
87
+ async function loadPcm() {
88
+ const buf = await fetch('greeting-capture.wav').then(r => r.arrayBuffer());
89
+ const ctx = new OfflineAudioContext(1, 1, 24000);
90
+ return ctx.decodeAudioData(buf);
91
+ }
92
+
93
+ // Play the buffer through a realtime context and measure clock drift.
94
+ // ratio = audioClockDelta / wallClockDelta — 1.000 = perfect. Oscillation = audible wobble.
95
+ async function driftTest(label, withMic, rate) {
96
+ let stream = null;
97
+ if (withMic) {
98
+ log(label + ': opening mic (echoCancellation:true)...', 'info');
99
+ stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, channelCount: 1 } });
100
+ log(label + ': mic up, letting it settle 500ms...', 'info');
101
+ await new Promise(r => setTimeout(r, 500));
102
+ }
103
+
104
+ const ctx = rate === 'native' ? new AudioContext() : new AudioContext({ sampleRate: rate });
105
+ await ctx.resume();
106
+ log(label + ': AudioContext sampleRate=' + ctx.sampleRate + ' state=' + ctx.state +
107
+ ' baseLatency=' + (ctx.baseLatency * 1000).toFixed(1) + 'ms' +
108
+ (ctx.outputLatency ? ' outputLatency=' + (ctx.outputLatency * 1000).toFixed(1) + 'ms' : ''), 'info');
109
+
110
+ const decoded = await loadPcm();
111
+ const src = ctx.createBufferSource();
112
+ src.buffer = decoded;
113
+ src.connect(ctx.destination);
114
+
115
+ let lastCtx = ctx.currentTime, lastNow = performance.now();
116
+ let minR = Infinity, maxR = -Infinity, samples = 0, sumSq = 0;
117
+ const rows = [];
118
+ const timer = setInterval(() => {
119
+ const c = ctx.currentTime, n = performance.now();
120
+ const ratio = (c - lastCtx) / ((n - lastNow) / 1000);
121
+ lastCtx = c; lastNow = n;
122
+ if (isFinite(ratio) && ratio > 0) {
123
+ minR = Math.min(minR, ratio); maxR = Math.max(maxR, ratio);
124
+ samples++; sumSq += (ratio - 1) * (ratio - 1);
125
+ const pct = ((ratio - 1) * 100).toFixed(2);
126
+ rows.push(ratio);
127
+ log(label + ': t=' + c.toFixed(2) + 's drift=' + (ratio >= 1 ? '+' : '') + pct + '%' +
128
+ (Math.abs(ratio - 1) > 0.005 ? ' <<< audible' : ''),
129
+ Math.abs(ratio - 1) > 0.005 ? 'bad' : null);
130
+ }
131
+ }, 250);
132
+
133
+ const t0 = performance.now();
134
+ src.start();
135
+ log(label + ': playback started', 'info');
136
+ await new Promise(r => { src.onended = r; });
137
+ clearInterval(timer);
138
+
139
+ const wall = (performance.now() - t0) / 1000;
140
+ const audioDur = decoded.duration;
141
+ const rms = Math.sqrt(sumSq / Math.max(samples, 1));
142
+ log(label + ': DONE. wall=' + wall.toFixed(2) + 's audio=' + audioDur.toFixed(2) + 's' +
143
+ ' drift min=' + ((minR - 1) * 100).toFixed(2) + '% max=' + ((maxR - 1) * 100).toFixed(2) + '%' +
144
+ ' wobble(rms)=' + (rms * 100).toFixed(3) + '%', 'info');
145
+ const verdict = (rms < 0.002 && Math.abs(wall - audioDur) < 0.15)
146
+ ? label + ' VERDICT: CLEAN — clock stable, duration correct'
147
+ : label + ' VERDICT: WOBBLE DETECTED — clock unstable or duration wrong';
148
+ log(verdict, rms < 0.002 && Math.abs(wall - audioDur) < 0.15 ? 'ok verdict' : 'bad verdict');
149
+
150
+ if (stream) stream.getTracks().forEach(t => t.stop());
151
+ await ctx.close();
152
+ }
153
+
154
+ document.querySelectorAll('button[data-rate]').forEach(btn => {
155
+ btn.onclick = () => {
156
+ const rate = btn.dataset.rate === 'native' ? 'native' : parseInt(btn.dataset.rate, 10);
157
+ driftTest('Rate[' + btn.dataset.rate + ']', false, rate).catch(e => log('drift test error: ' + e.message, 'bad'));
158
+ };
159
+ });
160
+ document.getElementById('runC').onclick = () => driftTest('TestC', true, 48000).catch(e => log('TestC error: ' + e.message, 'bad'));
161
+ document.getElementById('copy').onclick = () => {
162
+ navigator.clipboard.writeText(logEl.innerText).then(() => log('log copied to clipboard', 'ok'));
163
+ };
164
+ </script>
165
+ </body>
166
+ </html>
@@ -526,6 +526,12 @@
526
526
  }
527
527
  };
528
528
 
529
+ // A/B test hook (local testing): ?hop=0 disables the MediaStream→<audio> playback hop
530
+ if (new URLSearchParams(window.location.search).get('hop') === '0') {
531
+ config.htmlAudioPlayback = false;
532
+ console.log('🧪 TEST MODE: htmlAudioPlayback DISABLED — playing directly to AudioContext.destination');
533
+ }
534
+
529
535
  // Add optional configs
530
536
  if (getSessionUrl) {
531
537
  config.getSessionUrl = getSessionUrl;
@@ -0,0 +1,166 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Audio Wobble Diagnostics</title>
6
+ <style>
7
+ body { font-family: system-ui, sans-serif; max-width: 900px; margin: 24px auto; padding: 0 16px; background: #0f172a; color: #e2e8f0; }
8
+ h1 { font-size: 20px; } h2 { font-size: 16px; margin-top: 28px; }
9
+ .card { background: #1e293b; border-radius: 10px; padding: 16px; margin: 12px 0; }
10
+ button { background: #3b82f6; color: #fff; border: 0; border-radius: 8px; padding: 10px 18px; font-size: 14px; cursor: pointer; margin-right: 8px; }
11
+ button:hover { background: #2563eb; }
12
+ audio { width: 100%; margin-top: 8px; }
13
+ #log { background: #020617; border-radius: 8px; padding: 12px; font-family: ui-monospace, monospace; font-size: 12px; height: 320px; overflow-y: auto; white-space: pre-wrap; }
14
+ .verdict { font-weight: 700; }
15
+ .ok { color: #4ade80; } .bad { color: #f87171; } .info { color: #93c5fd; }
16
+ </style>
17
+ </head>
18
+ <body>
19
+ <h1>Audio Wobble Diagnostics</h1>
20
+ <p>This page plays a <b>known-clean</b> greeting recording (captured straight from the server) three ways.
21
+ The greeting is 2.75s of speech at 1.4x voice speed — that speed is intentional.</p>
22
+
23
+ <div class="card">
24
+ <h2>Test A — plain &lt;audio&gt; element (no Web Audio, no mic)</h2>
25
+ <p>Play this. If it wobbles, the machine's audio output path itself is the problem.</p>
26
+ <audio controls src="greeting-capture.wav"></audio>
27
+ </div>
28
+
29
+ <div class="card">
30
+ <h2>Test A3 — OCADO greeting (Cartesia path, captured from server)</h2>
31
+ <p>"Hello! How can I help you today?" — the exact bytes the server sends for the Ocado agent. Does this sound clean?</p>
32
+ <audio controls src="ocado-greeting.wav"></audio>
33
+ <p>Ocado response to "Hi, how are you?" — 3 sentences:</p>
34
+ <p>"Hey," (0.6s):</p>
35
+ <audio controls src="ocado-segment-3.wav"></audio>
36
+ <p>"doing well thanks for asking!" (1.7s):</p>
37
+ <audio controls src="ocado-segment-2.wav"></audio>
38
+ <p>"What can I help you shop for today?" (1.7s):</p>
39
+ <audio controls src="ocado-segment-4.wav"></audio>
40
+ </div>
41
+
42
+ <div class="card">
43
+ <h2>Test A2 — captured server RESPONSES (streaming path, plain &lt;audio&gt;)</h2>
44
+ <p>These are the exact bytes the server streamed for a real response ("Hi, how are you?"). Listen to each — especially the long one.</p>
45
+ <p>Seg 5 — "Hello!" (0.7s):</p>
46
+ <audio controls src="segment-5.wav"></audio>
47
+ <p>Seg 2 — "I'm doing great, thank you for asking." (1.6s):</p>
48
+ <audio controls src="segment-2.wav"></audio>
49
+ <p>Seg 3 — "How can I assist you today?" (1.4s):</p>
50
+ <audio controls src="segment-3.wav"></audio>
51
+ <p>Seg 4 — "Are you interested in booking a room with us at Daniel Hotel" (5.1s):</p>
52
+ <audio controls src="segment-4.wav"></audio>
53
+ </div>
54
+
55
+ <div class="card">
56
+ <h2>Test B — AudioContext drift by sample rate (no mic)</h2>
57
+ <p>Run each. The rate whose drift stays flat at +0.00% is the one your machine's audio path actually likes.</p>
58
+ <button data-rate="native">Native rate</button>
59
+ <button data-rate="44100">44100 Hz</button>
60
+ <button data-rate="48000">48000 Hz</button>
61
+ <button data-rate="24000">24000 Hz</button>
62
+ </div>
63
+
64
+ <div class="card">
65
+ <h2>Test C — AudioContext @48kHz WITH mic open (real call conditions)</h2>
66
+ <p>Opens the mic with echo cancellation first (like a call), then plays. Watch for the drift ratio jumping when playback starts.</p>
67
+ <button id="runC">Run Test C</button>
68
+ </div>
69
+
70
+ <div class="card">
71
+ <h2>Log</h2>
72
+ <div id="log"></div>
73
+ <button id="copy">Copy log</button>
74
+ </div>
75
+
76
+ <script>
77
+ const logEl = document.getElementById('log');
78
+ function log(msg, cls) {
79
+ const line = document.createElement('div');
80
+ if (cls) line.className = cls;
81
+ line.textContent = msg;
82
+ logEl.appendChild(line);
83
+ logEl.scrollTop = logEl.scrollHeight;
84
+ console.log(msg);
85
+ }
86
+
87
+ async function loadPcm() {
88
+ const buf = await fetch('greeting-capture.wav').then(r => r.arrayBuffer());
89
+ const ctx = new OfflineAudioContext(1, 1, 24000);
90
+ return ctx.decodeAudioData(buf);
91
+ }
92
+
93
+ // Play the buffer through a realtime context and measure clock drift.
94
+ // ratio = audioClockDelta / wallClockDelta — 1.000 = perfect. Oscillation = audible wobble.
95
+ async function driftTest(label, withMic, rate) {
96
+ let stream = null;
97
+ if (withMic) {
98
+ log(label + ': opening mic (echoCancellation:true)...', 'info');
99
+ stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, channelCount: 1 } });
100
+ log(label + ': mic up, letting it settle 500ms...', 'info');
101
+ await new Promise(r => setTimeout(r, 500));
102
+ }
103
+
104
+ const ctx = rate === 'native' ? new AudioContext() : new AudioContext({ sampleRate: rate });
105
+ await ctx.resume();
106
+ log(label + ': AudioContext sampleRate=' + ctx.sampleRate + ' state=' + ctx.state +
107
+ ' baseLatency=' + (ctx.baseLatency * 1000).toFixed(1) + 'ms' +
108
+ (ctx.outputLatency ? ' outputLatency=' + (ctx.outputLatency * 1000).toFixed(1) + 'ms' : ''), 'info');
109
+
110
+ const decoded = await loadPcm();
111
+ const src = ctx.createBufferSource();
112
+ src.buffer = decoded;
113
+ src.connect(ctx.destination);
114
+
115
+ let lastCtx = ctx.currentTime, lastNow = performance.now();
116
+ let minR = Infinity, maxR = -Infinity, samples = 0, sumSq = 0;
117
+ const rows = [];
118
+ const timer = setInterval(() => {
119
+ const c = ctx.currentTime, n = performance.now();
120
+ const ratio = (c - lastCtx) / ((n - lastNow) / 1000);
121
+ lastCtx = c; lastNow = n;
122
+ if (isFinite(ratio) && ratio > 0) {
123
+ minR = Math.min(minR, ratio); maxR = Math.max(maxR, ratio);
124
+ samples++; sumSq += (ratio - 1) * (ratio - 1);
125
+ const pct = ((ratio - 1) * 100).toFixed(2);
126
+ rows.push(ratio);
127
+ log(label + ': t=' + c.toFixed(2) + 's drift=' + (ratio >= 1 ? '+' : '') + pct + '%' +
128
+ (Math.abs(ratio - 1) > 0.005 ? ' <<< audible' : ''),
129
+ Math.abs(ratio - 1) > 0.005 ? 'bad' : null);
130
+ }
131
+ }, 250);
132
+
133
+ const t0 = performance.now();
134
+ src.start();
135
+ log(label + ': playback started', 'info');
136
+ await new Promise(r => { src.onended = r; });
137
+ clearInterval(timer);
138
+
139
+ const wall = (performance.now() - t0) / 1000;
140
+ const audioDur = decoded.duration;
141
+ const rms = Math.sqrt(sumSq / Math.max(samples, 1));
142
+ log(label + ': DONE. wall=' + wall.toFixed(2) + 's audio=' + audioDur.toFixed(2) + 's' +
143
+ ' drift min=' + ((minR - 1) * 100).toFixed(2) + '% max=' + ((maxR - 1) * 100).toFixed(2) + '%' +
144
+ ' wobble(rms)=' + (rms * 100).toFixed(3) + '%', 'info');
145
+ const verdict = (rms < 0.002 && Math.abs(wall - audioDur) < 0.15)
146
+ ? label + ' VERDICT: CLEAN — clock stable, duration correct'
147
+ : label + ' VERDICT: WOBBLE DETECTED — clock unstable or duration wrong';
148
+ log(verdict, rms < 0.002 && Math.abs(wall - audioDur) < 0.15 ? 'ok verdict' : 'bad verdict');
149
+
150
+ if (stream) stream.getTracks().forEach(t => t.stop());
151
+ await ctx.close();
152
+ }
153
+
154
+ document.querySelectorAll('button[data-rate]').forEach(btn => {
155
+ btn.onclick = () => {
156
+ const rate = btn.dataset.rate === 'native' ? 'native' : parseInt(btn.dataset.rate, 10);
157
+ driftTest('Rate[' + btn.dataset.rate + ']', false, rate).catch(e => log('drift test error: ' + e.message, 'bad'));
158
+ };
159
+ });
160
+ document.getElementById('runC').onclick = () => driftTest('TestC', true, 48000).catch(e => log('TestC error: ' + e.message, 'bad'));
161
+ document.getElementById('copy').onclick = () => {
162
+ navigator.clipboard.writeText(logEl.innerText).then(() => log('log copied to clipboard', 'ok'));
163
+ };
164
+ </script>
165
+ </body>
166
+ </html>
@@ -526,6 +526,12 @@
526
526
  }
527
527
  };
528
528
 
529
+ // A/B test hook (local testing): ?hop=0 disables the MediaStream→<audio> playback hop
530
+ if (new URLSearchParams(window.location.search).get('hop') === '0') {
531
+ config.htmlAudioPlayback = false;
532
+ console.log('🧪 TEST MODE: htmlAudioPlayback DISABLED — playing directly to AudioContext.destination');
533
+ }
534
+
529
535
  // Add optional configs
530
536
  if (getSessionUrl) {
531
537
  config.getSessionUrl = getSessionUrl;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ttp-agent-sdk",
3
- "version": "2.48.7",
3
+ "version": "2.48.9",
4
4
  "description": "Comprehensive Voice Agent SDK with Customizable Widget - Real-time audio, WebSocket communication, React components, and extensive customization options",
5
5
  "main": "dist/agent-widget.js",
6
6
  "module": "dist/agent-widget.esm.js",