Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7af0b76ad3 | ||
|
|
614ba48531 |
@@ -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 20504
|
versionCode 20505
|
||||||
versionName "0.2.5.4"
|
versionName "0.2.5.5"
|
||||||
// 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.2.5.4",
|
"version": "0.2.5.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -217,6 +217,13 @@ function formatDur(sec: number): string {
|
|||||||
// TAP — was sie auf ausdruecklichen Tipp holt. Hoeher, aber immer noch
|
// TAP — was sie auf ausdruecklichen Tipp holt. Hoeher, aber immer noch
|
||||||
// weit unter der Schmerzgrenze. Drueber gibt's eine ehrliche
|
// weit unter der Schmerzgrenze. Drueber gibt's eine ehrliche
|
||||||
// Meldung statt eines Absturzes.
|
// Meldung statt eines Absturzes.
|
||||||
|
/** Signatur von sendPendingAttachments — fuer den Ref weiter unten. */
|
||||||
|
type SendAttachmentsFn = (
|
||||||
|
messageText: string,
|
||||||
|
explicitAttachments?: { file: any; isPhoto: boolean }[],
|
||||||
|
voiceMode?: { audioRequestId: string; clientMsgId: string },
|
||||||
|
) => Promise<void>;
|
||||||
|
|
||||||
const AUTO_DL_MAX_BYTES = 25 * 1024 * 1024;
|
const AUTO_DL_MAX_BYTES = 25 * 1024 * 1024;
|
||||||
const TAP_DL_MAX_BYTES = 64 * 1024 * 1024;
|
const TAP_DL_MAX_BYTES = 64 * 1024 * 1024;
|
||||||
|
|
||||||
@@ -391,6 +398,16 @@ const ChatScreen: React.FC = () => {
|
|||||||
// Service-Status (AI-Box: F5-TTS / Whisper Lade-Status) + Banner-Sichtbarkeit
|
// Service-Status (AI-Box: F5-TTS / Whisper Lade-Status) + Banner-Sichtbarkeit
|
||||||
const [serviceStatus, setServiceStatus] = useState<Record<string, {state: string, model?: string, loadSeconds?: number, error?: string, downloading?: boolean, freshlyDownloaded?: boolean}>>({});
|
const [serviceStatus, setServiceStatus] = useState<Record<string, {state: string, model?: string, loadSeconds?: number, error?: string, downloading?: boolean, freshlyDownloaded?: boolean}>>({});
|
||||||
const [serviceBannerDismissed, setServiceBannerDismissed] = useState(false);
|
const [serviceBannerDismissed, setServiceBannerDismissed] = useState(false);
|
||||||
|
// Zwischenruf per Sprache: der 📣-Knopf ohne Text "spannt" die naechste
|
||||||
|
// Aufnahme als Zwischenruf. Mit Text schickt er sofort (wie bisher). Der Ref
|
||||||
|
// wird gelesen, wenn die Aufnahme startet — der State nur fuer die Optik.
|
||||||
|
const [interjectArmed, setInterjectArmed] = useState(false);
|
||||||
|
const interjectArmedRef = useRef(false);
|
||||||
|
// sendPendingAttachments ueber einen Ref: handleVoiceButtonStart steht weiter
|
||||||
|
// oben in der Datei und wuerde die Funktion sonst in ihrer Temporal Dead Zone
|
||||||
|
// greifen (TS2448 — siehe Kommentar an sendTextMessage). Umsortieren waere
|
||||||
|
// riskanter als dieser Ref, weil an der Reihenfolge mehrere Callbacks haengen.
|
||||||
|
const sendPendingAttachmentsRef = useRef<SendAttachmentsFn | null>(null);
|
||||||
// Gerätelokale TTS-Config: globaler Toggle (aus Settings) + temporäres Muten (Mund-Button)
|
// Gerätelokale TTS-Config: globaler Toggle (aus Settings) + temporäres Muten (Mund-Button)
|
||||||
const [ttsDeviceEnabled, setTtsDeviceEnabled] = useState(true);
|
const [ttsDeviceEnabled, setTtsDeviceEnabled] = useState(true);
|
||||||
const [ttsMuted, setTtsMuted] = useState(false);
|
const [ttsMuted, setTtsMuted] = useState(false);
|
||||||
@@ -1324,7 +1341,11 @@ const ChatScreen: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setMessages(prev => {
|
setMessages(prev => {
|
||||||
const newText = `\uD83C\uDFA4 ${sttText}`;
|
// Zwischenruf per Sprache bekommt das Megafon statt des Mikros \u2014
|
||||||
|
// sonst sieht die Bubble wie eine normale Frage aus, obwohl sie in
|
||||||
|
// den laufenden Turn gegangen ist und keine eigene Antwort bekommt.
|
||||||
|
const isInterject = !!(message.payload as any).interject;
|
||||||
|
const newText = isInterject ? `\uD83D\uDCE3 Zwischenruf: ${sttText}` : `\uD83C\uDFA4 ${sttText}`;
|
||||||
// Primaer: matche per audioRequestId (eindeutig pro Aufnahme).
|
// Primaer: matche per audioRequestId (eindeutig pro Aufnahme).
|
||||||
// So gibt's keine Verwechslung wenn zwei Audios kurz hintereinander
|
// So gibt's keine Verwechslung wenn zwei Audios kurz hintereinander
|
||||||
// gesendet wurden und ihre STT-Results ueberlappen.
|
// gesendet wurden und ihre STT-Results ueberlappen.
|
||||||
@@ -1339,7 +1360,9 @@ const ChatScreen: React.FC = () => {
|
|||||||
// Fallback: alte Bridge-Version ohne audioRequestId \u2014 match per Substring,
|
// Fallback: alte Bridge-Version ohne audioRequestId \u2014 match per Substring,
|
||||||
// nimmt die ERSTE noch unaufgeloeste Placeholder.
|
// nimmt die ERSTE noch unaufgeloeste Placeholder.
|
||||||
const idx = prev.findIndex(m =>
|
const idx = prev.findIndex(m =>
|
||||||
m.sender === 'user' && m.text.includes('Spracheingabe wird verarbeitet')
|
m.sender === 'user'
|
||||||
|
&& (m.text.includes('Spracheingabe wird verarbeitet')
|
||||||
|
|| m.text.includes('Zwischenruf wird verarbeitet'))
|
||||||
);
|
);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
const next = prev.slice();
|
const next = prev.slice();
|
||||||
@@ -2413,11 +2436,18 @@ const ChatScreen: React.FC = () => {
|
|||||||
// produziert das Brain weiter TTS, die ins offene Mikro laeuft → genau der
|
// produziert das Brain weiter TTS, die ins offene Mikro laeuft → genau der
|
||||||
// "Mischmasch" (ARIA antwortet weiter waehrend ich rede). Fuer bewusstes
|
// "Mischmasch" (ARIA antwortet weiter waehrend ich rede). Fuer bewusstes
|
||||||
// Nicht-Abbrechen gibt es weiterhin den separaten Zwischenruf-Button (📣).
|
// Nicht-Abbrechen gibt es weiterhin den separaten Zwischenruf-Button (📣).
|
||||||
const interruptAriaIfBusy = useCallback(() => {
|
// forInterject: bei einem Zwischenruf darf der laufende Turn NICHT abgebrochen
|
||||||
|
// werden — das ist der ganze Sinn. Die Wiedergabe stoppen wir trotzdem, sonst
|
||||||
|
// redet ARIA in das offene Mikro hinein.
|
||||||
|
const interruptAriaIfBusy = useCallback((forInterject = false) => {
|
||||||
if (audioService.isPlayingAudio()) {
|
if (audioService.isPlayingAudio()) {
|
||||||
audioService.haltAllPlayback('user startet Aufnahme — Interrupt');
|
audioService.haltAllPlayback(forInterject
|
||||||
rvs.send('cancel_request' as any, { hard: true, source: 'voice-interrupt' });
|
? 'user ruft dazwischen — nur Wiedergabe stoppen'
|
||||||
return true;
|
: 'user startet Aufnahme — Interrupt');
|
||||||
|
if (!forInterject) {
|
||||||
|
rvs.send('cancel_request' as any, { hard: true, source: 'voice-interrupt' });
|
||||||
|
}
|
||||||
|
return !forInterject;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}, []);
|
}, []);
|
||||||
@@ -2430,18 +2460,35 @@ const ChatScreen: React.FC = () => {
|
|||||||
const handleVoiceButtonStart = useCallback(async (): Promise<boolean> => {
|
const handleVoiceButtonStart = useCallback(async (): Promise<boolean> => {
|
||||||
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 wasInterrupted = interruptAriaIfBusy();
|
const asInterject = interjectArmedRef.current;
|
||||||
|
const wasInterrupted = interruptAriaIfBusy(asInterject);
|
||||||
const location = await getCurrentLocation();
|
const location = await getCurrentLocation();
|
||||||
|
|
||||||
const userMsg: ChatMessage = {
|
// Warten Anhaenge? Dann sind sie das Thema dieser Aufnahme: sofort hochladen
|
||||||
id: nextId(),
|
// (laeuft parallel zum Reden) und der Bridge per clientMsgId sagen, dass der
|
||||||
sender: 'user',
|
// Satz dazu noch kommt. Die Bubble mit den Bildern macht
|
||||||
text: '🎙 Spracheingabe wird verarbeitet...',
|
// sendPendingAttachments — sie traegt die audioRequestId und bekommt den
|
||||||
timestamp: Date.now(),
|
// gesprochenen Text spaeter eingesetzt. Also hier KEINE zweite Bubble.
|
||||||
attachments: [{ type: 'audio', name: 'Sprachaufnahme' }],
|
let attachmentCmid = '';
|
||||||
audioRequestId,
|
if (pendingAttachments.length > 0 && !asInterject && sendPendingAttachmentsRef.current) {
|
||||||
};
|
attachmentCmid = nextClientMsgId();
|
||||||
setMessages(prev => capMessages([...prev, userMsg]));
|
const forVoice = pendingAttachments;
|
||||||
|
setPendingAttachments([]);
|
||||||
|
await sendPendingAttachmentsRef.current('', forVoice, {
|
||||||
|
audioRequestId, clientMsgId: attachmentCmid,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const userMsg: ChatMessage = {
|
||||||
|
id: nextId(),
|
||||||
|
sender: 'user',
|
||||||
|
text: asInterject ? '📣 Zwischenruf wird verarbeitet...'
|
||||||
|
: '🎙 Spracheingabe wird verarbeitet...',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
attachments: [{ type: 'audio', name: 'Sprachaufnahme' }],
|
||||||
|
audioRequestId,
|
||||||
|
};
|
||||||
|
setMessages(prev => capMessages([...prev, userMsg]));
|
||||||
|
}
|
||||||
|
|
||||||
const { ok } = await audioService.startStreamingRecording({
|
const { ok } = await audioService.startStreamingRecording({
|
||||||
audioRequestId,
|
audioRequestId,
|
||||||
@@ -2456,15 +2503,37 @@ const ChatScreen: React.FC = () => {
|
|||||||
endpointMs: await loadSttEndpointMs(),
|
endpointMs: await loadSttEndpointMs(),
|
||||||
hardCapMs: await loadMaxRecordingMs(),
|
hardCapMs: await loadMaxRecordingMs(),
|
||||||
projectId: focusedProjectIdRef.current,
|
projectId: focusedProjectIdRef.current,
|
||||||
|
...(attachmentCmid && { clientMsgId: attachmentCmid }),
|
||||||
|
...(asInterject && { interject: true }),
|
||||||
});
|
});
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
|
if (attachmentCmid) {
|
||||||
|
// Die Anhaenge sind SCHON hochgeladen — die Bubble darf nicht weg,
|
||||||
|
// sonst verschwinden die Bilder aus der Ansicht, obwohl sie auf dem
|
||||||
|
// Server liegen. Stattdessen ehrlich kennzeichnen; der Nutzer kann den
|
||||||
|
// Auftrag einfach tippen.
|
||||||
|
setMessages(prev => prev.map(m => m.audioRequestId === audioRequestId
|
||||||
|
? { ...m, text: '📎 Anhang gesendet — Mikro war belegt, bitte tippen',
|
||||||
|
audioRequestId: undefined }
|
||||||
|
: m));
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
ToastAndroid.show('Mikro nicht verfügbar — Anhang ist aber da', ToastAndroid.LONG);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Mikro nicht verfuegbar (Anruf? OpenWakeWord blockiert?) — Bubble weg.
|
// Mikro nicht verfuegbar (Anruf? OpenWakeWord blockiert?) — Bubble weg.
|
||||||
setMessages(prev => prev.filter(m => m.audioRequestId !== audioRequestId));
|
setMessages(prev => prev.filter(m => m.audioRequestId !== audioRequestId));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Zwischenruf-Scharfschaltung gilt genau fuer EINE Aufnahme.
|
||||||
|
if (asInterject) {
|
||||||
|
interjectArmedRef.current = false;
|
||||||
|
setInterjectArmed(false);
|
||||||
|
}
|
||||||
scheduleStaleAudioCleanup(audioRequestId, 60000);
|
scheduleStaleAudioCleanup(audioRequestId, 60000);
|
||||||
return true;
|
return true;
|
||||||
}, [getCurrentLocation, interruptAriaIfBusy, scheduleStaleAudioCleanup]);
|
}, [getCurrentLocation, interruptAriaIfBusy, scheduleStaleAudioCleanup,
|
||||||
|
pendingAttachments]);
|
||||||
|
|
||||||
// ARIA schliesst die Aufnahme SELBST — das programmatische Gegenstueck zum
|
// ARIA schliesst die Aufnahme SELBST — das programmatische Gegenstueck zum
|
||||||
// Stop-Button. Aufgerufen nach einem stillen Steuerbefehl (speak=false): der
|
// Stop-Button. Aufgerufen nach einem stillen Steuerbefehl (speak=false): der
|
||||||
@@ -2570,6 +2639,11 @@ const ChatScreen: React.FC = () => {
|
|||||||
// Explizite Liste (Teilen-Weg): der State waere beim direkten Aufruf noch
|
// Explizite Liste (Teilen-Weg): der State waere beim direkten Aufruf noch
|
||||||
// nicht aktualisiert. Ohne Angabe wie bisher die Pending-Liste.
|
// nicht aktualisiert. Ohne Angabe wie bisher die Pending-Liste.
|
||||||
explicitAttachments?: { file: any; isPhoto: boolean }[],
|
explicitAttachments?: { file: any; isPhoto: boolean }[],
|
||||||
|
// Sprach-Modus: der Nutzer spricht die Beschreibung gerade erst ein. Dann
|
||||||
|
// gehen die Dateien SOFORT raus (der Upload laeuft parallel zum Reden),
|
||||||
|
// aber ohne Text — den liefert spaeter der stt_endpoint, und die Bridge
|
||||||
|
// merged beides ueber die clientMsgId zu einem Turn.
|
||||||
|
voiceMode?: { audioRequestId: string; clientMsgId: string },
|
||||||
) => {
|
) => {
|
||||||
const atts = explicitAttachments ?? pendingAttachments;
|
const atts = explicitAttachments ?? pendingAttachments;
|
||||||
if (atts.length === 0) return;
|
if (atts.length === 0) return;
|
||||||
@@ -2597,16 +2671,23 @@ const ChatScreen: React.FC = () => {
|
|||||||
|
|
||||||
// Chat-Nachricht mit allen Anhaengen. clientMsgId nur wenn Text dabei
|
// Chat-Nachricht mit allen Anhaengen. clientMsgId nur wenn Text dabei
|
||||||
// ist — files selber haben (noch) kein ACK-Tracking auf der Bridge.
|
// ist — files selber haben (noch) kein ACK-Tracking auf der Bridge.
|
||||||
const cmid = messageText ? nextClientMsgId() : undefined;
|
// Im Sprach-Modus kommt sie von aussen: dieselbe ID muss am
|
||||||
|
// stt_stream_start haengen, damit die Bridge Anhang und Satz zusammenfuehrt.
|
||||||
|
const cmid = voiceMode ? voiceMode.clientMsgId : (messageText ? nextClientMsgId() : undefined);
|
||||||
const activePid = focusedProjectIdRef.current;
|
const activePid = focusedProjectIdRef.current;
|
||||||
const userMsg: ChatMessage = {
|
const userMsg: ChatMessage = {
|
||||||
id: msgId,
|
id: msgId,
|
||||||
sender: 'user',
|
sender: 'user',
|
||||||
text: messageText || `${atts.length} Anhang/Anhaenge`,
|
// Im Sprach-Modus bekommt DIESE Bubble gleich den gesprochenen Text —
|
||||||
|
// via audioRequestId-Match, genau wie eine normale Sprachnachricht. So
|
||||||
|
// stehen Bild und Satz zusammen in einer Bubble statt in zwei.
|
||||||
|
text: voiceMode ? '🎙 Spracheingabe wird verarbeitet...'
|
||||||
|
: (messageText || `${atts.length} Anhang/Anhaenge`),
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
attachments,
|
attachments,
|
||||||
projectId: activePid,
|
projectId: activePid,
|
||||||
...(cmid && {
|
...(voiceMode && { audioRequestId: voiceMode.audioRequestId }),
|
||||||
|
...(cmid && !voiceMode && {
|
||||||
clientMsgId: cmid,
|
clientMsgId: cmid,
|
||||||
deliveryStatus: connectionStateRef.current === 'connected' ? 'sending' : 'queued',
|
deliveryStatus: connectionStateRef.current === 'connected' ? 'sending' : 'queued',
|
||||||
sendAttempts: 1,
|
sendAttempts: 1,
|
||||||
@@ -2644,6 +2725,9 @@ const ChatScreen: React.FC = () => {
|
|||||||
// die Datei genau DIESER Nachricht zuordnet — auch wenn Files (fire-
|
// die Datei genau DIESER Nachricht zuordnet — auch wenn Files (fire-
|
||||||
// and-forget) und Text (ACK-getrackt, bei Queue verzoegert) desyncen.
|
// and-forget) und Text (ACK-getrackt, bei Queue verzoegert) desyncen.
|
||||||
...(cmid && { clientMsgId: cmid }),
|
...(cmid && { clientMsgId: cmid }),
|
||||||
|
// awaitVoice: die Bridge soll den Anhang NICHT nach 0.8s alleine ans
|
||||||
|
// Brain schicken, sondern auf die Sprachnachricht warten.
|
||||||
|
...(voiceMode && { awaitVoice: true }),
|
||||||
...(isPhoto && file.width && { width: file.width, height: file.height }),
|
...(isPhoto && file.width && { width: file.width, height: file.height }),
|
||||||
...(location && { location }),
|
...(location && { location }),
|
||||||
});
|
});
|
||||||
@@ -2651,8 +2735,8 @@ const ChatScreen: React.FC = () => {
|
|||||||
|
|
||||||
// Text als separate Nachricht (damit ARIA weiss was zu tun ist) — mit
|
// Text als separate Nachricht (damit ARIA weiss was zu tun ist) — mit
|
||||||
// dem clientMsgId der Bubble, damit Bridge+ACK die richtige Bubble
|
// dem clientMsgId der Bubble, damit Bridge+ACK die richtige Bubble
|
||||||
// adressieren.
|
// adressieren. Im Sprach-Modus gibt es hier noch keinen Text.
|
||||||
if (messageText && cmid) {
|
if (messageText && cmid && !voiceMode) {
|
||||||
dispatchWithAck(cmid, 'chat', {
|
dispatchWithAck(cmid, 'chat', {
|
||||||
text: messageText,
|
text: messageText,
|
||||||
voice: localXttsVoiceRef.current,
|
voice: localXttsVoiceRef.current,
|
||||||
@@ -2665,6 +2749,9 @@ const ChatScreen: React.FC = () => {
|
|||||||
setPendingAttachments([]);
|
setPendingAttachments([]);
|
||||||
setInputText('');
|
setInputText('');
|
||||||
}, [pendingAttachments, getCurrentLocation, dispatchWithAck]);
|
}, [pendingAttachments, getCurrentLocation, dispatchWithAck]);
|
||||||
|
// Ref aktuell halten — handleVoiceButtonStart greift die Funktion von hier
|
||||||
|
// (steht weiter oben in der Datei, siehe sendPendingAttachmentsRef).
|
||||||
|
sendPendingAttachmentsRef.current = sendPendingAttachments;
|
||||||
|
|
||||||
// Reine Text-Nachricht senden. Steht bewusst NACH interruptAriaIfBusy und
|
// Reine Text-Nachricht senden. Steht bewusst NACH interruptAriaIfBusy und
|
||||||
// sendPendingAttachments — beide stehen in seinem deps-Array, und wenn es
|
// sendPendingAttachments — beide stehen in seinem deps-Array, und wenn es
|
||||||
@@ -3529,7 +3616,9 @@ const ChatScreen: React.FC = () => {
|
|||||||
style={styles.textInput}
|
style={styles.textInput}
|
||||||
value={inputText}
|
value={inputText}
|
||||||
onChangeText={setInputText}
|
onChangeText={setInputText}
|
||||||
placeholder={pendingAttachments.length > 0 ? "Text zu den Anhaengen (optional)..." : "Nachricht an ARIA..."}
|
placeholder={pendingAttachments.length > 0
|
||||||
|
? "Text zu den Anhaengen — oder einsprechen 🎤"
|
||||||
|
: "Nachricht an ARIA..."}
|
||||||
placeholderTextColor="#555570"
|
placeholderTextColor="#555570"
|
||||||
multiline
|
multiline
|
||||||
maxLength={4000}
|
maxLength={4000}
|
||||||
@@ -3537,29 +3626,52 @@ const ChatScreen: React.FC = () => {
|
|||||||
returnKeyType="send"
|
returnKeyType="send"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Senden oder Sprache */}
|
{/* Rechte Knopfreihe.
|
||||||
|
Das Mikro ist jetzt IMMER da \u2014 auch mit Anhang oder getipptem Text.
|
||||||
|
Vorher war es ein Entweder-oder: sobald etwas im Feld stand oder ein
|
||||||
|
Bild dranhing, verschwand das Mikro. Damit ging weder "Bild anhaengen
|
||||||
|
und den Auftrag dazu sprechen" noch ein Zwischenruf per Sprache.
|
||||||
|
Der Senden-Pfeil und die Mund-/Ohr-Knoepfe weichen dafuer je nach
|
||||||
|
Lage, damit die Zeile nicht ueberlaeuft. */}
|
||||||
|
|
||||||
|
{/* Zwischenruf \u2014 sichtbar, wenn ARIA im aktiven Kontext arbeitet (oder
|
||||||
|
noch scharf geschaltet ist). Mit Text: sofort abschicken, wie bisher.
|
||||||
|
Ohne Text: die naechste Aufnahme geht als Zwischenruf in den
|
||||||
|
laufenden Turn, statt sich als neue Anfrage anzustellen. */}
|
||||||
|
{interjectArmed || (agentActivityByCtx[focusedProjectId]?.activity || 'idle') !== 'idle' ? (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.interjectButton, interjectArmed && styles.interjectButtonArmed]}
|
||||||
|
onPress={() => {
|
||||||
|
if (inputText.trim()) { sendInterject(); return; }
|
||||||
|
const next = !interjectArmedRef.current;
|
||||||
|
interjectArmedRef.current = next;
|
||||||
|
setInterjectArmed(next);
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
ToastAndroid.show(
|
||||||
|
next ? 'N\u00E4chste Aufnahme geht als Zwischenruf raus'
|
||||||
|
: 'Zwischenruf abgew\u00E4hlt',
|
||||||
|
ToastAndroid.SHORT);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
accessibilityLabel={inputText.trim() ? 'Zwischenruf senden' : 'Zwischenruf per Sprache'}
|
||||||
|
>
|
||||||
|
<Text style={styles.interjectIcon}>{'\uD83D\uDCE3'}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<VoiceButton
|
||||||
|
onTapStart={handleVoiceButtonStart}
|
||||||
|
onTapStop={handleVoiceButtonStop}
|
||||||
|
disabled={connectionState !== 'connected'}
|
||||||
|
wakeWordActive={wakeWordActive}
|
||||||
|
/>
|
||||||
|
|
||||||
{inputText.trim() || pendingAttachments.length > 0 ? (
|
{inputText.trim() || pendingAttachments.length > 0 ? (
|
||||||
<>
|
<TouchableOpacity style={styles.sendButton} onPress={sendTextMessage}>
|
||||||
{/* Zwischenruf: nur wenn ARIA im aktiven Kontext gerade arbeitet und
|
<Text style={styles.sendIcon}>{'\u2B06\uFE0F'}</Text>
|
||||||
Text da ist. Schiebt die Korrektur in den laufenden Turn statt
|
</TouchableOpacity>
|
||||||
sie anzustellen. */}
|
|
||||||
{inputText.trim() && (agentActivityByCtx[focusedProjectId]?.activity || 'idle') !== 'idle' ? (
|
|
||||||
<TouchableOpacity style={styles.interjectButton} onPress={sendInterject} accessibilityLabel="Zwischenruf">
|
|
||||||
<Text style={styles.interjectIcon}>{'\uD83D\uDCE3'}</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
) : null}
|
|
||||||
<TouchableOpacity style={styles.sendButton} onPress={sendTextMessage}>
|
|
||||||
<Text style={styles.sendIcon}>{'\u2B06\uFE0F'}</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<VoiceButton
|
|
||||||
onTapStart={handleVoiceButtonStart}
|
|
||||||
onTapStop={handleVoiceButtonStop}
|
|
||||||
disabled={connectionState !== 'connected'}
|
|
||||||
wakeWordActive={wakeWordActive}
|
|
||||||
/>
|
|
||||||
{/* Mund-Button: TTS auf diesem Geraet muten/aufheben.
|
{/* Mund-Button: TTS auf diesem Geraet muten/aufheben.
|
||||||
Nur sichtbar wenn TTS in den Settings grundsaetzlich aktiv ist. */}
|
Nur sichtbar wenn TTS in den Settings grundsaetzlich aktiv ist. */}
|
||||||
{ttsDeviceEnabled && (
|
{ttsDeviceEnabled && (
|
||||||
@@ -4085,6 +4197,13 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginRight: 6,
|
marginRight: 6,
|
||||||
},
|
},
|
||||||
|
// Scharf geschaltet: die naechste Aufnahme geht als Zwischenruf raus. Heller
|
||||||
|
// plus Rand, damit man das vor dem Sprechen sieht und nicht erst danach.
|
||||||
|
interjectButtonArmed: {
|
||||||
|
backgroundColor: '#FFB340',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: '#FFF',
|
||||||
|
},
|
||||||
interjectIcon: {
|
interjectIcon: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1141,6 +1141,13 @@ class AudioService {
|
|||||||
* Leer = Hauptchat. Ohne Prefix / Sticky landet die STT-Nachricht damit
|
* Leer = Hauptchat. Ohne Prefix / Sticky landet die STT-Nachricht damit
|
||||||
* automatisch in dem Kontext den Stefan gerade sieht. */
|
* automatisch in dem Kontext den Stefan gerade sieht. */
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
/** clientMsgId wartender Anhaenge: diese Aufnahme ist ihre Beschreibung.
|
||||||
|
* Die Bridge fuehrt Anhang + gesprochenen Satz darueber zu EINEM Turn
|
||||||
|
* zusammen, statt das Bild ohne Auftrag und den Satz ohne Bild zu sehen. */
|
||||||
|
clientMsgId?: string;
|
||||||
|
/** Zwischenruf: der Text geht in den LAUFENDEN Turn, nicht als neue
|
||||||
|
* Anfrage in die Warteschlange (Sprach-Gegenstueck zum 📣-Knopf). */
|
||||||
|
interject?: boolean;
|
||||||
}): Promise<{ requestId: string; ok: boolean; error?: string }> {
|
}): Promise<{ requestId: string; ok: boolean; error?: string }> {
|
||||||
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);
|
||||||
@@ -1245,6 +1252,8 @@ class AudioService {
|
|||||||
sampleRate: 16000,
|
sampleRate: 16000,
|
||||||
projectId: opts.projectId || '',
|
projectId: opts.projectId || '',
|
||||||
targetInstance: this.streamTargetInstance,
|
targetInstance: this.streamTargetInstance,
|
||||||
|
...(opts.clientMsgId ? { clientMsgId: opts.clientMsgId } : {}),
|
||||||
|
...(opts.interject ? { interject: true } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const started = await PcmStreamRecorder.start();
|
const started = await PcmStreamRecorder.start();
|
||||||
|
|||||||
+104
-11
@@ -739,6 +739,14 @@ class ARIABridge:
|
|||||||
# weder Meta-Kommando noch Prefix ist und der Sticky abgelaufen,
|
# weder Meta-Kommando noch Prefix ist und der Sticky abgelaufen,
|
||||||
# nutzen wir das als Default (Voice folgt dem sichtbaren Kontext).
|
# nutzen wir das als Default (Voice folgt dem sichtbaren Kontext).
|
||||||
self._stt_stream_projects: dict[str, str] = {}
|
self._stt_stream_projects: dict[str, str] = {}
|
||||||
|
# Zusatz-Absicht pro Stream, auch von stt_stream_start:
|
||||||
|
# cmid — clientMsgId der Anhaenge, die zu DIESER Aufnahme gehoeren
|
||||||
|
# (Sprachnachricht als Beschreibung zu einem Bild/Video).
|
||||||
|
# interject — die Aufnahme ist ein Zwischenruf in den laufenden Turn,
|
||||||
|
# keine neue Anfrage.
|
||||||
|
# Getrennt von _stt_stream_projects gehalten: der Fokus-Default greift
|
||||||
|
# auch fuer alte App-Versionen, die diese Felder nicht mitschicken.
|
||||||
|
self._stt_stream_meta: dict[str, dict] = {}
|
||||||
# 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).
|
||||||
@@ -808,6 +816,18 @@ class ARIABridge:
|
|||||||
# Projekt landen statt im Hauptchat.
|
# Projekt landen statt im Hauptchat.
|
||||||
self._pending_files_project_id: str = ""
|
self._pending_files_project_id: str = ""
|
||||||
self._PENDING_FILES_WINDOW_SEC: float = 0.8
|
self._PENDING_FILES_WINDOW_SEC: float = 0.8
|
||||||
|
# Wartet ein Upload auf eine SPRACHNACHRICHT als Beschreibung, reichen
|
||||||
|
# 0.8s nicht: der Nutzer spricht erst, und Whisper braucht danach noch
|
||||||
|
# ein paar Sekunden fuers Final-Transkript. Mit dem kurzen Fenster waere
|
||||||
|
# der Anhang laengst alleine rausgegangen ("warte auf seine Anweisung"),
|
||||||
|
# und der gesprochene Text kaeme als zweiter, unverbundener Turn.
|
||||||
|
# Das lange Fenster ist nur eine Notbremse gegen Haengenbleiben, falls
|
||||||
|
# gar kein Endpoint mehr kommt — der Normalfall ist der Merge im
|
||||||
|
# stt_endpoint-Handler. Es MUSS ueber dem Aufnahme-Hard-Cap der App
|
||||||
|
# liegen (Default 5 Minuten) plus Whisper-Final, sonst waeren die
|
||||||
|
# Anhaenge bei einer langen Aufnahme vorher alleine rausgegangen.
|
||||||
|
self._PENDING_FILES_VOICE_WINDOW_SEC: float = 420.0
|
||||||
|
self._pending_files_await_voice: bool = False
|
||||||
|
|
||||||
def initialize(self) -> None:
|
def initialize(self) -> None:
|
||||||
"""Initialisiert alle Komponenten.
|
"""Initialisiert alle Komponenten.
|
||||||
@@ -1879,6 +1899,13 @@ class ARIABridge:
|
|||||||
return (f"Stefan hat dir {n} {anhang} geschickt:\n{files_summary}\n\n"
|
return (f"Stefan hat dir {n} {anhang} geschickt:\n{files_summary}\n\n"
|
||||||
f"Warte auf seine Anweisung was du damit tun sollst.")
|
f"Warte auf seine Anweisung was du damit tun sollst.")
|
||||||
|
|
||||||
|
def _pending_files_window(self) -> float:
|
||||||
|
"""Wie lange auf den zugehoerigen Text gewartet wird, bevor die Anhaenge
|
||||||
|
alleine rausgehen. Lang, wenn eine Sprachnachricht unterwegs ist."""
|
||||||
|
return (self._PENDING_FILES_VOICE_WINDOW_SEC
|
||||||
|
if self._pending_files_await_voice
|
||||||
|
else self._PENDING_FILES_WINDOW_SEC)
|
||||||
|
|
||||||
async def _flush_pending_files_after(self, delay: float) -> None:
|
async def _flush_pending_files_after(self, delay: float) -> None:
|
||||||
"""Wenn nach `delay`s kein chat-Text gekommen ist: alle noch gepufferten
|
"""Wenn nach `delay`s kein chat-Text gekommen ist: alle noch gepufferten
|
||||||
Files alleine an aria-core senden ('warte auf Anweisung'-Variante)."""
|
Files alleine an aria-core senden ('warte auf Anweisung'-Variante)."""
|
||||||
@@ -1892,13 +1919,15 @@ class ARIABridge:
|
|||||||
text = self._build_pending_files_message("", files)
|
text = self._build_pending_files_message("", files)
|
||||||
self._pending_files = []
|
self._pending_files = []
|
||||||
self._pending_files_flush_task = None
|
self._pending_files_flush_task = None
|
||||||
|
self._pending_files_await_voice = False
|
||||||
pid = self._pending_files_project_id
|
pid = self._pending_files_project_id
|
||||||
self._pending_files_project_id = ""
|
self._pending_files_project_id = ""
|
||||||
await self.send_to_core(text, source="app-file", project_id=pid)
|
await self.send_to_core(text, source="app-file", project_id=pid)
|
||||||
|
|
||||||
async def _flush_pending_files_with_text(self, user_text: str,
|
async def _flush_pending_files_with_text(self, user_text: str,
|
||||||
project_id: str = "",
|
project_id: str = "",
|
||||||
client_msg_id: str = "") -> bool:
|
client_msg_id: str = "",
|
||||||
|
require_match: bool = False) -> bool:
|
||||||
"""Wenn ein chat-Text reinkommt waehrend Files gepuffert sind:
|
"""Wenn ein chat-Text reinkommt waehrend Files gepuffert sind:
|
||||||
Files + Text zu einer einzigen aria-core-Nachricht mergen.
|
Files + Text zu einer einzigen aria-core-Nachricht mergen.
|
||||||
Returns True wenn gemerged wurde (Caller soll dann nicht nochmal senden).
|
Returns True wenn gemerged wurde (Caller soll dann nicht nochmal senden).
|
||||||
@@ -1913,12 +1942,20 @@ class ARIABridge:
|
|||||||
|
|
||||||
project_id: Projekt-Kontext aus dem chat-Payload (der sichtbare Focus
|
project_id: Projekt-Kontext aus dem chat-Payload (der sichtbare Focus
|
||||||
beim Absenden). Faellt auf den beim File-Upload gemerkten Kontext zurueck.
|
beim Absenden). Faellt auf den beim File-Upload gemerkten Kontext zurueck.
|
||||||
|
|
||||||
|
require_match: nur mergen, wenn die cmid wirklich passt. Der Sprach-Pfad
|
||||||
|
nutzt das — sonst wuerde eine Sprachnachricht, die zufaellig waehrend
|
||||||
|
eines fremden Uploads fertig wird, dessen Anhaenge an sich reissen. Beim
|
||||||
|
Text-Pfad bleibt der Fallback (alles mergen) wie bisher, damit nie ein
|
||||||
|
Bild verloren geht.
|
||||||
"""
|
"""
|
||||||
if not self._pending_files:
|
if not self._pending_files:
|
||||||
return False
|
return False
|
||||||
cmid = (client_msg_id or "").strip()
|
cmid = (client_msg_id or "").strip()
|
||||||
matching = [f for f in self._pending_files if cmid and f[6] == cmid]
|
matching = [f for f in self._pending_files if cmid and f[6] == cmid]
|
||||||
if not matching:
|
if not matching:
|
||||||
|
if require_match:
|
||||||
|
return False
|
||||||
# Kein cmid-Treffer → altes Verhalten: alle gepufferten Files mergen.
|
# Kein cmid-Treffer → altes Verhalten: alle gepufferten Files mergen.
|
||||||
matching = list(self._pending_files)
|
matching = list(self._pending_files)
|
||||||
remaining: list = []
|
remaining: list = []
|
||||||
@@ -1935,10 +1972,11 @@ class ARIABridge:
|
|||||||
self._pending_files_flush_task = None
|
self._pending_files_flush_task = None
|
||||||
if remaining:
|
if remaining:
|
||||||
self._pending_files_flush_task = asyncio.create_task(
|
self._pending_files_flush_task = asyncio.create_task(
|
||||||
self._flush_pending_files_after(self._PENDING_FILES_WINDOW_SEC)
|
self._flush_pending_files_after(self._pending_files_window())
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self._pending_files_project_id = ""
|
self._pending_files_project_id = ""
|
||||||
|
self._pending_files_await_voice = False
|
||||||
# create_task statt await — sonst blockt der RVS-recv-Loop bis Brain
|
# create_task statt await — sonst blockt der RVS-recv-Loop bis Brain
|
||||||
# fertig ist (siehe chat-handler oben).
|
# fertig ist (siehe chat-handler oben).
|
||||||
asyncio.create_task(self.send_to_core(text, source="app-file+chat", project_id=pid))
|
asyncio.create_task(self.send_to_core(text, source="app-file+chat", project_id=pid))
|
||||||
@@ -2752,10 +2790,17 @@ class ARIABridge:
|
|||||||
# genau SEINER Nachricht zu (Queue-Korrelation, s. _flush_*).
|
# genau SEINER Nachricht zu (Queue-Korrelation, s. _flush_*).
|
||||||
file_cmid = str(payload.get("clientMsgId") or "")
|
file_cmid = str(payload.get("clientMsgId") or "")
|
||||||
self._pending_files.append((file_path, file_name, file_type, size_kb, int(width or 0), int(height or 0), file_cmid))
|
self._pending_files.append((file_path, file_name, file_type, size_kb, int(width or 0), int(height or 0), file_cmid))
|
||||||
|
# awaitVoice: der Nutzer spricht gerade die Beschreibung zu diesem
|
||||||
|
# Anhang ein. Dann NICHT nach 0.8s alleine rausschicken, sondern auf
|
||||||
|
# den stt_endpoint warten (siehe _PENDING_FILES_VOICE_WINDOW_SEC).
|
||||||
|
if payload.get("awaitVoice"):
|
||||||
|
self._pending_files_await_voice = True
|
||||||
|
logger.info("[rvs] Anhang wartet auf Sprachnachricht: %s (cmid=%s)",
|
||||||
|
file_name, file_cmid[:12] or "?")
|
||||||
if self._pending_files_flush_task and not self._pending_files_flush_task.done():
|
if self._pending_files_flush_task and not self._pending_files_flush_task.done():
|
||||||
self._pending_files_flush_task.cancel()
|
self._pending_files_flush_task.cancel()
|
||||||
self._pending_files_flush_task = asyncio.create_task(
|
self._pending_files_flush_task = asyncio.create_task(
|
||||||
self._flush_pending_files_after(self._PENDING_FILES_WINDOW_SEC)
|
self._flush_pending_files_after(self._pending_files_window())
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -3324,8 +3369,18 @@ class ARIABridge:
|
|||||||
focused_pid = str(payload.get("projectId") or "")
|
focused_pid = str(payload.get("projectId") or "")
|
||||||
if req_id:
|
if req_id:
|
||||||
self._stt_stream_projects[req_id] = focused_pid
|
self._stt_stream_projects[req_id] = focused_pid
|
||||||
logger.info("[rvs] stt_stream_start id=%s focus=%s",
|
# Absicht dieser Aufnahme: gehoert sie zu wartenden Anhaengen,
|
||||||
req_id[:12], focused_pid or "(main)")
|
# und ist sie ein Zwischenruf statt einer neuen Anfrage?
|
||||||
|
att_cmid = str(payload.get("clientMsgId") or "")
|
||||||
|
is_interject = bool(payload.get("interject"))
|
||||||
|
if att_cmid or is_interject:
|
||||||
|
self._stt_stream_meta[req_id] = {
|
||||||
|
"cmid": att_cmid, "interject": is_interject,
|
||||||
|
}
|
||||||
|
logger.info("[rvs] stt_stream_start id=%s focus=%s%s%s",
|
||||||
|
req_id[:12], focused_pid or "(main)",
|
||||||
|
f" anhang-cmid={att_cmid[:12]}" if att_cmid else "",
|
||||||
|
" [ZWISCHENRUF]" if is_interject else "")
|
||||||
return
|
return
|
||||||
|
|
||||||
elif msg_type == "stt_stream_end":
|
elif msg_type == "stt_stream_end":
|
||||||
@@ -3340,10 +3395,10 @@ class ARIABridge:
|
|||||||
if req_id:
|
if req_id:
|
||||||
try:
|
try:
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.call_later(
|
def _drop(rid: str = req_id) -> None:
|
||||||
20.0,
|
self._stt_stream_projects.pop(rid, None)
|
||||||
lambda rid=req_id: self._stt_stream_projects.pop(rid, None),
|
self._stt_stream_meta.pop(rid, None)
|
||||||
)
|
loop.call_later(20.0, _drop)
|
||||||
except Exception:
|
except Exception:
|
||||||
# Kein laufender Loop (sollte im ws-Handler nie passieren) —
|
# Kein laufender Loop (sollte im ws-Handler nie passieren) —
|
||||||
# dann lieber gar nicht aufraeumen als crashen.
|
# dann lieber gar nicht aufraeumen als crashen.
|
||||||
@@ -3427,11 +3482,36 @@ class ARIABridge:
|
|||||||
# App-Version die projectId noch nicht mitschickt: leer = Hauptchat).
|
# App-Version die projectId noch nicht mitschickt: leer = Hauptchat).
|
||||||
stream_req_id = payload.get("requestId", "") or ""
|
stream_req_id = payload.get("requestId", "") or ""
|
||||||
focused_pid = self._stt_stream_projects.pop(stream_req_id, "")
|
focused_pid = self._stt_stream_projects.pop(stream_req_id, "")
|
||||||
|
meta = self._stt_stream_meta.pop(stream_req_id, {})
|
||||||
|
|
||||||
|
# Zwischenruf per Sprache: in den LAUFENDEN Turn schieben, nicht als
|
||||||
|
# neue Anfrage anstellen. Gleicher Weg wie der 📣-Knopf mit Text.
|
||||||
|
if meta.get("interject"):
|
||||||
|
logger.info("[rvs] stt_endpoint ist ein Zwischenruf → project=%s: %s",
|
||||||
|
focused_pid or "(main)", text[:80])
|
||||||
|
asyncio.create_task(
|
||||||
|
self._interject_proxy_for_project(focused_pid, text))
|
||||||
|
# Den erkannten Text trotzdem broadcasten, damit die
|
||||||
|
# Sprach-Bubble in der App ihren Text bekommt statt ewig auf
|
||||||
|
# "wird verarbeitet..." zu stehen.
|
||||||
|
asyncio.create_task(self._send_to_rvs({
|
||||||
|
"type": "chat",
|
||||||
|
"payload": {
|
||||||
|
"text": text,
|
||||||
|
"sender": "stt",
|
||||||
|
"audioRequestId": audio_request_id,
|
||||||
|
"projectId": focused_pid,
|
||||||
|
"interject": True,
|
||||||
|
},
|
||||||
|
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
|
||||||
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))
|
focused_project_id=focused_pid,
|
||||||
|
attachment_client_msg_id=str(meta.get("cmid") or "")))
|
||||||
return
|
return
|
||||||
|
|
||||||
elif msg_type == "oauth_callback":
|
elif msg_type == "oauth_callback":
|
||||||
@@ -3891,7 +3971,8 @@ class ARIABridge:
|
|||||||
audio_request_id: str = "",
|
audio_request_id: str = "",
|
||||||
location: Optional[dict] = None,
|
location: Optional[dict] = None,
|
||||||
client_msg_id: Optional[str] = None,
|
client_msg_id: Optional[str] = None,
|
||||||
focused_project_id: str = "") -> None:
|
focused_project_id: str = "",
|
||||||
|
attachment_client_msg_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.
|
||||||
|
|
||||||
@@ -3960,6 +4041,18 @@ class ARIABridge:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("[rvs] STT-Text (endpoint) konnte nicht broadcastet werden: %s", e)
|
logger.warning("[rvs] STT-Text (endpoint) konnte nicht broadcastet werden: %s", e)
|
||||||
|
|
||||||
|
# Sprachnachricht als Beschreibung zu Anhaengen: die liegen schon
|
||||||
|
# gepuffert (awaitVoice) und warten genau auf diesen Text — beides geht
|
||||||
|
# als EIN Turn ans Brain, sonst sieht ARIA ein Bild ohne Auftrag und
|
||||||
|
# danach einen Satz ohne Bild.
|
||||||
|
if attachment_client_msg_id and await self._flush_pending_files_with_text(
|
||||||
|
cleaned, project_id=project_id,
|
||||||
|
client_msg_id=attachment_client_msg_id,
|
||||||
|
require_match=True):
|
||||||
|
logger.info("[rvs] Sprachnachricht mit ihren Anhaengen gemerged (cmid=%s)",
|
||||||
|
attachment_client_msg_id[:12])
|
||||||
|
return
|
||||||
|
|
||||||
core_text = self._build_core_text(cleaned, interrupted, location)
|
core_text = self._build_core_text(cleaned, interrupted, location)
|
||||||
await self.send_to_core(core_text,
|
await self.send_to_core(core_text,
|
||||||
source="app-voice-stream" + (" [barge-in]" if interrupted else ""),
|
source="app-voice-stream" + (" [barge-in]" if interrupted else ""),
|
||||||
|
|||||||
Reference in New Issue
Block a user