Files
AgapHost/openai/pipecat/static/index.html
Alvis 85033136d8 openai: add AI stack (litellm + langfuse + pipecat + silero-tts) and oO aliases
- LiteLLM proxy with langfuse callbacks, postgres backends, and OpenRouter fallbacks.
- Langfuse observability UI.
- Pipecat voice pipeline (LiveKit + STT + TTS + LLM) and Silero TTS build contexts.
- Ollama tuned for GPU (OLLAMA_NUM_GPU=999, mem_limit=4g, max 2 loaded models).
- open-webui wired to litellm + faster-whisper + silero for voice.
- litellm-config.yaml publishes oO's model aliases (tip-generator, embedder, judge)
  pointing at the host ollama on :11434 so ml/serving can call them via LiteLLM.

.env skipped (secrets).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 14:28:24 +00:00

242 lines
7.5 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Adolf Voice</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, sans-serif;
background: #0f0f0f;
color: #e0e0e0;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.card {
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 16px;
padding: 40px;
text-align: center;
width: 360px;
}
h1 { font-size: 1.4rem; font-weight: 600; margin-bottom: 8px; }
.subtitle { color: #666; font-size: 0.85rem; margin-bottom: 32px; }
#orb {
width: 100px;
height: 100px;
border-radius: 50%;
background: radial-gradient(circle, #3a3a3a 0%, #1a1a1a 100%);
border: 2px solid #333;
margin: 0 auto 24px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
user-select: none;
}
#orb.listening {
background: radial-gradient(circle, #1e3a5f 0%, #0d1f33 100%);
border-color: #3b82f6;
box-shadow: 0 0 20px #3b82f640;
animation: pulse-blue 1.5s ease-in-out infinite;
}
#orb.speaking {
background: radial-gradient(circle, #1e4034 0%, #0d2018 100%);
border-color: #22c55e;
box-shadow: 0 0 20px #22c55e40;
animation: pulse-green 0.8s ease-in-out infinite;
}
#orb.thinking {
background: radial-gradient(circle, #3a2e1e 0%, #1a160d 100%);
border-color: #f59e0b;
box-shadow: 0 0 20px #f59e0b40;
animation: pulse-amber 1s ease-in-out infinite;
}
#orb.user-speaking {
background: radial-gradient(circle, #3a1e3a 0%, #1a0d1a 100%);
border-color: #a855f7;
box-shadow: 0 0 20px #a855f740;
animation: pulse-purple 0.6s ease-in-out infinite;
}
@keyframes pulse-blue { 0%,100%{box-shadow:0 0 20px #3b82f640} 50%{box-shadow:0 0 35px #3b82f680} }
@keyframes pulse-green { 0%,100%{box-shadow:0 0 20px #22c55e40} 50%{box-shadow:0 0 35px #22c55e80} }
@keyframes pulse-amber { 0%,100%{box-shadow:0 0 20px #f59e0b40} 50%{box-shadow:0 0 35px #f59e0b80} }
@keyframes pulse-purple { 0%,100%{box-shadow:0 0 20px #a855f740} 50%{box-shadow:0 0 35px #a855f780} }
#status {
font-size: 0.9rem;
color: #888;
margin-bottom: 16px;
min-height: 1.2em;
}
#transcript {
font-size: 0.8rem;
color: #555;
margin-bottom: 20px;
min-height: 2.4em;
line-height: 1.4;
font-style: italic;
word-break: break-word;
}
#transcript .user-text { color: #7ba8d4; font-style: normal; }
#transcript .bot-text { color: #6ab88a; font-style: normal; }
#btn {
background: #2a2a2a;
border: 1px solid #3a3a3a;
color: #e0e0e0;
padding: 10px 28px;
border-radius: 8px;
font-size: 0.9rem;
cursor: pointer;
transition: background 0.2s;
}
#btn:hover { background: #333; }
#btn:disabled { opacity: 0.4; cursor: default; }
#btn.active { border-color: #ef4444; color: #ef4444; }
</style>
</head>
<body>
<div class="card">
<h1>Adolf</h1>
<p class="subtitle">Voice assistant</p>
<div id="orb" onclick="toggle()">🎙️</div>
<div id="status">Press to connect</div>
<div id="transcript"></div>
<button id="btn" onclick="toggle()">Connect</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/livekit-client/dist/livekit-client.umd.min.js"></script>
<script>
let room = null;
let audioCtx = null;
// Unlock browser autoplay — must happen on first user gesture
function unlockAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === 'suspended') audioCtx.resume();
}
}
function setUI(state, msg) {
const orb = document.getElementById('orb');
const status = document.getElementById('status');
const btn = document.getElementById('btn');
orb.className = state || '';
status.textContent = msg;
if (state === null) {
btn.textContent = 'Connect';
btn.classList.remove('active');
orb.textContent = '🎙️';
} else {
btn.textContent = 'Disconnect';
btn.classList.add('active');
orb.textContent = state === 'thinking' ? '💭' :
state === 'speaking' ? '🔊' :
state === 'user-speaking' ? '🗣️' : '🎙️';
}
}
function addTranscript(role, text) {
const div = document.getElementById('transcript');
const cls = role === 'user' ? 'user-text' : 'bot-text';
const prefix = role === 'user' ? 'You: ' : 'Adolf: ';
div.innerHTML = `<span class="${cls}">${prefix}${text}</span>`;
}
async function toggle() {
unlockAudio();
if (room) {
room.disconnect();
return;
}
document.getElementById('btn').disabled = true;
setUI('thinking', 'Connecting…');
try {
const res = await fetch('/connect', { method: 'POST' });
const { token, url } = await res.json();
room = new LivekitClient.Room({ adaptiveStream: true, dynacast: true });
room.on(LivekitClient.RoomEvent.Connected, () => {
setUI('listening', 'Listening…');
document.getElementById('btn').disabled = false;
});
room.on(LivekitClient.RoomEvent.Disconnected, () => {
setUI(null, 'Press to connect');
document.getElementById('btn').disabled = false;
document.getElementById('transcript').innerHTML = '';
room = null;
});
room.on(LivekitClient.RoomEvent.ActiveSpeakersChanged, (speakers) => {
if (!room) return;
const botSpeaking = speakers.some(s => s.identity === 'pipecat-bot');
const userSpeaking = speakers.some(s => s.identity === 'user');
if (botSpeaking) {
setUI('speaking', 'Adolf is speaking…');
} else if (userSpeaking) {
setUI('user-speaking', 'Listening to you…');
} else {
setUI('listening', 'Listening…');
}
});
// Attach remote audio so browser plays it
room.on(LivekitClient.RoomEvent.TrackSubscribed, (track, pub, participant) => {
if (track.kind === 'audio') {
// Remove old element if any
const old = document.getElementById(`audio-${participant.identity}`);
if (old) old.remove();
const el = track.attach();
el.id = `audio-${participant.identity}`;
el.autoplay = true;
// Resume audio context on attach to beat autoplay restrictions
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
document.body.appendChild(el);
el.play().catch(() => {});
}
});
room.on(LivekitClient.RoomEvent.TrackUnsubscribed, (track) => {
track.detach().forEach(el => el.remove());
});
room.on(LivekitClient.RoomEvent.ParticipantConnected, (p) => {
if (p.identity === 'pipecat-bot') {
setUI('listening', 'Listening…');
}
});
// Data messages from bot (transcripts/events if pipecat sends them)
room.on(LivekitClient.RoomEvent.DataReceived, (data, participant) => {
try {
const msg = JSON.parse(new TextDecoder().decode(data));
if (msg.type === 'transcript' && msg.role === 'user') addTranscript('user', msg.text);
if (msg.type === 'transcript' && msg.role === 'bot') addTranscript('bot', msg.text);
} catch {}
});
const wsUrl = url.replace(/^http/, 'ws');
await room.connect(wsUrl, token);
await room.localParticipant.setMicrophoneEnabled(true);
} catch (err) {
console.error(err);
setUI(null, 'Error: ' + err.message);
document.getElementById('btn').disabled = false;
room = null;
}
}
</script>
</body>
</html>