Compare commits

...
3 Commits
Author SHA1 Message Date
duffyduck 5f09e2bca3 release: bump version to 0.2.4.0 2026-08-16 01:48:43 +02:00
duffyduckandClaude Opus 4.8 ebe0e8065f feat(voice): 30s-Passiv-Fenster raus — Stille-Toleranz regiert alles
Stefans Modell: es braucht keinen separaten 30s-Wert. Nach ARIAs Antwort geht das
Mikro auf; fängst du nicht innerhalb der Stille-Toleranz (z.B. 5s) an zu reden, ist
Schluss → zurück aufs Wake-Word. Derselbe Wert wie die Pause-Toleranz beim Reden.
Ein Befehl ohne "fortführen" = Ende; nach einer normalen Antwort = ein Stille-
Fenster zum Weiterreden.

- Alle Aufnahme-Pfade (wake/barge/passiv): noSpeechTimeoutMs = endpointMs =
  loadSttEndpointMs() statt loadConvWindowMs()/loadPassiveListenMs(). Ein Wert.
- Passiv-Hardcap: loadMaxRecordingMs() statt fix 35s (schnitt langes Reden ab).
- Leeres Endpoint im listening-State: KEIN Re-Arm mehr → exitPassiveListening
  (ein 5s-Fenster, dann Ende). Der 30s-Master-Timer in wakeword.ts wird dadurch
  nie mehr scharf (harmloser Backstop).

Redest du weiter → Antwort → wieder ein Stille-Fenster (Multi-Turn bleibt, nur ohne
30s-Leerlauf). Die Settings "Konversations-Fenster"/"Passiv-Lauschen" sind damit
obsolet (UI-Cleanup später).

Deploy: neue APK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-16 01:46:26 +02:00
duffyduckandClaude Opus 4.8 9d2c07d8d1 feat(voice): "Konversation fortführen" hält offen — auch bei Fast-Path-Befehl
Stefans Repro: "spiel Spotify auf Smartphone ab ABER Konversation fortführen" →
die Kette endete trotzdem. Grund: "spiel spotify" trifft den Fast-Path (Regex),
der den Satz-Rest nicht versteht → converse=false aus dem Skill-Manifest; ARIAs
[[WEITER]]-Marker lebt in Claude, der wurde uebersprungen.

Fix: _user_wants_conversation_continue() — Gegenstueck zu _user_wants_conversation_
end(). Erkennt "Konversation/Gespraech fortfuehren/weiterfuehren/offen halten/nicht
beenden", "weiter reden/sprechen" etc. und erzwingt converse=true an ALLEN Returns
(fast-path/local/claude), auch wenn das Manifest false sagt. [[ENDE]]/_wants_end hat
Vorrang bei Widerspruch. Getestet inkl. Negativfaelle (sofort/spotify ab).

Deploy: Brain-Neustart.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-16 01:35:49 +02:00
4 changed files with 64 additions and 23 deletions
+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 20309 versionCode 20400
versionName "0.2.3.9" versionName "0.2.4.0"
// 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.2.3.9", "version": "0.2.4.0",
"private": true, "private": true,
"scripts": { "scripts": {
"android": "react-native run-android", "android": "react-native run-android",
+23 -15
View File
@@ -1679,7 +1679,11 @@ const ChatScreen: React.FC = () => {
rememberMyRequest(audioRequestId); rememberMyRequest(audioRequestId);
const wasInterrupted = interruptAriaIfBusy(); const wasInterrupted = interruptAriaIfBusy();
const location = await getCurrentLocation(); const location = await getCurrentLocation();
const windowMs = await loadConvWindowMs(); // EIN Wert regiert: die Stille-Toleranz. Sie gilt sowohl als Pause WÄHREND
// des Redens (endpointMs) ALS AUCH als "wenn du nicht anfängst zu reden,
// ist Schluss" (noSpeechTimeoutMs). Kein separates 30s-Konversationsfenster
// mehr — Stefans Modell: sagst du nichts, greift der Stille-Wert.
const sttEndpointMs = await loadSttEndpointMs();
const userMsg: ChatMessage = { const userMsg: ChatMessage = {
id: nextId(), id: nextId(),
@@ -1697,8 +1701,8 @@ const ChatScreen: React.FC = () => {
speed: ttsSpeedRef.current, speed: ttsSpeedRef.current,
interrupted: wasInterrupted, interrupted: wasInterrupted,
location: location || null, location: location || null,
noSpeechTimeoutMs: windowMs, noSpeechTimeoutMs: sttEndpointMs,
endpointMs: await loadSttEndpointMs(), endpointMs: sttEndpointMs,
// Notbremse 5 min (nicht 1 min) — der Stille-Endpoint beendet normale // Notbremse 5 min (nicht 1 min) — der Stille-Endpoint beendet normale
// Turns eh sofort; der Cap darf lange Diktate nicht mitten drin kappen. // Turns eh sofort; der Cap darf lange Diktate nicht mitten drin kappen.
hardCapMs: await loadMaxRecordingMs(), hardCapMs: await loadMaxRecordingMs(),
@@ -1756,12 +1760,12 @@ const ChatScreen: React.FC = () => {
!(m.audioRequestId === ev.audioRequestId !(m.audioRequestId === ev.audioRequestId
&& m.text.includes('Spracheingabe wird verarbeitet')))); && m.text.includes('Spracheingabe wird verarbeitet'))));
} }
// Bei Passive-Listen + speaker_mismatch oder no-speech: erneut passiv // Kein Re-Arm mehr: nach ARIAs Antwort gab es EIN Stille-Fenster (=
// lauschen (Timer im wakeword-service laeuft weiter, regelt das Ende). // Stille-Toleranz). Kam nichts, ist Schluss → zurück aufs Wake-Word.
// Sonst endConversation wie bisher. // Kein 30s-Nachlauschen. (speaker_mismatch/no-speech landen beide hier.)
if (wakeWordService.getState() === 'listening') { if (wakeWordService.getState() === 'listening') {
console.log('[Chat] Passive-Listen: leeres Endpoint — naechste passive Aufnahme'); console.log('[Chat] Passive-Listen: leeres Endpoint — Ende, zurueck aufs Wake-Word');
startPassiveStreamingRecording(); wakeWordService.exitPassiveListening('timeout').catch(() => {});
} else { } else {
wakeWordService.endConversation(); wakeWordService.endConversation();
if (!wakeWordService.isActive()) setWakeWordActive(false); if (!wakeWordService.isActive()) setWakeWordActive(false);
@@ -1791,7 +1795,7 @@ const ChatScreen: React.FC = () => {
const audioRequestId = `audio_${Date.now()}_${Math.floor(Math.random() * 100000)}`; const audioRequestId = `audio_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
rememberMyRequest(audioRequestId); rememberMyRequest(audioRequestId);
const location = await getCurrentLocation(); const location = await getCurrentLocation();
const windowMs = await loadConvWindowMs(); const sttEndpointMs = await loadSttEndpointMs(); // ein Wert für Pause + No-Speech
const userMsg: ChatMessage = { const userMsg: ChatMessage = {
id: nextId(), id: nextId(),
@@ -1809,8 +1813,8 @@ const ChatScreen: React.FC = () => {
speed: ttsSpeedRef.current, speed: ttsSpeedRef.current,
interrupted: true, // Barge-In → Brain weiss "User hat unterbrochen" interrupted: true, // Barge-In → Brain weiss "User hat unterbrochen"
location: location || null, location: location || null,
noSpeechTimeoutMs: windowMs, noSpeechTimeoutMs: sttEndpointMs,
endpointMs: await loadSttEndpointMs(), endpointMs: sttEndpointMs,
// Notbremse 5 min (s.o.) — lange Diktate nicht bei 1 min abschneiden. // Notbremse 5 min (s.o.) — lange Diktate nicht bei 1 min abschneiden.
hardCapMs: await loadMaxRecordingMs(), hardCapMs: await loadMaxRecordingMs(),
projectId: focusedProjectIdRef.current, projectId: focusedProjectIdRef.current,
@@ -1871,16 +1875,20 @@ const ChatScreen: React.FC = () => {
const audioRequestId = `audio_passive_${Date.now()}_${Math.floor(Math.random() * 100000)}`; const audioRequestId = `audio_passive_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
rememberMyRequest(audioRequestId); rememberMyRequest(audioRequestId);
const location = await getCurrentLocation(); const location = await getCurrentLocation();
const passiveMs = await loadPassiveListenMs(); // Kein 30s-Passiv-Fenster mehr: nach ARIAs Antwort geht das Mikro auf, und
// fängst du nicht innerhalb der Stille-Toleranz an zu reden, ist Schluss →
// zurück aufs Wake-Word. Derselbe Wert wie die Pause-Toleranz beim Reden.
const sttEndpointMs = await loadSttEndpointMs();
const { ok } = await audioService.startStreamingRecording({ const { ok } = await audioService.startStreamingRecording({
audioRequestId, audioRequestId,
voice: localXttsVoiceRef.current, voice: localXttsVoiceRef.current,
speed: ttsSpeedRef.current, speed: ttsSpeedRef.current,
interrupted: false, interrupted: false,
location: location || null, location: location || null,
noSpeechTimeoutMs: Math.min(passiveMs, 30000), noSpeechTimeoutMs: sttEndpointMs,
endpointMs: await loadSttEndpointMs(), endpointMs: sttEndpointMs,
hardCapMs: Math.max(passiveMs + 5000, 35000), // Lange Antworten nicht kappen (früher 35s → schnitt langes Reden ab).
hardCapMs: await loadMaxRecordingMs(),
projectId: focusedProjectIdRef.current, projectId: focusedProjectIdRef.current,
}); });
if (!ok) { if (!ok) {
+38 -5
View File
@@ -1418,6 +1418,30 @@ def _user_wants_conversation_end(text: str) -> bool:
return bool(_END_CONVERSATION_RE.search(_strip_leading_hint_blocks(text))) return bool(_END_CONVERSATION_RE.search(_strip_leading_hint_blocks(text)))
# Gegenstueck zu _END: expliziter "Konversation OFFEN halten / fortfuehren"-Wunsch.
# Wichtig fuer BEFEHLE die den Fast-Path treffen: "spiel Spotify ab ABER Konversation
# fortfuehren" — der Fast-Path (Regex) versteht den Satz-Rest nicht und wuerde mit
# converse=false schliessen. Dieser Detektor erzwingt converse=true, auch am
# Fast-Path, egal was das Skill-Manifest sagt. Nomen+Verb in einem Satzteil, plus
# "weiter reden/sprechen" ohne Nomen.
_CONT_VERB = (r"(?:fortf[uü]hr\w*|fortsetz\w*|weiterf[uü]hr\w*|weiter\s*mach\w*|"
r"weiter\b|fort\b|offen\s+(?:halten|lassen)|nicht\s+beenden|weiterlauf\w*)")
_CONTINUE_CONVERSATION_RE = re.compile(
rf"\b{_CONV_NOUN}\b[^.!?]{{0,20}}\b{_CONT_VERB}"
rf"|\b{_CONT_VERB}[^.!?]{{0,20}}\b{_CONV_NOUN}\b"
rf"|\bweiter\s*(?:reden|sprechen|quatschen|plaudern|labern)\b",
re.IGNORECASE,
)
def _user_wants_conversation_continue(text: str) -> bool:
"""True, wenn der User explizit weiter im Gespraech bleiben will (converse=true
erzwingen — auch bei einem Fast-Path-Befehl). [[ENDE]]/_wants_end hat Vorrang."""
if not text:
return False
return bool(_CONTINUE_CONVERSATION_RE.search(_strip_leading_hint_blocks(text)))
def _normalize_for_fast_match(text: str) -> str: def _normalize_for_fast_match(text: str) -> str:
norm = _strip_leading_hint_blocks(text).lower() norm = _strip_leading_hint_blocks(text).lower()
norm = _fold_umlauts(norm) norm = _fold_umlauts(norm)
@@ -1826,10 +1850,13 @@ class Agent:
if not user_message: if not user_message:
raise ValueError("Leere Nachricht") raise ValueError("Leere Nachricht")
# Expliziter "Konversation/Kette beenden"-Wunsch (deterministisch). Wird an # Explizite Gespraechs-Steuerung vom USER (deterministisch, an JEDEM Return
# JEDEM Return auf converse=false angewendet — unabhaengig davon, ob ARIA # angewendet — auch am Fast-Path, den die LLM-Marker nicht erreichen):
# den [[ENDE]]-Marker setzt. Stefans fester Trigger "Konversation Ende". # _wants_end → converse=false ("Konversation Ende")
# _wants_continue → converse=true ("... aber Konversation fortfuehren")
# End hat Vorrang bei Widerspruch.
_wants_end = _user_wants_conversation_end(user_message) _wants_end = _user_wants_conversation_end(user_message)
_wants_continue = (not _wants_end) and _user_wants_conversation_continue(user_message)
# Events vom letzten Turn weglassen # Events vom letzten Turn weglassen
self._pending_events = [] self._pending_events = []
@@ -1860,6 +1887,8 @@ class Agent:
converse = bool(getattr(self, "_fast_path_converse", False)) converse = bool(getattr(self, "_fast_path_converse", False))
if _wants_end: if _wants_end:
converse = False converse = False
elif _wants_continue:
converse = True
# Fast-Path = reiner Steuerbefehl, nie eine Rueckfrage → awaiting=False. # Fast-Path = reiner Steuerbefehl, nie eine Rueckfrage → awaiting=False.
return fast_reply, "fast-path", speak, converse, False return fast_reply, "fast-path", speak, converse, False
@@ -1881,6 +1910,8 @@ class Agent:
converse = getattr(self, "_local_turn_converse", True) converse = getattr(self, "_local_turn_converse", True)
if _wants_end: if _wants_end:
converse = False converse = False
elif _wants_continue:
converse = True
# Local ist tool-loses Reden; blockierende Rueckfragen macht Claude. # Local ist tool-loses Reden; blockierende Rueckfragen macht Claude.
return local_reply, "local", speak, converse, False return local_reply, "local", speak, converse, False
@@ -2129,10 +2160,12 @@ class Agent:
speak = _speak_ov speak = _speak_ov
if _conv_ov is not None: if _conv_ov is not None:
converse = _conv_ov converse = _conv_ov
# Expliziter User-Wunsch "Konversation beenden" gewinnt IMMER — Frage wird # Explizite User-Woerter gewinnen ueber Marker/Manifest: "Konversation
# noch beantwortet (speak bleibt), aber danach zurueck aufs Wake-Word. # beenden" → zu; "... fortfuehren" → offen halten. End hat Vorrang.
if _wants_end: if _wants_end:
converse = False converse = False
elif _wants_continue:
converse = True
# awaiting_reply = ARIA stellt eine blockierende Rueckfrage (Queue pausiert). # awaiting_reply = ARIA stellt eine blockierende Rueckfrage (Queue pausiert).
return (final_reply, "claude", speak, converse, awaiting_reply) return (final_reply, "claude", speak, converse, awaiting_reply)