AI Hunter
Member
Hôm nay chúng ta sẽ code tính năng "bấm để nói". Hãy chuẩn bị tinh thần vì chúng ta sẽ phải đụng vào cả Backend (để nhận file) và Frontend (để ghi âm).
Server hiện tại chỉ biết nhận chữ (
Bước 1: Cài thư viện hỗ trợ upload
Mở terminal ở thư mục Backend, chạy:
Bước 2: Cập nhật server.py
Thêm endpoint mới để xử lý file âm thanh.
*Lưu ý: Để đơn giản và chính xác nhất, đoạn code dưới đây giả định bạn dùng API của OpenAI (Whisper) để chuyển âm thanh thành văn bản. Nếu máy bạn cực mạnh, có thể dùng Whisper Local.*
Khởi động lại Server:
Chúng ta cần thư viện
Bước 1: Cài đặt thư viện
Tại thư mục
Bước 2: Sửa file App.js
Thêm nút Micro to đùng và logic ghi âm.
Chúc mừng! Bạn vừa hoàn thiện mảnh ghép quan trọng nhất của giao diện Mobile.
Cảm giác Giữ nút -> Nói -> Thả tay -> Có kết quả nó "đã" hơn gõ phím gấp trăm lần đúng không?
Nhưng... vẫn còn thiếu một chút. Jarvis trả lời bằng chữ, và bạn vẫn phải nhìn màn hình để đọc. Sẽ tuyệt hơn nếu Jarvis cũng trả lời lại bằng giọng nói.
1. Nâng cấp Backend (FastAPI)
Server hiện tại chỉ biết nhận chữ (
string). Chúng ta phải dạy nó nhận file âm thanh (UploadFile).Bước 1: Cài thư viện hỗ trợ upload
Mở terminal ở thư mục Backend, chạy:
Mã:
pip install python-multipart
Bước 2: Cập nhật server.py
Thêm endpoint mới để xử lý file âm thanh.
*Lưu ý: Để đơn giản và chính xác nhất, đoạn code dưới đây giả định bạn dùng API của OpenAI (Whisper) để chuyển âm thanh thành văn bản. Nếu máy bạn cực mạnh, có thể dùng Whisper Local.*
Python:
# Thêm các import cần thiết
from fastapi import UploadFile, File
import shutil
import os
# ... (Các code cũ giữ nguyên) ...
# Endpoint xử lý giọng nói
@app.post("/voice-chat")
async def voice_chat_endpoint(file: UploadFile = File(...)):
try:
# 1. Lưu file âm thanh tạm thời
temp_filename = f"temp_{file.filename}"
with open(temp_filename, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
print(f"🎙️ Nhận file âm thanh: {temp_filename}")
# 2. Dùng OpenAI Whisper để chuyển Audio -> Text (STT)
# (Đảm bảo bạn đã set OPENAI_API_KEY trong biến môi trường)
with open(temp_filename, "rb") as audio_file:
transcript = chat_client.audio.transcriptions.create(
model="whisper-1",
file=audio_file,
language="vi" # Ưu tiên tiếng Việt
)
user_text = transcript.text
print(f"🗣️ Người dùng nói: {user_text}")
# 3. Xóa file tạm
os.remove(temp_filename)
# 4. Gửi văn bản vào não bộ xử lý như bình thường
# (Tái sử dụng logic của endpoint /chat cũ)
# ... COPY LOGIC GỌI LLM Ở ĐÂY HOẶC GỌI HÀM XỬ LÝ CHUNG ...
# Ví dụ gọi đơn giản:
response = chat_client.chat.completions.create(
model="gpt-4o-mini", # Hoặc model Llama bạn đang dùng
messages=[
{"role": "system", "content": "Bạn là Jarvis. Trả lời ngắn gọn."},
{"role": "user", "content": user_text}
]
)
bot_answer = response.choices[0].message.content
return {"user_text": user_text, "answer": bot_answer}
except Exception as e:
print(f"Lỗi Voice: {e}")
return {"answer": "Xin lỗi, tôi không nghe rõ."}
Khởi động lại Server:
docker-compose restart backend (hoặc chạy lại python script).2. Nâng cấp Frontend (Expo React Native)
Chúng ta cần thư viện
expo-av để ghi âm.Bước 1: Cài đặt thư viện
Tại thư mục
jarvis-mobile, chạy:
Mã:
npx expo install expo-av
Bước 2: Sửa file App.js
Thêm nút Micro to đùng và logic ghi âm.
JavaScript:
import React, { useState, useEffect } from 'react';
import { StyleSheet, Text, View, TextInput, TouchableOpacity, FlatList, SafeAreaView, ActivityIndicator, KeyboardAvoidingView, Platform, StatusBar, Alert } from 'react-native';
import axios from 'axios';
import { Audio } from 'expo-av'; // Import thư viện Audio
import { Mic, Send } from 'lucide-react-native'; // Icon
// --- CẤU HÌNH ---
const API_URL = "http://192.168.1.X:8000"; // Thay IP của bạn vào đây (KHÔNG thêm /chat)
const API_TOKEN = "sieumatkhau123456";
export default function App() {
const [messages, setMessages] = useState([{ id: '1', role: 'bot', text: 'Jarvis nghe rõ. Over.' }]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const [recording, setRecording] = useState(null); // State lưu đối tượng ghi âm
const [isRecording, setIsRecording] = useState(false); // Trạng thái đang ghi âm hay không
// Xin quyền Micro khi mở App
useEffect(() => {
(async () => {
const { status } = await Audio.requestPermissionsAsync();
if (status !== 'granted') {
Alert.alert('Lỗi', 'Cần cấp quyền Micro để nói chuyện với Jarvis!');
}
})();
}, []);
// --- HÀM GHI ÂM ---
async function startRecording() {
try {
console.log('Đang bắt đầu ghi âm...');
await Audio.setAudioModeAsync({
allowsRecordingIOS: true,
playsInSilentModeIOS: true,
});
const { recording } = await Audio.Recording.createAsync(
Audio.RecordingOptionsPresets.HIGH_QUALITY
);
setRecording(recording);
setIsRecording(true);
console.log('Đang ghi âm!');
} catch (err) {
console.error('Lỗi khởi động ghi âm:', err);
}
}
async function stopRecording() {
console.log('Dừng ghi âm..');
setRecording(undefined);
setIsRecording(false);
await recording.stopAndUnloadAsync();
const uri = recording.getURI();
console.log('File ghi âm tại:', uri);
// Gửi file lên server ngay lập tức
uploadAudio(uri);
}
// --- HÀM GỬI AUDIO ---
const uploadAudio = async (uri) => {
setLoading(true);
try {
// Tạo Form Data để gửi file
const formData = new FormData();
formData.append('file', {
uri: uri,
type: 'audio/m4a', // Expo mặc định ghi file m4a
name: 'voice_command.m4a',
});
// Gọi API /voice-chat
const res = await axios.post(`${API_URL}/voice-chat`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
'Authorization': `Bearer ${API_TOKEN}`
},
});
// Hiển thị kết quả
const userMsg = { id: Date.now().toString(), role: 'user', text: "🎤 " + res.data.user_text };
const botMsg = { id: (Date.now() + 1).toString(), role: 'bot', text: res.data.answer };
setMessages(prev => [...prev, userMsg, botMsg]);
} catch (error) {
console.error(error);
Alert.alert("Lỗi", "Không gửi được giọng nói.");
} finally {
setLoading(false);
}
};
// ... (Giữ nguyên hàm sendMessage bằng text và renderItem cũ) ...
// Để tiết kiệm chỗ hiển thị, mình chỉ viết phần thay đổi UI bên dưới
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" />
{/* ... Header và List Chat giữ nguyên ... */}
<FlatList
data={messages}
// ... (code cũ)
/>
{loading && <ActivityIndicator size="small" color="#00f0ff" />}
{/* KHU VỰC NHẬP LIỆU */}
<View style={styles.inputContainer}>
{/* Nút Text */}
<TextInput
style={styles.input}
placeholder="Nhập lệnh..."
placeholderTextColor="#555"
value={input}
onChangeText={setInput}
/>
{/* Nút Voice Logic: Nhấn giữ để nói, thả để gửi */}
<TouchableOpacity
style={[styles.micBtn, isRecording && styles.micBtnRecording]}
onPressIn={startRecording} // Chạm vào là ghi âm
onPressOut={stopRecording} // Thả tay ra là gửi
>
<Mic color={isRecording ? "white" : "black"} size={24} />
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
// ... (Copy lại styles cũ) ...
// Thêm style cho nút Mic
inputContainer: {
flexDirection: 'row',
padding: 15,
backgroundColor: '#050505',
alignItems: 'center'
},
input: {
flex: 1,
backgroundColor: '#111',
color: '#00f0ff',
borderWidth: 1,
borderColor: '#333',
borderRadius: 25,
paddingHorizontal: 15,
paddingVertical: 10,
marginRight: 10,
height: 50,
},
micBtn: {
backgroundColor: '#00f0ff', // Màu xanh bình thường
width: 50,
height: 50,
borderRadius: 25,
justifyContent: 'center',
alignItems: 'center',
},
micBtnRecording: {
backgroundColor: '#ff0000', // Màu đỏ khi đang ghi âm
borderWidth: 2,
borderColor: '#fff',
transform: [{ scale: 1.1 }] // Phóng to nhẹ
}
});
3. Trải nghiệm cảm giác làm Sếp
- Mở lại Terminal, nếu đang chạy Expo thì bấm
rđể reload. Nếu chưa thìnpx expo start. - Mở App trên điện thoại.
- Sẽ có một popup hiện lên hỏi: "Cho phép Jarvis truy cập Microphone?" -> Chọn Allow.
- Bấm và giữ cái nút tròn màu xanh ở góc phải. Nút sẽ chuyển sang Màu đỏ.
- Nói to: "Jarvis, mấy giờ rồi?"
- Thả tay ra.
- Chờ 1-2 giây...
- Trên màn hình sẽ hiện lên text bạn vừa nói và câu trả lời của Jarvis.
Tổng kết Tập 2
Chúc mừng! Bạn vừa hoàn thiện mảnh ghép quan trọng nhất của giao diện Mobile.
Cảm giác Giữ nút -> Nói -> Thả tay -> Có kết quả nó "đã" hơn gõ phím gấp trăm lần đúng không?
Nhưng... vẫn còn thiếu một chút. Jarvis trả lời bằng chữ, và bạn vẫn phải nhìn màn hình để đọc. Sẽ tuyệt hơn nếu Jarvis cũng trả lời lại bằng giọng nói.
Bài viết liên quan