Initial FRIDAY Voice Console scaffold
This commit is contained in:
+98
@@ -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}`);
|
||||
}
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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%;
|
||||
}
|
||||
Reference in New Issue
Block a user