feat: FRIDAY Voice stack on the GPU machine; VoiceCoach history
FRIDAY Voice (speaches GPU STT/TTS + hermes agent + push-to-talk console) on .150 Docker Desktop, fronted by coach.hobbs.farm. Also captures the VoiceCoach compose/agent trees and deploy notes it replaced, which had never been committed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
const LIVEKIT_URL = "wss://voice.hobbs.farm";
|
||||
let room = null;
|
||||
|
||||
function log(msg, level) {
|
||||
const el = document.getElementById("logOutput");
|
||||
const time = new Date().toLocaleTimeString();
|
||||
const cls = level === "error" ? "log-error" : level === "warn" ? "log-warn" : "log-info";
|
||||
el.innerHTML += `<div class="${cls}"><span class="log-time">${time}</span> ${msg}</div>`;
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
document.getElementById("logOutput").innerHTML = "";
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
setStatus("connecting", "Connecting...");
|
||||
log("Requesting token from server...");
|
||||
try {
|
||||
const response = await fetch("/api/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ identity: "jeremy-" + Date.now() }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error("Token request failed: " + response.status + " " + text);
|
||||
}
|
||||
const data = await response.json();
|
||||
log("Token received. LiveKit URL: " + data.url);
|
||||
|
||||
room = new LivekitClient.Room({
|
||||
audioCaptureDefaults: {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true,
|
||||
},
|
||||
});
|
||||
|
||||
room.on(LivekitClient.RoomEvent.Connected, () => {
|
||||
setStatus("connected", "Connected");
|
||||
document.getElementById("connectBtn").style.display = "none";
|
||||
document.getElementById("disconnectBtn").style.display = "block";
|
||||
log("Connected to LiveKit room");
|
||||
log("Participants in room: " + room.remoteParticipants.size);
|
||||
if (room.remoteParticipants.size === 0) {
|
||||
log("No agent participant yet - waiting for voicecoach-agent to join...", "warn");
|
||||
}
|
||||
});
|
||||
|
||||
room.on(LivekitClient.RoomEvent.Disconnected, (reason) => {
|
||||
setStatus("disconnected", "Disconnected");
|
||||
document.getElementById("connectBtn").style.display = "block";
|
||||
document.getElementById("disconnectBtn").style.display = "none";
|
||||
log("Disconnected from LiveKit" + (reason ? ": " + reason : ""), "warn");
|
||||
});
|
||||
|
||||
room.on(LivekitClient.RoomEvent.ParticipantConnected, (participant) => {
|
||||
log("Participant joined: " + participant.identity);
|
||||
});
|
||||
|
||||
room.on(LivekitClient.RoomEvent.ParticipantDisconnected, (participant) => {
|
||||
log("Participant left: " + participant.identity, "warn");
|
||||
});
|
||||
|
||||
room.on(LivekitClient.RoomEvent.TrackSubscribed, (track, publication, participant) => {
|
||||
log("Track subscribed: " + track.kind + " from " + participant.identity);
|
||||
if (track.kind === LivekitClient.Track.Kind.Audio) {
|
||||
const element = track.attach();
|
||||
document.body.appendChild(element);
|
||||
element.play().then(() => {
|
||||
log("Audio playback started");
|
||||
}).catch((err) => {
|
||||
log("Audio play failed: " + err.message, "error");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
room.on(LivekitClient.RoomEvent.TrackPublished, (publication, participant) => {
|
||||
log("Track published by " + participant.identity + ": " + publication.kind);
|
||||
});
|
||||
|
||||
room.on(LivekitClient.RoomEvent.TrackUnsubscribed, (track, publication, participant) => {
|
||||
log("Track unsubscribed: " + track.kind + " from " + participant.identity, "warn");
|
||||
});
|
||||
|
||||
room.on(LivekitClient.RoomEvent.DataReceived, (payload, participant) => {
|
||||
try {
|
||||
const msg = new TextDecoder().decode(payload);
|
||||
log("Data from " + (participant ? participant.identity : "server") + ": " + msg);
|
||||
} catch (e) {
|
||||
log("Data received (binary, " + payload.byteLength + " bytes)");
|
||||
}
|
||||
});
|
||||
|
||||
room.on(LivekitClient.RoomEvent.ConnectionQualityChanged, (quality, participant) => {
|
||||
if (participant.isLocal) {
|
||||
log("Connection quality: " + quality);
|
||||
}
|
||||
});
|
||||
|
||||
room.on(LivekitClient.RoomEvent.MediaDevicesError, (err) => {
|
||||
log("Media device error: " + err.message, "error");
|
||||
});
|
||||
|
||||
room.on(LivekitClient.RoomEvent.RoomMetadataChanged, (metadata) => {
|
||||
log("Room metadata changed: " + metadata);
|
||||
});
|
||||
|
||||
log("Connecting to " + LIVEKIT_URL + "...");
|
||||
await room.connect(data.url || LIVEKIT_URL, data.token);
|
||||
|
||||
log("Enabling microphone...");
|
||||
await room.localParticipant.setMicrophoneEnabled(true);
|
||||
log("Microphone enabled. Speak now.");
|
||||
|
||||
// Log local tracks
|
||||
room.localParticipant.audioTrackPublications.forEach((pub) => {
|
||||
log("Local audio track: " + pub.trackSid + " muted=" + pub.isMuted);
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
setStatus("disconnected", "Error: " + err.message);
|
||||
log("Connection error: " + err.message, "error");
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
if (room) {
|
||||
log("Disconnecting...");
|
||||
await room.disconnect();
|
||||
room = null;
|
||||
}
|
||||
}
|
||||
|
||||
function setMode(mode) {
|
||||
document.getElementById("modeDisplay").textContent = "Mode: " + mode;
|
||||
log("Mode changed to: " + mode);
|
||||
if (room && room.localParticipant) {
|
||||
const encoder = new TextEncoder();
|
||||
room.localParticipant.publishData(
|
||||
encoder.encode(JSON.stringify({ type: "mode_change", mode })),
|
||||
{ reliable: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(state, text) {
|
||||
const el = document.getElementById("status");
|
||||
el.className = "status " + state;
|
||||
el.textContent = text;
|
||||
}
|
||||
|
||||
document.getElementById("connectBtn").addEventListener("click", connect);
|
||||
document.getElementById("disconnectBtn").addEventListener("click", disconnect);
|
||||
|
||||
log("VoiceCoach client loaded. Ready to connect.");
|
||||
Reference in New Issue
Block a user