Nguyen5 commited on
Commit
bca3e7a
·
1 Parent(s): 921fc8a
.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
  ]