feat: Max-Aufnahmedauer konfigurierbar + Barge-In gibt aria-core Kontext
Max-Aufnahme: Default rauf von 2 auf 5 Minuten, in den App-Settings konfigurierbar zwischen 1 und 30 Minuten (loadMaxRecordingMs aus AsyncStorage, Storage-Key aria_max_recording_sec). Notbremse-Verhalten bleibt: nach Ablauf wird die Aufnahme automatisch beendet und gesendet. Barge-In Kontext: Wenn der User waehrend ARIA noch redet/arbeitet eine neue Sprach- oder Text-Nachricht sendet, geht jetzt ein 'interrupted: true' Flag mit. Bridge praefixed den Text fuer aria-core dann mit: "[Hinweis: Stefan hat dich gerade unterbrochen waehrend du noch gesprochen oder gearbeitet hast. Folgendes ist eine Korrektur, Ergaenzung oder ein Themenwechsel zu deiner letzten Antwort.]" So weiss ARIA dass die neue Message KEINE eigenstaendige Folgefrage ist sondern auf den abgebrochenen Run bezogen. Der User sieht in seinem Chat nur den reinen Text — der Hint geht nur an aria-core. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -505,7 +505,7 @@ const ChatScreen: React.FC = () => {
|
||||
if (result && result.durationMs > 500) {
|
||||
// User hat im Fenster gesprochen → Sprachnachricht senden
|
||||
// Barge-In: laufende ARIA-Aktivitaet abbrechen wenn welche da ist.
|
||||
interruptAriaIfBusy();
|
||||
const wasInterrupted = interruptAriaIfBusy();
|
||||
const location = await getCurrentLocation();
|
||||
const userMsg: ChatMessage = {
|
||||
id: nextId(),
|
||||
@@ -521,6 +521,7 @@ const ChatScreen: React.FC = () => {
|
||||
mimeType: result.mimeType,
|
||||
voice: localXttsVoiceRef.current,
|
||||
speed: ttsSpeedRef.current,
|
||||
interrupted: wasInterrupted,
|
||||
...(location && { location }),
|
||||
});
|
||||
// resume() wird durch onPlaybackFinished nach ARIAs Antwort getriggert.
|
||||
@@ -623,6 +624,8 @@ const ChatScreen: React.FC = () => {
|
||||
|
||||
setInputText('');
|
||||
|
||||
// Barge-In: laufende ARIA-Aktivitaet abbrechen wenn welche da ist.
|
||||
const wasInterrupted = interruptAriaIfBusy();
|
||||
const location = await getCurrentLocation();
|
||||
|
||||
const userMsg: ChatMessage = {
|
||||
@@ -633,16 +636,17 @@ const ChatScreen: React.FC = () => {
|
||||
};
|
||||
setMessages(prev => capMessages([...prev, userMsg]));
|
||||
|
||||
console.log('[Chat] sende mit voice=%s speed=%s',
|
||||
localXttsVoiceRef.current || '(default)', ttsSpeedRef.current);
|
||||
console.log('[Chat] sende mit voice=%s speed=%s interrupted=%s',
|
||||
localXttsVoiceRef.current || '(default)', ttsSpeedRef.current, wasInterrupted);
|
||||
// An RVS senden — mit geraetelokaler Voice (Bridge nutzt sie fuer die Antwort)
|
||||
rvs.send('chat', {
|
||||
text,
|
||||
voice: localXttsVoiceRef.current,
|
||||
speed: ttsSpeedRef.current,
|
||||
interrupted: wasInterrupted,
|
||||
...(location && { location }),
|
||||
});
|
||||
}, [inputText, getCurrentLocation, pendingAttachments, sendPendingAttachments]);
|
||||
}, [inputText, getCurrentLocation, pendingAttachments, sendPendingAttachments, interruptAriaIfBusy]);
|
||||
|
||||
// Anfrage abbrechen — sofort lokalen Indicator weg, Bridge triggert doctor --fix
|
||||
const cancelRequest = useCallback(() => {
|
||||
@@ -671,7 +675,7 @@ const ChatScreen: React.FC = () => {
|
||||
// Sprachaufnahme abgeschlossen
|
||||
const handleVoiceRecording = useCallback(async (result: RecordingResult) => {
|
||||
// Barge-In: laufende ARIA-Aktivitaet abbrechen falls aktiv.
|
||||
interruptAriaIfBusy();
|
||||
const wasInterrupted = interruptAriaIfBusy();
|
||||
const location = await getCurrentLocation();
|
||||
|
||||
const userMsg: ChatMessage = {
|
||||
@@ -688,6 +692,7 @@ const ChatScreen: React.FC = () => {
|
||||
mimeType: result.mimeType,
|
||||
voice: localXttsVoiceRef.current,
|
||||
speed: ttsSpeedRef.current,
|
||||
interrupted: wasInterrupted,
|
||||
...(location && { location }),
|
||||
});
|
||||
}, [getCurrentLocation, interruptAriaIfBusy]);
|
||||
|
||||
@@ -35,6 +35,10 @@ import {
|
||||
CONV_WINDOW_MIN_SEC,
|
||||
CONV_WINDOW_MAX_SEC,
|
||||
CONV_WINDOW_STORAGE_KEY,
|
||||
MAX_RECORDING_DEFAULT_SEC,
|
||||
MAX_RECORDING_MIN_SEC,
|
||||
MAX_RECORDING_MAX_SEC,
|
||||
MAX_RECORDING_STORAGE_KEY,
|
||||
TTS_SPEED_DEFAULT,
|
||||
TTS_SPEED_MIN,
|
||||
TTS_SPEED_MAX,
|
||||
@@ -102,6 +106,7 @@ const SettingsScreen: React.FC = () => {
|
||||
const [ttsPrerollSec, setTtsPrerollSec] = useState<number>(TTS_PREROLL_DEFAULT_SEC);
|
||||
const [vadSilenceSec, setVadSilenceSec] = useState<number>(VAD_SILENCE_DEFAULT_SEC);
|
||||
const [convWindowSec, setConvWindowSec] = useState<number>(CONV_WINDOW_DEFAULT_SEC);
|
||||
const [maxRecordingSec, setMaxRecordingSec] = useState<number>(MAX_RECORDING_DEFAULT_SEC);
|
||||
const [ttsSpeed, setTtsSpeed] = useState<number>(TTS_SPEED_DEFAULT);
|
||||
const [wakeKeyword, setWakeKeyword] = useState<string>(DEFAULT_KEYWORD);
|
||||
const [wakeStatus, setWakeStatus] = useState<string>('');
|
||||
@@ -156,6 +161,14 @@ const SettingsScreen: React.FC = () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
AsyncStorage.getItem(MAX_RECORDING_STORAGE_KEY).then(saved => {
|
||||
if (saved != null) {
|
||||
const n = parseFloat(saved);
|
||||
if (isFinite(n) && n >= MAX_RECORDING_MIN_SEC && n <= MAX_RECORDING_MAX_SEC) {
|
||||
setMaxRecordingSec(n);
|
||||
}
|
||||
}
|
||||
});
|
||||
AsyncStorage.getItem(TTS_SPEED_STORAGE_KEY).then(saved => {
|
||||
if (saved != null) {
|
||||
const n = parseFloat(saved);
|
||||
@@ -671,6 +684,38 @@ const SettingsScreen: React.FC = () => {
|
||||
<Text style={styles.prerollButtonText}>+1</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.toggleLabel, {marginTop: 24}]}>Maximale Aufnahmedauer</Text>
|
||||
<Text style={styles.toggleHint}>
|
||||
Notbremse: nach so vielen Minuten wird die Aufnahme automatisch beendet,
|
||||
auch wenn keine Stille erkannt wurde. Nuetzlich fuer lange Erklaerungen
|
||||
oder Diktate. Default: {Math.round(MAX_RECORDING_DEFAULT_SEC / 60)} Min, max {Math.round(MAX_RECORDING_MAX_SEC / 60)} Min.
|
||||
</Text>
|
||||
<View style={styles.prerollRow}>
|
||||
<TouchableOpacity
|
||||
style={styles.prerollButton}
|
||||
onPress={() => {
|
||||
const next = Math.max(MAX_RECORDING_MIN_SEC, maxRecordingSec - 60);
|
||||
setMaxRecordingSec(next);
|
||||
AsyncStorage.setItem(MAX_RECORDING_STORAGE_KEY, String(next));
|
||||
}}
|
||||
disabled={maxRecordingSec <= MAX_RECORDING_MIN_SEC}
|
||||
>
|
||||
<Text style={styles.prerollButtonText}>−1m</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.prerollValue}>{Math.round(maxRecordingSec / 60)} min</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.prerollButton}
|
||||
onPress={() => {
|
||||
const next = Math.min(MAX_RECORDING_MAX_SEC, maxRecordingSec + 60);
|
||||
setMaxRecordingSec(next);
|
||||
AsyncStorage.setItem(MAX_RECORDING_STORAGE_KEY, String(next));
|
||||
}}
|
||||
disabled={maxRecordingSec >= MAX_RECORDING_MAX_SEC}
|
||||
>
|
||||
<Text style={styles.prerollButtonText}>+1m</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* === Wake-Word (komplett on-device, openWakeWord) === */}
|
||||
|
||||
@@ -145,7 +145,24 @@ async function loadVadSilenceMs(): Promise<number> {
|
||||
|
||||
// Max-Dauer einer Aufnahme (Notbremse gegen Runaway-Loops). Auf 2 Minuten
|
||||
// hochgezogen damit auch laengere Erklaerungen durchgehen.
|
||||
const MAX_RECORDING_MS = 120000;
|
||||
// Default 5 Minuten — konfigurierbar in den App-Settings (1-30 Minuten).
|
||||
export const MAX_RECORDING_DEFAULT_SEC = 300;
|
||||
export const MAX_RECORDING_MIN_SEC = 60;
|
||||
export const MAX_RECORDING_MAX_SEC = 1800;
|
||||
export const MAX_RECORDING_STORAGE_KEY = 'aria_max_recording_sec';
|
||||
|
||||
export async function loadMaxRecordingMs(): Promise<number> {
|
||||
try {
|
||||
const raw = await AsyncStorage.getItem(MAX_RECORDING_STORAGE_KEY);
|
||||
if (raw != null) {
|
||||
const n = parseFloat(raw);
|
||||
if (isFinite(n) && n >= MAX_RECORDING_MIN_SEC && n <= MAX_RECORDING_MAX_SEC) {
|
||||
return Math.round(n * 1000);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return MAX_RECORDING_DEFAULT_SEC * 1000;
|
||||
}
|
||||
|
||||
// Pre-Roll: Wie lange Audio im AudioTrack-Buffer liegt bevor play() startet.
|
||||
// Einstellbar via Diagnostic/Settings (Key: aria_tts_preroll_sec).
|
||||
@@ -440,18 +457,19 @@ class AudioService {
|
||||
};
|
||||
if (autoStop) {
|
||||
const vadSilenceMs = await loadVadSilenceMs();
|
||||
const maxRecordingMs = await loadMaxRecordingMs();
|
||||
console.log('[Audio] startRecording: autoStop=true, VAD-Stille=%dms, MAX=%dms',
|
||||
vadSilenceMs, MAX_RECORDING_MS);
|
||||
vadSilenceMs, maxRecordingMs);
|
||||
this.vadTimer = setInterval(() => {
|
||||
const silenceDuration = Date.now() - this.lastSpeechTime;
|
||||
if (silenceDuration >= vadSilenceMs) {
|
||||
fireSilenceOnce(`VAD ${silenceDuration}ms Stille (Schwelle=${vadSilenceMs}ms)`);
|
||||
}
|
||||
}, 200);
|
||||
// Notbremse: Nach MAX_RECORDING_MS zwangsweise stoppen
|
||||
// Notbremse: Nach maxRecordingMs zwangsweise stoppen
|
||||
this.maxDurationTimer = setTimeout(() => {
|
||||
fireSilenceOnce(`Max-Dauer ${MAX_RECORDING_MS}ms`);
|
||||
}, MAX_RECORDING_MS);
|
||||
fireSilenceOnce(`Max-Dauer ${maxRecordingMs}ms`);
|
||||
}, maxRecordingMs);
|
||||
}
|
||||
|
||||
// Conversation-Window: Wenn der User innerhalb noSpeechTimeoutMs nicht
|
||||
|
||||
Reference in New Issue
Block a user