commit
Browse files- .trae/documents/Triển khai OpenAI Audio API + Audiomodus live cho chatbot.md +67 -0
- app.py +1 -14
- realtime_server.py +97 -0
- requirements.txt +4 -0
- speech_io.py +0 -1
.trae/documents/Triển khai OpenAI Audio API + Audiomodus live cho chatbot.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## Lựa chọn nền tảng
|
| 2 |
+
- Chọn OpenAI làm nền tảng chính cho Audio API (Whisper-1 và Realtime API) vì:
|
| 3 |
+
- Độ chính xác đa ngôn ngữ cao, ổn định
|
| 4 |
+
- SDK Python đơn giản, tương thích với hệ thống đang dùng OpenAI Embeddings/LLM/Chat
|
| 5 |
+
- Có Realtime API cho khả năng hội thoại live hai chiều
|
| 6 |
+
|
| 7 |
+
## Kiến trúc tổng quan
|
| 8 |
+
- Tầng Audio API:
|
| 9 |
+
- Transcribe: OpenAI Whisper (`audio.transcriptions.create`, model `whisper-1`) – xử lý file WAV từ Gradio
|
| 10 |
+
- Audiomodus (live): Gradio streaming + VAD để phát hiện nói, auto gửi transcript vào chat; tùy chọn tích hợp OpenAI Realtime API cho streaming real-time
|
| 11 |
+
- Tầng Chatbot/RAG giữ nguyên; thêm state điều phối audio: `is_listening`, `status_text`, `last_record_path`
|
| 12 |
+
|
| 13 |
+
## Files sẽ chỉnh sửa
|
| 14 |
+
- `app.py`
|
| 15 |
+
- Thêm tuỳ chọn Audiomodus (live): streaming callback, VAD indicator, auto send transcript
|
| 16 |
+
- Tạo state quản lý hội thoại và trạng thái ghi âm
|
| 17 |
+
- UI: thanh nhập pill trong khung chat, mic icon, toggle Audiomodus/Text, trạng thái rõ ràng
|
| 18 |
+
- `speech_io.py`
|
| 19 |
+
- Thêm `transcribe_with_openai(audio_path, language)` dùng Whisper-1
|
| 20 |
+
- Giữ `transcribe_audio` (local) để fallback
|
| 21 |
+
- VAD đơn giản (`detect_voice_activity`) để hands‑free
|
| 22 |
+
- `requirements.txt`
|
| 23 |
+
- Đảm bảo có `openai`
|
| 24 |
+
|
| 25 |
+
## Tools/Function calls
|
| 26 |
+
- Có sử dụng function calls nội bộ:
|
| 27 |
+
- `transcribe_with_openai(audio_path, language)` – gửi WAV lên OpenAI, trả `text`
|
| 28 |
+
- `detect_voice_activity(audio_data, sr, threshold)` – quyết định khi nào gửi transcript
|
| 29 |
+
- `transcribe_audio_optimized(audio_path, language)` – router backend theo ENV (ưu tiên OpenAI)
|
| 30 |
+
- Tùy chọn Realtime API (phase 2):
|
| 31 |
+
- WebRTC/WebSocket client bên trình duyệt (Gradio JS hook) để stream audio tới OpenAI Realtime
|
| 32 |
+
- Python server relay (nếu cần) để giữ khóa API an toàn
|
| 33 |
+
|
| 34 |
+
## Các bước triển khai
|
| 35 |
+
1. `speech_io.py`:
|
| 36 |
+
- Thêm `OPENAI_API_KEY`; viết `transcribe_with_openai(...)` dùng `OpenAI().audio.transcriptions.create(model="whisper-1")`
|
| 37 |
+
- Cải thiện tiền xử lý: high‑pass, normalize, mono, resample 16kHz; tăng `ASR_MAX_DURATION_S`
|
| 38 |
+
- VAD đơn giản: tính RMS/peak + frame energy để phát hiện nói
|
| 39 |
+
2. `app.py`:
|
| 40 |
+
- State `ConversationState` và UI control (Audiomodus toggle, status, VAD indicator)
|
| 41 |
+
- `chat_audio.stream/change` điền transcript vào `chat_text` và chain gọi chat để gửi tự động
|
| 42 |
+
- Hiển thị “Gesprochener Text wird gesendet” và player bản ghi
|
| 43 |
+
3. ENV & cấu hình:
|
| 44 |
+
- `OPENAI_API_KEY`, `ASR_LANGUAGE=auto|de|en|vi`, `ASR_MAX_DURATION_S`
|
| 45 |
+
4. Tùy chọn Realtime API (phase 2):
|
| 46 |
+
- Thêm triển khai WebRTC client; server relay để giữ an toàn API key
|
| 47 |
+
|
| 48 |
+
## Kiểm thử
|
| 49 |
+
- Test cases:
|
| 50 |
+
- Happy: câu nói 5–15s, tiếng Đức/Anh/Việt, transcript chính xác và gửi thẳng vào chat
|
| 51 |
+
- Error: API key thiếu/sai, file rỗng, tiếng nói quá nhỏ, VAD không phát hiện – không crash, có thông báo
|
| 52 |
+
- Streaming: transcript điền dần, tự gửi khi kết thúc nói
|
| 53 |
+
- Metrics:
|
| 54 |
+
- Latency end‑to‑end (kết thúc nói → có câu trả lời)
|
| 55 |
+
- WER/char‑accuracy ước lượng (mẫu test nội bộ)
|
| 56 |
+
- Tỷ lệ no‑speech/mishear
|
| 57 |
+
- Sử dụng CPU/RAM khi local fallback
|
| 58 |
+
|
| 59 |
+
## Bảo mật và mở rộng
|
| 60 |
+
- API key đọc từ ENV, không log dữ liệu âm thanh
|
| 61 |
+
- Cho phép xóa bản ghi khỏi server sau khi dùng (UI nút xoá)
|
| 62 |
+
- Dễ mở rộng Realtime API và thêm TTS trả lời nếu bật
|
| 63 |
+
|
| 64 |
+
## Deliverables
|
| 65 |
+
- Code cập nhật ở `app.py`, `speech_io.py`, `requirements.txt`
|
| 66 |
+
- UI Audiomodus live với VAD indicator, auto‑send transcript
|
| 67 |
+
- Hướng dẫn cấu hình ENV và test nhanh
|
app.py
CHANGED
|
@@ -699,13 +699,6 @@ with gr.Blocks(title="Prüfungsrechts-Chatbot (RAG + Sprache) - Enhanced") as de
|
|
| 699 |
on_audio_change,
|
| 700 |
inputs=[chat_audio, vad_toggle],
|
| 701 |
outputs=[chat_text, vad_indicator, status_display]
|
| 702 |
-
).then(
|
| 703 |
-
process_chat,
|
| 704 |
-
inputs=[chat_text, chat_audio, chatbot, lang_selector, vad_toggle],
|
| 705 |
-
outputs=[chatbot, chat_text, chat_audio, status_display]
|
| 706 |
-
).then(
|
| 707 |
-
lambda: update_vad_indicator(),
|
| 708 |
-
outputs=[vad_indicator]
|
| 709 |
)
|
| 710 |
|
| 711 |
# Audio Streaming
|
|
@@ -713,13 +706,6 @@ with gr.Blocks(title="Prüfungsrechts-Chatbot (RAG + Sprache) - Enhanced") as de
|
|
| 713 |
on_audio_change,
|
| 714 |
inputs=[chat_audio, vad_toggle],
|
| 715 |
outputs=[chat_text, vad_indicator, status_display]
|
| 716 |
-
).then(
|
| 717 |
-
process_chat,
|
| 718 |
-
inputs=[chat_text, chat_audio, chatbot, lang_selector, vad_toggle],
|
| 719 |
-
outputs=[chatbot, chat_text, chat_audio, status_display]
|
| 720 |
-
).then(
|
| 721 |
-
lambda: update_vad_indicator(),
|
| 722 |
-
outputs=[vad_indicator]
|
| 723 |
)
|
| 724 |
|
| 725 |
# TTS Button
|
|
@@ -744,3 +730,4 @@ with gr.Blocks(title="Prüfungsrechts-Chatbot (RAG + Sprache) - Enhanced") as de
|
|
| 744 |
|
| 745 |
if __name__ == "__main__":
|
| 746 |
demo.queue().launch(ssr_mode=False, show_error=True)
|
|
|
|
|
|
| 699 |
on_audio_change,
|
| 700 |
inputs=[chat_audio, vad_toggle],
|
| 701 |
outputs=[chat_text, vad_indicator, status_display]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 702 |
)
|
| 703 |
|
| 704 |
# Audio Streaming
|
|
|
|
| 706 |
on_audio_change,
|
| 707 |
inputs=[chat_audio, vad_toggle],
|
| 708 |
outputs=[chat_text, vad_indicator, status_display]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 709 |
)
|
| 710 |
|
| 711 |
# TTS Button
|
|
|
|
| 730 |
|
| 731 |
if __name__ == "__main__":
|
| 732 |
demo.queue().launch(ssr_mode=False, show_error=True)
|
| 733 |
+
|
realtime_server.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
realtime_server.py — v0.1 (2025-12-08)
|
| 3 |
+
|
| 4 |
+
Realtime signaling & streaming server (WebSocket-based) for live audio chat.
|
| 5 |
+
This module is optional and preserves backward compatibility with existing
|
| 6 |
+
Gradio UI. When enabled, clients can stream microphone audio chunks to
|
| 7 |
+
`/ws` and receive live transcripts (OpenAI Whisper API) and bot replies.
|
| 8 |
+
|
| 9 |
+
NOTE: A full WebRTC peer-to-peer relay with SDP/ICE is scaffolded via
|
| 10 |
+
`/webrtc/offer` but returns 501 until the upstream Realtime API is wired.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import os
|
| 14 |
+
import asyncio
|
| 15 |
+
import json
|
| 16 |
+
from typing import Optional
|
| 17 |
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
| 18 |
+
from fastapi.responses import JSONResponse
|
| 19 |
+
|
| 20 |
+
# Minimal import guard for OpenAI
|
| 21 |
+
try:
|
| 22 |
+
from openai import OpenAI
|
| 23 |
+
OPENAI_AVAILABLE = True
|
| 24 |
+
except Exception:
|
| 25 |
+
OPENAI_AVAILABLE = False
|
| 26 |
+
|
| 27 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
|
| 28 |
+
|
| 29 |
+
app = FastAPI()
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _openai_transcribe_file(path: str, language: Optional[str] = None) -> str:
|
| 33 |
+
"""Transcribe a local WAV chunk via OpenAI Whisper-1.
|
| 34 |
+
Returns empty string on failure to keep the stream resilient."""
|
| 35 |
+
if not (OPENAI_AVAILABLE and OPENAI_API_KEY and path and os.path.exists(path)):
|
| 36 |
+
return ""
|
| 37 |
+
try:
|
| 38 |
+
client = OpenAI(api_key=OPENAI_API_KEY)
|
| 39 |
+
with open(path, "rb") as f:
|
| 40 |
+
resp = client.audio.transcriptions.create(
|
| 41 |
+
model="whisper-1",
|
| 42 |
+
file=f,
|
| 43 |
+
language=language if language and language != "auto" else None,
|
| 44 |
+
)
|
| 45 |
+
txt = getattr(resp, "text", "") or (resp.get("text") if isinstance(resp, dict) else "")
|
| 46 |
+
return (txt or "").strip()
|
| 47 |
+
except Exception:
|
| 48 |
+
return ""
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@app.get("/health")
|
| 52 |
+
async def health():
|
| 53 |
+
"""Basic health endpoint."""
|
| 54 |
+
return JSONResponse({"status": "ok"})
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@app.post("/webrtc/offer")
|
| 58 |
+
async def webrtc_offer(body: dict):
|
| 59 |
+
"""SDP offer scaffold (not fully implemented).
|
| 60 |
+
Returns 501 until Realtime API relay is wired (to keep backward compatibility)."""
|
| 61 |
+
return JSONResponse({"error": "not_implemented"}, status_code=501)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@app.websocket("/ws")
|
| 65 |
+
async def ws_stream(ws: WebSocket):
|
| 66 |
+
"""WebSocket bidirectional streaming.
|
| 67 |
+
Client sends JSON frames:
|
| 68 |
+
{"type":"audio_chunk","path":"/tmp/chunk.wav","lang":"de"}
|
| 69 |
+
Server responds with transcript frames:
|
| 70 |
+
{"type":"transcript","text":"..."}
|
| 71 |
+
and bot reply frames (if desired in future).
|
| 72 |
+
"""
|
| 73 |
+
await ws.accept()
|
| 74 |
+
try:
|
| 75 |
+
while True:
|
| 76 |
+
raw = await ws.receive_text()
|
| 77 |
+
try:
|
| 78 |
+
msg = json.loads(raw)
|
| 79 |
+
except Exception:
|
| 80 |
+
await ws.send_text(json.dumps({"type": "error", "message": "invalid_json"}))
|
| 81 |
+
continue
|
| 82 |
+
|
| 83 |
+
if msg.get("type") == "audio_chunk":
|
| 84 |
+
path = msg.get("path")
|
| 85 |
+
lang = msg.get("lang")
|
| 86 |
+
text = _openai_transcribe_file(path, language=lang)
|
| 87 |
+
await ws.send_text(json.dumps({"type": "transcript", "text": text}))
|
| 88 |
+
else:
|
| 89 |
+
await ws.send_text(json.dumps({"type": "error", "message": "unknown_type"}))
|
| 90 |
+
except WebSocketDisconnect:
|
| 91 |
+
pass
|
| 92 |
+
except Exception:
|
| 93 |
+
try:
|
| 94 |
+
await ws.close()
|
| 95 |
+
except Exception:
|
| 96 |
+
pass
|
| 97 |
+
|
requirements.txt
CHANGED
|
@@ -15,6 +15,10 @@ langchain-text-splitters
|
|
| 15 |
langchain-openai
|
| 16 |
huggingface-hub
|
| 17 |
groq
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
# === VectorStore ===
|
| 20 |
faiss-cpu
|
|
|
|
| 15 |
langchain-openai
|
| 16 |
huggingface-hub
|
| 17 |
groq
|
| 18 |
+
google-generativeai
|
| 19 |
+
fastapi
|
| 20 |
+
uvicorn
|
| 21 |
+
websockets
|
| 22 |
|
| 23 |
# === VectorStore ===
|
| 24 |
faiss-cpu
|
speech_io.py
CHANGED
|
@@ -517,4 +517,3 @@ __all__ = [
|
|
| 517 |
'normalize_audio',
|
| 518 |
'preprocess_audio_for_vad'
|
| 519 |
]
|
| 520 |
-
|
|
|
|
| 517 |
'normalize_audio',
|
| 518 |
'preprocess_audio_for_vad'
|
| 519 |
]
|
|
|