Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 97b6ea1b3e | |||
| 94ee0455a2 | |||
| 0bf6d49432 | |||
| 493cba36a2 |
@@ -79,8 +79,8 @@ android {
|
||||
applicationId "com.ariacockpit"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 10800
|
||||
versionName "0.1.8.0"
|
||||
versionCode 10801
|
||||
versionName "0.1.8.1"
|
||||
// Fallback fuer Libraries mit Product Flavors
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aria-cockpit",
|
||||
"version": "0.1.8.0",
|
||||
"version": "0.1.8.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -312,6 +312,10 @@ class AudioService {
|
||||
// lich Chunks einer alten Session in eine neue mischen.
|
||||
private streamRequestId: string = '';
|
||||
private streamAudioRequestId: string = '';
|
||||
// Latch: ist endpointListeners fuer den aktuellen Session-Cycle schon gefeuert
|
||||
// worden? Wird auf false gesetzt beim startStreamingRecording, auf true beim
|
||||
// ersten Endpoint (egal ob via RVS oder Fallback). Verhindert Doppel-Fires.
|
||||
private streamEndpointFired: boolean = false;
|
||||
// Subscriber-Handles fuer Native-Events + RVS-Listener (cleanup beim stop)
|
||||
private streamPcmChunkSub: { remove: () => void } | null = null;
|
||||
private streamPcmErrorSub: { remove: () => void } | null = null;
|
||||
@@ -389,10 +393,8 @@ class AudioService {
|
||||
// Wir stoppen die Aufnahme — whisper hat alles was es braucht.
|
||||
// Kein stt_stream_end senden: das Endpoint kam von der Bridge,
|
||||
// sie hat schon finalisiert.
|
||||
this._fireEndpoint(ev);
|
||||
this._cleanupStreamLocal('endpoint');
|
||||
this.endpointListeners.forEach(cb => {
|
||||
try { cb(ev); } catch (e) { console.warn('[Audio] endpoint listener err:', e); }
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (t === 'stt_stream_done') {
|
||||
@@ -979,6 +981,7 @@ class AudioService {
|
||||
this.streamRequestId = requestId;
|
||||
this.streamAudioRequestId = opts.audioRequestId || '';
|
||||
this.streamGotPartial = false;
|
||||
this.streamEndpointFired = false;
|
||||
this.recordingStartTime = Date.now();
|
||||
|
||||
try {
|
||||
@@ -1066,10 +1069,17 @@ class AudioService {
|
||||
}
|
||||
|
||||
/** Sauberer User-initiated Stop. Sendet stt_stream_end an die Bridge,
|
||||
* die noch ihren Final-Transcribe macht. */
|
||||
* die noch ihren Final-Transcribe macht.
|
||||
*
|
||||
* Plus: Fallback-Timer (3s). Wenn die Bridge nicht antwortet (z.B. weil
|
||||
* veraltete Version ohne Streaming-Handler laeuft), feuern wir den
|
||||
* Endpoint-Listener trotzdem mit text='' damit die App-UI nicht in
|
||||
* "wird verarbeitet..." haengt. ChatScreen behandelt das wie den
|
||||
* No-Speech-Fall (Bubble weg + endConversation). */
|
||||
async stopStreamingRecording(reason: string = 'user'): Promise<void> {
|
||||
const reqId = this.streamRequestId;
|
||||
if (!reqId) return;
|
||||
const audioReqId = this.streamAudioRequestId;
|
||||
try {
|
||||
rvs.send('stt_stream_end' as any, { requestId: reqId, reason });
|
||||
} catch (e) {
|
||||
@@ -1078,6 +1088,21 @@ class AudioService {
|
||||
// Recorder lokal abschalten — Bridge feuert dann ihrerseits noch
|
||||
// stt_endpoint + stt_stream_done.
|
||||
this._cleanupStreamLocal(`stop:${reason}`);
|
||||
// Fallback-Watchdog: nach 3s noch immer kein Endpoint via RVS angekommen
|
||||
// → _fireEndpoint mit text='' (idempotent via streamEndpointFired-Latch,
|
||||
// d.h. wenn echtes stt_endpoint zwischen jetzt und +3s ankommt feuert
|
||||
// dieser Fallback NICHT).
|
||||
setTimeout(() => {
|
||||
if (this.streamEndpointFired) return;
|
||||
console.log('[Audio] stopStreamingRecording: 3s ohne Bridge-Antwort — fallback fire');
|
||||
this._fireEndpoint({
|
||||
audioRequestId: audioReqId,
|
||||
text: '',
|
||||
reason: `stop:${reason}:no-response`,
|
||||
durationS: 0,
|
||||
sttMs: 0,
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/** Abbruch ohne dass Brain den Text verarbeitet — z.B. wenn der User
|
||||
@@ -1095,15 +1120,23 @@ class AudioService {
|
||||
} catch {}
|
||||
this._cleanupStreamLocal(`cancel:${reason}`);
|
||||
// Listener feuern damit ChatScreen reagieren kann (endConversation etc.)
|
||||
const ev: SttEndpointEvent = {
|
||||
this._fireEndpoint({
|
||||
audioRequestId: audioReqId,
|
||||
text: '',
|
||||
reason: `cancel:${reason}`,
|
||||
durationS: 0,
|
||||
sttMs: 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Feuert den Endpoint-Listener — aber nur einmal pro Session-Cycle.
|
||||
* Wird sowohl vom RVS-stt_endpoint-Pfad als auch vom Fallback-Watchdog
|
||||
* und cancelStreamingRecording aufgerufen. */
|
||||
private _fireEndpoint(ev: SttEndpointEvent): void {
|
||||
if (this.streamEndpointFired) return;
|
||||
this.streamEndpointFired = true;
|
||||
this.endpointListeners.forEach(cb => {
|
||||
try { cb(ev); } catch (e) { console.warn('[Audio] endpoint listener (cancel) err:', e); }
|
||||
try { cb(ev); } catch (e) { console.warn('[Audio] endpoint listener err:', e); }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -556,6 +556,12 @@ class ARIABridge:
|
||||
for k in ("fluxDefaultModel", "fluxKeywordRaw", "fluxKeywordSwitch", "huggingfaceToken"):
|
||||
if k in vc:
|
||||
self._flux_config[k] = vc[k]
|
||||
# Debug-Log-Toggles fuer Whisper / F5TTS Bridges (Diagnostic-Toggle).
|
||||
# Default: aus — sonst muellen wir uns volle Disk wenn alles laeuft.
|
||||
self._debug_log_config: dict = {}
|
||||
for k in ("whisperDebugLog", "f5ttsDebugLog"):
|
||||
if k in vc:
|
||||
self._debug_log_config[k] = bool(vc[k])
|
||||
logger.info("Voice-Config geladen: tts=%s voice=%s f5tts=%s flux=%s",
|
||||
self.tts_enabled, self.xtts_voice or "default",
|
||||
self._f5tts_config or "defaults",
|
||||
@@ -1304,6 +1310,7 @@ class ARIABridge:
|
||||
payload["xttsSpeed"] = self._persistent_xtts_speed
|
||||
payload.update(getattr(self, "_f5tts_config", {}) or {})
|
||||
payload.update(getattr(self, "_flux_config", {}) or {})
|
||||
payload.update(getattr(self, "_debug_log_config", {}) or {})
|
||||
await self._send_to_rvs({
|
||||
"type": "config",
|
||||
"payload": payload,
|
||||
@@ -1978,6 +1985,15 @@ class ARIABridge:
|
||||
self._flux_config = {}
|
||||
self._flux_config[k] = payload[k]
|
||||
changed = True
|
||||
# Debug-Log-Toggles fuer Whisper- und F5TTS-Bridge — werden via
|
||||
# naechstem config-Broadcast an die jeweiligen Bridges weitergegeben.
|
||||
# Persistent damit Toggle einen Container-Restart ueberlebt.
|
||||
for k in ("whisperDebugLog", "f5ttsDebugLog"):
|
||||
if k in payload:
|
||||
if not hasattr(self, "_debug_log_config"):
|
||||
self._debug_log_config = {}
|
||||
self._debug_log_config[k] = bool(payload[k])
|
||||
changed = True
|
||||
# Persistent speichern in Shared Volume
|
||||
if changed:
|
||||
try:
|
||||
@@ -1991,6 +2007,7 @@ class ARIABridge:
|
||||
config_data["xttsSpeed"] = self._persistent_xtts_speed
|
||||
config_data.update(getattr(self, "_f5tts_config", {}))
|
||||
config_data.update(getattr(self, "_flux_config", {}))
|
||||
config_data.update(getattr(self, "_debug_log_config", {}))
|
||||
with open("/shared/config/voice_config.json", "w") as f:
|
||||
json.dump(config_data, f, indent=2)
|
||||
logger.info("[rvs] Voice-Config gespeichert: %s", config_data)
|
||||
|
||||
@@ -38,6 +38,10 @@ const ALLOWED_TYPES = new Set([
|
||||
"xtts_delete_voice",
|
||||
"voice_preload", "voice_ready",
|
||||
"stt_request", "stt_response",
|
||||
// Streaming-STT (Phase 1+2): App schickt PCM live an whisper-bridge,
|
||||
// die feuert stt_endpoint mit dem finalen Text — kein Audio-Roundtrip.
|
||||
"stt_stream_start", "stt_audio_chunk", "stt_stream_end",
|
||||
"stt_partial", "stt_endpoint", "stt_stream_done",
|
||||
"service_status",
|
||||
"config_request",
|
||||
"flux_request", "flux_response",
|
||||
|
||||
@@ -375,6 +375,41 @@ async def _send(ws, mtype: str, payload: dict) -> None:
|
||||
logger.warning("Send fehlgeschlagen (%s): %s", mtype, e)
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# DEBUG-LOG ueber RVS → /shared/logs/app.log
|
||||
#
|
||||
# Gleiches Pattern wie in whisper-bridge: Stefan's Gamebox ist
|
||||
# Windows (kein SSH), in Zukunft koennten whisper + f5tts auf
|
||||
# unterschiedlichen Hosts laufen. Logs ueber RVS heisst: ein Pfad.
|
||||
#
|
||||
# Toggle via aria-bridge config broadcast: f5ttsDebugLog (bool).
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
_DEBUG_LOG_TO_BRIDGE: bool = False # default OFF — TTS-Renders sind teurer
|
||||
# zu debuggen, normalerweise nicht noetig
|
||||
|
||||
|
||||
async def _debug_log(ws, scope: str, message: str, level: str = "info") -> None:
|
||||
"""Schickt einen app_log via RVS → /shared/logs/app.log mit platform='f5tts'.
|
||||
No-op wenn Toggle aus."""
|
||||
if not _DEBUG_LOG_TO_BRIDGE:
|
||||
return
|
||||
try:
|
||||
await ws.send(json.dumps({
|
||||
"type": "app_log",
|
||||
"payload": {
|
||||
"ts": int(time.time() * 1000),
|
||||
"platform": "f5tts",
|
||||
"level": level,
|
||||
"scope": scope,
|
||||
"message": str(message)[:2000],
|
||||
"stack": "",
|
||||
},
|
||||
"timestamp": int(time.time() * 1000),
|
||||
}))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ── Interne Transkription via whisper-bridge ────────────────
|
||||
|
||||
_pending_stt: dict[str, asyncio.Future] = {}
|
||||
@@ -867,6 +902,30 @@ async def run_loop(runner: F5Runner) -> None:
|
||||
else:
|
||||
fut.set_result(payload.get("text") or "")
|
||||
elif mtype == "config":
|
||||
# Debug-Toggle (gleiche Semantik wie in whisper-bridge)
|
||||
if "f5ttsDebugLog" in payload:
|
||||
global _DEBUG_LOG_TO_BRIDGE
|
||||
old = _DEBUG_LOG_TO_BRIDGE
|
||||
_DEBUG_LOG_TO_BRIDGE = bool(payload.get("f5ttsDebugLog", False))
|
||||
if old != _DEBUG_LOG_TO_BRIDGE:
|
||||
logger.info("Debug-Log-to-Bridge: %s", "ON" if _DEBUG_LOG_TO_BRIDGE else "OFF")
|
||||
# Last gasp wenn ausgeschaltet wird
|
||||
if not _DEBUG_LOG_TO_BRIDGE:
|
||||
try:
|
||||
await ws.send(json.dumps({
|
||||
"type": "app_log",
|
||||
"payload": {
|
||||
"ts": int(time.time() * 1000),
|
||||
"platform": "f5tts",
|
||||
"level": "info",
|
||||
"scope": "config",
|
||||
"message": "debug-log OFF (toggle aus)",
|
||||
"stack": "",
|
||||
},
|
||||
"timestamp": int(time.time() * 1000),
|
||||
}))
|
||||
except Exception:
|
||||
pass
|
||||
# F5-TTS-Settings aktualisieren (Modell, cfg_strength, nfe)
|
||||
async def _update_with_status(p):
|
||||
# Schaut ob ein Modell-Wechsel ansteht — falls ja:
|
||||
|
||||
@@ -171,6 +171,43 @@ async def _send(ws, mtype: str, payload: dict) -> None:
|
||||
logger.warning("Send fehlgeschlagen (%s): %s", mtype, e)
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# DEBUG-LOG ueber RVS → /shared/logs/app.log
|
||||
#
|
||||
# Stefan's Gamebox ist Windows, kein SSH → wir brauchen Whisper-Bridge-
|
||||
# Logs ueber den gleichen Pfad wie die App: app_log-Messages via RVS,
|
||||
# aria-bridge schreibt sie in /shared/logs/app.log. Diagnostic / App-
|
||||
# Logs-Tab zeigen sie dann mit platform="whisper".
|
||||
#
|
||||
# Toggle via aria-bridge config broadcast: whisperDebugLog (bool).
|
||||
# Default ON solange wir Phase-1/2-Pipeline einfahren — danach
|
||||
# defaultet aria-bridge ihn aus damit kein Spam.
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
_DEBUG_LOG_TO_BRIDGE: bool = True
|
||||
|
||||
|
||||
async def _debug_log(ws, scope: str, message: str, level: str = "info") -> None:
|
||||
"""Schickt einen app_log via RVS → landet in /shared/logs/app.log mit
|
||||
platform='whisper'. Idempotent: wenn Toggle aus → no-op."""
|
||||
if not _DEBUG_LOG_TO_BRIDGE:
|
||||
return
|
||||
try:
|
||||
await ws.send(json.dumps({
|
||||
"type": "app_log",
|
||||
"payload": {
|
||||
"ts": int(time.time() * 1000),
|
||||
"platform": "whisper",
|
||||
"level": level,
|
||||
"scope": scope,
|
||||
"message": str(message)[:2000],
|
||||
"stack": "",
|
||||
},
|
||||
"timestamp": int(time.time() * 1000),
|
||||
}))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# STREAMING-SESSIONS
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
@@ -365,6 +402,8 @@ class SessionManager:
|
||||
"audioRequestId": sess.audio_request_id,
|
||||
"text": text,
|
||||
})
|
||||
await _debug_log(ws, "stream.partial",
|
||||
f"id={sess.request_id[:12]} text={text[:80]!r}")
|
||||
else:
|
||||
# Stagnation pruefen — Endpoint-Bedingung
|
||||
if sess.last_growth_at == 0.0:
|
||||
@@ -410,6 +449,9 @@ class SessionManager:
|
||||
|
||||
logger.info("Stream %s: FINAL (reason=%s, %.1fs Audio, %dms): %r",
|
||||
sess.request_id[:8], reason, duration_s, stt_ms, final_text[:120])
|
||||
await _debug_log(ws, "stream.final",
|
||||
f"id={sess.request_id[:12]} reason={reason} "
|
||||
f"audio={duration_s:.1f}s stt={stt_ms}ms text={final_text[:80]!r}")
|
||||
|
||||
# stt_endpoint: das ist DAS Event auf das aria-bridge horcht fuer den
|
||||
# Brain-Shortcut. Enthaelt alle Felder die bisher in 'audio' lagen,
|
||||
@@ -537,6 +579,11 @@ async def run_loop(runner: WhisperRunner, sessions: SessionManager) -> None:
|
||||
await _broadcast_status(ws, "loading", model=init_model)
|
||||
logger.info("Initial: sende config_request an aria-bridge")
|
||||
await _send(ws, "config_request", {"service": "whisper"})
|
||||
# Startup-Marker — App-Logs zeigen damit ob Streaming-Code
|
||||
# ueberhaupt aktiv ist (Stefan baut auf Gamebox via PS,
|
||||
# Build/Restart kann unbeabsichtigt alte Version weiterfahren).
|
||||
await _debug_log(ws, "boot",
|
||||
"whisper-bridge online — streaming-mode ENABLED, debug-log ON")
|
||||
except Exception as e:
|
||||
logger.exception("Initial-Handshake crashed: %s", e)
|
||||
asyncio.create_task(_initial_handshake())
|
||||
@@ -557,6 +604,11 @@ async def run_loop(runner: WhisperRunner, sessions: SessionManager) -> None:
|
||||
asyncio.create_task(handle_stt_request(ws, payload, runner))
|
||||
|
||||
elif mtype == "stt_stream_start":
|
||||
await _debug_log(ws, "stream.start",
|
||||
f"received id={payload.get('requestId', '?')[:12]} "
|
||||
f"audioReqId={payload.get('audioRequestId', '?')[:16]} "
|
||||
f"endpointMs={payload.get('endpointMs')} "
|
||||
f"hardCapMs={payload.get('hardCapMs')}")
|
||||
# Ggf. Modell sicherstellen — sonst antwortet der erste
|
||||
# transcribe-Call mit Leerstring weil Model None.
|
||||
target_model = payload.get("model") or runner.model_size or WHISPER_MODEL
|
||||
@@ -581,14 +633,52 @@ async def run_loop(runner: WhisperRunner, sessions: SessionManager) -> None:
|
||||
# Sehr verbose im Schlimmstfall — debug-Level reicht.
|
||||
logger.debug("stt_audio_chunk: unbekannte/closed session %s",
|
||||
payload.get("requestId", "")[:8])
|
||||
await _debug_log(ws, "stream.chunk.reject",
|
||||
f"unknown/closed session id={payload.get('requestId', '?')[:12]}",
|
||||
level="warn")
|
||||
else:
|
||||
# Nur alle 25 Chunks loggen (=5s Audio) — sonst Spam.
|
||||
try:
|
||||
seq = int(payload.get("seq", 0) or 0)
|
||||
if seq % 25 == 0:
|
||||
await _debug_log(ws, "stream.chunk",
|
||||
f"id={payload.get('requestId', '?')[:12]} seq={seq}")
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
elif mtype == "stt_stream_end":
|
||||
req_id = payload.get("requestId", "")
|
||||
logger.info("stt_stream_end empfangen: id=%s reason=%s",
|
||||
req_id[:8], payload.get("reason", ""))
|
||||
await _debug_log(ws, "stream.end",
|
||||
f"received id={req_id[:12]} reason={payload.get('reason', '')}")
|
||||
sessions.end_session(req_id)
|
||||
|
||||
elif mtype == "config":
|
||||
# Debug-Toggle: aria-bridge broadcastet jetzt whisperDebugLog
|
||||
# damit Stefan im laufenden Betrieb via Diagnostic-Settings
|
||||
# die Logs an/aus schalten kann.
|
||||
if "whisperDebugLog" in payload:
|
||||
global _DEBUG_LOG_TO_BRIDGE
|
||||
old = _DEBUG_LOG_TO_BRIDGE
|
||||
_DEBUG_LOG_TO_BRIDGE = bool(payload.get("whisperDebugLog", False))
|
||||
if old != _DEBUG_LOG_TO_BRIDGE:
|
||||
logger.info("Debug-Log-to-Bridge: %s", "ON" if _DEBUG_LOG_TO_BRIDGE else "OFF")
|
||||
# Last gasp wenn ausgeschaltet wird damit Stefan im Log sieht
|
||||
# dass der Toggle griff.
|
||||
if not _DEBUG_LOG_TO_BRIDGE:
|
||||
await ws.send(json.dumps({
|
||||
"type": "app_log",
|
||||
"payload": {
|
||||
"ts": int(time.time() * 1000),
|
||||
"platform": "whisper",
|
||||
"level": "info",
|
||||
"scope": "config",
|
||||
"message": "debug-log OFF (toggle aus)",
|
||||
"stack": "",
|
||||
},
|
||||
"timestamp": int(time.time() * 1000),
|
||||
}))
|
||||
new_model = payload.get("whisperModel") or WHISPER_MODEL
|
||||
needs_load = (runner.model is None) or (new_model != runner.model_size)
|
||||
if needs_load:
|
||||
|
||||
Reference in New Issue
Block a user