Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da38cdfefa | |||
| 9c0c13d1f6 | |||
| ba26fa5880 | |||
| 027ba2896d | |||
| 86f20d3b64 | |||
| 78211f09ce | |||
| b2edee9adb | |||
| bb13477ef9 | |||
| 710e7c88d8 | |||
| b6ee5552f0 | |||
| 570eb031e0 | |||
| e9615d987e | |||
| 5e95eacd11 | |||
| ece08f0f2f |
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(ssh root@172.0.2.33 \"ls -la /root/ARIA-AGENT/aria-shared/logs/\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -79,8 +79,8 @@ android {
|
||||
applicationId "com.ariacockpit"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 10804
|
||||
versionName "0.1.8.4"
|
||||
versionCode 10809
|
||||
versionName "0.1.8.9"
|
||||
// Fallback fuer Libraries mit Product Flavors
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aria-cockpit",
|
||||
"version": "0.1.8.4",
|
||||
"version": "0.1.8.9",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -497,6 +497,49 @@ const SettingsScreen: React.FC = () => {
|
||||
})();
|
||||
}
|
||||
|
||||
// Datei-Manager: Einzel-Datei-Download. ChatScreen subscribet auch auf
|
||||
// file_response — der versucht aber nur Chat-Bubble-Attachments zu
|
||||
// patchen und macht nix wenn die requestId nicht zu einer Nachricht
|
||||
// passt. Hier behandeln wir die Manager-initiierten Downloads
|
||||
// (requestId-Praefix 'single-' aus bulkDownload). Schreibt nach
|
||||
// ~/Download/ wie der ZIP-Pfad.
|
||||
if (message.type === ('file_response' as any)) {
|
||||
const p: any = message.payload || {};
|
||||
const reqId = (p.requestId as string) || '';
|
||||
if (!reqId.startsWith('single-')) return; // nicht unsere Anfrage
|
||||
if (p.error) {
|
||||
ToastAndroid.show('Download fehlgeschlagen: ' + p.error, ToastAndroid.LONG);
|
||||
return;
|
||||
}
|
||||
const b64 = (p.base64 as string) || '';
|
||||
if (!b64) return;
|
||||
const fileName = (p.name as string) ||
|
||||
(p.serverPath as string || '').split('/').pop() ||
|
||||
'aria-download';
|
||||
(async () => {
|
||||
try {
|
||||
const dir = RNFS.DownloadDirectoryPath;
|
||||
const filePath = `${dir}/${fileName}`;
|
||||
// Falls Datei schon existiert: Suffix anhaengen damit nichts
|
||||
// ueberschrieben wird.
|
||||
let target = filePath;
|
||||
let i = 1;
|
||||
while (await RNFS.exists(target)) {
|
||||
const dot = fileName.lastIndexOf('.');
|
||||
const base = dot > 0 ? fileName.slice(0, dot) : fileName;
|
||||
const ext = dot > 0 ? fileName.slice(dot) : '';
|
||||
target = `${dir}/${base} (${i})${ext}`;
|
||||
i++;
|
||||
}
|
||||
await RNFS.writeFile(target, b64, 'base64');
|
||||
const sizeKb = Math.round(((b64.length * 0.75)) / 1024);
|
||||
ToastAndroid.show(`Gespeichert: ${target.split('/').pop()} (${sizeKb} KB)`, ToastAndroid.LONG);
|
||||
} catch (e: any) {
|
||||
ToastAndroid.show('Speichern fehlgeschlagen: ' + e.message, ToastAndroid.LONG);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// Voice wurde gespeichert → Liste neu laden + ggf. auswaehlen
|
||||
if (message.type === ('xtts_voice_saved' as any)) {
|
||||
const name = (message.payload as any).name as string;
|
||||
|
||||
@@ -341,8 +341,21 @@ class AudioService {
|
||||
try {
|
||||
const emitter = new NativeEventEmitter(NativeModules.PcmStreamPlayer as any);
|
||||
emitter.addListener('PcmPlaybackFinished', () => {
|
||||
console.log('[Audio] PcmPlaybackFinished — Focus jetzt freigeben');
|
||||
console.log('[Audio] PcmPlaybackFinished — AudioTrack drained');
|
||||
this._releaseFocusDeferred();
|
||||
// Erst HIER playbackFinished-Listener feuern — nicht schon beim
|
||||
// Empfang des letzten PCM-Chunks (siehe handlePcmChunk). AudioTrack
|
||||
// braucht nach end() noch 1-2s zum Drainen seines Hardware-Buffers.
|
||||
// Wenn wir die Listener zu frueh feuern, re-armt OpenWakeWord
|
||||
// waehrend ARIA noch hoerbar spricht → ARIAs Stimme verwirrt die
|
||||
// Wake-Word-Detection (kein gemeinsames AEC zwischen AudioTrack-
|
||||
// und AudioRecord-Session). Stefan-Reproduktion: nach jeder ARIA-
|
||||
// Antwort schluckte das Wake-Word den naechsten Trigger.
|
||||
import('./logger').then(m => m.reportAppDebug('audio.playback',
|
||||
'PcmPlaybackFinished native event → fire listeners')).catch(()=>{});
|
||||
this.playbackFinishedListeners.forEach(cb => {
|
||||
try { cb(); } catch (e) { console.warn('[Audio] playbackFinished cb err:', e); }
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn('[Audio] PcmPlaybackFinished-Subscription fehlgeschlagen:', err);
|
||||
@@ -1368,12 +1381,13 @@ class AudioService {
|
||||
// releasen den AudioFocus NICHT hier — der writer braucht u.U. noch
|
||||
// 30+ Sekunden bis der Buffer wirklich abgespielt ist. Den release
|
||||
// triggert das native Event "PcmPlaybackFinished" wenn AudioTrack
|
||||
// wirklich am Ende ist (siehe ensurePlaybackFinishedListener).
|
||||
// wirklich am Ende ist (siehe Constructor-PcmPlaybackFinished-Handler).
|
||||
//
|
||||
// playbackFinishedListeners feuern AUCH erst dort — frueher feuerten
|
||||
// sie hier (beim Eintreffen des letzten Chunks), das fuehrte zu
|
||||
// einem Race: OpenWakeWord re-armte waehrend AudioTrack noch hoerbar
|
||||
// ARIAs Stimme abspielte → naechstes Wake-Word ging unter.
|
||||
try { await PcmStreamPlayer!.end(); } catch {}
|
||||
// playbackFinished-Listener informieren (UI-Logik)
|
||||
this.playbackFinishedListeners.forEach(cb => {
|
||||
try { cb(); } catch (e) { console.warn('[Audio] playbackFinished cb err:', e); }
|
||||
});
|
||||
}
|
||||
this.pcmStreamActive = false;
|
||||
|
||||
@@ -1504,6 +1518,18 @@ class AudioService {
|
||||
this.playbackStartTime = Date.now();
|
||||
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 => {
|
||||
try { cb(); } catch (e) { console.warn('[Audio] playbackStarted listener err:', e); }
|
||||
});
|
||||
|
||||
@@ -344,21 +344,51 @@ class WakeWordService {
|
||||
/** Konversation beenden — User hat im Window nichts gesagt.
|
||||
* Mit Wake-Word: zurueck zu 'armed' (Listener wieder an).
|
||||
* 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> {
|
||||
if (this.state !== 'conversing') return;
|
||||
if (this.state !== 'conversing') {
|
||||
import('./logger').then(m => m.reportAppDebug('wake.end',
|
||||
`endConversation called but state=${this.state} → noop`)).catch(()=>{});
|
||||
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',
|
||||
`endConversation called, wasBarge=${wasBarge}, nativeReady=${this.nativeReady}`)).catch(()=>{});
|
||||
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 {
|
||||
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',
|
||||
`OpenWakeWord.start() OK → state=armed, wasBarge=${wasBarge}`)).catch(()=>{});
|
||||
ToastAndroid.show(`Lausche wieder auf "${KEYWORD_LABELS[this.keyword]}"`, ToastAndroid.SHORT);
|
||||
this.setState('armed');
|
||||
return;
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.warn('[WakeWord] re-arm fehlgeschlagen:', err);
|
||||
import('./logger').then(m => m.reportAppDebug('wake.end',
|
||||
`OpenWakeWord.start() FAIL: ${err?.message || err} → state=off`,
|
||||
)).catch(()=>{});
|
||||
}
|
||||
}
|
||||
console.log('[WakeWord] Konversation zu Ende — Ohr aus');
|
||||
import('./logger').then(m => m.reportAppDebug('wake.end',
|
||||
`fallback: nativeReady=${this.nativeReady} → state=off`)).catch(()=>{});
|
||||
ToastAndroid.show('Mikro aus', ToastAndroid.SHORT);
|
||||
this.setState('off');
|
||||
}
|
||||
|
||||
+9
-1
@@ -940,11 +940,19 @@ class Agent:
|
||||
# Tools ausfuehren + Ergebnis als role=tool zurueck
|
||||
for tc in result.tool_calls:
|
||||
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(
|
||||
role="tool",
|
||||
tool_call_id=tc["id"],
|
||||
name=tc["name"],
|
||||
content=tool_result[:8000],
|
||||
content=tool_result[:50000],
|
||||
))
|
||||
continue # next iteration mit Tool-Results
|
||||
# Kein Tool-Call mehr → final reply
|
||||
|
||||
@@ -602,6 +602,135 @@ SEED_RULES: List[dict] = [
|
||||
"'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
@@ -683,8 +683,13 @@ def run_skill(name: str, args: Optional[dict] = None, timeout_sec: int = 300) ->
|
||||
timed_out = True
|
||||
duration = time.time() - t0
|
||||
|
||||
# Log schreiben (gekuerzt damit es nicht explodiert)
|
||||
record = {
|
||||
# Log auf der Disk wird gekuerzt (8000 chars) — sonst sammeln sich
|
||||
# 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(),
|
||||
"args": args or {},
|
||||
"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,
|
||||
}
|
||||
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:
|
||||
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
|
||||
write_manifest(name, manifest)
|
||||
|
||||
record["ok"] = exit_code == 0
|
||||
record["log_path"] = str(log_path)
|
||||
# Return-Value: nicht kuerzen (Agent kuerzt downstream selbst). Nur
|
||||
# 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
|
||||
|
||||
|
||||
|
||||
+35
-4
@@ -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:
|
||||
"""Bereitet Chat-Text fuer Sprachausgabe auf.
|
||||
|
||||
@@ -1150,11 +1174,15 @@ class ARIABridge:
|
||||
f"aber nicht erstellt:\n{missing_list}\n"
|
||||
"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.
|
||||
display_text = strip_voice_tag_for_display(text)
|
||||
assistant_backup_ts = self._append_chat_backup({
|
||||
"role": "assistant",
|
||||
"text": text,
|
||||
"text": display_text,
|
||||
"files": [{"serverPath": f["serverPath"], "name": f["name"],
|
||||
"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_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({
|
||||
"type": "chat",
|
||||
"payload": {
|
||||
"text": text,
|
||||
"text": display_text,
|
||||
"sender": "aria",
|
||||
"messageId": message_id,
|
||||
# backupTs = der ts in chat_backup.jsonl. Wird von Clients als
|
||||
|
||||
Reference in New Issue
Block a user