feat: add Hermes WebUI provisioning and English Dragon HD Omni voices
Rebuild & Deploy / rebuild (push) Successful in 2m17s

This commit is contained in:
snowspeeder committed 2026-10-02 16:07:24 -04:00
1 parent 47cc9c88a0
commit 6b8d55fc92
59 files changed
+1175 -70

No files matched your search

+68 -19
View File
@@ -8,20 +8,11 @@ const { Room, RoomEvent } = LivekitClient;
const LIVEKIT_URL = `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/livekit`;
const AGENT_NAME = "voice-assistant";
const VOICES = [
{ id: "en-US-Ava:DragonHDOmniLatestNeural", name: "Ava", tag: "Dragon HD Omni" },
{ id: "en-US-JennyNeural", name: "Jenny" },
{ id: "en-US-GuyNeural", name: "Guy" },
{ id: "en-US-AndrewNeural", name: "Andrew" },
{ id: "en-US-AriaNeural", name: "Aria" },
{ id: "en-US-EmmaNeural", name: "Emma" },
{ id: "en-US-EricNeural", name: "Eric" },
{ id: "en-US-BrianNeural", name: "Brian" },
{ id: "en-US-AshleyNeural", name: "Ashley" },
{ id: "en-US-RichardNeural", name: "Richard" },
{ id: "en-US-TinaNeural", name: "Tina" },
{ id: "en-US-SteffanNeural", name: "Steffan" },
];
const OMNI_DEFAULT = "en-US-Ava:DragonHDOmniLatestNeural";
let defaultVoice = OMNI_DEFAULT;
let VOICES = [{ id: OMNI_DEFAULT, name: "Ava", locale: "en-US", gender: "Female", tag: "Dragon HD Omni" }];
let voiceCatalogRequest = null;
let voiceCatalogGeneration = 0;
let historyKey = null;
const HISTORY_MAX = 50;
@@ -574,6 +565,7 @@ function handleDataPacket(payload, participant, topic) {
if (!HopeAuth.user || !participant || !isAgent(participant)) return;
if (msg.type === "agent_ready" && topic === "conversation-control") {
agentReady = true;
sendVoiceMessage(currentVoiceId());
} else if (msg.type === "transcript" && topic === "transcript") {
if (!["agent", "user"].includes(msg.role) || typeof msg.text !== "string") return;
addMessage(msg.role, msg.text);
@@ -745,21 +737,71 @@ modelSettingsForm.addEventListener("submit", async event => {
// ── Voice settings sheet ────────────────────────────────────────────────────
function currentVoiceId() {
const saved = localStorage.getItem("preferred_voice");
return VOICES.some((v) => v.id === saved) ? saved : VOICES[0].id;
let saved = localStorage.getItem("preferred_voice");
// Migrate older Dragon HD selections to the equivalent Omni persona.
if (saved?.endsWith(":DragonHDLatestNeural")) {
saved = saved.replace(":DragonHDLatestNeural", ":DragonHDOmniLatestNeural");
localStorage.setItem("preferred_voice", saved);
}
return VOICES.some((v) => v.id === saved) ? saved : defaultVoice;
}
const voiceSearchEl = document.getElementById("voiceSearch");
const voiceStatusEl = document.getElementById("voiceCatalogStatus");
const retryVoicesBtn = document.getElementById("retryVoices");
voiceSearchEl.addEventListener("input", renderVoiceList);
retryVoicesBtn.addEventListener("click", loadVoiceCatalog);
async function loadVoiceCatalog() {
if (!HopeAuth.user) return;
if (voiceCatalogRequest) return voiceCatalogRequest;
const generation = voiceCatalogGeneration;
voiceStatusEl.textContent = "Loading English voices from Azure…";
retryVoicesBtn.hidden = true;
voiceCatalogRequest = (async () => {
try {
const data = await HopeAuth.request("/voices");
if (generation !== voiceCatalogGeneration) return;
const voices = Array.isArray(data.voices) ? data.voices.filter(v =>
v && typeof v.id === "string" && /^[A-Za-z0-9:_-]{1,120}$/.test(v.id) && typeof v.name === "string") : [];
if (!voices.length) throw new Error("Empty voice catalog");
VOICES = voices;
defaultVoice = VOICES.some(v => v.id === data.default_voice) ? data.default_voice : OMNI_DEFAULT;
voiceStatusEl.textContent = data.catalog_available
? `${VOICES.length} English voices · Dragon HD Omni first. Ava Omni is the recommended female default.`
: (data.message || "Azure’s full catalog is unavailable. Ava Omni remains available; retry to load all English voices.");
retryVoicesBtn.hidden = data.catalog_available === true;
renderVoiceList();
} catch (_) {
if (generation !== voiceCatalogGeneration) return;
voiceStatusEl.textContent = "Couldn’t load Azure’s full catalog. Your current choices remain available. Retry to load all English voices.";
retryVoicesBtn.hidden = false;
} finally {
if (generation === voiceCatalogGeneration) voiceCatalogRequest = null;
}
})();
return voiceCatalogRequest;
}
function renderVoiceList() {
voiceListEl.innerHTML = "";
voiceListEl.replaceChildren();
const active = currentVoiceId();
for (const v of VOICES) {
const query = voiceSearchEl.value.trim().toLowerCase();
const visible = VOICES.filter(v => `${v.name} ${v.locale || ""} ${v.gender || ""} ${v.tag || ""}`.toLowerCase().includes(query));
if (!visible.length) {
const empty = document.createElement("p");
empty.className = "model-settings-note";
empty.textContent = "No matching voices. Try a name, accent locale, gender, or model.";
voiceListEl.appendChild(empty);
}
for (const v of visible) {
const card = document.createElement("div");
card.className = "voice-card" + (v.id === active ? " active" : "");
card.dataset.voice = v.id;
const name = document.createElement("span");
name.className = "voice-name";
name.textContent = v.name + (v.tag ? ` (${v.tag})` : "");
name.textContent = `${v.name} · ${v.locale || "English"} · ${v.gender || ""}` + (v.tag ? ` (${v.tag})` : "");
const selected = v.id === active;
card.setAttribute("role", "radio");
@@ -790,6 +832,7 @@ function selectVoice(voiceId) {
function openSheet() {
if (!HopeAuth.user) return;
renderVoiceList();
loadVoiceCatalog();
settingsSheet.hidden = false; settingsSheet.inert = false;
document.getElementById("authenticatedApp").inert = true;
sheetBackdrop.hidden = false;
@@ -1130,6 +1173,12 @@ HopeAuth.onChange((user, changed) => {
if (changed || !user || user.status !== "ready") stopConversation();
if (changed || settingsAccount !== (user ? String(user.id) : null)) resetModelSettings(user);
if (changed) {
voiceCatalogGeneration++;
voiceCatalogRequest = null;
VOICES = [{ id: OMNI_DEFAULT, name: "Ava", locale: "en-US", gender: "Female", tag: "Dragon HD Omni" }];
defaultVoice = OMNI_DEFAULT;
voiceSearchEl.value = "";
if (user) loadVoiceCatalog();
historyKey = user ? `hope_history:${location.origin}:${encodeURIComponent(user.id)}` : null;
history = loadHistory(); renderHistory(); progress.clear(); tasksList.replaceChildren(); tasksBadge.hidden = true;
chatInput.value = ""; sendChatBtn.disabled = true;