diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index d6d1c84..d46222b 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -50,7 +50,7 @@ import VoiceButton from '../components/VoiceButton'; import FileUpload, { FileData } from '../components/FileUpload'; import CameraUpload, { PhotoData } from '../components/CameraUpload'; 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'; // --- Typen --- @@ -93,6 +93,9 @@ interface ChatMessage { * gespiegelt damit wir die EXAKT richtige Placeholder-Bubble ersetzen, * auch wenn mehrere Aufnahmen parallel offen sind. */ 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 */ skillCreated?: { name: string; @@ -184,6 +187,14 @@ function stripSystemHints(text: string): string { } 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 STORAGE_PATH_KEY = 'aria_attachment_storage_path'; @@ -1668,7 +1679,9 @@ const ChatScreen: React.FC = () => { location: location || null, noSpeechTimeoutMs: windowMs, 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, }); 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()) { console.log('[Chat] STT-Endpoint: %r (reason=%s, %dms, %.1fs Audio)', 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 // zu 'conversing' damit der normale Flow greift (TTS, resume, etc.) if (wakeWordService.getState() === 'listening') { @@ -1771,7 +1791,8 @@ const ChatScreen: React.FC = () => { location: location || null, noSpeechTimeoutMs: windowMs, endpointMs: await loadSttEndpointMs(), - hardCapMs: 60000, + // Notbremse 5 min (s.o.) — lange Diktate nicht bei 1 min abschneiden. + hardCapMs: await loadMaxRecordingMs(), projectId: focusedProjectIdRef.current, }); if (ok) { @@ -2255,7 +2276,7 @@ const ChatScreen: React.FC = () => { // die Session auch app-seitig haben wir +2s Toleranz. noSpeechTimeoutMs: 0, endpointMs: await loadSttEndpointMs(), - hardCapMs: 300000, + hardCapMs: await loadMaxRecordingMs(), projectId: focusedProjectIdRef.current, }); if (!ok) { @@ -2647,6 +2668,16 @@ const ChatScreen: React.FC = () => { {att.serverPath ? '(tippen zum Laden)' : '(nicht verfuegbar)'} + ) : att.type === 'audio' ? ( + + {'🎙'} + + {att.name || 'Sprachaufnahme'} + + {typeof item.durationS === 'number' && item.durationS > 0 ? ( + {formatDur(item.durationS)} + ) : null} + ) : (