Mobile Voice - Tích hợp tính năng Ghi âm & Xử lý giọng nói

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).

Mobile Voice - Tích hợp tính năng Ghi âm & Xử lý giọng nói.jpg

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​


  1. Mở lại Terminal, nếu đang chạy Expo thì bấm r để reload. Nếu chưa thì npx expo start.
  2. Mở App trên điện thoại.
  3. Sẽ có một popup hiện lên hỏi: "Cho phép Jarvis truy cập Microphone?" -> Chọn Allow.
  4. 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 đỏ.
  5. Nói to: "Jarvis, mấy giờ rồi?"
  6. Thả tay ra.
  7. Chờ 1-2 giây...
  8. 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.
 
Back
Top