Initial commit: полный проект my_speaker

This commit is contained in:
2025-12-25 12:16:19 +03:00
commit 8279fbb35f
85 changed files with 6027 additions and 0 deletions
View File
+29
View File
@@ -0,0 +1,29 @@
import { getSettings, updateSettings } from "../api.js";
export async function loadCommands() {
const content = document.getElementById("content");
const commandsHtml = await fetch("pages/commands.html").then((res) =>
res.text()
);
content.innerHTML = commandsHtml;
const settings = await getSettings();
document.getElementById("commands-file").value =
settings.commands.commands_file;
// Предполагаем, что API возвращает список команд, но по docs — только файл. Для списка — заглушка.
const commandsList = document.getElementById("commands-list");
commandsList.innerHTML = '<p>"Turn on light"</p><p>"Play music"</p>'; // Заглушка, расширить если нужно
document.getElementById("edit-btn").addEventListener("click", () => {
alert("Edit mode"); // Логика редактирования
});
document.getElementById("save-btn").addEventListener("click", async () => {
const data = {
commands_file: document.getElementById("commands-file").value,
};
await updateSettings("commands", data);
alert("Saved");
});
}
+196
View File
@@ -0,0 +1,196 @@
import {
getSpeechData,
getSysMonitor,
getRecentPhrases,
getSettings,
} from "../api.js";
let speechChart;
let energyChart;
let historyMaxLen = 20; // Значение по умолчанию в области видимости модуля
export async function loadDashboard() {
const content = document.getElementById("content");
const dashboardHtml = await fetch("pages/dashboard.html").then((res) =>
res.text()
);
content.innerHTML = dashboardHtml;
// Инициализация графика речи
const speechCtx = document.getElementById("speech-chart").getContext("2d");
speechChart = new Chart(speechCtx, {
type: "line",
data: {
labels: [],
datasets: [
{
label: "Speech Prob",
data: [],
borderColor: "#0f0",
backgroundColor: "rgba(0, 255, 0, 0.1)",
borderWidth: 2,
pointRadius: 0,
fill: false,
},
{
label: "Speech Threshold",
data: [],
borderColor: "#ffcc00",
borderWidth: 1,
pointRadius: 0,
borderDash: [5, 5],
fill: false,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 1,
title: {
display: true,
text: "Speech Probability",
},
},
},
},
});
// Инициализация графика энергии
const energyCtx = document.getElementById("energy-chart").getContext("2d");
energyChart = new Chart(energyCtx, {
type: "line",
data: {
labels: [],
datasets: [
{
label: "Energy",
data: [],
borderColor: "#ff00ff",
backgroundColor: "rgba(255, 0, 255, 0.1)",
borderWidth: 2,
pointRadius: 0,
fill: false,
},
{
label: "Energy Threshold",
data: [],
borderColor: "#ff6600",
borderWidth: 1,
pointRadius: 0,
borderDash: [5, 5],
fill: false,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: "Energy Level",
},
},
},
},
});
// Получение и обработка настроек
let settings;
try {
settings = await getSettings();
} catch (error) {
console.error("Failed to load settings:", error);
settings = { system: { polling_interval_ms: 1000, history_maxlen: 20 } }; // Значения по умолчанию
}
const pollingIntervalMs = settings.system.polling_interval_ms || 1000;
historyMaxLen = settings.system.history_maxlen || 20; // Обновляем глобальную переменную
// Установка интервала опроса и передача значения в хедер
setInterval(updateDashboard, pollingIntervalMs);
updateDashboard();
window.dispatchEvent(
new CustomEvent("updateHeader", { detail: pollingIntervalMs })
);
}
async function updateDashboard() {
try {
const [speech, sys, phrases] = await Promise.all([
getSpeechData(),
getSysMonitor(),
getRecentPhrases(5),
]);
// Update main-params
document.getElementById("status").textContent = `Status: ${speech.state}`;
// Обновление карточки речи
document.getElementById("speech-value").textContent =
speech.speech_prob.toFixed(2);
document.getElementById("speech-max").textContent =
speech.speech_threshold.toFixed(2);
document.getElementById("sp-detect").textContent = speech.speech_detected
? "True"
: "False";
// Обновление карточки энергии
document.getElementById("energy-value").textContent = Math.round(
speech.energy
);
document.getElementById("energy-max").textContent = Math.round(
speech.energy_threshold
);
document.getElementById("noise-floor").textContent = Math.round(
speech.noise_floor
);
// Обновление системного монитора
document.getElementById("cpu").textContent = `${sys.cpu_percent}%`;
document.getElementById("mem").textContent = `${sys.memory_percent}%`;
document.getElementById("temp").textContent = `${sys.temperature}°C`;
// Update speech chart
const now = new Date();
const timestamp =
now.toLocaleTimeString() +
"." +
now.getMilliseconds().toString().padStart(3, "0");
speechChart.data.labels.push(timestamp);
speechChart.data.datasets[0].data.push(speech.speech_prob);
speechChart.data.datasets[1].data.push(speech.speech_threshold);
if (speechChart.data.labels.length > historyMaxLen) {
speechChart.data.labels.shift();
speechChart.data.datasets.forEach((ds) => ds.data.shift());
}
speechChart.update("none"); // Используем 'none' для оптимизации при частых обновлениях
// Update energy chart
energyChart.data.labels.push(timestamp);
energyChart.data.datasets[0].data.push(speech.energy);
energyChart.data.datasets[1].data.push(speech.energy_threshold);
if (energyChart.data.labels.length > historyMaxLen) {
energyChart.data.labels.shift();
energyChart.data.datasets.forEach((ds) => ds.data.shift());
}
energyChart.update("none"); // Используем 'none' для оптимизации при частых обновлениях
// Update footer
const footer = document.getElementById("footer");
footer.innerHTML = phrases
.map(
(p) =>
`<p class="history-item">${p.timestamp} - ${
p.action || "неизвестно"
} - ${p.text}</p>`
)
.join("");
} catch (error) {
console.error("Error updating dashboard:", error);
}
}
+37
View File
@@ -0,0 +1,37 @@
import { logout as apiLogout } from "../api.js";
export function renderHeader(pollingIntervalMs) {
// Установка имени пользователя (заглушка)
const userNameElement = document.getElementById("user-name");
if (userNameElement) {
userNameElement.textContent = "User: admin"; // Заглушка, заменить на данные из /auth/me
}
// Отображение частоты опроса
const pollingIntervalElement = document.getElementById("polling-interval");
if (pollingIntervalElement) {
pollingIntervalElement.textContent = `Polling Interval: ${
pollingIntervalMs || 1000
} ms`; // Значение по умолчанию 1000 мс
}
// Отображение текущего времени
const currentTimeElement = document.getElementById("current-time");
if (currentTimeElement) {
function updateTime() {
const now = new Date().toLocaleTimeString("ru-RU", {
timeZone: "Europe/Moscow",
hour12: false,
});
currentTimeElement.textContent = `Current Time: ${now}`;
}
updateTime(); // Инициализация
setInterval(updateTime, 1000); // Обновление каждую секунду
}
// Обработчик кнопки выхода
const logoutBtn = document.getElementById("logout-btn");
if (logoutBtn) {
logoutBtn.addEventListener("click", apiLogout);
}
}
+55
View File
@@ -0,0 +1,55 @@
import { login as apiLogin, setBackendUrl } from "../api.js";
import { initRouter } from "./router.js";
import { setBgColor, setMainColor, setBorderColor } from "./utils.js";
export async function loadLogin() {
const app = document.getElementById("app");
app.innerHTML = "";
const loginHtml = await fetch("pages/login.html").then((res) => res.text());
app.insertAdjacentHTML("beforeend", loginHtml);
const form = document.getElementById("login-form");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
try {
const data = await apiLogin(username, password);
localStorage.setItem("token", data.access_token);
initRouter();
} catch (err) {
document.getElementById("error").textContent = "ERROR: Invalid login";
}
});
// Settings
const settingsBtn = document.getElementById("settings-btn");
settingsBtn.addEventListener("click", () => {
const settings = document.getElementById("settings-form");
settings.style.display =
settings.style.display === "none" ? "block" : "none";
});
const colorInput = document.getElementById("main-color");
colorInput.value = localStorage.getItem("mainColor") || "#0f0";
colorInput.addEventListener("change", (e) => {
setMainColor(e.target.value);
});
const colorBorderInput = document.getElementById("border-color");
colorBorderInput.value = localStorage.getItem("borderColor") || "#0f0";
colorBorderInput.addEventListener("change", (e) => {
setBorderColor(e.target.value);
});
const colorBgInput = document.getElementById("bg-color");
colorBgInput.value = localStorage.getItem("bgColor") || "#000";
colorBgInput.addEventListener("change", (e) => {
setBgColor(e.target.value);
});
const backendInput = document.getElementById("backend-url");
backendInput.value =
localStorage.getItem("backendUrl") || "http://localhost:8000";
backendInput.addEventListener("change", (e) => {
setBackendUrl(e.target.value);
});
}
+5
View File
@@ -0,0 +1,5 @@
export function renderNav(currentSection) {
document.querySelectorAll("nav button").forEach((btn) => {
btn.classList.toggle("active", btn.dataset.section === currentSection); // Используем переданный currentSection
});
}
+78
View File
@@ -0,0 +1,78 @@
import { loadDashboard } from "./dashboard.js";
import { loadSettings } from "./settings.js";
import { loadCommands } from "./commands.js";
import { renderHeader } from "./header.js";
import { renderNav } from "./nav.js";
import { apiFetch } from "../api.js";
let currentSection = "dashboard"; // Локальная переменная в модуле
export async function initRouter() {
const app = document.getElementById("app");
app.innerHTML = "";
// Загрузка header и nav
const headerHtml = await fetch("components/header.html").then((res) =>
res.text()
);
app.insertAdjacentHTML("beforeend", headerHtml);
renderHeader(1000); // Начальное значение до получения настроек
const navHtml = await fetch("components/nav.html").then((res) => res.text());
app.insertAdjacentHTML("beforeend", navHtml);
renderNav(currentSection);
// Основной контейнер
app.insertAdjacentHTML(
"beforeend",
'<div class="main-container"><div id="main-params" class="main-params"></div><div id="content" class="content"></div></div><footer id="footer"></footer>'
);
// Загрузка main-params
const mainParamsHtml = await fetch("pages/main-params.html").then((res) =>
res.text()
);
document.getElementById("main-params").innerHTML = mainParamsHtml;
// Футер
updateFooter();
// Навигация
document.querySelectorAll("nav button").forEach((btn) => {
btn.addEventListener("click", () => {
currentSection = btn.dataset.section;
renderNav(currentSection); // Обновляем навигацию
loadSection();
});
});
// Обработка события обновления хедера
window.addEventListener("updateHeader", (event) => {
renderHeader(event.detail); // Обновляем хедер с полученным pollingIntervalMs
});
loadSection();
}
async function loadSection() {
switch (currentSection) {
case "dashboard":
loadDashboard();
break;
case "settings":
loadSettings();
break;
case "commands":
loadCommands();
break;
}
}
async function updateFooter() {
const phrases = await apiFetch("/dashboard/recent-phrases?limit=5");
const footer = document.getElementById("footer");
footer.innerHTML =
'== RECENT PHRASES ==<div class="phrases">' +
phrases.map((p) => `<p>${p.text}</p>`).join("") +
"</div>";
}
+197
View File
@@ -0,0 +1,197 @@
import { getSettings, updateSettings, restart } from "../api.js";
export async function loadSettings() {
const content = document.getElementById("content");
if (!content) {
console.error("Content element not found");
return;
}
// Показать индикатор загрузки
content.innerHTML = '<div class="loading">Loading settings...</div>';
try {
const settingsHtml = await fetch("pages/settings.html").then((res) =>
res.text()
);
content.innerHTML = settingsHtml;
const allSettings = await getSettings();
// Заполнение форм
fillForm("vad-form", allSettings.vad);
fillForm("system-form", allSettings.system);
fillForm("stt-form", allSettings.stt);
fillForm("commands-form", allSettings.commands);
fillForm("auth-form", allSettings.auth);
fillForm("tts-form", allSettings.tts);
fillForm("audio-settings-form", allSettings.audio_settings);
// Инициализация аккордеона
initAccordion();
// Обработчики кнопок сохранения
document.querySelectorAll(".save-btn").forEach((btn) => {
btn.addEventListener("click", async () => {
const section = btn.dataset.section;
const formId = section.replace('_', '-') + '-form';
const form = document.getElementById(formId);
if (!form) {
console.error(`Form ${formId} not found`);
alert(`Form for ${section} not found`);
return;
}
// Собираем данные формы вручную
const formData = {};
// Обрабатываем все поля ввода
form.querySelectorAll("input, select").forEach((input) => {
if (input.type === "checkbox") {
formData[input.name] = input.checked;
} else if (input.value !== "") {
formData[input.name] = input.value;
}
});
// Выводим данные в консоль для отладки
console.log("Отправляемые данные:", formData);
try {
await updateSettings(section, formData);
alert(
`${
section.charAt(0).toUpperCase() + section.slice(1)
} settings saved successfully`
);
} catch (error) {
console.error(`Failed to save ${section} settings:`, error);
alert(`Failed to save ${section} settings`);
}
});
});
// Обработчик переинициализации VAD
const restartBtn = document.getElementById("restart-btn");
if (restartBtn) {
restartBtn.addEventListener("click", async () => {
if (confirm("Are you sure you want to restart VAD?")) {
try {
await restart();
alert("VAD restarted successfully");
} catch (error) {
console.error("Failed to restart VAD:", error);
alert("Failed to restart VAD");
}
}
});
}
} catch (error) {
console.error("Error loading settings:", error);
content.innerHTML =
'<div class="error">Failed to load settings. Check console for details.</div>';
}
}
// Функция инициализации аккордеона
function initAccordion() {
// Получаем все заголовки секций
const sectionHeaders = document.querySelectorAll(".section-header");
// Добавляем обработчик клика для каждого заголовка
sectionHeaders.forEach((header) => {
header.addEventListener("click", function () {
// Получаем содержимое секции
const content = this.nextElementSibling;
// Если секция уже активна, просто сворачиваем её
if (!this.classList.contains("collapsed")) {
this.classList.add("collapsed");
content.classList.remove("active");
return;
}
// Сворачиваем все секции
document.querySelectorAll(".section-header").forEach((h) => {
h.classList.add("collapsed");
h.nextElementSibling.classList.remove("active");
});
// Разворачиваем текущую секцию
this.classList.remove("collapsed");
content.classList.add("active");
});
});
}
function fillForm(formId, data) {
const form = document.getElementById(formId);
if (!form) {
console.warn(`Form ${formId} not found`);
return;
}
for (const [key, value] of Object.entries(data)) {
const input = form.querySelector(`[name="${key}"]`);
if (input) {
if (input.type === "checkbox") {
input.checked = !!value; // Приводим к булевому типу
} else {
input.value = value !== undefined ? value : "";
}
} else {
console.warn(`Input for ${key} not found in ${formId}`);
}
}
}
function processFormData(section, formData) {
const data = {};
for (let [key, value] of formData) {
if (value === "") continue; // Пропускаем пустые значения
switch (key) {
case "mode":
case "sample_rate":
case "frame_duration_ms":
case "ring_buffer_size":
case "energy_threshold":
case "min_speech_duration_ms":
case "max_record_duration_sec":
case "target_rms":
case "max_gain":
case "history_maxlen":
case "polling_interval_ms":
case "port":
case "timeout":
case "playback_device":
case "capture_device":
case "playback_volume":
case "capture_volume":
data[key] = parseInt(value);
break;
case "speech_threshold":
case "record_stop_delay_sec":
case "prebuffer_duration_sec":
case "noise_reduction_factor":
case "noise_update_alpha":
case "min_rms_threshold_factor":
case "gain_smoothing_alpha":
case "adaptive_energy_std_factor":
case "pause_duration":
case "post_stop_wait":
data[key] = parseFloat(value);
break;
case "adaptive_energy":
case "noise_reduction_enabled":
case "auto_gain_enabled":
case "record_on_speech":
case "log_save_file":
case "save_TTS_files":
data[key] = value === "on"; // Checkbox возвращает 'on' при checked или 'off' при unchecked
break;
default:
data[key] = value;
}
}
return data;
}
+15
View File
@@ -0,0 +1,15 @@
export function setMainColor(color) {
document.documentElement.style.setProperty("--main-color", color);
localStorage.setItem("mainColor", color);
}
export function setBorderColor(color) {
document.documentElement.style.setProperty("--border-color", color);
localStorage.setItem("borderColor", color);
}
export function setBgColor(color) {
document.documentElement.style.setProperty("--bg-color", color);
localStorage.setItem("bgColor", color);
}
export function showError(msg) {
alert(msg); // Или в UI
}