Skip to content

Instantly share code, notes, and snippets.

@chapimenge3
Created June 14, 2026 20:40
Show Gist options
  • Select an option

  • Save chapimenge3/dc7ae492d35632394d2910efb14ffa5d to your computer and use it in GitHub Desktop.

Select an option

Save chapimenge3/dc7ae492d35632394d2910efb14ffa5d to your computer and use it in GitHub Desktop.
Test implementation of addis ai realtime api https://platform.addisassistant.com/docs/capabilities/realtime-api
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Realtime Audio Test</title>
<style>
:root {
--bg: #0f1117;
--panel: #1a1d27;
--border: #2a2f3d;
--text: #e6e8ee;
--muted: #8b91a3;
--accent: #5b8cff;
--green: #3ecf8e;
--red: #ff5c5c;
--amber: #ffb84d;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
justify-content: center;
padding: 32px 16px;
}
.wrap { width: 100%; max-width: 720px; }
h1 { font-size: 20px; margin: 0 0 4px; }
.sub { color: var(--muted); font-size: 13px; margin-bottom: 24px; }
.card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
}
label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
input[type="text"] {
width: 100%;
background: var(--bg);
border: 1px solid var(--border);
color: var(--text);
padding: 10px 12px;
border-radius: 8px;
font-size: 13px;
font-family: ui-monospace, monospace;
}
.controls { display: flex; gap: 10px; align-items: center; margin-top: 16px; }
button {
background: var(--accent);
color: #fff;
border: none;
padding: 10px 18px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: opacity .15s;
}
button:hover { opacity: .9; }
button:disabled { opacity: .4; cursor: not-allowed; }
button.stop { background: var(--red); }
.status {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--muted);
margin-left: auto;
}
.dot { width: 9px; height: 9px; border-radius: 50%; background: var(--muted); }
.dot.connecting { background: var(--amber); animation: pulse 1s infinite; }
.dot.live { background: var(--green); }
.dot.error { background: var(--red); }
@keyframes pulse { 50% { opacity: .3; } }
.logwrap { margin-top: 4px; }
.logheader { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
#log {
background: #0a0c11;
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px;
height: 280px;
overflow-y: auto;
font-family: ui-monospace, monospace;
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.log-line { margin: 0 0 2px; }
.log-info { color: var(--text); }
.log-sys { color: var(--accent); }
.log-err { color: var(--red); }
.log-ok { color: var(--green); }
.log-time { color: var(--muted); }
.clearbtn {
background: transparent;
border: 1px solid var(--border);
color: var(--muted);
padding: 4px 10px;
font-size: 11px;
font-weight: 500;
}
</style>
</head>
<body>
<div class="wrap">
<h1>Realtime Audio Test</h1>
<div class="sub">Mic → WebSocket relay → AI audio playback. Open the browser console for full event objects.</div>
<div class="card">
<label for="apikey">API Key</label>
<input type="text" id="apikey" placeholder="Paste your API key here" autocomplete="off" spellcheck="false" />
<div class="controls">
<button id="startBtn">Start</button>
<button id="stopBtn" class="stop" disabled>Stop</button>
<span class="status"><span class="dot" id="dot"></span><span id="statusText">Idle</span></span>
</div>
</div>
<div class="card logwrap">
<div class="logheader">
<label style="margin:0;">Event Log</label>
<button class="clearbtn" id="clearBtn">Clear</button>
</div>
<div id="log"></div>
</div>
</div>
<script>
// ===== Config =====
const RELAY_BASE = "wss://relay.addisassistant.com/ws";
const INPUT_RATE = 16000;
const OUTPUT_RATE = 24000;
// ===== State =====
let socket;
let inputContext;
let outputContext;
let stream;
let source;
let processor;
let canStreamAudio = false;
let nextStartTime = 0;
// ===== UI helpers =====
const $ = (id) => document.getElementById(id);
const startBtn = $("startBtn");
const stopBtn = $("stopBtn");
const clearBtn = $("clearBtn");
const dot = $("dot");
const statusText = $("statusText");
function setStatus(state, text) {
dot.className = "dot" + (state ? " " + state : "");
statusText.textContent = text;
}
function log(msg, type = "info") {
const el = $("log");
const time = new Date().toLocaleTimeString();
const line = document.createElement("div");
line.className = "log-line log-" + type;
line.innerHTML = '<span class="log-time">' + time + '</span> ' +
String(msg).replace(/</g, "&lt;");
el.appendChild(line);
el.scrollTop = el.scrollHeight;
}
clearBtn.onclick = () => ($("log").innerHTML = "");
startBtn.onclick = () => startRealtime();
stopBtn.onclick = () => stopRealtime();
// ===== Main =====
async function startRealtime() {
const apiKey = $("apikey").value.trim();
if (!apiKey) {
log("Enter an API key first.", "err");
return;
}
startBtn.disabled = true;
setStatus("connecting", "Connecting…");
try {
// Output context for AI playback
outputContext = new (window.AudioContext || window.webkitAudioContext)({
sampleRate: OUTPUT_RATE,
});
await outputContext.resume();
nextStartTime = outputContext.currentTime;
// Input context for mic capture
inputContext = new (window.AudioContext || window.webkitAudioContext)({
sampleRate: INPUT_RATE,
});
await inputContext.resume();
stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
log("Mic access granted.", "ok");
source = inputContext.createMediaStreamSource(stream);
processor = inputContext.createScriptProcessor(2048, 1, 1);
// Connect processor through a muted gain so callback keeps firing without mic echo
const mute = inputContext.createGain();
mute.gain.value = 0;
source.connect(processor);
processor.connect(mute);
mute.connect(inputContext.destination);
processor.onaudioprocess = (event) => {
if (!canStreamAudio || socket?.readyState !== WebSocket.OPEN) return;
// Browser gives Float32 (-1..1), convert to PCM16 and send as base64 JSON envelope
const float32 = event.inputBuffer.getChannelData(0);
const int16 = floatTo16BitPCM(float32);
socket.send(
JSON.stringify({
data: arrayBufferToBase64(int16.buffer),
mimeType: "audio/pcm;rate=16000",
}),
);
};
const wsUrl = RELAY_BASE + "?apiKey=" + encodeURIComponent(apiKey);
socket = new WebSocket(wsUrl);
socket.onopen = () => {
log("Connected. Waiting for setupComplete…", "sys");
setStatus("connecting", "Handshaking…");
};
socket.onmessage = async (event) => {
let message;
try {
message = JSON.parse(event.data);
} catch {
log("Non-JSON message received.", "err");
return;
}
console.log("Event:", message);
if (message.setupComplete || (message.type === "status" && /ready/i.test(message.message || ""))) {
canStreamAudio = true;
log("Setup complete — streaming mic audio.", "ok");
setStatus("live", "Live");
return;
}
const b64 = message?.serverContent?.modelTurn?.parts?.[0]?.inlineData?.data;
if (typeof b64 === "string" && b64.length) {
log("Received audio chunk (" + b64.length + " b64 chars).", "info");
await playPcm16Base64(b64, OUTPUT_RATE);
} else {
// Log other message types so you can inspect the protocol
const keys = Object.keys(message).join(", ");
log("Message: { " + keys + " }", "sys");
}
if (message?.error) {
log("Realtime error: " + JSON.stringify(message.error), "err");
console.error("Realtime error:", message.error);
}
};
socket.onerror = (event) => {
log("WebSocket error (see console).", "err");
setStatus("error", "Error");
console.error("WebSocket error:", event);
};
socket.onclose = (event) => {
log("WebSocket closed. code=" + event.code + " reason=" + (event.reason || "n/a"), "sys");
console.log("WebSocket closed code=" + event.code + " reason=" + (event.reason || "n/a"));
teardownAudio();
setStatus("", "Idle");
startBtn.disabled = false;
stopBtn.disabled = true;
};
stopBtn.disabled = false;
} catch (err) {
log("Failed to start: " + err.message, "err");
console.error(err);
setStatus("error", "Error");
teardownAudio();
startBtn.disabled = false;
}
}
function stopRealtime() {
canStreamAudio = false;
log("Stopping…", "sys");
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
socket.close(1000, "client-stop");
}
teardownAudio();
setStatus("", "Idle");
startBtn.disabled = false;
stopBtn.disabled = true;
}
function teardownAudio() {
try { if (processor) processor.disconnect(); } catch {}
try { if (source) source.disconnect(); } catch {}
try { if (stream) stream.getTracks().forEach((t) => t.stop()); } catch {}
try { if (inputContext && inputContext.state !== "closed") inputContext.close(); } catch {}
try { if (outputContext && outputContext.state !== "closed") outputContext.close(); } catch {}
processor = source = stream = inputContext = outputContext = null;
}
// ===== Audio conversion =====
function floatTo16BitPCM(float32Array) {
const int16Array = new Int16Array(float32Array.length);
for (let i = 0; i < float32Array.length; i++) {
const s = Math.max(-1, Math.min(1, float32Array[i]));
int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
}
return int16Array;
}
function arrayBufferToBase64(arrayBuffer) {
const bytes = new Uint8Array(arrayBuffer);
let binary = "";
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
function base64ToUint8Array(base64) {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
async function playPcm16Base64(base64Audio, sampleRate = 24000) {
const bytes = base64ToUint8Array(base64Audio);
const pcm16 = new Int16Array(bytes.buffer);
const float32 = new Float32Array(pcm16.length);
for (let i = 0; i < pcm16.length; i++) {
float32[i] = pcm16[i] / 32768;
}
const buffer = outputContext.createBuffer(1, float32.length, sampleRate);
buffer.copyToChannel(float32, 0);
const src = outputContext.createBufferSource();
src.buffer = buffer;
src.connect(outputContext.destination);
const startAt = Math.max(outputContext.currentTime, nextStartTime);
src.start(startAt);
nextStartTime = startAt + buffer.duration;
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment