Initial FRIDAY Voice Console scaffold

This commit is contained in:
2026-06-25 07:24:34 -05:00
commit c2a304642a
17 changed files with 997 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
const recordButton = document.getElementById('recordButton');
const sendTextButton = document.getElementById('sendTextButton');
const textInput = document.getElementById('textInput');
const statusEl = document.getElementById('status');
const transcriptEl = document.getElementById('transcript');
const replyEl = document.getElementById('reply');
const audioReply = document.getElementById('audioReply');
let recorder;
let chunks = [];
function setStatus(text) {
statusEl.textContent = text;
}
async function askFriday(text) {
setStatus('Asking FRIDAY...');
const response = await fetch('/api/ask', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, speaker_id: null, confidence: 0 })
});
if (!response.ok) throw new Error(await response.text());
const data = await response.json();
replyEl.textContent = data.reply;
setStatus(`Authorization: ${data.authorization_level}; ${data.reason}`);
await speak(data.reply);
}
async function speak(text) {
const response = await fetch('/api/speak', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
});
if (!response.ok) return;
const blob = await response.blob();
audioReply.src = URL.createObjectURL(blob);
await audioReply.play().catch(() => {});
}
async function transcribe(blob) {
setStatus('Transcribing...');
const form = new FormData();
form.append('audio', blob, 'utterance.webm');
const response = await fetch('/api/transcribe', {
method: 'POST',
headers: { 'Content-Type': 'audio/webm' },
body: blob
});
if (!response.ok) throw new Error(await response.text());
const data = await response.json();
transcriptEl.textContent = data.text;
textInput.value = data.text;
return data.text;
}
async function startRecording() {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
chunks = [];
recorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
recorder.addEventListener('dataavailable', event => {
if (event.data.size > 0) chunks.push(event.data);
});
recorder.addEventListener('stop', async () => {
stream.getTracks().forEach(track => track.stop());
recordButton.classList.remove('recording');
recordButton.textContent = 'Hold to Talk';
try {
const text = await transcribe(new Blob(chunks, { type: 'audio/webm' }));
if (text.trim()) await askFriday(text);
} catch (error) {
setStatus(`Error: ${error.message}`);
}
});
recorder.start();
recordButton.classList.add('recording');
recordButton.textContent = 'Listening... release to send';
setStatus('Listening...');
}
function stopRecording() {
if (recorder && recorder.state === 'recording') recorder.stop();
}
recordButton.addEventListener('pointerdown', startRecording);
recordButton.addEventListener('pointerup', stopRecording);
recordButton.addEventListener('pointerleave', stopRecording);
sendTextButton.addEventListener('click', async () => {
const text = textInput.value.trim();
if (!text) return;
transcriptEl.textContent = text;
try {
await askFriday(text);
} catch (error) {
setStatus(`Error: ${error.message}`);
}
});
+36
View File
@@ -0,0 +1,36 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FRIDAY Voice Console</title>
<link rel="stylesheet" href="/static/style.css" />
</head>
<body>
<main class="shell">
<section class="card hero">
<p class="eyebrow">FRIDAY Voice Console</p>
<h1>Talk to FRIDAY</h1>
<p class="muted">Push-to-talk MVP. Audio stays inside the residence path: browser → gateway → Speaches → Hermes.</p>
<div class="controls">
<button id="recordButton">Hold to Talk</button>
<button id="sendTextButton" class="secondary">Send Text</button>
</div>
<textarea id="textInput" placeholder="Or type a request here for testing..."></textarea>
<p id="status" class="status">Idle.</p>
</section>
<section class="card">
<h2>Transcript</h2>
<pre id="transcript"></pre>
</section>
<section class="card">
<h2>FRIDAY</h2>
<pre id="reply"></pre>
<audio id="audioReply" controls></audio>
</section>
</main>
<script src="/static/app.js"></script>
</body>
</html>
+91
View File
@@ -0,0 +1,91 @@
:root {
color-scheme: dark;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #05070a;
color: #f5f7fb;
}
body {
margin: 0;
min-height: 100vh;
background: radial-gradient(circle at top left, #15324a 0, #05070a 42rem);
}
.shell {
display: grid;
gap: 1rem;
max-width: 960px;
margin: 0 auto;
padding: 2rem;
}
.card {
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 20px;
padding: 1.25rem;
background: rgba(7, 12, 18, 0.82);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
}
.hero h1 {
margin: 0 0 0.5rem;
font-size: clamp(2.25rem, 8vw, 4.5rem);
}
.eyebrow {
color: #80d8ff;
text-transform: uppercase;
letter-spacing: 0.18em;
font-weight: 700;
}
.muted, .status {
color: #aeb8c5;
}
.controls {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
margin: 1rem 0;
}
button {
border: 0;
border-radius: 999px;
padding: 0.85rem 1.25rem;
color: #03121f;
background: #80d8ff;
font-weight: 800;
cursor: pointer;
}
button.recording {
background: #ff6b6b;
color: white;
}
button.secondary {
color: #f5f7fb;
background: rgba(255, 255, 255, 0.14);
}
textarea {
width: 100%;
min-height: 6rem;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.35);
color: #f5f7fb;
padding: 1rem;
box-sizing: border-box;
}
pre {
white-space: pre-wrap;
color: #dbeafe;
}
audio {
width: 100%;
}