si
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { Audio, InterruptionModeAndroid, InterruptionModeIOS } from "expo-av";
|
import { Audio, InterruptionModeAndroid, InterruptionModeIOS } from "expo-av";
|
||||||
|
import * as Speech from "expo-speech";
|
||||||
import { router, useFocusEffect } from "expo-router";
|
import { router, useFocusEffect } from "expo-router";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import Svg, { Path } from "react-native-svg";
|
import Svg, { Path } from "react-native-svg";
|
||||||
@@ -58,6 +59,8 @@ export default function RecorderScreen() {
|
|||||||
const [recordingMs, setRecordingMs] = useState(0);
|
const [recordingMs, setRecordingMs] = useState(0);
|
||||||
const [statusMessage, setStatusMessage] = useState("");
|
const [statusMessage, setStatusMessage] = useState("");
|
||||||
const [responsePreview, setResponsePreview] = useState("");
|
const [responsePreview, setResponsePreview] = useState("");
|
||||||
|
const [llmResponseText, setLlmResponseText] = useState("");
|
||||||
|
const [transcriptionText, setTranscriptionText] = useState("");
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [isHolding, setIsHolding] = useState(false);
|
const [isHolding, setIsHolding] = useState(false);
|
||||||
const recordingRef = useRef<Audio.Recording | null>(null);
|
const recordingRef = useRef<Audio.Recording | null>(null);
|
||||||
@@ -120,9 +123,60 @@ export default function RecorderScreen() {
|
|||||||
};
|
};
|
||||||
}, [recording]);
|
}, [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() {
|
async function startRecording() {
|
||||||
try {
|
try {
|
||||||
|
Speech.stop();
|
||||||
|
setTranscriptionText("");
|
||||||
setResponsePreview("");
|
setResponsePreview("");
|
||||||
|
setLlmResponseText("");
|
||||||
setRecordingUri(null);
|
setRecordingUri(null);
|
||||||
|
|
||||||
const permission = await Audio.requestPermissionsAsync();
|
const permission = await Audio.requestPermissionsAsync();
|
||||||
@@ -167,6 +221,8 @@ export default function RecorderScreen() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("[APP] stopRecordingAndUpload called");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const activeRecording = recordingRef.current;
|
const activeRecording = recordingRef.current;
|
||||||
const currentStatus = await activeRecording.getStatusAsync();
|
const currentStatus = await activeRecording.getStatusAsync();
|
||||||
@@ -220,12 +276,40 @@ export default function RecorderScreen() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const responseText = await response.text();
|
const responseText = await response.text();
|
||||||
setResponsePreview(responseText.slice(0, 400));
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`${response.status}. ${responseText}`);
|
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);
|
setStatusMessage(strings.voiceMessageSent);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatusMessage(strings.uploadFailed);
|
setStatusMessage(strings.uploadFailed);
|
||||||
@@ -283,7 +367,10 @@ export default function RecorderScreen() {
|
|||||||
try {
|
try {
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
setStatusMessage(strings.uploadingRecording);
|
setStatusMessage(strings.uploadingRecording);
|
||||||
|
Speech.stop();
|
||||||
|
setTranscriptionText("");
|
||||||
setResponsePreview("");
|
setResponsePreview("");
|
||||||
|
setLlmResponseText("");
|
||||||
|
|
||||||
const mimeType = buildMimeType(targetUri);
|
const mimeType = buildMimeType(targetUri);
|
||||||
const extension = buildFileExtension(targetUri);
|
const extension = buildFileExtension(targetUri);
|
||||||
@@ -308,12 +395,39 @@ export default function RecorderScreen() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const responseText = await response.text();
|
const responseText = await response.text();
|
||||||
setResponsePreview(responseText.slice(0, 400));
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`${response.status}. ${responseText}`);
|
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);
|
setStatusMessage(strings.uploadComplete);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatusMessage(strings.uploadFailed);
|
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 releaseLabel = t("releaseToStop", locale);
|
||||||
const holdLabel = t("holdToRecord", locale);
|
const holdLabel = t("holdToRecord", locale);
|
||||||
const openSettingsLabel = t("openSettingsHint", locale);
|
const openSettingsLabel = t("openSettingsHint", locale);
|
||||||
@@ -419,7 +538,30 @@ export default function RecorderScreen() {
|
|||||||
: openSettingsLabel}
|
: openSettingsLabel}
|
||||||
</Text>
|
</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}>
|
<View style={styles.responseBox}>
|
||||||
<Text style={styles.responseLabel}>{serverResponseLabel}</Text>
|
<Text style={styles.responseLabel}>{serverResponseLabel}</Text>
|
||||||
<Text style={styles.responseText}>{responsePreview}</Text>
|
<Text style={styles.responseText}>{responsePreview}</Text>
|
||||||
@@ -557,4 +699,58 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
lineHeight: 20,
|
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.",
|
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.",
|
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.",
|
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.",
|
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.",
|
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.",
|
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": {
|
"dependencies": {
|
||||||
"@react-native-async-storage/async-storage": "2.2.0",
|
"@react-native-async-storage/async-storage": "2.2.0",
|
||||||
"@react-native-picker/picker": "^2.11.4",
|
"@react-native-picker/picker": "2.11.1",
|
||||||
"expo": "~54.0.33",
|
"expo": "~54.0.35",
|
||||||
"expo-av": "~16.0.8",
|
"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-splash-screen": "~31.0.13",
|
||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
@@ -22,7 +23,7 @@
|
|||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.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"
|
"react-native-web": "~0.21.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
Ets la QuiBot, un robot femení que ajuda als nens a aprendre sobre quimica. Disposes de dos rodes i dos braços.
|
Ets la QuiBot, un robot femení que ajuda als nens a aprendre sobre quimica. Disposes de dos rodes i dos braços.
|
||||||
Has de ser educada i tenir perspectiva de gènere.
|
Has de ser educada i tenir perspectiva de gènere.
|
||||||
|
Les teves respostes han de ser curtes.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user