fix(voice): Hard-Cap an 'Max. Aufnahmedauer'-Setting + Dauer an Bubble

Wake-Word/Barge-In waren hart auf 60s gecappt (schnitt lange Diktate bei 1 min ab). Jetzt lesen sie loadMaxRecordingMs() — der bestehende, aber vom Streaming-Pfad abgeklemmte 'Maximale Aufnahmedauer'-Regler (1-30 min) steuert nun wirklich. Voice-Bubbles zeigen die Aufnahmedauer (durationS aus stt_endpoint) als 'M:SS'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 10:48:01 +02:00
co-authored by Claude Opus 4.8
parent 03021a6787
commit 73fee27e90
+35 -4
View File
@@ -50,7 +50,7 @@ import VoiceButton from '../components/VoiceButton';
import FileUpload, { FileData } from '../components/FileUpload'; import FileUpload, { FileData } from '../components/FileUpload';
import CameraUpload, { PhotoData } from '../components/CameraUpload'; import CameraUpload, { PhotoData } from '../components/CameraUpload';
import MessageText from '../components/MessageText'; import MessageText from '../components/MessageText';
import { loadConvWindowMs, loadTtsSpeed, TTS_SPEED_DEFAULT, loadSttEndpointMs } from '../services/audio'; import { loadConvWindowMs, loadTtsSpeed, TTS_SPEED_DEFAULT, loadSttEndpointMs, loadMaxRecordingMs } from '../services/audio';
import Geolocation from '@react-native-community/geolocation'; import Geolocation from '@react-native-community/geolocation';
// --- Typen --- // --- Typen ---
@@ -93,6 +93,9 @@ interface ChatMessage {
* gespiegelt damit wir die EXAKT richtige Placeholder-Bubble ersetzen, * gespiegelt damit wir die EXAKT richtige Placeholder-Bubble ersetzen,
* auch wenn mehrere Aufnahmen parallel offen sind. */ * auch wenn mehrere Aufnahmen parallel offen sind. */
audioRequestId?: string; audioRequestId?: string;
/** Laenge der Sprachaufnahme in Sekunden (aus dem stt_endpoint) — fuer die
* Dauer-Anzeige an der Voice-Bubble. */
durationS?: number;
/** Skill-Created-Bubble: ARIA hat einen neuen Skill angelegt */ /** Skill-Created-Bubble: ARIA hat einen neuen Skill angelegt */
skillCreated?: { skillCreated?: {
name: string; name: string;
@@ -184,6 +187,14 @@ function stripSystemHints(text: string): string {
} }
return out; return out;
} }
/** Sekunden → "M:SS" fuer die Sprachnachricht-Dauer. */
function formatDur(sec: number): string {
const s = Math.max(0, Math.round(sec));
const m = Math.floor(s / 60);
const r = s % 60;
return `${m}:${r.toString().padStart(2, '0')}`;
}
const DEFAULT_ATTACHMENT_DIR = `${RNFS.DocumentDirectoryPath}/chat_attachments`; const DEFAULT_ATTACHMENT_DIR = `${RNFS.DocumentDirectoryPath}/chat_attachments`;
const STORAGE_PATH_KEY = 'aria_attachment_storage_path'; const STORAGE_PATH_KEY = 'aria_attachment_storage_path';
@@ -1668,7 +1679,9 @@ const ChatScreen: React.FC = () => {
location: location || null, location: location || null,
noSpeechTimeoutMs: windowMs, noSpeechTimeoutMs: windowMs,
endpointMs: await loadSttEndpointMs(), endpointMs: await loadSttEndpointMs(),
hardCapMs: 60000, // Notbremse 5 min (nicht 1 min) — der Stille-Endpoint beendet normale
// Turns eh sofort; der Cap darf lange Diktate nicht mitten drin kappen.
hardCapMs: await loadMaxRecordingMs(),
projectId: focusedProjectIdRef.current, 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(()=>{});
@@ -1696,6 +1709,13 @@ const ChatScreen: React.FC = () => {
if (ev.text && ev.text.trim()) { if (ev.text && ev.text.trim()) {
console.log('[Chat] STT-Endpoint: %r (reason=%s, %dms, %.1fs Audio)', console.log('[Chat] STT-Endpoint: %r (reason=%s, %dms, %.1fs Audio)',
ev.text.slice(0, 80), ev.reason, ev.sttMs, ev.durationS); ev.text.slice(0, 80), ev.reason, ev.sttMs, ev.durationS);
// Aufnahme-Dauer an die passende Voice-Bubble haengen (Anzeige). Der
// spaetere STT-Text-Update spreadet die Message, die Dauer bleibt.
if (ev.audioRequestId && typeof ev.durationS === 'number' && ev.durationS > 0) {
const dur = ev.durationS;
setMessages(prev => prev.map(m =>
m.audioRequestId === ev.audioRequestId ? { ...m, durationS: dur } : m));
}
// Wenn passive lauschend: User hat tatsaechlich was gesagt → uebergang // Wenn passive lauschend: User hat tatsaechlich was gesagt → uebergang
// zu 'conversing' damit der normale Flow greift (TTS, resume, etc.) // zu 'conversing' damit der normale Flow greift (TTS, resume, etc.)
if (wakeWordService.getState() === 'listening') { if (wakeWordService.getState() === 'listening') {
@@ -1771,7 +1791,8 @@ const ChatScreen: React.FC = () => {
location: location || null, location: location || null,
noSpeechTimeoutMs: windowMs, noSpeechTimeoutMs: windowMs,
endpointMs: await loadSttEndpointMs(), endpointMs: await loadSttEndpointMs(),
hardCapMs: 60000, // Notbremse 5 min (s.o.) — lange Diktate nicht bei 1 min abschneiden.
hardCapMs: await loadMaxRecordingMs(),
projectId: focusedProjectIdRef.current, projectId: focusedProjectIdRef.current,
}); });
if (ok) { if (ok) {
@@ -2255,7 +2276,7 @@ const ChatScreen: React.FC = () => {
// die Session auch app-seitig haben wir +2s Toleranz. // die Session auch app-seitig haben wir +2s Toleranz.
noSpeechTimeoutMs: 0, noSpeechTimeoutMs: 0,
endpointMs: await loadSttEndpointMs(), endpointMs: await loadSttEndpointMs(),
hardCapMs: 300000, hardCapMs: await loadMaxRecordingMs(),
projectId: focusedProjectIdRef.current, projectId: focusedProjectIdRef.current,
}); });
if (!ok) { if (!ok) {
@@ -2647,6 +2668,16 @@ const ChatScreen: React.FC = () => {
{att.serverPath ? '(tippen zum Laden)' : '(nicht verfuegbar)'} {att.serverPath ? '(tippen zum Laden)' : '(nicht verfuegbar)'}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
) : att.type === 'audio' ? (
<View style={styles.attachmentFile}>
<Text style={styles.attachmentFileIcon}>{'🎙'}</Text>
<Text style={styles.attachmentFileName} numberOfLines={1}>
{att.name || 'Sprachaufnahme'}
</Text>
{typeof item.durationS === 'number' && item.durationS > 0 ? (
<Text style={styles.attachmentFileSize}>{formatDur(item.durationS)}</Text>
) : null}
</View>
) : ( ) : (
<TouchableOpacity <TouchableOpacity
style={styles.attachmentFile} style={styles.attachmentFile}