Compare commits

..

8 Commits

Author SHA1 Message Date
duffyduck da38cdfefa release: bump version to 0.1.8.9 2026-05-31 23:54:23 +02:00
duffyduck 9c0c13d1f6 fix(audio): expliziter AudioFocus-Request beim TTS-Start — Spotify resumed wieder zuverlaessig
Stefan-Symptom: Spotify pausiert wenn ARIA zuhoert/spricht ✓, aber
resumed nach TTS-Ende NICHT (oder nur unzuverlaessig).

Diagnose aus dem Log-Trace:

  Manual-Button-Flow:
    recording start → AudioFocus.requestExclusive (Spotify pausiert)
    STT-Endpoint    → _cleanupStreamLocal → _releaseFocusDeferred → 800ms
                      Timer → release+nudge (Spotify wuerde resumen)
    Brain processing 50s lang...
    TTS-Start       → KEIN expliziter Focus-Request! AudioTrack-USAGE_
                      ASSISTANT pausiert Spotify nur IMPLIZIT (versions-
                      abhaengig). Wir wissen nicht ob Spotify gerade
                      gepaust ist.
    TTS-End         → release+nudge — aber wenn Spotify implizit-paused
                      ist, hat es keinen sauberen Focus-Owner gesehen
                      und nudge alleine reicht nicht zum Resume.

  Wake-Word-Flow:
    Aehnliches Problem wenn state schon armed ist beim TTS-Ende
    (vom 'no-speech in conv-window'-Pfad), dann ist mein
    endConversation ein noop → releaseConversationFocus laeuft nie,
    nur die PcmPlaybackFinished-Direkt-Release greift, hat aber
    dasselbe nudge-zu-schwach-Problem.

Fix in _firePlaybackStarted (audio.ts): EXPLIZITES AudioFocus.requestDuck
beim ersten TTS-PCM-Chunk. Damit IST Spotify ueber unseren Focus
gepaused, statt nur implizit. Der spaetere release beim Pcm-Playback-
Finished ist dann das normale 'Owner ist fertig'-Pattern das Spotify
zuverlaessig zum Resume triggert.

Idempotenz: requestDuck released vorher den vorigen Focus (in Kotlin),
also harmlos wenn wake-word-acquireConversationFocus eh schon requestDuck
gerufen hat. Plus _cancelDeferredFocusRelease vorne, damit kein noch
pendender 800ms-Timer mitten in der TTS Spotify falsch resumed.

Stefan testet im Auto und auf dem Tisch — beide Spotify-Versionen
sollten mit explizitem Focus-Owner-Wechsel sauber pausieren+resumen.
2026-05-31 23:51:43 +02:00
duffyduck ba26fa5880 feat(voice): ARIA produziert TTS-sprechbare Variante via <voice>-Tag
Stefan-Beobachtung: Wetterbericht klang scheisse, weil 'kt'/'°C'/Komma-
Zahlen literal vorgelesen wurden. Der clean_text_for_tts-Regex kennt nur
Markdown + ein paar Uhrzeit-Patterns — Einheiten ausschreiben war never
on the table mit Regex (waechst sonst zur Mammut-Tabelle).

Loesung: hybrid. ARIA selbst macht die semantisch korrekte TTS-Variante,
Regex bleibt als Safety-Net.

bridge/aria_bridge.py:
  - neue Helper strip_voice_tag_for_display(text)
  - chat-broadcast (1188) und chat_backup-Persist (1157) strippen den Tag
    BEVOR Output. ARIA's `<voice>...</voice>` lebt nur transient in `text`
    bis clean_text_for_tts ihn fuer TTS extrahiert.
  - clean_text_for_tts unveraendert (kennt den Tag schon seit Phase 1).

aria-brain/seed_rules.py:
  - neue seed-rule voice/tts-voice-tag mit klarer Trigger-Liste
    (Einheiten, Komma-Zahlen, Uhrzeiten, Statusberichte, lange IDs/Codes)
  - klare Anti-Trigger (kurze 'OK'/'mache ich'-Antworten — kein doppelter
    Text fuer Trivia)
  - drei konkrete Beispiele (Wetter, Uhrzeit, Server-Status, Music-Now-Playing)

Output-Format: ARIA schreibt erst Chat-Display-Variante (mit Markdown OK),
haengt dann an EINER neuen Zeile den <voice>-Block an. Tag wird automatisch
gestrippt fuer App-Anzeige + Chat-Backup. f5tts kriegt nur den voice-Inhalt.

Beide Welten happy: Stefan sieht hervorgehobenes Markdown in der Bubble,
hoert sprechbar formulierten Text aus dem Lautsprecher. Keine wachsende
Regex-Tabelle mehr.

Deploy: brain rebuild + restart, bridge restart.
2026-05-31 01:20:17 +02:00
duffyduck 027ba2896d fix(brain): dritter Tool-Result-Truncation-Punkt — agent.py:947 von 8KB auf 50KB
Im Stefan-Test (31.05.2026) hat ARIA das spotify-Skill korrekt mit
_all=true aufgerufen, der Skill paginierte sauber alle 90 Playlists
in 34 KB compact JSON. Aber: ARIA's Thinking sagte 'tool result was
cut off'. Disk-Log + Skill-Return waren beide OK. agent.py:1160 cap
ist 50 KB (passt). Aber:

agent.py:947 cap'd tool_result[:8000] beim Append in die Proxy-
Conversation. DRITTER Truncation-Punkt fuer denselben Output, fuer
Claude effectiv abgeschnitten — Skill liefert 34 KB, ARIA sieht 8 KB.

Auf 50 KB hochgesetzt (konsistent mit dem anderen Cap in derselben
Datei). Tests + nochmal restart noetig.

Lerneffekt: bei Stdout-Caps suche IMMER alle Truncation-Punkte. War
jetzt der dritte den ich heute gefunden hab. ;-)
2026-05-31 01:03:20 +02:00
duffyduck 86f20d3b64 clude config 2026-05-31 00:19:54 +02:00
duffyduck 78211f09ce feat(brain): Listen-API-Pagination strukturell loesen + seed-rule
Stefan-Reproduktion vom 31.05.2026: bei 'Such Playlist Prodigy raus'
hat ARIA die Spotify-Pagination drei Mal hintereinander laufen lassen,
jedes Mal eine andere Playlist-ID gefunden, am Ende falsche abgespielt.
Spotify sortiert /v1/me/playlists nach recently-played — die Reihen-
folge aendert sich zwischen Calls wenn parallel was laeuft, also
liefern aufeinanderfolgende paginierte Runs inkonsistente Snapshots.

Loesungen:

1. **spotify-Skill _all=true** (via skill_update angewendet, lebt nur
   in /data/skills/spotify/ im Container, nicht in git): Skill prueft
   _all=true im URL-Query, paginiert dann intern ueber Spotifys
   next-Field bis MAX_PAGES (20) oder fertig. Liefert konsolidiertes
   JSON {items, total, fetched_count, fetched_pages}. EIN Tool-Call,
   konsistenter Snapshot.

2. **skills.py: Stdout-Truncation entkoppeln**. Vorher: 8000-char-Cap
   sowohl fuer Disk-Log als auch fuer Return-Value an Agent.
   Konsequenz: _all=true Output (50 KB JSON) wurde fuer ARIA auf 8 KB
   gekuerzt, sie sah nur die ersten ~20 Playlists. Jetzt:
     - Disk-Log: weiterhin 8 KB pro stdout (Disk-Schoner)
     - Return-Value: ungekuerzt, agent.py macht 50 KB downstream-Cap
   Skills.py:687 — record-Dict aufgesplittet in log_record + record.

3. **seed_rule list-api-pagination-snapshot**: dokumentiert das
   Pattern fuer ARIA — bei Pagination-Resultaten IMMER vollstaendig
   laden bevor Entscheidung; _all=true bevorzugen wo verfuegbar;
   bei inkonsistenten Match-Resultaten ehrlich nachfragen statt
   raten. Mit konkreter Antipattern-Sammlung aus Stefans Test.

Deployment: brain restart noetig damit (2) und (3) greifen. Skill-
Code (1) ist schon via PATCH /skills/spotify aktiv.
2026-05-31 00:14:06 +02:00
duffyduck b2edee9adb release: bump version to 0.1.8.8 2026-05-30 23:32:27 +02:00
duffyduck bb13477ef9 fix(wake): Race zwischen endConversation und stopBargeListening killt
Wake-Word-Listener nach jeder Konversation

Aus dem Log diagnostiziert: zwei onPlaybackFinished-Listener feuern
direkt hintereinander wenn TTS endet:
  1. mein neuer Listener (Background): endConversation()
     → state=armed, OpenWakeWord.start() (idempotent)
  2. existierender Listener:           stopBargeListening()
     → bargeListening=true → OpenWakeWord.stop()  ← killt re-armed Listener

State zeigte 'armed' (UI: Ohr-Icon ausgefuellt, sieht aktiv aus), aber
das Native-Modul war gestoppt → Stefan's "Computer" verpufft.

Fix: endConversation setzt bargeListening=false BEVOR Native gerufen
wird. stopBargeListening checkt das Flag oben:
  async stopBargeListening() { if (!this.bargeListening) return; ... }
→ wird zum No-Op wenn endConversation schon gelaufen ist.

Bonus: OpenWakeWord.start() darf jetzt auch gerufen werden wenn der
Listener via barge-listening schon lief — Kotlin checkt running.get()
und resolved idempotent. Sicherer als state-vorher-Check.
2026-05-30 23:31:25 +02:00
9 changed files with 236 additions and 18 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(ssh root@172.0.2.33 \"ls -la /root/ARIA-AGENT/aria-shared/logs/\")"
]
}
}
+2 -2
View File
@@ -79,8 +79,8 @@ android {
applicationId "com.ariacockpit" applicationId "com.ariacockpit"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 10807 versionCode 10809
versionName "0.1.8.7" versionName "0.1.8.9"
// Fallback fuer Libraries mit Product Flavors // Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'react-native-camera', 'general'
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "aria-cockpit", "name": "aria-cockpit",
"version": "0.1.8.7", "version": "0.1.8.9",
"private": true, "private": true,
"scripts": { "scripts": {
"android": "react-native run-android", "android": "react-native run-android",
+12
View File
@@ -1518,6 +1518,18 @@ class AudioService {
this.playbackStartTime = Date.now(); this.playbackStartTime = Date.now();
this.currentPlaybackMsgId = this.pcmMessageId; this.currentPlaybackMsgId = this.pcmMessageId;
} }
// AudioFocus EXPLIZIT fuer TTS halten — sonst pausiert Spotify zwar
// beim Recording-requestExclusive, der wird aber 800ms nach STT-Endpoint
// released (Brain-Processing-Gap), und wenn dann TTS startet ist niemand
// mehr Focus-Owner. Spotify pausiert evtl. implizit beim AudioTrack-
// USAGE_ASSISTANT, aber unsere nachtraegliche release+nudge-Sequenz
// kann es dann nicht zuverlaessig wieder anstossen. Mit explizitem
// requestDuck IST Spotify sauber-via-Focus pausiert, und der Release
// beim PcmPlaybackFinished triggert das normale "Owner fertig → resume"-
// Pattern in Spotify — funktioniert versionsunabhaengig.
// Pending Release-Timer canceln damit der nicht mitten in der TTS feuert.
this._cancelDeferredFocusRelease();
AudioFocus?.requestDuck().catch(() => {});
this.playbackStartedListeners.forEach(cb => { this.playbackStartedListeners.forEach(cb => {
try { cb(); } catch (e) { console.warn('[Audio] playbackStarted listener err:', e); } try { cb(); } catch (e) { console.warn('[Audio] playbackStarted listener err:', e); }
}); });
+20 -5
View File
@@ -344,23 +344,38 @@ class WakeWordService {
/** Konversation beenden — User hat im Window nichts gesagt. /** Konversation beenden — User hat im Window nichts gesagt.
* Mit Wake-Word: zurueck zu 'armed' (Listener wieder an). * Mit Wake-Word: zurueck zu 'armed' (Listener wieder an).
* Ohne: zurueck zu 'off'. * Ohne: zurueck zu 'off'.
*
* WICHTIG: setzt bargeListening=false BEVOR OpenWakeWord.start() laeuft.
* Grund: wenn endConversation aus dem onPlaybackFinished-Handler kommt,
* feuert direkt danach ein zweiter Listener (stopBargeListening) — der
* wuerde sonst OpenWakeWord.stop() rufen weil bargeListening noch true
* ist, und unseren frisch re-armierten Listener killen.
*/ */
async endConversation(): Promise<void> { async endConversation(): Promise<void> {
if (this.state !== 'conversing') { if (this.state !== 'conversing') {
// Nicht in conversing — typ. nach App-Resume bevor Streaming endete.
// Trotzdem loggen damit wir's im Diagnostic sehen.
import('./logger').then(m => m.reportAppDebug('wake.end', import('./logger').then(m => m.reportAppDebug('wake.end',
`endConversation called but state=${this.state} → noop`)).catch(()=>{}); `endConversation called but state=${this.state} → noop`)).catch(()=>{});
return; return;
} }
const wasBarge = this.bargeListening;
// Flag NULLEN bevor wir die Listener triggern. Sonst killt der parallele
// stopBargeListening-Listener (TTS-end) gleich danach unseren Native-
// OpenWakeWord, weil er bargeListening=true sieht und annimmt er muss
// den Listener stoppen.
this.bargeListening = false;
import('./logger').then(m => m.reportAppDebug('wake.end', import('./logger').then(m => m.reportAppDebug('wake.end',
`endConversation called, nativeReady=${this.nativeReady}, calling OpenWakeWord.start()`)).catch(()=>{}); `endConversation called, wasBarge=${wasBarge}, nativeReady=${this.nativeReady}`)).catch(()=>{});
if (this.nativeReady && OpenWakeWord) { if (this.nativeReady && OpenWakeWord) {
// Wenn wakeword schon laeuft (war Barge-Listener waehrend TTS):
// OpenWakeWord.start() ist idempotent (Kotlin checkt running.get()
// und resolved sofort). Wir koennen es trotzdem rufen — billiger
// als state extra zu fragen, garantiert dass nach diesem Pfad
// Native auch wirklich an ist falls es out-of-band gestoppt wurde.
try { try {
await OpenWakeWord.start(); await OpenWakeWord.start();
console.log('[WakeWord] Konversation zu Ende — zurueck zu armed'); console.log('[WakeWord] Konversation zu Ende — zurueck zu armed (wasBarge=%s)', wasBarge);
import('./logger').then(m => m.reportAppDebug('wake.end', import('./logger').then(m => m.reportAppDebug('wake.end',
`OpenWakeWord.start() OK → state=armed, keyword=${this.keyword}`)).catch(()=>{}); `OpenWakeWord.start() OK → state=armed, wasBarge=${wasBarge}`)).catch(()=>{});
ToastAndroid.show(`Lausche wieder auf "${KEYWORD_LABELS[this.keyword]}"`, ToastAndroid.SHORT); ToastAndroid.show(`Lausche wieder auf "${KEYWORD_LABELS[this.keyword]}"`, ToastAndroid.SHORT);
this.setState('armed'); this.setState('armed');
return; return;
+9 -1
View File
@@ -940,11 +940,19 @@ class Agent:
# Tools ausfuehren + Ergebnis als role=tool zurueck # Tools ausfuehren + Ergebnis als role=tool zurueck
for tc in result.tool_calls: for tc in result.tool_calls:
tool_result = self._dispatch_tool(tc["name"], tc["arguments"]) tool_result = self._dispatch_tool(tc["name"], tc["arguments"])
# Cap auf 50 KB — passt zur Cap in _dispatch_tool fuer
# Skill-Outputs (siehe agent.py weiter unten). 8 KB war
# viel zu wenig: Spotify _all=true mit 90 Playlists
# liefert ~34 KB compact, das wurde hier auf 8 KB
# zugeschnitten und ARIA glaubte die Liste sei
# abgeschnitten obwohl der Skill alles korrekt
# paginiert hatte. Claude-Context vertraegt locker
# 50 KB pro Tool-Result.
messages.append(ProxyMessage( messages.append(ProxyMessage(
role="tool", role="tool",
tool_call_id=tc["id"], tool_call_id=tc["id"],
name=tc["name"], name=tc["name"],
content=tool_result[:8000], content=tool_result[:50000],
)) ))
continue # next iteration mit Tool-Results continue # next iteration mit Tool-Results
# Kein Tool-Call mehr → final reply # Kein Tool-Call mehr → final reply
+129
View File
@@ -602,6 +602,135 @@ SEED_RULES: List[dict] = [
"'API Key' im Auth-Kapitel). Nicht raten." "'API Key' im Auth-Kapitel). Nicht raten."
), ),
}, },
{
"migration_key": "seed/voice/tts-voice-tag",
"type": "rule",
"title": "TTS-sprechbar: `<voice>...</voice>`-Tag fuer Antworten mit Einheiten/Zahlen/Markdown",
"category": "voice",
"content": (
"Die App spielt jede ARIA-Antwort als TTS ab. Der Brain-Bridge "
"filtert Markdown raus (Sternchen, Code-Bloecke, URLs), kennt "
"aber keine Einheiten-/Zahlen-Konvention — der Sprecher liest "
"dann '15 kt' als 'fuenfzehn k t' und '23,5°C' als 'dreiund-"
"zwanzig komma fuenf grad c'. Klingt scheisse.\n"
"\n"
"LOESUNG: Wenn deine Antwort eine der folgenden Eigenschaften hat, "
"haenge einen `<voice>...</voice>`-Block ans ENDE der Antwort. "
"Was DRIN steht ersetzt komplett den TTS-Text — Markdown im "
"Chat-Display bleibt unangetastet, gesprochen wird ausschliess-"
"lich die <voice>-Variante.\n"
"\n"
"WANN <voice>-Tag setzen:\n"
" - Einheiten-Abkuerzungen: kt, kg, km/h, °C, hPa, mbar, mph, "
" psi, dB, GB, MB, kWh, mAh ...\n"
" - Zahlen mit Komma (23,5 → 'dreiundzwanzig komma fuenf')\n"
" - Uhrzeiten mit Minuten (8:42 → 'acht Uhr zweiundvierzig')\n"
" - Wettervorhersagen / Statusberichte mit mehreren Daten\n"
" - Tabellen oder Listen mit Werten\n"
" - Lange Zahlen / IDs / Codes ('spotify:playlist:abc' nicht "
" vorlesen)\n"
" - Code-Bloecke (sollte ARIA in Sprache eh nicht zitieren)\n"
"\n"
"WANN NICHT (Overhead vermeiden):\n"
" - Kurze Statussaetze ('OK', 'mach ich', 'klar', 'spielt')\n"
" - Reine Prosa ohne Zahlen oder Einheiten\n"
" - Antworten unter 15 Worten ohne komplexes Element\n"
"\n"
"FORMAT:\n"
" Erst die Chat-Display-Variante (mit Markdown OK), dann an einer "
" neuen Zeile der <voice>-Block:\n"
"\n"
" Antwort-Text mit **Markdown**, Zahlen, Einheiten\n"
" <voice>Antwort-Text fuer den Lautsprecher, ausgeschrieben</voice>\n"
"\n"
"BEISPIEL Wetter:\n"
" **Wetter Berlin:** 23,5°C, Wind 15 kt aus NW, Druck 1018 hPa.\n"
" <voice>Das Wetter in Berlin: dreiundzwanzig Grad fuenf, "
" Wind mit fuenfzehn Knoten aus Nordwest, Luftdruck "
" tausendachtzehn Hektopascal.</voice>\n"
"\n"
"BEISPIEL Uhrzeit:\n"
" Stefan, dein Termin ist um **8:42** — noch 25 Minuten.\n"
" <voice>Stefan, dein Termin ist um acht Uhr zweiundvierzig. "
" Du hast noch fuenfundzwanzig Minuten.</voice>\n"
"\n"
"BEISPIEL Akku/Speicher:\n"
" Server: 87% Last, 12,4 GB RAM frei, Uptime 142h.\n"
" <voice>Server bei siebenundachtzig Prozent Last, zwoelf "
" Komma vier Gigabyte RAM frei, Laufzeit hundertzweiundvierzig "
" Stunden.</voice>\n"
"\n"
"BEISPIEL Multi-Track (NICHT vorlesen was nicht sprechbar ist):\n"
" Spielt jetzt: **Firestarter** (3:47) auf duffy-desktop.\n"
" <voice>Spielt jetzt Firestarter, drei Minuten siebenund-"
" vierzig.</voice> ← Device weglassen, war im Chat zur Info, "
" fuer Stefan akustisch redundant\n"
"\n"
"Der Voice-Tag wird automatisch aus Chat-Bubble und Chat-Backup "
"gestrippt — Stefan sieht NUR die Markdown-Variante in der App. "
"Voice-Text geht ausschliesslich an F5-TTS. Beide Welten happy.\n"
"\n"
"Sicherheitsnetz: wenn Du den Tag mal vergisst, faellt clean_text_"
"for_tts auf die alte Regex-Cleanup-Pipeline zurueck (Markdown weg, "
"Uhrzeiten teilweise ausgeschrieben). Aber 'kt' wird dann literal "
"vorgelesen. Also: lieber Tag setzen wenn unsicher."
),
},
{
"migration_key": "seed/skill-rule/list-api-pagination-snapshot",
"type": "rule",
"title": "Listen-API: einmal vollstaendig laden, DANN entscheiden",
"category": "verhalten",
"content": (
"Wenn ein Tool-Resultat ein Pagination-Schema hat (limit/offset/"
"next oder total > limit): ALLE Seiten in EINEM Tool-Call holen, "
"in EINEM Snapshot durchsuchen, ERST DANN handeln.\n"
"\n"
"Antipattern (31.05.2026, Stefan reproduziert mit 'Playlist Prodigy "
"raussuchen'):\n"
" - run_spotify path=/v1/me/playlists?limit=50\n"
"'nicht dabei'\n"
" - run_spotify path=/v1/me/playlists?limit=50&offset=50\n"
"'gefunden, ID=X' (46 Tracks)\n"
" - run_spotify path=/v1/me/player/play body={context_uri: ...:X}\n"
" → spielt aber FALSCHE Playlist\n"
" - Neue Suche, wieder paginiert → drittes Match ID=Y (15 Tracks)\n"
" - Insgesamt drei verschiedene IDs fuer dieselbe gesuchte Playlist\n"
" generiert, am Ende die falsche gespielt.\n"
"\n"
"Wurzel: Spotify sortiert /v1/me/playlists nach recently-played. "
"Zwischen aufeinanderfolgenden paginierten Calls AENDERT SICH die "
"Reihenfolge wenn parallel was abgespielt wird. Teilresultate aus "
"verschiedenen Calls vergleichen → inkonsistent.\n"
"\n"
"Richtig fuer Spotify (seit 31.05.2026 unterstuetzt):\n"
" run_spotify path=/v1/me/playlists?limit=50&_all=true\n"
" → Skill paginiert intern, liefert {items, total, fetched_count}.\n"
" → In items[] suchen, EINE ID waehlen, sofort handeln.\n"
" → Match-Logik: bevorzugt exakter Name (case-insensitive). "
"Wenn mehrere Substring-Matches: explizit nachfragen statt raten.\n"
"\n"
"Wann _all=true sinnvoll:\n"
" - /v1/me/playlists (alle eigenen Playlists)\n"
" - /v1/playlists/{id}/tracks (alle Tracks einer Playlist)\n"
" - /v1/me/tracks (Liked Songs)\n"
" - /v1/search?type=playlist&q=... (Such-Ergebnisse mit next)\n"
" - Andere Endpunkte mit items+next-Schema.\n"
"\n"
"Wann NICHT _all=true:\n"
" - /v1/me/player/currently-playing (kein Listen-Endpunkt)\n"
" - /v1/me/player/devices (kurze Liste, kein next)\n"
" - Wenn Du explizit nur 'die ersten 10' willst.\n"
"\n"
"Fuer andere Skills (yt-dlp, andere APIs) die noch kein _all "
"unterstuetzen: manuell paginieren bis total erreicht, ALLES in "
"EINEM mentalen Snapshot mergen, NIEMALS auf Teilresultaten "
"Entscheidungen treffen. Wenn zwei Pagination-Runs unterschiedliche "
"Matches liefern: ehrlich melden ('zwei verschiedene Playlists "
"namens X gefunden — welche meinst Du?') statt sich auf eine "
"festzulegen."
),
},
] ]
+21 -5
View File
@@ -683,8 +683,13 @@ def run_skill(name: str, args: Optional[dict] = None, timeout_sec: int = 300) ->
timed_out = True timed_out = True
duration = time.time() - t0 duration = time.time() - t0
# Log schreiben (gekuerzt damit es nicht explodiert) # Log auf der Disk wird gekuerzt (8000 chars) — sonst sammeln sich
record = { # logs/*.json mit MBs an grossen Skill-Outputs an. Der Return-Value
# an den Caller (Agent) bekommt aber den vollen Output, dort wird
# nochmal in agent.py auf 50000 gecappt. Stefan-Fall: spotify-Skill
# mit _all=true liefert 50+ KB JSON, das hier wurde vorher auf 8 KB
# gekappt → ARIA sah immer nur den Anfang der Liste.
log_record = {
"ts": _now(), "ts": _now(),
"args": args or {}, "args": args or {},
"exit_code": exit_code, "exit_code": exit_code,
@@ -694,7 +699,7 @@ def run_skill(name: str, args: Optional[dict] = None, timeout_sec: int = 300) ->
"timed_out": timed_out, "timed_out": timed_out,
} }
try: try:
log_path.write_text(json.dumps(record, indent=2, ensure_ascii=False), encoding="utf-8") log_path.write_text(json.dumps(log_record, indent=2, ensure_ascii=False), encoding="utf-8")
except Exception: except Exception:
pass pass
@@ -703,8 +708,19 @@ def run_skill(name: str, args: Optional[dict] = None, timeout_sec: int = 300) ->
manifest["use_count"] = int(manifest.get("use_count", 0)) + 1 manifest["use_count"] = int(manifest.get("use_count", 0)) + 1
write_manifest(name, manifest) write_manifest(name, manifest)
record["ok"] = exit_code == 0 # Return-Value: nicht kuerzen (Agent kuerzt downstream selbst). Nur
record["log_path"] = str(log_path) # die Disk-Log-Variante war beschnitten.
record = {
"ts": log_record["ts"],
"args": log_record["args"],
"exit_code": exit_code,
"duration_sec": log_record["duration_sec"],
"stdout": out_text or "",
"stderr": err_text or "",
"timed_out": timed_out,
"ok": exit_code == 0,
"log_path": str(log_path),
}
return record return record
+35 -4
View File
@@ -208,6 +208,30 @@ _UNIT_WORDS = [
] ]
def strip_voice_tag_for_display(text: str) -> str:
"""Entfernt `<voice>...</voice>`-Bloecke aus dem Chat-Display-Text.
ARIA kann einen <voice>-Block ANHAENGEN um eine TTS-freundliche Variante
ihrer Antwort zu liefern (Zahlen ausgeschrieben, Einheiten als Wort,
Markdown entfernt). Der Block wird dann von clean_text_for_tts als
TTS-Quelle benutzt — fuer die Chat-Bubble in der App soll er aber NICHT
sichtbar sein, sonst sieht Stefan literal '<voice>...' in seinem Chat.
Beispiel-Input (Stefan-typisch fuer Wetterbericht):
'**Wetter:** 23,5°C, Wind 15 kt NW\\n<voice>Wetter: dreiundzwanzig
komma fuenf Grad, Wind fuenfzehn Knoten Nordwest.</voice>'
Output:
'**Wetter:** 23,5°C, Wind 15 kt NW'
Mehrere Voice-Bloecke werden alle entfernt (ARIA koennte theoretisch
mehrere setzen, machen wir robust). Trailing-Whitespace nach dem Block
auch wegtrimmen.
"""
if not text or "<voice>" not in text.lower():
return text
return _re_tts.sub(r'\s*<voice>[\s\S]*?</voice>\s*', '\n', text, flags=_re_tts.IGNORECASE).strip()
def clean_text_for_tts(text: str) -> str: def clean_text_for_tts(text: str) -> str:
"""Bereitet Chat-Text fuer Sprachausgabe auf. """Bereitet Chat-Text fuer Sprachausgabe auf.
@@ -1150,11 +1174,15 @@ class ARIABridge:
f"aber nicht erstellt:\n{missing_list}\n" f"aber nicht erstellt:\n{missing_list}\n"
"Bitte ARIA bitten, sie wirklich zu schreiben.").strip() "Bitte ARIA bitten, sie wirklich zu schreiben.").strip()
# Antwort in chat_backup.jsonl loggen (gecleanter Text, ohne File-Marker) # Antwort in chat_backup.jsonl loggen (gecleanter Text, ohne File-Marker
# UND ohne <voice>-Tag — der ist eine TTS-Annotation, gehoert nicht in
# die Chat-Historie weil ARIA ihre eigene Vorgaenger-Antwort sonst mit
# Voice-Tag-Noise als Kontext sieht).
# File-Marker werden separat als file_from_aria-Events ausgeliefert. # File-Marker werden separat als file_from_aria-Events ausgeliefert.
display_text = strip_voice_tag_for_display(text)
assistant_backup_ts = self._append_chat_backup({ assistant_backup_ts = self._append_chat_backup({
"role": "assistant", "role": "assistant",
"text": text, "text": display_text,
"files": [{"serverPath": f["serverPath"], "name": f["name"], "files": [{"serverPath": f["serverPath"], "name": f["name"],
"mimeType": f["mimeType"], "size": f["size"]} for f in aria_files], "mimeType": f["mimeType"], "size": f["size"]} for f in aria_files],
}) })
@@ -1181,11 +1209,14 @@ class ARIABridge:
# TTS-aufbereitete Variante fuer Debug (Diagnostic zeigt optional) # TTS-aufbereitete Variante fuer Debug (Diagnostic zeigt optional)
tts_text_preview = clean_text_for_tts(text) tts_text_preview = clean_text_for_tts(text)
# Antwort an die App weiterleiten (als Chat-Nachricht) # Antwort an die App weiterleiten (als Chat-Nachricht).
# display_text == text aber ohne <voice>-Tag — der lebt nur transient
# in `text` damit clean_text_for_tts weiter unten daraus die TTS-
# Variante zieht. Im Chat-Bubble soll der Tag nicht erscheinen.
await self._send_to_rvs({ await self._send_to_rvs({
"type": "chat", "type": "chat",
"payload": { "payload": {
"text": text, "text": display_text,
"sender": "aria", "sender": "aria",
"messageId": message_id, "messageId": message_id,
# backupTs = der ts in chat_backup.jsonl. Wird von Clients als # backupTs = der ts in chat_backup.jsonl. Wird von Clients als