Nguyen5 commited on
Commit
99af630
·
1 Parent(s): ff0d3a4
Files changed (1) hide show
  1. app.py +143 -114
app.py CHANGED
@@ -1,5 +1,3 @@
1
- # app.py – Prüfungsrechts-Chatbot (RAG + Sprache, UI kiểu ChatGPT)
2
- #
3
  import os
4
  import gradio as gr
5
  from gradio_pdf import PDF
@@ -12,10 +10,11 @@ from llm import load_llm
12
  from rag_pipeline import answer
13
  from speech_io import transcribe_audio, synthesize_speech
14
 
15
- ASR_LANGUAGE_HINT = os.getenv("ASR_LANGUAGE", "de") # set to "auto" for detection, or e.g. "en"
 
16
 
17
  # =====================================================
18
- # INITIALISIERUNG (global)
19
  # =====================================================
20
 
21
  print("📚 Lade Dokumente…")
@@ -33,179 +32,209 @@ retriever = get_retriever(vs)
33
  print("🤖 Lade LLM…")
34
  llm = load_llm()
35
 
36
- # Dokument-Metadaten für UI
37
  pdf_meta = next(d.metadata for d in docs if d.metadata.get("type") == "pdf")
38
  hg_meta = next(d.metadata for d in docs if d.metadata.get("type") == "hg")
39
  hg_url = hg_meta.get("viewer_url")
40
 
 
41
  # =====================================================
42
- # Quellen formatieren – Markdown für Chat
43
  # =====================================================
44
  def format_sources(src):
45
  if not src:
46
  return ""
47
-
48
  out = ["", "## 📚 Quellen"]
49
-
50
  for s in src:
51
  line = f"- [{s['source']}]({s['url']})"
52
  if s.get("page") is not None:
53
  line += f" (Seite {s['page']})"
54
  out.append(line)
55
-
56
  return "\n".join(out)
57
 
58
 
59
  # =====================================================
60
- # CORE CHAT-FUNKTION (Text + separates Mikro-Audio)
61
  # =====================================================
62
  def chat_fn(text_input, audio_path, history):
63
- """
64
- text_input: Textbox-Inhalt (str)
65
- audio_path: Pfad zu WAV/FLAC vom Mikro (gr.Audio, type="filepath")
66
- history: Liste von OpenAI-ähnlichen Messages (role, content)
67
- """
68
  text = (text_input or "").strip()
69
 
70
- # Wenn Audio vorhanden: transkribieren
71
  if audio_path:
72
- spoken = transcribe_audio(audio_path, language=ASR_LANGUAGE_HINT)
73
  if text:
74
  text = (text + " " + spoken).strip()
75
  else:
76
  text = spoken
77
 
78
  if not text:
79
- # Nichts zu tun
80
  return history, None, "", None
81
 
82
- # 2) RAG-Antwort berechnen
83
  ans, sources = answer(text, retriever, llm)
84
  bot_msg = ans + format_sources(sources)
85
 
86
- # 3) History aktualisieren (ChatGPT-Style)
87
  history = history + [
88
  {"role": "user", "content": text},
89
  {"role": "assistant", "content": bot_msg},
90
  ]
91
 
92
- # 4) TTS für Antwort
93
  tts_audio = synthesize_speech(bot_msg)
94
 
95
- # 5) Input-Felder leeren
96
  return history, tts_audio, "", None
97
 
98
 
99
- # =====================================================
100
- # LAST ANSWER → TTS (für Button "Antwort erneut vorlesen")
101
- # =====================================================
102
  def read_last_answer(history):
103
  if not history:
104
  return None
105
-
106
  for msg in reversed(history):
107
- if msg.get("role") == "assistant":
108
- return synthesize_speech(msg.get("content", ""))
109
-
110
  return None
111
 
112
 
113
  # =====================================================
114
- # UI – GRADIO
115
  # =====================================================
116
- with gr.Blocks(title="Prüfungsrechts-Chatbot (RAG + Sprache)") as demo:
117
- # Leichtes Styling: zentriert, schmale Breite, kompakte Input-Zeile
118
- gr.HTML(
119
- """
120
- <style>
121
- html, body {height: auto !important; overflow-y: auto !important;}
122
- .gradio-container {max-width: 960px; margin: 0 auto; padding: 12px;}
123
- #chat-input-row {align-items: center; gap: 8px;}
124
- #chat-textbox textarea {min-height: 56px;}
125
- </style>
126
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  )
128
- gr.Markdown("# 🧑‍⚖️ Prüfungsrechts-Chatbot")
129
  gr.Markdown(
130
- "Dieser Chatbot beantwortet Fragen **ausschließlich** aus der "
131
- "Prüfungsordnung (PDF) und dem Hochschulgesetz NRW. "
132
- "Du kannst Text eingeben oder direkt ins Mikrofon sprechen."
133
  )
134
 
135
- # Einspaltiges Layout, alles untereinander (verhindert abgeschnittene Bereiche)
136
- with gr.Column():
137
- chatbot = gr.Chatbot(
138
- label="Chat",
139
- height=280,
140
- )
 
141
 
142
- # Audio-Ausgabe (TTS)
143
- voice_out = gr.Audio(label="Vorgelesene Antwort", type="numpy", interactive=False)
144
-
145
- # Eingabezeile à la ChatGPT: Text + Mikro + Senden
146
- with gr.Row(elem_id="chat-input-row"):
147
- chat_text = gr.Textbox(
148
- elem_id="chat-textbox",
149
- label=None,
150
- placeholder="Schreibe deine Frage oder klicke das Mikro und sprich. Enter sendet.",
151
- lines=2,
152
- max_lines=4,
153
- autofocus=True,
154
- scale=8,
155
- )
156
- chat_audio = gr.Audio(
157
- label="🎤",
158
- sources=["microphone"],
159
- type="filepath",
160
- interactive=True,
161
- scale=1,
162
- show_label=False,
163
- )
164
- send_btn = gr.Button("Senden", elem_classes=["compact-btn"], scale=1)
165
-
166
- # Senden bei Enter
167
- chat_text.submit(
168
- chat_fn,
169
- [chat_text, chat_audio, chatbot],
170
- [chatbot, voice_out, chat_text, chat_audio],
171
- )
172
- # Auto-submit sobald eine Aufnahme fertig ist (Text + Audio werden kombiniert)
173
- chat_audio.change(
174
- chat_fn,
175
- [chat_text, chat_audio, chatbot],
176
- [chatbot, voice_out, chat_text, chat_audio],
177
- )
178
- send_btn.click(
179
- chat_fn,
180
- [chat_text, chat_audio, chatbot],
181
- [chatbot, voice_out, chat_text, chat_audio],
182
- )
183
 
184
- # Button: Antwort erneut vorlesen
185
- read_btn = gr.Button("🔁 Antwort erneut vorlesen")
186
- read_btn.click(
187
- read_last_answer,
188
- [chatbot],
189
- [voice_out],
 
 
190
  )
191
-
192
- # Chat löschen
193
- clear_btn = gr.Button("Chat zurücksetzen")
194
- clear_btn.click(
195
- lambda: ([], None, "", None),
196
- None,
197
- [chatbot, voice_out, chat_text, chat_audio],
198
  )
 
199
 
200
- # Quellen & Dokumente kompakt unterhalb
201
- with gr.Accordion("Quellen & Dokumente", open=False):
202
- gr.Markdown("### 📄 Prüfungsordnung (PDF)")
203
- PDF(pdf_meta["pdf_url"], height=250)
204
- gr.Markdown("### 📘 Hochschulgesetz NRW")
205
- if isinstance(hg_url, str) and hg_url.startswith("http"):
206
- gr.Markdown(f"[Im Viewer öffnen]({hg_url})")
207
- else:
208
- gr.Markdown("Viewer-Link nicht verfügbar.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
 
210
  if __name__ == "__main__":
211
  demo.queue().launch(ssr_mode=False, show_error=True)
 
 
 
1
  import os
2
  import gradio as gr
3
  from gradio_pdf import PDF
 
10
  from rag_pipeline import answer
11
  from speech_io import transcribe_audio, synthesize_speech
12
 
13
+
14
+ ASR_LANGUAGE_HINT = os.getenv("ASR_LANGUAGE", "de")
15
 
16
  # =====================================================
17
+ # BACKEND INITIALIZATION
18
  # =====================================================
19
 
20
  print("📚 Lade Dokumente…")
 
32
  print("🤖 Lade LLM…")
33
  llm = load_llm()
34
 
 
35
  pdf_meta = next(d.metadata for d in docs if d.metadata.get("type") == "pdf")
36
  hg_meta = next(d.metadata for d in docs if d.metadata.get("type") == "hg")
37
  hg_url = hg_meta.get("viewer_url")
38
 
39
+
40
  # =====================================================
41
+ # Quellen formatieren
42
  # =====================================================
43
  def format_sources(src):
44
  if not src:
45
  return ""
 
46
  out = ["", "## 📚 Quellen"]
 
47
  for s in src:
48
  line = f"- [{s['source']}]({s['url']})"
49
  if s.get("page") is not None:
50
  line += f" (Seite {s['page']})"
51
  out.append(line)
 
52
  return "\n".join(out)
53
 
54
 
55
  # =====================================================
56
+ # Core Chat-Funktion
57
  # =====================================================
58
  def chat_fn(text_input, audio_path, history):
 
 
 
 
 
59
  text = (text_input or "").strip()
60
 
61
+ # Audio Text
62
  if audio_path:
63
+ spoken = transcribe_audio(audio_path)
64
  if text:
65
  text = (text + " " + spoken).strip()
66
  else:
67
  text = spoken
68
 
69
  if not text:
 
70
  return history, None, "", None
71
 
72
+ # RAG Antwort
73
  ans, sources = answer(text, retriever, llm)
74
  bot_msg = ans + format_sources(sources)
75
 
76
+ # Chat format wie ChatGPT
77
  history = history + [
78
  {"role": "user", "content": text},
79
  {"role": "assistant", "content": bot_msg},
80
  ]
81
 
 
82
  tts_audio = synthesize_speech(bot_msg)
83
 
 
84
  return history, tts_audio, "", None
85
 
86
 
 
 
 
87
  def read_last_answer(history):
88
  if not history:
89
  return None
 
90
  for msg in reversed(history):
91
+ if msg["role"] == "assistant":
92
+ return synthesize_speech(msg["content"])
 
93
  return None
94
 
95
 
96
  # =====================================================
97
+ # UI – ChatGPT-STYLE
98
  # =====================================================
99
+ CUSTOM_CSS = """
100
+ /* === GLOBAL BACKGROUND === */
101
+ body {
102
+ background-color: #f0f0f0 !important;
103
+ }
104
+
105
+ /* === CENTER CHAT AREA LIKE CHATGPT === */
106
+ .gradio-container {
107
+ max-width: 820px !important;
108
+ margin: 0 auto !important;
109
+ padding-bottom: 120px !important; /* Platz für Input-Bar */
110
+ }
111
+
112
+ /* === CHAT BUBBLES === */
113
+ .chatbot .message.user {
114
+ background-color: #ffffff !important;
115
+ color: #000 !important;
116
+ border-radius: 12px !important;
117
+ padding: 12px 14px !important;
118
+ max-width: 85% !important;
119
+ }
120
+
121
+ .chatbot .message.assistant {
122
+ background-color: #e6e6e6 !important;
123
+ color: #000 !important;
124
+ border-radius: 12px !important;
125
+ padding: 12px 14px !important;
126
+ max-width: 85% !important;
127
+ }
128
+
129
+ /* === INPUT BAR FIXED AT BOTTOM LIKE CHATGPT === */
130
+ #input-bar {
131
+ position: fixed !important;
132
+ bottom: 0;
133
+ left: 0;
134
+ right: 0;
135
+ background: #ffffff;
136
+ padding: 14px 20px;
137
+ box-shadow: 0 -2px 10px rgba(0,0,0,0.08);
138
+ display: flex;
139
+ gap: 10px;
140
+ align-items: center;
141
+ z-index: 999;
142
+ }
143
+
144
+ #input-text textarea {
145
+ min-height: 50px !important;
146
+ resize: none;
147
+ }
148
+
149
+ .send-btn {
150
+ background-color: #0b57d0 !important;
151
+ color: white !important;
152
+ border-radius: 8px !important;
153
+ height: 50px !important;
154
+ }
155
+
156
+ /* Microphone button round */
157
+ .mic-btn button {
158
+ border-radius: 50% !important;
159
+ width: 50px !important;
160
+ height: 50px !important;
161
+ }
162
+ """
163
+
164
+
165
+ with gr.Blocks(css=CUSTOM_CSS, title="Prüfungsrechts-Chatbot") as demo:
166
+
167
+ # Titel
168
+ gr.Markdown(
169
+ "<h1 style='text-align:center;'>🧑‍⚖️ Prüfungsrechts-Chatbot</h1>",
170
  )
 
171
  gr.Markdown(
172
+ "<p style='text-align:center;'>Fragen zur Prüfungsordnung & zum Hochschulgesetz NRW – wie ChatGPT, aber spezialisiert.</p>"
 
 
173
  )
174
 
175
+ # CHAT WINDOW (groß, wie ChatGPT)
176
+ chatbot = gr.Chatbot(
177
+ label="Chat",
178
+ elem_classes=["chatbot"],
179
+ height=600,
180
+ show_label=False,
181
+ )
182
 
183
+ voice_out = gr.Audio(
184
+ label="Vorgelesene Antwort",
185
+ type="numpy",
186
+ interactive=False,
187
+ visible=False,
188
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
+ # FIXED INPUT BAR = CHATGPT
191
+ with gr.Row(elem_id="input-bar"):
192
+ chat_text = gr.Textbox(
193
+ placeholder="Frag etwas oder nutze das Mikrofon…",
194
+ label=None,
195
+ lines=1,
196
+ elem_id="input-text",
197
+ scale=10,
198
  )
199
+ chat_audio = gr.Audio(
200
+ sources=["microphone"],
201
+ type="filepath",
202
+ label=None,
203
+ elem_classes=["mic-btn"],
204
+ scale=1,
 
205
  )
206
+ send_btn = gr.Button("Senden", elem_classes=["send-btn"], scale=2)
207
 
208
+ # Input logic
209
+ chat_text.submit(
210
+ chat_fn,
211
+ [chat_text, chat_audio, chatbot],
212
+ [chatbot, voice_out, chat_text, chat_audio],
213
+ )
214
+ chat_audio.change(
215
+ chat_fn,
216
+ [chat_text, chat_audio, chatbot],
217
+ [chatbot, voice_out, chat_text, chat_audio],
218
+ )
219
+ send_btn.click(
220
+ chat_fn,
221
+ [chat_text, chat_audio, chatbot],
222
+ [chatbot, voice_out, chat_text, chat_audio],
223
+ )
224
+
225
+ # Read button (optional)
226
+ read_btn = gr.Button("🔁 Antwort erneut vorlesen")
227
+ read_btn.click(read_last_answer, [chatbot], [voice_out])
228
+
229
+ # Quellen & Dokumente unten
230
+ with gr.Accordion("📄 Quellen & Dokumente", open=False):
231
+ gr.Markdown("### Prüfungsordnung")
232
+ PDF(pdf_meta["pdf_url"], height=250)
233
+ gr.Markdown("### Hochschulgesetz NRW")
234
+ if isinstance(hg_url, str) and hg_url.startswith("http"):
235
+ gr.Markdown(f"[Viewer öffnen]({hg_url})")
236
+ else:
237
+ gr.Markdown("Viewer nicht verfügbar.")
238
 
239
  if __name__ == "__main__":
240
  demo.queue().launch(ssr_mode=False, show_error=True)