Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8b586ec92 | |||
| 632e1e4fa1 | |||
| 7e12816ebd | |||
| 8f64f8fb30 | |||
| b3ff3991c4 | |||
| a4ea387c98 | |||
| 68fbf74a23 | |||
| b857f778e9 | |||
| 31aa82b68c | |||
| de8eeb69e2 | |||
| f5970ce700 | |||
| ef1a4436ca |
@@ -79,8 +79,8 @@ android {
|
||||
applicationId "com.ariacockpit"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 907
|
||||
versionName "0.0.9.7"
|
||||
versionCode 10001
|
||||
versionName "0.1.0.1"
|
||||
// Fallback fuer Libraries mit Product Flavors
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aria-cockpit",
|
||||
"version": "0.0.9.7",
|
||||
"version": "0.1.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -363,6 +363,10 @@ class AudioService {
|
||||
console.log('[Audio] pauseForCall: %s', reason || '(no reason)');
|
||||
this._conversationFocusActive = false;
|
||||
this._pausedForCall = true;
|
||||
// Queue + isPlaying ruecksetzen — sonst klemmt der naechste Play-Button
|
||||
// (playAudio sieht isPlaying=true und ruft _playNext nicht mehr auf).
|
||||
this.audioQueue = [];
|
||||
this.isPlaying = false;
|
||||
// Foreground-Service stoppen — Notification waere sonst irrefuehrend
|
||||
stopBackgroundAudio().catch(() => {});
|
||||
// SoundPool/RNSound (Resume-Sound, Play-Button) stoppen — nicht relevant fuer Auto-Resume
|
||||
@@ -778,8 +782,13 @@ class AudioService {
|
||||
if (!base64Data) return;
|
||||
// Mute-Flag respektieren — robust gegen Race-Conditions zwischen User-
|
||||
// Klick auf Mute und einem TTS-Chunk der im selben Tick eintrifft.
|
||||
if (this._muted) return;
|
||||
if (this._muted) {
|
||||
console.log('[Audio] playAudio: muted=true → skip');
|
||||
return;
|
||||
}
|
||||
this.audioQueue.push(base64Data);
|
||||
console.log('[Audio] playAudio: queued (queue=%d isPlaying=%s pausedForCall=%s)',
|
||||
this.audioQueue.length, this.isPlaying, this._pausedForCall);
|
||||
if (!this.isPlaying) {
|
||||
this._playNext();
|
||||
}
|
||||
@@ -1055,9 +1064,15 @@ class AudioService {
|
||||
}
|
||||
|
||||
private _firePlaybackStarted(): void {
|
||||
// Tracking fuer Auto-Resume nach Anruf-Pause
|
||||
this.playbackStartTime = Date.now();
|
||||
this.currentPlaybackMsgId = this.pcmMessageId || '';
|
||||
// Tracking fuer Auto-Resume nach Anruf-Pause: NUR setzen wenn ein
|
||||
// PCM-Stream laeuft (Live-TTS). Bei Play-Button / Resume-Sound hat der
|
||||
// Caller (playFromPath / _playFromPathAtPosition) das Tracking schon
|
||||
// korrekt mit der msgId aus dem Pfad gesetzt — sonst wuerden wir hier
|
||||
// mit leerem pcmMessageId ueberschreiben.
|
||||
if (this.pcmMessageId) {
|
||||
this.playbackStartTime = Date.now();
|
||||
this.currentPlaybackMsgId = this.pcmMessageId;
|
||||
}
|
||||
this.playbackStartedListeners.forEach(cb => {
|
||||
try { cb(); } catch (e) { console.warn('[Audio] playbackStarted listener err:', e); }
|
||||
});
|
||||
@@ -1151,6 +1166,8 @@ class AudioService {
|
||||
* Interruption zurueckgenommen. */
|
||||
private _pausedForCall: boolean = false;
|
||||
setMuted(muted: boolean): void {
|
||||
console.log('[Audio] setMuted: %s (currentSound=%s pcmStreamActive=%s)',
|
||||
muted, this.currentSound ? 'aktiv' : 'null', this.pcmStreamActive);
|
||||
this._muted = muted;
|
||||
if (muted) this.stopPlayback();
|
||||
}
|
||||
@@ -1158,6 +1175,8 @@ class AudioService {
|
||||
|
||||
/** Laufende Wiedergabe stoppen + Queue leeren */
|
||||
stopPlayback(): void {
|
||||
console.log('[Audio] stopPlayback: currentSound=%s queue=%d pcm=%s',
|
||||
this.currentSound ? 'aktiv' : 'null', this.audioQueue.length, this.pcmStreamActive);
|
||||
// Foreground-Service auch stoppen — sonst bleibt die Notification haengen
|
||||
// wenn Wiedergabe abgebrochen wird (Anruf, Cancel, Barge-In).
|
||||
stopBackgroundAudio().catch(() => {});
|
||||
@@ -1168,6 +1187,11 @@ class AudioService {
|
||||
this.currentSound.release();
|
||||
this.currentSound = null;
|
||||
}
|
||||
if (this.resumeSound) {
|
||||
this.resumeSound.stop();
|
||||
this.resumeSound.release();
|
||||
this.resumeSound = null;
|
||||
}
|
||||
if (this.preloadedSound) {
|
||||
this.preloadedSound.release();
|
||||
this.preloadedSound = null;
|
||||
|
||||
@@ -202,14 +202,19 @@ class PhoneCallService {
|
||||
audioService.endCallPause();
|
||||
wakeWordService.resumeFromCall().catch(() => {});
|
||||
ToastAndroid.show(toast, ToastAndroid.SHORT);
|
||||
// Auto-Resume: ab gemerkter Position weiterspielen wenn ARIA vor dem
|
||||
// Anruf gerade redete. Wartet bis zu 30s auf den WAV-Cache (falls
|
||||
// final-Marker erst nach dem Anruf-Ende kam).
|
||||
audioService.resumeFromInterruption(30000).then(ok => {
|
||||
if (ok) {
|
||||
console.log('[PhoneCall] Auto-Resume von gemerkter Position gestartet');
|
||||
}
|
||||
}).catch(() => {});
|
||||
// 800ms warten bevor Auto-Resume — sonst kollidiert ARIA's neuer Focus-
|
||||
// Request mit Spotify's Auto-Resume nach Anruf-Ende. System haengt nach
|
||||
// dem Auflegen noch im IN_CALL-Mode-Uebergang, Spotify schaut auf Focus-
|
||||
// Gain und wuerde sofort wieder LOSS sehen → bleibt pausiert.
|
||||
// Mit Delay: Spotify resumed kurz, dann pausiert ARIA wieder ordnungs-
|
||||
// gemaess. Wenn ARIA nichts pending hat, bleibt Spotify einfach an.
|
||||
setTimeout(() => {
|
||||
audioService.resumeFromInterruption(30000).then(ok => {
|
||||
if (ok) {
|
||||
console.log('[PhoneCall] Auto-Resume von gemerkter Position gestartet');
|
||||
}
|
||||
}).catch(() => {});
|
||||
}, 800);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+54
-4
@@ -677,7 +677,10 @@ class ARIABridge:
|
||||
while self.running:
|
||||
try:
|
||||
logger.info("[core] Verbinde: %s", self.ws_url)
|
||||
async with websockets.connect(self.ws_url) as ws:
|
||||
# max_size=50MB damit grosse Bilder/Voice-Uploads durchgehen.
|
||||
# Python-websockets Default ist nur 1 MiB → 5MB JPEG sprengt
|
||||
# das Limit, Connection wird silent gedroppt.
|
||||
async with websockets.connect(self.ws_url, max_size=50 * 1024 * 1024) as ws:
|
||||
# OpenClaw Handshake durchfuehren
|
||||
if not await self._openclaw_handshake(ws):
|
||||
logger.error("[core] Handshake fehlgeschlagen — Reconnect")
|
||||
@@ -783,13 +786,29 @@ class ARIABridge:
|
||||
await self._emit_activity("idle", "")
|
||||
if not text:
|
||||
logger.warning("[core] chat final ohne Text: %s", json.dumps(payload)[:200])
|
||||
# App+Diagnostic informieren statt stumm — sonst wartet die
|
||||
# UI ewig auf eine Antwort die nicht kommt. Passiert z.B.
|
||||
# wenn Claude-Vision das Bild ablehnt (leere Antwort)
|
||||
# oder die Antwort nur aus Tool-Calls ohne Final-Text bestand.
|
||||
await self._send_to_rvs({
|
||||
"type": "chat",
|
||||
"payload": {
|
||||
"text": "[Hinweis] Antwort ohne Text — moeglicherweise Bild zu gross fuer Vision-API oder reine Tool-Ausfuehrung.",
|
||||
"sender": "aria",
|
||||
},
|
||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||
})
|
||||
return
|
||||
logger.info("[core] Antwort: '%s'", text[:80])
|
||||
await self._process_core_response(text, payload)
|
||||
return
|
||||
|
||||
if state == "error":
|
||||
error = payload.get("error", "Unbekannt")
|
||||
# OpenClaw nutzt errorMessage statt error bei state=error.
|
||||
error = (payload.get("error")
|
||||
or payload.get("errorMessage")
|
||||
or payload.get("message")
|
||||
or "Unbekannt")
|
||||
logger.error("[core] Chat-Fehler: %s", error)
|
||||
self._last_chat_final_at = asyncio.get_event_loop().time()
|
||||
await self._emit_activity("idle", "")
|
||||
@@ -825,7 +844,12 @@ class ARIABridge:
|
||||
return
|
||||
|
||||
if event_name == "chat:error":
|
||||
error = payload.get("error", payload.get("message", "Unbekannt"))
|
||||
# OpenClaw legt den echten Text manchmal in errorMessage ab
|
||||
# (state=error). Vorher wurde nur error/message gechecked → "Unbekannt".
|
||||
error = (payload.get("error")
|
||||
or payload.get("errorMessage")
|
||||
or payload.get("message")
|
||||
or "Unbekannt")
|
||||
logger.error("[core] Chat-Fehler (legacy): %s", error)
|
||||
await self._send_to_rvs({
|
||||
"type": "chat",
|
||||
@@ -1141,7 +1165,8 @@ class ARIABridge:
|
||||
try:
|
||||
url = f"{current_url}?token={self.rvs_token}"
|
||||
logger.info("[rvs] Verbinde: %s", current_url)
|
||||
async with websockets.connect(url) as ws:
|
||||
# max_size=50MB (siehe core-Connect oben — gleicher Grund).
|
||||
async with websockets.connect(url, max_size=50 * 1024 * 1024) as ws:
|
||||
self.ws_rvs = ws
|
||||
retry_delay = 2
|
||||
logger.info("[rvs] Verbunden — warte auf App-Nachrichten")
|
||||
@@ -1461,6 +1486,31 @@ class ARIABridge:
|
||||
size_kb = len(file_b64) // 1365
|
||||
logger.info("[rvs] Datei gespeichert: %s (%dKB)", file_path, size_kb)
|
||||
|
||||
# Pixel-Bilder fuer Claude-Vision shrinken wenn > 2 MB. SVG/PDF/ZIP
|
||||
# bleiben unangetastet (Vision laeuft eh nur auf Raster-Formaten).
|
||||
CLAUDE_VISION_FORMATS = ("image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif")
|
||||
if file_type.lower() in CLAUDE_VISION_FORMATS:
|
||||
file_size_bytes = os.path.getsize(file_path)
|
||||
if file_size_bytes > 2 * 1024 * 1024:
|
||||
try:
|
||||
from PIL import Image
|
||||
with Image.open(file_path) as img:
|
||||
orig_w, orig_h = img.size
|
||||
# Anthropic-Empfehlung: max 1568px lange Seite. RGB-Konvertierung
|
||||
# falls RGBA/Palette (JPEG braucht RGB).
|
||||
img.thumbnail((1568, 1568), Image.Resampling.LANCZOS)
|
||||
if img.mode in ("RGBA", "P"):
|
||||
img = img.convert("RGB")
|
||||
img.save(file_path, "JPEG", quality=85, optimize=True)
|
||||
new_size_bytes = os.path.getsize(file_path)
|
||||
logger.info("[rvs] Bild verkleinert: %dx%d → %dx%d, %.1fMB → %.1fMB",
|
||||
orig_w, orig_h, img.size[0], img.size[1],
|
||||
file_size_bytes / 1024 / 1024,
|
||||
new_size_bytes / 1024 / 1024)
|
||||
except Exception as e:
|
||||
logger.warning("[rvs] Bild-Resize fehlgeschlagen (%s) — Original wird genutzt: %s",
|
||||
file_name, e)
|
||||
|
||||
# In Pending-Queue + Flush-Timer (anti-spam Buffering)
|
||||
self._pending_files.append((file_path, file_name, file_type, size_kb, int(width or 0), int(height or 0)))
|
||||
if self._pending_files_flush_task and not self._pending_files_flush_task.done():
|
||||
|
||||
@@ -16,3 +16,6 @@ sounddevice
|
||||
|
||||
# Wake-Word Erkennung
|
||||
openwakeword
|
||||
|
||||
# Bild-Resizing (zu grosse Pixel-Bilder shrinken bevor Claude-Vision sie sieht — 5MB-Limit)
|
||||
Pillow
|
||||
|
||||
Reference in New Issue
Block a user