feat: добавить Audio Level monitoring — RMS громкости микрофона в WebSocket

- settings.py: audio_level_enabled, audio_level_interval_ms
- satellite.py: _calculate_rms() — вычисление RMS (0.0-1.0)
- satellite.py: _send_audio_level() — фоновая задача отправки
- satellite.py: _last_mic_level, _audio_level_task атрибуты
- satellite.py: интеграция в _start() / _stop()
- satellite.py: вычисление RMS в event_from_mic() для всех классов
- __main__.py: CLI --audio-level-enabled, --audio-level-interval-ms

Событие в WebSocket (10 раз в секунду):
{type: audio-level, data: {level: 0.42}}
This commit is contained in:
2026-05-20 17:17:18 +03:00
parent 5c92eed55b
commit b28052e93e
4 changed files with 291 additions and 13 deletions
+193
View File
@@ -30,6 +30,7 @@
### ⏳ Ожидается
- [ ] **Этап WS-6: Audio level** — трансляция RMS громкости микрофона
- [ ] Merge в master
- [ ] Production развёртывание
@@ -656,6 +657,198 @@ python3 -m wyoming_satellite \
---
### Этап WS-6: Audio level — RMS громкость микрофона (30 мин) ⏳ ОЖИДАЕТСЯ
**Трансляция уровня громкости в WebSocket для визуализации.**
#### 6.1 Формат события
```json
{
"type": "audio-level",
"data": {
"level": 0.42 // 0.0 - 1.0, RMS нормализованный
}
}
```
**Частота:** 10 раз в секунду (100 мс)
#### 6.2 Модификация `wyoming_satellite/settings.py`
Добавить настройку:
```python
@dataclass(frozen=True)
class EventSettings(ServiceSettings):
# ... существующие поля ...
# Audio level monitoring
audio_level_enabled: bool = False
"""True если нужно вычислять и отправлять RMS громкость микрофона."""
audio_level_interval_ms: int = 100
"""Интервал отправки audio-level событий (миллисекунды)."""
```
#### 6.3 Модификация `wyoming_satellite/satellite.py`
**Утилита для расчёта RMS:**
```python
def _calculate_rms(self, audio_bytes: bytes) -> float:
"""
Вычислить RMS громкость (0.0 - 1.0).
Использует тот же алгоритм что и WebRtcVadEnergy из vad.py
"""
pcm = array.array("h")
pcm.frombytes(audio_bytes)
if not pcm:
return 0.0
rms = math.sqrt(sum(int(s) * int(s) for s in pcm) / len(pcm))
return rms / 32767.0 # Нормализация для 16-bit
```
**Атрибуты в `__init__()`:**
```python
self._last_mic_level: float = 0.0
self._audio_level_task: Optional[asyncio.Task] = None
```
**Вычисление в `event_from_mic()`:**
```python
async def event_from_mic(
self, event: Event, audio_bytes: Optional[bytes] = None
) -> None:
"""Called when an event is received from the mic service."""
# Вычислить RMS для аудио чанка
if AudioChunk.is_type(event.type) and audio_bytes:
self._last_mic_level = self._calculate_rms(audio_bytes)
# ... остальной код ...
```
**Фоновая задача отправки:**
```python
async def _send_audio_level(self) -> None:
"""
Периодически отправлять audio-level события в WebSocket.
"""
from wyoming.event import Event as WyomingEvent
interval = self.settings.event.audio_level_interval_ms / 1000.0
while self.is_running:
await asyncio.sleep(interval)
if self.settings.event.audio_level_enabled and self._websocket_clients:
event = WyomingEvent(
"audio-level",
{"level": self._last_mic_level}
).event()
await self._broadcast_to_websocket(event)
```
**Интеграция в `_start()` / `_stop()`:**
```python
async def _start(self) -> None:
# ... существующий код ...
# Запустить задачу audio level (новый)
if self.settings.event.audio_level_enabled:
self._audio_level_task = asyncio.create_task(
self._send_audio_level(), name="audio_level"
)
async def _stop(self) -> None:
# ... существующий код ...
# Остановить задачу audio level
if self._audio_level_task:
self._audio_level_task.cancel()
self._audio_level_task = None
```
#### 6.4 Модификация `wyoming_satellite/__main__.py`
CLI аргументы:
```python
parser.add_argument(
"--audio-level-enabled",
action="store_true",
help="Enable audio level monitoring and send audio-level events to WebSocket"
)
parser.add_argument(
"--audio-level-interval-ms",
type=int,
default=100,
help="Interval for audio-level events in milliseconds (default: 100)"
)
```
Передача в настройки:
```python
event=EventSettings(
# ... существующие поля ...
# Audio level
audio_level_enabled=args.audio_level_enabled,
audio_level_interval_ms=args.audio_level_interval_ms,
)
```
#### 6.5 Пример использования в браузере
```html
<div id="level-meter" style="width: 100%; height: 20px; background: #333;">
<div id="level-bar" style="width: 0%; height: 100%; background: #0f0;"></div>
</div>
<script>
const ws = new WebSocket('ws://localhost:8675');
const levelBar = document.getElementById('level-bar');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'audio-level') {
const percent = data.level * 100;
levelBar.style.width = percent + '%';
// Цвет от уровня
if (data.level > 0.7) {
levelBar.style.background = '#f00'; // Громко
} else if (data.level > 0.3) {
levelBar.style.background = '#ff0'; // Средне
} else {
levelBar.style.background = '#0f0'; // Тихо
}
}
};
</script>
```
#### 6.6 Запуск с audio level
```bash
python3 -m wyoming_satellite \
--uri tcp://0.0.0.0:10700 \
--websocket-enabled \
--websocket-port 8675 \
--audio-level-enabled \
--audio-level-interval-ms 100
```
**Итого изменений:** ~40 строк
---
## Итоговые изменения
| Файл | Тип | Изменения | Строк |