d901208032
- pipeline.py: switched LLM from OpenClaw/Claude to local Ollama, added settings loading, sentence aggregator, async SpeechBrain, tuned VAD - main.py: simplified (no more token server thread, just pipeline) - token_server.py: NEW - standalone token + settings API server - docker-compose.yml: all services on GPU machine, added token-server service, CUDA speaches, Ollama LLM - web-client/index.html: added settings panel with sliders - web-client/app.js: settings management, localStorage persistence, push to server - web-client/style.css: settings panel styles - web-client/nginx.conf: proxy to token-server instead of pipecat-agent - patch_livekit.py: updated with proper indentation-safe patching
200 lines
6.1 KiB
JavaScript
Executable File
200 lines
6.1 KiB
JavaScript
Executable File
const LIVEKIT_URL = "wss://voice.hobbs.farm";
|
|
let room = null;
|
|
|
|
// --- Settings ---
|
|
const DEFAULTS = {
|
|
confidence: 0.7, min_volume: 0.3, start_secs: 0.4,
|
|
stop_secs: 0.5, max_tokens: 150, tts_voice: "af_nova"
|
|
};
|
|
|
|
function getSettings() {
|
|
try {
|
|
const saved = JSON.parse(localStorage.getItem("vc_settings"));
|
|
return saved ? { ...DEFAULTS, ...saved } : { ...DEFAULTS };
|
|
} catch (e) { return { ...DEFAULTS }; }
|
|
}
|
|
|
|
function loadSettingsUI() {
|
|
const s = getSettings();
|
|
for (const [key, val] of Object.entries(s)) {
|
|
const input = document.getElementById("set-" + key);
|
|
const display = document.getElementById("val-" + key);
|
|
if (input) input.value = val;
|
|
if (display) display.textContent = val;
|
|
}
|
|
// Bind live value display
|
|
document.querySelectorAll(".settings-body input[type=range]").forEach(function(el) {
|
|
el.addEventListener("input", function() {
|
|
const key = el.id.replace("set-", "");
|
|
document.getElementById("val-" + key).textContent = el.value;
|
|
});
|
|
});
|
|
var sel = document.getElementById("set-tts_voice");
|
|
if (sel) sel.addEventListener("change", function() {
|
|
document.getElementById("val-tts_voice").textContent = sel.value;
|
|
});
|
|
}
|
|
|
|
function saveSettings() {
|
|
var s = {};
|
|
for (var key of Object.keys(DEFAULTS)) {
|
|
var el = document.getElementById("set-" + key);
|
|
if (el) s[key] = el.tagName === "SELECT" ? el.value : parseFloat(el.value);
|
|
}
|
|
localStorage.setItem("vc_settings", JSON.stringify(s));
|
|
// Push to server
|
|
fetch("/api/settings", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(s),
|
|
}).then(function(r) {
|
|
if (r.ok) log("Settings saved and applied");
|
|
else log("Settings saved locally but server update failed", "warn");
|
|
}).catch(function() {
|
|
log("Settings saved locally (server unreachable)", "warn");
|
|
});
|
|
}
|
|
|
|
function resetSettings() {
|
|
localStorage.removeItem("vc_settings");
|
|
loadSettingsUI();
|
|
saveSettings();
|
|
log("Settings reset to defaults");
|
|
}
|
|
|
|
function toggleSettings() {
|
|
var body = document.getElementById("settingsBody");
|
|
var toggle = document.getElementById("settingsToggle");
|
|
if (body.style.display === "none") {
|
|
body.style.display = "block";
|
|
toggle.textContent = "-";
|
|
} else {
|
|
body.style.display = "none";
|
|
toggle.textContent = "+";
|
|
}
|
|
}
|
|
|
|
// --- Logging ---
|
|
function log(msg, level) {
|
|
var el = document.getElementById("logOutput");
|
|
var time = new Date().toLocaleTimeString();
|
|
var 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 = "";
|
|
}
|
|
|
|
// --- Connection ---
|
|
async function connect() {
|
|
setStatus("connecting", "Connecting...");
|
|
log("Requesting token...");
|
|
|
|
// Push current settings before connecting
|
|
var settings = getSettings();
|
|
try {
|
|
await fetch("/api/settings", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(settings),
|
|
});
|
|
log("Settings pushed: confidence=" + settings.confidence + " vol=" + settings.min_volume);
|
|
} catch (e) {
|
|
log("Could not push settings", "warn");
|
|
}
|
|
|
|
try {
|
|
var response = await fetch("/api/token", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ identity: "jeremy-" + Date.now() }),
|
|
});
|
|
if (!response.ok) throw new Error("Token " + response.status);
|
|
var data = await response.json();
|
|
log("Token received");
|
|
|
|
room = new LivekitClient.Room({
|
|
audioCaptureDefaults: {
|
|
echoCancellation: true,
|
|
noiseSuppression: true,
|
|
autoGainControl: true,
|
|
},
|
|
});
|
|
|
|
room.on(LivekitClient.RoomEvent.Connected, function() {
|
|
setStatus("connected", "Connected");
|
|
document.getElementById("connectBtn").style.display = "none";
|
|
document.getElementById("disconnectBtn").style.display = "block";
|
|
log("Connected to LiveKit");
|
|
});
|
|
|
|
room.on(LivekitClient.RoomEvent.Disconnected, function(reason) {
|
|
setStatus("disconnected", "Disconnected");
|
|
document.getElementById("connectBtn").style.display = "block";
|
|
document.getElementById("disconnectBtn").style.display = "none";
|
|
log("Disconnected" + (reason ? ": " + reason : ""), "warn");
|
|
});
|
|
|
|
room.on(LivekitClient.RoomEvent.ParticipantConnected, function(p) {
|
|
log("Agent joined: " + p.identity);
|
|
});
|
|
|
|
room.on(LivekitClient.RoomEvent.TrackSubscribed, function(track, pub, p) {
|
|
if (track.kind === LivekitClient.Track.Kind.Audio) {
|
|
var el = track.attach();
|
|
document.body.appendChild(el);
|
|
el.play().catch(function(e) { log("Audio play error: " + e.message, "error"); });
|
|
}
|
|
});
|
|
|
|
room.on(LivekitClient.RoomEvent.MediaDevicesError, function(err) {
|
|
log("Mic error: " + err.message, "error");
|
|
});
|
|
|
|
await room.connect(data.url || LIVEKIT_URL, data.token);
|
|
await room.localParticipant.setMicrophoneEnabled(true);
|
|
log("Mic enabled. Speak now.");
|
|
|
|
} catch (err) {
|
|
setStatus("disconnected", "Error: " + err.message);
|
|
log("Error: " + err.message, "error");
|
|
}
|
|
}
|
|
|
|
async function disconnect() {
|
|
if (room) { await room.disconnect(); room = null; }
|
|
}
|
|
|
|
function setMode(mode) {
|
|
document.getElementById("modeDisplay").textContent = "Mode: " + mode;
|
|
log("Mode: " + mode);
|
|
if (room && room.localParticipant) {
|
|
room.localParticipant.publishData(
|
|
new TextEncoder().encode(JSON.stringify({ type: "mode_change", mode })),
|
|
{ reliable: true }
|
|
);
|
|
}
|
|
}
|
|
|
|
function setStatus(state, text) {
|
|
var el = document.getElementById("status");
|
|
el.className = "status " + state;
|
|
el.textContent = text;
|
|
}
|
|
|
|
document.getElementById("connectBtn").addEventListener("click", connect);
|
|
document.getElementById("disconnectBtn").addEventListener("click", disconnect);
|
|
|
|
// Init
|
|
loadSettingsUI();
|
|
// Push saved settings on load
|
|
fetch("/api/settings", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(getSettings()),
|
|
}).catch(function() {});
|
|
|
|
log("VoiceCoach loaded. Ready.");
|