feat: управление яркостью/WiFi очков через Mercury + накопленные доработки

- Mercury (app-phone): управление яркостью (с авто-выкл. перед
  применением) и WiFi (скан/подключение/список сетей с паролем);
  добавлено право BLUETOOTH_ADVERTISE в манифест и runtime-запрос
  (без него GATT-сервер не рекламируется → очки не подключаются)
- логирование (lib-core core.log): BatchingLogCollector + LogSink +
  LogSpool + LokiLogSink; цепочка очки→телефон→Loki; lib-core log()
  переадресован в коллектор
- STT: Qwen3-ASR через pw.binom.asr:asr-qwen3-android, убран in-house
  whisper-фоллбек
- audio-sync: P-регулятор с насыщением вместо фиксированной скорости
- media-split: очки только video, телефон только audio (UI-чипы)
- Components: форматирование размера через Locale.ROOT
This commit is contained in:
2026-08-29 23:06:52 +03:00
parent 62d2f06797
commit 6cd4a8de43
40 changed files with 1409 additions and 203 deletions
@@ -33,7 +33,7 @@ class AudioSyncPlayerTest {
@Test
fun speedsUpWhenAudioLagsBehind() {
val d = decideSync(audioPosition = 10_000, videoPosition = 10_300, audioPlaying = true, videoPlaying = true)
assertEquals(1f + DRIFT_RATE, d.speed, "звук отстаёт — догоняем ускорением ${1f + DRIFT_RATE}")
assertEquals(1f + 300f * DRIFT_GAIN, d.speed, "звук отстаёт — догоняем ускорением ${1f + 300f * DRIFT_GAIN}")
assertNull(d.seekToMs, "diff=300мс внутри HARD-порога — seek не нужен")
assertNull(d.playing)
}
@@ -41,9 +41,22 @@ class AudioSyncPlayerTest {
@Test
fun slowsDownWhenAudioAhead() {
val d = decideSync(audioPosition = 10_300, videoPosition = 10_000, audioPlaying = true, videoPlaying = true)
assertEquals(1f - DRIFT_RATE, d.speed, "звук впереди — тормозим до ${1f - DRIFT_RATE}")
assertEquals(1f - 300f * DRIFT_GAIN, d.speed, "звук впереди — тормозим до ${1f - 300f * DRIFT_GAIN}")
assertNull(d.seekToMs)
assertNull(d.playing)
}
@Test
fun smallDriftJustOutsideBandIsProportional() {
val d = decideSync(audioPosition = 10_151, videoPosition = 10_000, audioPlaying = true, videoPlaying = true)
assertEquals(1f - 151f * DRIFT_GAIN, d.speed, "diff=151мс (чуть за зоной) — слабый пропорциональный темп")
}
@Test
fun speedSaturatesAtMaxDrift() {
val behind = decideSync(audioPosition = 9_000, videoPosition = 10_000, audioPlaying = true, videoPlaying = true)
assertEquals(1f + MAX_DRIFT_SPEED, behind.speed, "diff=−1000мс → 1+0.2 упирается в потолок ${1f + MAX_DRIFT_SPEED}")
val ahead = decideSync(audioPosition = 11_000, videoPosition = 10_000, audioPlaying = true, videoPlaying = true)
assertEquals(1f - MAX_DRIFT_SPEED, ahead.speed, "diff=+1000мс → 1−0.2 упирается в потолок ${1f - MAX_DRIFT_SPEED}")
}
@Test
@@ -73,7 +86,7 @@ class AudioSyncPlayerTest {
fun hardThresholdBoundaryDoesNotSeek() {
val d = decideSync(audioPosition = 10_000, videoPosition = 12_000, audioPlaying = true, videoPlaying = true)
assertNull(d.seekToMs, "diff=2000мс не БОЛЬШЕ порога — подгоняем скоростью, без рывка")
assertEquals(1f + DRIFT_RATE, d.speed)
assertEquals(1f + MAX_DRIFT_SPEED, d.speed, "diff=2000мс → 1+0.4 — упирается в потолок ${1f + MAX_DRIFT_SPEED}")
}
@Test