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:
@@ -17,6 +17,11 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import pw.binom.viewmate.core.GlassesMode
|
||||
import pw.binom.viewmate.core.log.BatchingLogCollector
|
||||
import pw.binom.viewmate.core.log.FileLogSpool
|
||||
import pw.binom.viewmate.core.log.LokiConfig
|
||||
import pw.binom.viewmate.core.log.SystemClock
|
||||
import pw.binom.viewmate.core.log.lokiLogSink
|
||||
import pw.binom.viewmate.core.media.JellyfinClient
|
||||
import pw.binom.viewmate.core.media.MirrorClient
|
||||
import pw.binom.viewmate.core.phone.PhoneActions
|
||||
@@ -49,7 +54,6 @@ import pw.binom.viewmate.phone.agent.ToolsetRegistry
|
||||
import pw.binom.viewmate.phone.stt.PhraseRecognizer
|
||||
import pw.binom.viewmate.phone.stt.Qwen3AsrStt
|
||||
import pw.binom.viewmate.phone.stt.SttStreamer
|
||||
import pw.binom.viewmate.phone.stt.WhisperStt
|
||||
|
||||
/**
|
||||
* Application телефона: создаёт клиентов (Jellyfin/mirror), PhoneState,
|
||||
@@ -84,6 +88,10 @@ class PhoneApp : Application() {
|
||||
/** Синхронизированный аудио-плеер (звук на телефоне при просмотре на очках). */
|
||||
val audioSync: AudioSyncPlayer by lazy { AudioSyncPlayer(this) }
|
||||
|
||||
/** Лог-коллектор: свои записи + пришедшие от очков → сервер (спул — filesDir/logspool). */
|
||||
lateinit var logCollector: BatchingLogCollector
|
||||
private set
|
||||
|
||||
/** История общения с ассистентом (SQLite, "assistant.db"). */
|
||||
val chatDao: ChatDao by lazy { ChatDao(ChatDb(this)) }
|
||||
|
||||
@@ -345,9 +353,9 @@ class PhoneApp : Application() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Лениво создаёт стриминговое распознавание (модели во filesDir/models —
|
||||
* Лениво создаёт стриминговое распознавание (модели Qwen3-ASR во filesDir/models —
|
||||
* кладутся через adb push + run-as cp, как в этапе 1). Вызывается из хаба
|
||||
* на первый SttAudio; тяжёлое создание Whisper — один раз на процесс.
|
||||
* на первый SttAudio; тяжёлое создание Qwen3-ASR — один раз на процесс.
|
||||
*/
|
||||
private fun ensureStt(): SttStreamer? {
|
||||
synchronized(sttLock) {
|
||||
@@ -357,39 +365,24 @@ class PhoneApp : Application() {
|
||||
val qwenEnc = File(modelsDir, "encoder.int8.onnx")
|
||||
val qwenDec = File(modelsDir, "decoder.int8.onnx")
|
||||
val qwenTok = File(modelsDir, "tokenizer")
|
||||
var stt: PhraseRecognizer
|
||||
if (STT_BACKEND == "qwen3_asr" &&
|
||||
qwenConv.isFile && qwenEnc.isFile && qwenDec.isFile && qwenTok.isDirectory
|
||||
) {
|
||||
stt = Qwen3AsrStt(
|
||||
convFrontendPath = qwenConv.absolutePath,
|
||||
encoderPath = qwenEnc.absolutePath,
|
||||
decoderPath = qwenDec.absolutePath,
|
||||
tokenizerDir = qwenTok.absolutePath,
|
||||
numThreads = 4,
|
||||
hotwords = STT_HOTWORDS,
|
||||
)
|
||||
} else {
|
||||
val encoder = File(modelsDir, "small-encoder.int8.onnx")
|
||||
val decoder = File(modelsDir, "small-decoder.int8.onnx")
|
||||
val tokens = File(modelsDir, "small-tokens.txt")
|
||||
if (!encoder.isFile || !decoder.isFile || !tokens.isFile) {
|
||||
log("stt", "модели не найдены в ${modelsDir.absolutePath} — STT недоступен")
|
||||
return null
|
||||
}
|
||||
stt = WhisperStt(
|
||||
encoderPath = encoder.absolutePath,
|
||||
decoderPath = decoder.absolutePath,
|
||||
tokensPath = tokens.absolutePath,
|
||||
numThreads = 4,
|
||||
)
|
||||
if (!qwenConv.isFile || !qwenEnc.isFile || !qwenDec.isFile || !qwenTok.isDirectory) {
|
||||
log("stt", "модели Qwen3-ASR не найдены в ${modelsDir.absolutePath} — STT недоступен")
|
||||
return null
|
||||
}
|
||||
val stt: PhraseRecognizer = Qwen3AsrStt(
|
||||
convFrontendPath = qwenConv.absolutePath,
|
||||
encoderPath = qwenEnc.absolutePath,
|
||||
decoderPath = qwenDec.absolutePath,
|
||||
tokenizerDir = qwenTok.absolutePath,
|
||||
numThreads = 4,
|
||||
hotwords = STT_HOTWORDS,
|
||||
)
|
||||
val dumpPcm = if (File(filesDir, "stt_dump.marker").exists()) {
|
||||
File(filesDir, "stt_dump_phone.raw")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
log("stt", "бэкенд: $STT_BACKEND (hotwords: $STT_HOTWORDS)")
|
||||
log("stt", "бэкенд: qwen3_asr (hotwords: $STT_HOTWORDS)")
|
||||
val s = SttStreamer(
|
||||
stt = stt,
|
||||
onSilence30s = {
|
||||
@@ -406,7 +399,7 @@ class PhoneApp : Application() {
|
||||
)
|
||||
sttStreamer = s
|
||||
log("stt", "PCM-дампа (телефон): ${dumpPcm?.absolutePath ?: "off"}")
|
||||
log("stt", "SttStreamer готов ($STT_BACKEND)")
|
||||
log("stt", "SttStreamer готов (qwen3_asr)")
|
||||
return s
|
||||
}
|
||||
}
|
||||
@@ -452,6 +445,27 @@ class PhoneApp : Application() {
|
||||
nsd.publish()
|
||||
server.hub.sttFactory = { ensureStt() }
|
||||
server.hub.onStopFullText = { phrase -> scope.launch { assistantChannel.send(phrase) } }
|
||||
|
||||
// Лог-коллектор: свои записи + пачки от очков → Loki. instance в Loki
|
||||
// выставляется по source записи ("phone"/"glasses"), спул — filesDir/logspool.
|
||||
logCollector = BatchingLogCollector(
|
||||
device = "phone",
|
||||
sink = lokiLogSink(),
|
||||
spool = FileLogSpool(File(filesDir, "logspool").absolutePath, 64),
|
||||
clock = SystemClock(),
|
||||
flushIntervalMs = 60_000,
|
||||
maxBatchEntries = 500,
|
||||
)
|
||||
logCollector.start(scope)
|
||||
attachLogCollector(logCollector)
|
||||
pw.binom.viewmate.core.logCollector = logCollector
|
||||
server.hub.onLogBatch = { entries -> logCollector.recordBatch(entries) }
|
||||
log(
|
||||
"server",
|
||||
"лог-коллектор запущен (sink: loki ${LokiConfig().url}, instance по source; " +
|
||||
"спул: filesDir/logspool)",
|
||||
)
|
||||
|
||||
scope.launch { assistantLoop() }
|
||||
|
||||
val lastPositionAt = AtomicLong(0)
|
||||
@@ -534,9 +548,6 @@ class PhoneApp : Application() {
|
||||
lateinit var instance: PhoneApp
|
||||
private set
|
||||
|
||||
/** Бэкенд STT: "qwen3_asr" | "whisper" (откат). */
|
||||
private const val STT_BACKEND = "qwen3_asr"
|
||||
|
||||
/** Подсказки словаря Qwen3-ASR (запятая-разделитель). */
|
||||
private const val STT_HOTWORDS = "тулсет,тулы,умеешь"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user