Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fb08b4ea5 | |||
| d49ec64e27 |
@@ -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 10908
|
versionCode 10909
|
||||||
versionName "0.1.9.8"
|
versionName "0.1.9.9"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.1.9.8",
|
"version": "0.1.9.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -499,8 +499,14 @@ const ChatScreen: React.FC = () => {
|
|||||||
// fuer den Auto-Fall angenehm.
|
// fuer den Auto-Fall angenehm.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
AsyncStorage.setItem('aria_focused_project_id', focusedProjectId).catch(() => {});
|
AsyncStorage.setItem('aria_focused_project_id', focusedProjectId).catch(() => {});
|
||||||
|
focusedProjectIdRef.current = focusedProjectId;
|
||||||
}, [focusedProjectId]);
|
}, [focusedProjectId]);
|
||||||
|
|
||||||
|
// Ref-Spiegel damit useCallback-Handler die aktuelle Focus-ID lesen
|
||||||
|
// ohne dass wir die Deps in jedes Callback muessen (sonst re-createn
|
||||||
|
// die sich bei jedem Wechsel).
|
||||||
|
const focusedProjectIdRef = useRef<string>('');
|
||||||
|
|
||||||
// Queue-Status alle 2s pollen — fuers Status-Dot im Focus-Header und
|
// Queue-Status alle 2s pollen — fuers Status-Dot im Focus-Header und
|
||||||
// fuer die Drawer-Anzeige. Nur wenn RVS verbunden ist (sonst 30s Timeout).
|
// fuer die Drawer-Anzeige. Nur wenn RVS verbunden ist (sonst 30s Timeout).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1414,6 +1420,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
noSpeechTimeoutMs: windowMs,
|
noSpeechTimeoutMs: windowMs,
|
||||||
endpointMs: 1500,
|
endpointMs: 1500,
|
||||||
hardCapMs: 60000,
|
hardCapMs: 60000,
|
||||||
|
projectId: focusedProjectIdRef.current,
|
||||||
});
|
});
|
||||||
import('../services/logger').then(m => m.reportAppDebug('wake.cb', `startStreamingRecording returned ok=${ok}`)).catch(()=>{});
|
import('../services/logger').then(m => m.reportAppDebug('wake.cb', `startStreamingRecording returned ok=${ok}`)).catch(()=>{});
|
||||||
if (ok) {
|
if (ok) {
|
||||||
@@ -1509,6 +1516,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
noSpeechTimeoutMs: windowMs,
|
noSpeechTimeoutMs: windowMs,
|
||||||
endpointMs: 1500,
|
endpointMs: 1500,
|
||||||
hardCapMs: 60000,
|
hardCapMs: 60000,
|
||||||
|
projectId: focusedProjectIdRef.current,
|
||||||
});
|
});
|
||||||
if (ok) {
|
if (ok) {
|
||||||
ToastAndroid.show('🎤 Mikro offen — sprich jetzt', ToastAndroid.SHORT);
|
ToastAndroid.show('🎤 Mikro offen — sprich jetzt', ToastAndroid.SHORT);
|
||||||
@@ -1565,6 +1573,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
noSpeechTimeoutMs: Math.min(passiveMs, 30000),
|
noSpeechTimeoutMs: Math.min(passiveMs, 30000),
|
||||||
endpointMs: 1500,
|
endpointMs: 1500,
|
||||||
hardCapMs: Math.max(passiveMs + 5000, 35000),
|
hardCapMs: Math.max(passiveMs + 5000, 35000),
|
||||||
|
projectId: focusedProjectIdRef.current,
|
||||||
});
|
});
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
console.warn('[Chat] passive streaming start failed — exit passive listening');
|
console.warn('[Chat] passive streaming start failed — exit passive listening');
|
||||||
@@ -1880,7 +1889,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
const location = await getCurrentLocation();
|
const location = await getCurrentLocation();
|
||||||
|
|
||||||
const cmid = nextClientMsgId();
|
const cmid = nextClientMsgId();
|
||||||
const activePid = focusedProjectId;
|
const activePid = focusedProjectIdRef.current;
|
||||||
const userMsg: ChatMessage = {
|
const userMsg: ChatMessage = {
|
||||||
id: nextId(),
|
id: nextId(),
|
||||||
sender: 'user',
|
sender: 'user',
|
||||||
@@ -1963,6 +1972,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
noSpeechTimeoutMs: 0,
|
noSpeechTimeoutMs: 0,
|
||||||
endpointMs: 1500,
|
endpointMs: 1500,
|
||||||
hardCapMs: 300000,
|
hardCapMs: 300000,
|
||||||
|
projectId: focusedProjectIdRef.current,
|
||||||
});
|
});
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
// Mikro nicht verfuegbar (Anruf? OpenWakeWord blockiert?) — Bubble weg.
|
// Mikro nicht verfuegbar (Anruf? OpenWakeWord blockiert?) — Bubble weg.
|
||||||
@@ -2584,7 +2594,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}
|
style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}
|
||||||
hitSlop={{top:6,bottom:6,left:6,right:6}}
|
hitSlop={{top:6,bottom:6,left:6,right:6}}
|
||||||
>
|
>
|
||||||
<Text style={{ fontSize: 20 }}>☰</Text>
|
<Text style={{ fontSize: 22, color: '#E0E0F0', fontWeight: '700' }}>☰</Text>
|
||||||
{otherActive > 0 && (
|
{otherActive > 0 && (
|
||||||
<View style={{ backgroundColor: '#FF6E6E', borderRadius: 8, minWidth: 16, height: 16, paddingHorizontal: 4, alignItems: 'center', justifyContent: 'center' }}>
|
<View style={{ backgroundColor: '#FF6E6E', borderRadius: 8, minWidth: 16, height: 16, paddingHorizontal: 4, alignItems: 'center', justifyContent: 'center' }}>
|
||||||
<Text style={{ color: '#fff', fontSize: 10, fontWeight: '700' }}>{otherActive}</Text>
|
<Text style={{ color: '#fff', fontSize: 10, fontWeight: '700' }}>{otherActive}</Text>
|
||||||
|
|||||||
@@ -982,6 +982,10 @@ class AudioService {
|
|||||||
noSpeechTimeoutMs?: number;
|
noSpeechTimeoutMs?: number;
|
||||||
endpointMs?: number;
|
endpointMs?: number;
|
||||||
hardCapMs?: number;
|
hardCapMs?: number;
|
||||||
|
/** Focused projectId — Bridge nutzt das als Default fuer den Voice-Router.
|
||||||
|
* Leer = Hauptchat. Ohne Prefix / Sticky landet die STT-Nachricht damit
|
||||||
|
* automatisch in dem Kontext den Stefan gerade sieht. */
|
||||||
|
projectId?: string;
|
||||||
}): Promise<{ requestId: string; ok: boolean }> {
|
}): Promise<{ requestId: string; ok: boolean }> {
|
||||||
if (this.recordingState !== 'idle') {
|
if (this.recordingState !== 'idle') {
|
||||||
console.warn('[Audio] startStreamingRecording: bereits aktiv (state=%s)', this.recordingState);
|
console.warn('[Audio] startStreamingRecording: bereits aktiv (state=%s)', this.recordingState);
|
||||||
@@ -1055,6 +1059,7 @@ class AudioService {
|
|||||||
endpointMs: typeof opts.endpointMs === 'number' ? opts.endpointMs : 1500,
|
endpointMs: typeof opts.endpointMs === 'number' ? opts.endpointMs : 1500,
|
||||||
hardCapMs: typeof opts.hardCapMs === 'number' ? opts.hardCapMs : 60000,
|
hardCapMs: typeof opts.hardCapMs === 'number' ? opts.hardCapMs : 60000,
|
||||||
sampleRate: 16000,
|
sampleRate: 16000,
|
||||||
|
projectId: opts.projectId || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
// No-Speech-Watchdog — ersetzt den alten VAD-noSpeechTimer.
|
// No-Speech-Watchdog — ersetzt den alten VAD-noSpeechTimer.
|
||||||
|
|||||||
+64
-8
@@ -618,6 +618,11 @@ class ARIABridge:
|
|||||||
# interceptiert und aendern hier den Sticky OHNE Brain-Roundtrip.
|
# interceptiert und aendern hier den Sticky OHNE Brain-Roundtrip.
|
||||||
self._voice_sticky_project_id: str = ""
|
self._voice_sticky_project_id: str = ""
|
||||||
self._voice_sticky_expires_at: float = 0.0
|
self._voice_sticky_expires_at: float = 0.0
|
||||||
|
# Focused-project pro Stream: die App schickt bei stt_stream_start
|
||||||
|
# die projectId ihres aktuellen Focus mit. Wenn das Voice-Ergebnis
|
||||||
|
# weder Meta-Kommando noch Prefix ist und der Sticky abgelaufen,
|
||||||
|
# nutzen wir das als Default (Voice folgt dem sichtbaren Kontext).
|
||||||
|
self._stt_stream_projects: dict[str, str] = {}
|
||||||
# Voice-Override aus letzter Chat-Nachricht einer App.
|
# Voice-Override aus letzter Chat-Nachricht einer App.
|
||||||
# Wird fuer die direkt folgende ARIA-Antwort genutzt und dann zurueckgesetzt.
|
# Wird fuer die direkt folgende ARIA-Antwort genutzt und dann zurueckgesetzt.
|
||||||
# So kann jedes Geraet seine bevorzugte Stimme bekommen (pro Request).
|
# So kann jedes Geraet seine bevorzugte Stimme bekommen (pro Request).
|
||||||
@@ -2788,6 +2793,25 @@ class ARIABridge:
|
|||||||
future.set_result(text)
|
future.set_result(text)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
elif msg_type == "stt_stream_start":
|
||||||
|
# App startet eine neue Streaming-STT-Session. Wir merken uns
|
||||||
|
# ihre Focus-projectId damit der Voice-Router beim spaeteren
|
||||||
|
# stt_endpoint einen sinnvollen Default hat (Voice folgt dem
|
||||||
|
# visuellen Focus).
|
||||||
|
req_id = payload.get("requestId", "") or ""
|
||||||
|
focused_pid = str(payload.get("projectId") or "")
|
||||||
|
if req_id:
|
||||||
|
self._stt_stream_projects[req_id] = focused_pid
|
||||||
|
logger.info("[rvs] stt_stream_start id=%s focus=%s",
|
||||||
|
req_id[:12], focused_pid or "(main)")
|
||||||
|
return
|
||||||
|
|
||||||
|
elif msg_type == "stt_stream_end":
|
||||||
|
# Session vorbei — Focus-Tracking fuer diese requestId aufraeumen.
|
||||||
|
req_id = payload.get("requestId", "") or ""
|
||||||
|
self._stt_stream_projects.pop(req_id, None)
|
||||||
|
return
|
||||||
|
|
||||||
elif msg_type == "stt_endpoint":
|
elif msg_type == "stt_endpoint":
|
||||||
# Phase 2 Brain-Shortcut: die whisper-bridge hat im Streaming-Modus
|
# Phase 2 Brain-Shortcut: die whisper-bridge hat im Streaming-Modus
|
||||||
# einen Endpoint erkannt und schickt den finalen Text direkt.
|
# einen Endpoint erkannt und schickt den finalen Text direkt.
|
||||||
@@ -2836,9 +2860,15 @@ class ARIABridge:
|
|||||||
if self._is_duplicate_client_msg(client_msg_id):
|
if self._is_duplicate_client_msg(client_msg_id):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# App-Focus aus stt_stream_start-Registry auflösen (falls die
|
||||||
|
# App-Version die projectId noch nicht mitschickt: leer = Hauptchat).
|
||||||
|
stream_req_id = payload.get("requestId", "") or ""
|
||||||
|
focused_pid = self._stt_stream_projects.pop(stream_req_id, "")
|
||||||
|
|
||||||
asyncio.create_task(self._process_endpoint_text(
|
asyncio.create_task(self._process_endpoint_text(
|
||||||
text, interrupted, audio_request_id, location,
|
text, interrupted, audio_request_id, location,
|
||||||
client_msg_id=client_msg_id))
|
client_msg_id=client_msg_id,
|
||||||
|
focused_project_id=focused_pid))
|
||||||
return
|
return
|
||||||
|
|
||||||
elif msg_type == "oauth_callback":
|
elif msg_type == "oauth_callback":
|
||||||
@@ -2986,7 +3016,20 @@ class ARIABridge:
|
|||||||
# Voice-Router-Konstanten
|
# Voice-Router-Konstanten
|
||||||
_VOICE_STICKY_TIMEOUT_SEC = 30.0
|
_VOICE_STICKY_TIMEOUT_SEC = 30.0
|
||||||
_VOICE_META_BACK_TO_MAIN = re.compile(
|
_VOICE_META_BACK_TO_MAIN = re.compile(
|
||||||
r"^\s*(?:aria[,.]?\s+)?(?:zur(?:ü|ue)ck\s+zum\s+hauptchat|hauptchat\s+bitte|aria\s+hauptchat)\s*[.!?]?\s*$",
|
r"^\s*(?:aria[,.]?\s+)?"
|
||||||
|
r"(?:"
|
||||||
|
# „zurück zum hauptchat / hauptmenü / haupt / menü / main"
|
||||||
|
r"zur(?:ü|ue)ck\s+(?:zum|zur|ins?|in\s+den)\s+"
|
||||||
|
r"(?:hauptchat|hauptmen(?:ü|ue)|haupt|men(?:ü|ue)|main)"
|
||||||
|
r"|"
|
||||||
|
# „zurück hauptchat / zurück haupt"
|
||||||
|
r"zur(?:ü|ue)ck\s+(?:hauptchat|hauptmen(?:ü|ue)|haupt|main)"
|
||||||
|
r"|"
|
||||||
|
# „hauptchat bitte", „aria hauptchat" (auch mit Menü/Main)
|
||||||
|
r"(?:hauptchat|hauptmen(?:ü|ue)|main)\s+bitte"
|
||||||
|
r"|"
|
||||||
|
r"aria[,.]?\s+(?:hauptchat|hauptmen(?:ü|ue)|haupt|main)"
|
||||||
|
r")\s*[.!?]?\s*$",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
_VOICE_META_PROJECT_PREFIX = re.compile(
|
_VOICE_META_PROJECT_PREFIX = re.compile(
|
||||||
@@ -2994,7 +3037,8 @@ class ARIABridge:
|
|||||||
re.IGNORECASE | re.DOTALL,
|
re.IGNORECASE | re.DOTALL,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _apply_voice_router(self, text: str) -> tuple[bool, str, str, str]:
|
def _apply_voice_router(self, text: str,
|
||||||
|
default_project_id: str = "") -> tuple[bool, str, str, str]:
|
||||||
"""Voice-Router: entscheidet ob ein STT-Text ans Brain geht und wenn ja
|
"""Voice-Router: entscheidet ob ein STT-Text ans Brain geht und wenn ja
|
||||||
an welchen Projekt-Kontext.
|
an welchen Projekt-Kontext.
|
||||||
|
|
||||||
@@ -3004,9 +3048,14 @@ class ARIABridge:
|
|||||||
- should_forward=True: cleaned_text ans Brain, project_id ist Focus.
|
- should_forward=True: cleaned_text ans Brain, project_id ist Focus.
|
||||||
Bei Prefix wird der Prefix aus dem Text entfernt.
|
Bei Prefix wird der Prefix aus dem Text entfernt.
|
||||||
|
|
||||||
Sticky-Logik: nach einem projekt-getaggten Voice-Turn wird der Sticky
|
Prioritaets-Reihenfolge:
|
||||||
30s lang gehalten. Innerhalb dieses Fensters gehen weitere Voice-Msgs
|
1. Meta „zurueck zum hauptchat" → Sticky reset, kein Forward.
|
||||||
OHNE Prefix in dasselbe Projekt. Nach Ablauf: Default Hauptchat.
|
2. „fuer <name>:"-Prefix → Sticky auf gematchtes Projekt.
|
||||||
|
3. Sticky aktiv (<=30s alt) → dessen Projekt.
|
||||||
|
4. default_project_id (App-Focus) — Voice folgt dem sichtbaren
|
||||||
|
Kontext. Wenn App in Projekt X guckt, geht die STT-Nachricht
|
||||||
|
ohne weitere Marker dort rein.
|
||||||
|
5. Fallback: Hauptchat.
|
||||||
"""
|
"""
|
||||||
import time as _time
|
import time as _time
|
||||||
now = _time.time()
|
now = _time.time()
|
||||||
@@ -3056,13 +3105,18 @@ class ARIABridge:
|
|||||||
return (True, stripped, self._voice_sticky_project_id, "sticky")
|
return (True, stripped, self._voice_sticky_project_id, "sticky")
|
||||||
# Sticky abgelaufen — zurücksetzen
|
# Sticky abgelaufen — zurücksetzen
|
||||||
self._voice_sticky_project_id = ""
|
self._voice_sticky_project_id = ""
|
||||||
|
# 4) App-Focus als Default: Voice folgt dem sichtbaren Kontext
|
||||||
|
if default_project_id:
|
||||||
|
return (True, stripped, default_project_id, "app_focus")
|
||||||
|
# 5) Fallback Hauptchat
|
||||||
return (True, stripped, "", "default")
|
return (True, stripped, "", "default")
|
||||||
|
|
||||||
async def _process_endpoint_text(self, text: str,
|
async def _process_endpoint_text(self, text: str,
|
||||||
interrupted: bool = False,
|
interrupted: bool = False,
|
||||||
audio_request_id: str = "",
|
audio_request_id: str = "",
|
||||||
location: Optional[dict] = None,
|
location: Optional[dict] = None,
|
||||||
client_msg_id: Optional[str] = None) -> None:
|
client_msg_id: Optional[str] = None,
|
||||||
|
focused_project_id: str = "") -> None:
|
||||||
"""Phase-2 Brain-Shortcut: Streaming-Whisper hat den finalen Text
|
"""Phase-2 Brain-Shortcut: Streaming-Whisper hat den finalen Text
|
||||||
schon ermittelt — wir uebernehmen den Pfad ab broadcast-STT + brain.
|
schon ermittelt — wir uebernehmen den Pfad ab broadcast-STT + brain.
|
||||||
|
|
||||||
@@ -3075,7 +3129,9 @@ class ARIABridge:
|
|||||||
selbst geht NICHT ans Brain, sondern broadcastet als project_changed-
|
selbst geht NICHT ans Brain, sondern broadcastet als project_changed-
|
||||||
Event → App+Diagnostic wechseln den Focus.
|
Event → App+Diagnostic wechseln den Focus.
|
||||||
"""
|
"""
|
||||||
should_forward, cleaned, project_id, meta_action = self._apply_voice_router(text)
|
should_forward, cleaned, project_id, meta_action = self._apply_voice_router(
|
||||||
|
text, default_project_id=focused_project_id,
|
||||||
|
)
|
||||||
|
|
||||||
if meta_action in ("back_to_main", "project_prefix"):
|
if meta_action in ("back_to_main", "project_prefix"):
|
||||||
# UI-Focus-Update broadcasten
|
# UI-Focus-Update broadcasten
|
||||||
|
|||||||
Reference in New Issue
Block a user