feat: Web Client — canvas визуализация от satellite RMS

Изменения:
- Удалён локальный микрофон (initMicVisualizer)
- Canvas рисует RMS историю от satellite
- 64 столбика с градиентом (зелёный → красный)
- Меньше логов (только если RMS > 0.05)

Преимущества:
- ✅ Показывает реальную громкость satellite
- ✅ Нет зависимости от микрофона браузера
- ✅ Меньше спама в логах
This commit is contained in:
2026-05-21 19:26:53 +03:00
parent 6913625830
commit 4a79874807
2 changed files with 37 additions and 48 deletions
+33 -48
View File
@@ -281,7 +281,7 @@
<!-- заголовок -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
<div style="font-family: 'Orbitron'; font-weight: 700; font-size: 1.6rem; letter-spacing: 2px; background: linear-gradient(135deg, #aaffff, #3b8eff); -webkit-background-clip: text; background-clip: text; color: transparent;">
⚡ LVA • NEXUS
⚡ Wyoming Droid Assistant ⚡
</div>
<div class="connection-status">
<div class="led" id="statusLed"></div>
@@ -482,7 +482,9 @@
// Audio level events
if(eventType === 'audio-level') {
if(json.data && json.data.level !== undefined) {
updateRmsMeter(json.data.level);
const level = json.data.level;
currentRms = level; // ✅ Обновляем currentRms для визуализации
updateRmsMeter(level);
}
}
@@ -581,11 +583,13 @@
}
}
// Визуализация микрофона (если доступен) — рисуем абстракцию ритма
let fakeAudioArray = new Uint8Array(64);
function drawVisualization() {
// Визуализация на canvas — рисуем RMS от satellite
let satelliteRmsHistory = new Array(64).fill(0); // История RMS значений
function drawSatelliteVisualization() {
if(!ctx) return;
requestAnimationFrame(drawVisualization);
requestAnimationFrame(drawSatelliteVisualization);
if(!isConnected) {
ctx.clearRect(0,0,canvas.width, canvas.height);
ctx.fillStyle = '#0a1222';
@@ -595,51 +599,34 @@
ctx.fillText('⚡ idle', canvas.width-50, 20);
return;
}
// имитация уровня громкости + случайный шум, если нет микрофона, но выглядит живо
let avg = 20;
if(analyser && analyser.fftSize) {
const dataArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(dataArray);
let sum = 0;
for(let i=0;i<dataArray.length;i++) sum+=dataArray[i];
avg = Math.min(100, sum/(dataArray.length||1));
} else {
avg = 12 + Math.random() * 25;
if(stateText.innerText.includes('LISTEN') || stateText.innerText.includes('SPEAK')) avg = 45 + Math.random() * 55;
}
// Добавляем текущее RMS в историю
satelliteRmsHistory.shift();
satelliteRmsHistory.push(currentRms);
// Рисуем
ctx.clearRect(0,0,canvas.width, canvas.height);
ctx.fillStyle = '#03151f';
ctx.fillRect(0,0,canvas.width, canvas.height);
const barWidth = (canvas.width / 42);
const numBars = 42;
for(let i=0;i<numBars;i++) {
let barHeight = (avg * (Math.sin(i * 0.5) + 1)/2 + 5) * (0.5 + (Math.random()*0.3));
if(stateText.innerText.includes('SPEAK')) barHeight = avg * 1.6 * (Math.sin(i*0.3)+1.2);
barHeight = Math.min(barHeight, canvas.height-10);
const x = i * (barWidth+2);
const grad = ctx.createLinearGradient(x, canvas.height-barHeight, x, canvas.height);
grad.addColorStop(0, '#0ff');
grad.addColorStop(1, '#3f87ff');
ctx.fillStyle = grad;
ctx.fillRect(x, canvas.height-barHeight, barWidth, barHeight);
const barWidth = (canvas.width / satelliteRmsHistory.length);
for(let i=0; i<satelliteRmsHistory.length; i++) {
const rms = satelliteRmsHistory[i];
const barHeight = Math.min(rms * canvas.height * 1.5, canvas.height - 5);
const x = i * barWidth;
// Градиент от зелёного к красному
const hue = 120 - (rms * 120); // 120 = зелёный, 0 = красный
ctx.fillStyle = `hsl(${hue}, 100%, 50%)`;
ctx.fillRect(x, canvas.height - barHeight, barWidth - 1, barHeight);
}
}
drawVisualization();
// Подключение микрофона для визуализации (опционально)
async function initMicVisualizer() {
try {
if(audioCtx && audioCtx.state !== 'closed') await audioCtx.close();
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioCtx.createAnalyser();
analyser.fftSize = 128;
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaStream = stream;
source = audioCtx.createMediaStreamSource(stream);
source.connect(analyser);
if(audioCtx.state === 'suspended') await audioCtx.resume();
} catch(e) { console.log('mic visual not available', e); }
}
drawSatelliteVisualization();
// Подключение микрофона для визуализации (опционально) — УДАЛЕНО
// Теперь используем RMS от satellite вместо локального микрофона
// WebSocket подключение
function connectWebSocket() {
@@ -655,7 +642,7 @@
stateText.innerText = 'ONLINE';
orb.style.animation = 'pulse-glow 0.8s';
setTimeout(()=> orb.style.animation='',700);
initMicVisualizer();
// initMicVisualizer() — удалено, используем satellite RMS
};
ws.onmessage = handleIncomingMessage;
ws.onerror = (err) => { addEventToLog(`⚠️ WebSocket ошибка`); };
@@ -666,8 +653,6 @@
addEventToLog(`🔌 Отключено`);
stateText.innerText = 'OFFLINE';
document.body.classList.remove('listening-active');
if(mediaStream) mediaStream.getTracks().forEach(t=>t.stop());
if(audioCtx) audioCtx.close();
};
}
+4
View File
@@ -1236,6 +1236,10 @@ class SatelliteBase:
)
await self._broadcast_to_websocket(event)
# Логировать только если уровень выше порога (чтобы не спамить)
if self._last_mic_level > 0.05:
_LOGGER.debug(f"Audio level: {self._last_mic_level:.2f}")
# -------------------------------------------------------------------------
# Info
# -------------------------------------------------------------------------