feat: integrate JV Voice Profile cloning, Voicebox research, and ensure session voice/markdown prompt nudge

This commit is contained in:
Adolfo Reyna
2026-08-12 19:57:03 -04:00
parent 811a40f2cc
commit 70da5d857a
11 changed files with 661 additions and 50 deletions
+298 -47
View File
@@ -67,6 +67,64 @@ async def handle_index(request):
return web.Response(text=HTML_INDEX, content_type="text/html")
async def handle_manifest(request):
manifest = {
"name": "VoiceAgent Companion",
"short_name": "VoiceAgent",
"description": "Minimal High-Performance AI Voice Companion",
"start_url": "/",
"display": "standalone",
"background_color": "#0b0f19",
"theme_color": "#0b0f19",
"orientation": "any",
"icons": [
{
"src": "/icon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any maskable"
}
]
}
return web.json_response(manifest)
async def handle_service_worker(request):
sw_code = """
const CACHE_NAME = 'voiceagent-pwa-v1';
self.addEventListener('install', (e) => self.skipWaiting());
self.addEventListener('activate', (e) => e.waitUntil(clients.claim()));
self.addEventListener('fetch', (e) => {
if (e.request.url.includes('/api/')) return;
e.respondWith(fetch(e.request).catch(() => caches.match(e.request)));
});
"""
return web.Response(text=sw_code, content_type="application/javascript")
async def handle_icon_svg(request):
svg_icon = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<defs>
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0b0f19"/>
<stop offset="100%" stop-color="#1e1b4b"/>
</linearGradient>
<linearGradient id="glowGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#6366f1"/>
<stop offset="100%" stop-color="#a855f7"/>
</linearGradient>
</defs>
<rect width="512" height="512" rx="120" fill="url(#bgGrad)"/>
<circle cx="256" cy="256" r="170" fill="none" stroke="url(#glowGrad)" stroke-width="14" opacity="0.3"/>
<g transform="translate(106, 106)">
<rect x="120" y="60" width="60" height="150" rx="30" fill="url(#glowGrad)"/>
<path d="M 60 180 A 90 90 0 0 0 240 180" fill="none" stroke="url(#glowGrad)" stroke-width="20" stroke-linecap="round"/>
<line x1="150" y1="270" x2="150" y2="320" stroke="url(#glowGrad)" stroke-width="20" stroke-linecap="round"/>
</g>
</svg>"""
return web.Response(text=svg_icon, content_type="image/svg+xml")
async def handle_sse(request):
response = web.StreamResponse(
status=200,
@@ -283,6 +341,10 @@ async def start_server(workspace: Path, host: str = "127.0.0.1", port: int = 888
set_managers(workspace)
app = web.Application()
app.router.add_get("/", handle_index)
app.router.add_get("/manifest.json", handle_manifest)
app.router.add_get("/sw.js", handle_service_worker)
app.router.add_get("/icon.svg", handle_icon_svg)
app.router.add_get("/apple-touch-icon.png", handle_icon_svg)
app.router.add_get("/api/stream", handle_sse)
app.router.add_get("/api/history", handle_history)
app.router.add_get("/api/file", handle_file)
@@ -313,9 +375,18 @@ HTML_INDEX = """<!DOCTYPE html>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VoiceAgent Companion</title>
<link rel="manifest" href="/manifest.json">
<link rel="icon" type="image/svg+xml" href="/icon.svg">
<link rel="apple-touch-icon" href="/icon.svg">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="VoiceAgent">
<meta name="theme-color" content="#0b0f19">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
:root {
--bg: #0b0f19;
@@ -363,26 +434,32 @@ HTML_INDEX = """<!DOCTYPE html>
}
header {
padding: 16px 24px;
background: rgba(19, 27, 46, 0.85);
padding: 10px 14px;
background: rgba(19, 27, 46, 0.92);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--surface-border);
display: flex;
flex-direction: column;
gap: 8px;
z-index: 10;
}
.header-top {
display: flex;
align-items: center;
justify-content: space-between;
z-index: 10;
width: 100%;
}
.logo-group {
display: flex;
align-items: center;
gap: 12px;
gap: 8px;
}
.status-dot {
width: 10px;
height: 10px;
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--accent-green);
box-shadow: 0 0 10px var(--accent-green);
box-shadow: 0 0 8px var(--accent-green);
animation: pulse 2s infinite;
}
@keyframes pulse {
@@ -390,59 +467,83 @@ HTML_INDEX = """<!DOCTYPE html>
50% { transform: scale(1.15); opacity: 1; }
100% { transform: scale(0.95); opacity: 0.8; }
}
h1 { font-size: 1.1rem; font-weight: 600; letter-spacing: -0.02em; }
.controls {
display: flex;
align-items: center;
gap: 12px;
}
.pill {
background: var(--surface-card);
border: 1px solid var(--surface-border);
padding: 6px 14px;
border-radius: 20px;
font-size: 0.82rem;
color: var(--text-muted);
h1 { font-size: 0.95rem; font-weight: 600; letter-spacing: -0.01em; white-space: nowrap; }
.quick-actions {
display: flex;
align-items: center;
gap: 6px;
}
.controls {
display: flex;
align-items: center;
gap: 6px;
overflow-x: auto;
width: 100%;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
padding-bottom: 2px;
}
.controls::-webkit-scrollbar { display: none; }
.pill {
background: var(--surface-card);
border: 1px solid var(--surface-border);
padding: 4px 10px;
border-radius: 20px;
font-size: 0.76rem;
color: var(--text-muted);
display: inline-flex;
align-items: center;
gap: 4px;
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
transition: all 0.2s ease;
}
.pill:hover { border-color: var(--primary); color: var(--text-main); }
.pill strong { color: var(--text-main); font-weight: 500; }
.pill strong {
color: var(--text-main);
font-weight: 500;
max-width: 110px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
vertical-align: bottom;
}
.chat-feed {
flex: 1;
overflow-y: auto;
padding: 24px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 18px;
gap: 12px;
scroll-behavior: smooth;
}
.message-card {
display: flex;
flex-direction: column;
gap: 6px;
max-width: 820px;
gap: 4px;
max-width: 100%;
width: 100%;
animation: fadeIn 0.3s ease-out forwards;
animation: fadeIn 0.25s ease-out forwards;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
.message-card.user { align-self: flex-end; }
.message-bubble {
padding: 14px 18px;
border-radius: 16px;
font-size: 0.95rem;
line-height: 1.55;
padding: 10px 14px;
border-radius: 14px;
font-size: 0.9rem;
line-height: 1.5;
position: relative;
word-break: break-word;
overflow-wrap: anywhere;
}
.message-card.user .message-bubble {
@@ -512,11 +613,55 @@ HTML_INDEX = """<!DOCTYPE html>
.turn-text-content {
font-size: 0.95rem;
line-height: 1.55;
line-height: 1.6;
color: #f3f4f6;
}
.turn-text-content:empty {
display: none;
}
.turn-text-content p {
margin-bottom: 8px;
}
.turn-text-content p:last-child {
margin-bottom: 0;
}
.turn-text-content a {
color: #818cf8;
text-decoration: underline;
text-underline-offset: 3px;
font-weight: 500;
transition: color 0.15s;
}
.turn-text-content a:hover {
color: #a5b4fc;
}
.turn-text-content code {
background: rgba(0, 0, 0, 0.4);
border: 1px solid var(--surface-border);
padding: 2px 6px;
border-radius: 6px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.88em;
color: #e0e7ff;
}
.turn-text-content pre {
background: #090d16;
border: 1px solid var(--surface-border);
padding: 12px;
border-radius: 10px;
overflow-x: auto;
margin: 10px 0;
}
.turn-text-content pre code {
background: transparent;
border: none;
padding: 0;
}
.turn-text-content ul, .turn-text-content ol {
margin-left: 20px;
margin-top: 6px;
margin-bottom: 6px;
}
.file-link {
display: inline-flex;
@@ -724,13 +869,23 @@ HTML_INDEX = """<!DOCTYPE html>
<div class="app-container">
<div class="main-chat">
<header>
<div class="logo-group">
<div class="status-dot"></div>
<h1>VoiceAgent Companion</h1>
<div class="header-top">
<div class="logo-group">
<div class="status-dot"></div>
<h1>VoiceAgent</h1>
</div>
<div class="quick-actions">
<div class="pill" id="resetPill" onclick="resetSession()" title="Start a fresh conversation session">
🔄 <strong>Reset</strong>
</div>
<div class="pill" id="pwaInstallBtn" onclick="installPWA()" title="Install VoiceAgent Companion WebApp" style="display:none; background: linear-gradient(135deg, rgba(99, 102, 241, 0.25), rgba(168, 85, 247, 0.25)); border-color: rgba(168, 85, 247, 0.5);">
📲 <strong style="color: #c084fc;">Install</strong>
</div>
</div>
</div>
<div class="controls">
<div class="pill" id="hermesModePill" title="Hermes Mode: API (Daemon) vs CLI (Subprocess)">
Hermes: <strong id="hermesModeText" style="color: #10b981;">API (9119)</strong>
Hermes: <strong id="hermesModeText" style="color: #10b981;">API</strong>
</div>
<div class="pill" id="latencyPill" title="Turn Latency Profiling Metric">
Latency: <strong id="latencyText" style="color: #818cf8;">-- ms</strong>
@@ -741,9 +896,6 @@ HTML_INDEX = """<!DOCTYPE html>
<div class="pill" id="voicePill" onclick="openVoiceModal()">
Voice: <strong id="voiceName">Loading...</strong>
</div>
<div class="pill" id="resetPill" onclick="resetSession()" title="Start a fresh conversation session">
🔄 <strong>Reset Session</strong>
</div>
</div>
</header>
@@ -760,7 +912,7 @@ HTML_INDEX = """<!DOCTYPE html>
<!-- Bottom Text Input Bar -->
<div class="chat-input-container">
<input type="text" id="userInput" placeholder="Type a message or command (e.g. show bot.py, switch to luna, paseo ls)..." autocomplete="off" onkeydown="handleKeyDown(event)">
<input type="text" id="userInput" placeholder="Ask or type a command (e.g. show bot.py)..." autocomplete="off" onkeydown="handleKeyDown(event)">
<button class="send-btn" id="sendBtn" onclick="sendMessage()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/></svg>
</button>
@@ -799,14 +951,44 @@ HTML_INDEX = """<!DOCTYPE html>
<button class="close-btn" onclick="closeVoiceModal()">&times;</button>
</div>
<input type="text" class="search-input" id="voiceInput" placeholder="Enter voice name (e.g. af_heart, am_michael, Moira)...">
<div class="modal-buttons">
<div class="modal-buttons" style="margin-top:16px;">
<button class="btn btn-secondary" onclick="closeVoiceModal()">Cancel</button>
<button class="btn" style="background:var(--primary); color:white;" onclick="saveVoice()">Save Voice</button>
<button class="btn" style="background:var(--primary); color:white;" onclick="applyVoice()">Apply Voice</button>
</div>
</div>
</div>
<script>
// PWA Service Worker Registration & Installation logic
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('PWA ServiceWorker registered:', reg))
.catch(err => console.debug('ServiceWorker error:', err));
});
}
let deferredPrompt = null;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
const installBtn = document.getElementById('pwaInstallBtn');
if (installBtn) installBtn.style.display = 'inline-flex';
});
function installPWA() {
if (!deferredPrompt) return;
deferredPrompt.prompt();
deferredPrompt.userChoice.then((choiceResult) => {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted PWA installation');
}
deferredPrompt = null;
const installBtn = document.getElementById('pwaInstallBtn');
if (installBtn) installBtn.style.display = 'none';
});
}
const feed = document.getElementById('chatFeed');
const modelNameEl = document.getElementById('modelName');
const voiceNameEl = document.getElementById('voiceName');
@@ -823,6 +1005,11 @@ HTML_INDEX = """<!DOCTYPE html>
let currentToolsContainer = null;
let currentTextContent = null;
function formatModelName(model) {
if (!model) return 'Default';
return model.includes('/') ? model.split('/').pop() : model;
}
function connectSSE() {
const evtSource = new EventSource('/api/stream');
@@ -842,14 +1029,19 @@ HTML_INDEX = """<!DOCTYPE html>
function handleEvent(data) {
if (data.type === 'init') {
modelNameEl.textContent = data.model || 'Default';
modelNameEl.textContent = formatModelName(data.model);
modelNameEl.title = data.model || 'Default';
voiceNameEl.textContent = data.voice || 'af_heart';
activeModelId = data.model;
if (data.history && data.history.length > 0) {
data.history.forEach(ev => renderEvent(ev));
}
} else if (data.type === 'status_change') {
if (data.model) { modelNameEl.textContent = data.model; activeModelId = data.model; }
if (data.model) {
modelNameEl.textContent = formatModelName(data.model);
modelNameEl.title = data.model;
activeModelId = data.model;
}
if (data.voice) voiceNameEl.textContent = data.voice;
} else if (data.type === 'hermes_status') {
const hermesEl = document.getElementById('hermesModeText');
@@ -986,11 +1178,24 @@ HTML_INDEX = """<!DOCTYPE html>
feed.scrollTop = feed.scrollHeight;
}
function renderMarkdown(rawText) {
if (!rawText) return '';
if (typeof marked !== 'undefined' && marked.parse) {
try {
let parsed = marked.parse(rawText, { gfm: true, breaks: true });
return linkifyFiles(parsed);
} catch (e) {
console.warn("Marked parse error:", e);
}
}
return linkifyFiles(escapeHtml(rawText));
}
function appendFinalReply(text, timestamp) {
ensureAssistantCard(timestamp);
const formatted = linkifyFiles(escapeHtml(text));
const formatted = renderMarkdown(text);
currentTextContent.style.display = 'block';
currentTextContent.style.cssText = 'display: block; margin-top: 6px; font-size: 14.5px; line-height: 1.6; color: #f3f4f6; white-space: pre-wrap;';
currentTextContent.style.cssText = 'display: block; margin-top: 6px; font-size: 14.5px; line-height: 1.6; color: #f3f4f6;';
currentTextContent.innerHTML = formatted;
feed.scrollTop = feed.scrollHeight;
}
@@ -1099,7 +1304,49 @@ HTML_INDEX = """<!DOCTYPE html>
document.getElementById('voiceModalOverlay').style.display = 'none';
}
function saveVoice() {
function handleKeyDown(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
}
function sendMessage() {
const text = userInputEl.value.trim();
if (!text) return;
userInputEl.value = '';
fetch('/api/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: text })
})
.then(res => res.json())
.then(data => {
if (data.error) {
alert('Error sending message: ' + data.error);
}
})
.catch(err => {
console.error('Failed to send message:', err);
});
}
function resetSession() {
fetch('/api/session/reset', { method: 'POST' })
.then(r => r.json())
.then(res => {
if (res.error) alert(res.error);
else {
feed.innerHTML = '';
ensureAssistantCard();
currentTextContent.style.display = 'block';
currentTextContent.textContent = '🔄 Hermes session reset. Ready for a new conversation!';
}
});
}
function applyVoice() {
const val = document.getElementById('voiceInput').value.trim();
if (!val) return;
fetch('/api/voice', {
@@ -1117,6 +1364,10 @@ HTML_INDEX = """<!DOCTYPE html>
});
}
function saveVoice() {
applyVoice();
}
function escapeHtml(str) {
return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}