si
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Audio, InterruptionModeAndroid, InterruptionModeIOS } from "expo-av";
|
||||
import * as Speech from "expo-speech";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import Svg, { Path } from "react-native-svg";
|
||||
@@ -58,6 +59,8 @@ export default function RecorderScreen() {
|
||||
const [recordingMs, setRecordingMs] = useState(0);
|
||||
const [statusMessage, setStatusMessage] = useState("");
|
||||
const [responsePreview, setResponsePreview] = useState("");
|
||||
const [llmResponseText, setLlmResponseText] = useState("");
|
||||
const [transcriptionText, setTranscriptionText] = useState("");
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [isHolding, setIsHolding] = useState(false);
|
||||
const recordingRef = useRef<Audio.Recording | null>(null);
|
||||
@@ -120,9 +123,60 @@ export default function RecorderScreen() {
|
||||
};
|
||||
}, [recording]);
|
||||
|
||||
async function speak(text: string) {
|
||||
if (!text || !text.trim()) {
|
||||
console.log("[TTS] Skipping empty text");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[TTS] ===== START speak =====");
|
||||
Speech.stop();
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
||||
try {
|
||||
await Audio.setAudioModeAsync({
|
||||
allowsRecordingIOS: false,
|
||||
playsInSilentModeIOS: true,
|
||||
interruptionModeAndroid: InterruptionModeAndroid.DoNotMix,
|
||||
interruptionModeIOS: InterruptionModeIOS.DoNotMix,
|
||||
shouldDuckAndroid: true,
|
||||
staysActiveInBackground: false,
|
||||
});
|
||||
console.log("[TTS] Audio mode reset OK");
|
||||
} catch (err) {
|
||||
console.log("[TTS] Audio mode error:", err);
|
||||
}
|
||||
|
||||
const lang = locale === "ca" ? "ca-ES" : "en-US";
|
||||
await new Promise((r) => setTimeout(r, 800));
|
||||
|
||||
console.log("[TTS] Calling Speech.speak. Text length:", text.length, "Lang:", lang);
|
||||
try {
|
||||
Speech.speak(text, {
|
||||
language: lang,
|
||||
onDone: () => console.log("[TTS] ✅ onDone fired"),
|
||||
onError: (error) => console.log("[TTS] ❌ onError:", error),
|
||||
});
|
||||
console.log("[TTS] Speech.speak() call returned OK");
|
||||
} catch (err) {
|
||||
console.log("[TTS] Speech.speak() threw:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function speakSequentially(texts: string[]) {
|
||||
if (texts.length === 0) return;
|
||||
for (let i = 0; i < texts.length; i++) {
|
||||
await speak(texts[i]);
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
}
|
||||
}
|
||||
|
||||
async function startRecording() {
|
||||
try {
|
||||
Speech.stop();
|
||||
setTranscriptionText("");
|
||||
setResponsePreview("");
|
||||
setLlmResponseText("");
|
||||
setRecordingUri(null);
|
||||
|
||||
const permission = await Audio.requestPermissionsAsync();
|
||||
@@ -167,6 +221,8 @@ export default function RecorderScreen() {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[APP] stopRecordingAndUpload called");
|
||||
|
||||
try {
|
||||
const activeRecording = recordingRef.current;
|
||||
const currentStatus = await activeRecording.getStatusAsync();
|
||||
@@ -220,12 +276,40 @@ export default function RecorderScreen() {
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
setResponsePreview(responseText.slice(0, 400));
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.status}. ${responseText}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(responseText);
|
||||
setResponsePreview(responseText.slice(0, 400));
|
||||
|
||||
const textsToSpeak: string[] = [];
|
||||
|
||||
if (data.transcription) {
|
||||
setTranscriptionText(data.transcription);
|
||||
textsToSpeak.push(data.transcription);
|
||||
}
|
||||
|
||||
if (data.llmResponse) {
|
||||
setLlmResponseText(data.llmResponse);
|
||||
textsToSpeak.push(data.llmResponse);
|
||||
}
|
||||
|
||||
if (textsToSpeak.length > 0) {
|
||||
setStatusMessage(strings.voiceMessageSent + ". " + strings.playing);
|
||||
void speakSequentially(textsToSpeak);
|
||||
} else {
|
||||
setLlmResponseText("");
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.log("[APP] JSON parse failed:", parseError, "Response was:", responseText.substring(0, 200));
|
||||
setResponsePreview(responseText.slice(0, 400));
|
||||
setTranscriptionText("");
|
||||
setLlmResponseText("");
|
||||
}
|
||||
|
||||
setStatusMessage(strings.voiceMessageSent);
|
||||
} catch (error) {
|
||||
setStatusMessage(strings.uploadFailed);
|
||||
@@ -283,7 +367,10 @@ export default function RecorderScreen() {
|
||||
try {
|
||||
setIsUploading(true);
|
||||
setStatusMessage(strings.uploadingRecording);
|
||||
Speech.stop();
|
||||
setTranscriptionText("");
|
||||
setResponsePreview("");
|
||||
setLlmResponseText("");
|
||||
|
||||
const mimeType = buildMimeType(targetUri);
|
||||
const extension = buildFileExtension(targetUri);
|
||||
@@ -308,12 +395,39 @@ export default function RecorderScreen() {
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
setResponsePreview(responseText.slice(0, 400));
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.status}. ${responseText}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(responseText);
|
||||
setResponsePreview(responseText.slice(0, 400));
|
||||
|
||||
const textsToSpeak: string[] = [];
|
||||
|
||||
if (data.transcription) {
|
||||
setTranscriptionText(data.transcription);
|
||||
textsToSpeak.push(data.transcription);
|
||||
}
|
||||
|
||||
if (data.llmResponse) {
|
||||
setLlmResponseText(data.llmResponse);
|
||||
textsToSpeak.push(data.llmResponse);
|
||||
}
|
||||
|
||||
if (textsToSpeak.length > 0) {
|
||||
setStatusMessage(strings.voiceMessageSent + ". " + strings.playing);
|
||||
void speakSequentially(textsToSpeak);
|
||||
} else {
|
||||
setLlmResponseText("");
|
||||
}
|
||||
} catch {
|
||||
setResponsePreview(responseText.slice(0, 400));
|
||||
setTranscriptionText("");
|
||||
setLlmResponseText("");
|
||||
}
|
||||
|
||||
setStatusMessage(strings.uploadComplete);
|
||||
} catch (error) {
|
||||
setStatusMessage(strings.uploadFailed);
|
||||
@@ -326,6 +440,11 @@ export default function RecorderScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleSpeak() {
|
||||
const texts = [transcriptionText, llmResponseText].filter(Boolean);
|
||||
void speakSequentially(texts);
|
||||
}
|
||||
|
||||
const releaseLabel = t("releaseToStop", locale);
|
||||
const holdLabel = t("holdToRecord", locale);
|
||||
const openSettingsLabel = t("openSettingsHint", locale);
|
||||
@@ -419,7 +538,30 @@ export default function RecorderScreen() {
|
||||
: openSettingsLabel}
|
||||
</Text>
|
||||
|
||||
{responsePreview ? (
|
||||
{transcriptionText ? (
|
||||
<View style={styles.transcriptionBox}>
|
||||
<Text style={styles.transcriptionLabel}>{strings.yourMessage}</Text>
|
||||
<Text style={styles.transcriptionText}>{transcriptionText}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{llmResponseText ? (
|
||||
<View style={styles.llmResponseBox}>
|
||||
<View style={styles.llmResponseHeader}>
|
||||
<Text style={styles.llmResponseLabel}>{strings.aiReply}</Text>
|
||||
<Pressable onPress={handleSpeak} style={styles.speakButton}>
|
||||
<Svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||
<Path d="M11 5L6 9H2v6h4l5 4V5z" fill="#13304a" />
|
||||
<Path d="M15.5 8.5a5.5 5.5 0 0 1 0 7" stroke="#13304a" strokeWidth="2" strokeLinecap="round" />
|
||||
<Path d="M18.5 5.5a9 9 0 0 1 0 13" stroke="#13304a" strokeWidth="2" strokeLinecap="round" />
|
||||
</Svg>
|
||||
</Pressable>
|
||||
</View>
|
||||
<Text style={styles.llmResponseText}>{llmResponseText}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{responsePreview && !transcriptionText && !llmResponseText ? (
|
||||
<View style={styles.responseBox}>
|
||||
<Text style={styles.responseLabel}>{serverResponseLabel}</Text>
|
||||
<Text style={styles.responseText}>{responsePreview}</Text>
|
||||
@@ -557,4 +699,58 @@ const styles = StyleSheet.create({
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
},
|
||||
llmResponseBox: {
|
||||
backgroundColor: "#e8f4e8",
|
||||
borderRadius: 16,
|
||||
gap: 6,
|
||||
marginTop: 4,
|
||||
padding: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: "#b8d9b8",
|
||||
},
|
||||
llmResponseHeader: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
llmResponseLabel: {
|
||||
color: "#2a6a2a",
|
||||
fontSize: 13,
|
||||
fontWeight: "700",
|
||||
textTransform: "uppercase",
|
||||
textAlign: "center",
|
||||
},
|
||||
llmResponseText: {
|
||||
color: "#2d4a2d",
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
},
|
||||
speakButton: {
|
||||
backgroundColor: "#f7f0e0",
|
||||
borderRadius: 20,
|
||||
padding: 6,
|
||||
borderWidth: 1,
|
||||
borderColor: "#dccfb9",
|
||||
},
|
||||
transcriptionBox: {
|
||||
backgroundColor: "#e8ecf4",
|
||||
borderRadius: 16,
|
||||
gap: 6,
|
||||
marginTop: 4,
|
||||
padding: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: "#b8c9d9",
|
||||
},
|
||||
transcriptionLabel: {
|
||||
color: "#1a4a6a",
|
||||
fontSize: 13,
|
||||
fontWeight: "700",
|
||||
textTransform: "uppercase",
|
||||
textAlign: "center",
|
||||
},
|
||||
transcriptionText: {
|
||||
color: "#1f3a52",
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -38,6 +38,9 @@ export function en() {
|
||||
releaseToStop: "Release your finger to stop recording and send the audio.",
|
||||
holdToRecord: "Hold the microphone button to record a voice message. Release to send it immediately.",
|
||||
openSettingsHint: "Open settings to add your backend URL before sending voice messages.",
|
||||
aiReply: "Quibot reply",
|
||||
yourMessage: "Your message",
|
||||
playing: "Playing audio...",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,6 +82,9 @@ export function ca() {
|
||||
releaseToStop: "Allibera el dit per aturar l'enregistrament i enviar l'\u00e0udio.",
|
||||
holdToRecord: "Mant\u00e9s premut el micr\u00f2fon per enregistrar un missatge de veu. Allibera'l per enviar-lo immediatament.",
|
||||
openSettingsHint: "Obre la configuraci\u00f3 per afegir l'URL del servidor abans d'enviar missatges de veu.",
|
||||
aiReply: "Resposta del Quibot",
|
||||
yourMessage: "El teu missatge",
|
||||
playing: "Reproduint àudio...",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
2921
apk/package-lock.json
generated
2921
apk/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,10 +11,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-native-picker/picker": "^2.11.4",
|
||||
"expo": "~54.0.33",
|
||||
"@react-native-picker/picker": "2.11.1",
|
||||
"expo": "~54.0.35",
|
||||
"expo-av": "~16.0.8",
|
||||
"expo-router": "~6.0.23",
|
||||
"expo-router": "~6.0.24",
|
||||
"expo-speech": "~14.0.8",
|
||||
"expo-splash-screen": "~31.0.13",
|
||||
"expo-status-bar": "~3.0.9",
|
||||
"react": "19.1.0",
|
||||
@@ -22,7 +23,7 @@
|
||||
"react-native": "0.81.5",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-svg": "^15.15.5",
|
||||
"react-native-svg": "15.12.1",
|
||||
"react-native-web": "~0.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Reference in New Issue
Block a user