Nguyen5 commited on
Commit
adeae04
·
1 Parent(s): 1866768
Files changed (3) hide show
  1. app.py +79 -159
  2. load_documents.py +78 -63
  3. speech_io.py +17 -4
app.py CHANGED
@@ -1,40 +1,36 @@
1
  # app.py
2
  import os
3
- from typing import List, Tuple
4
 
5
  import gradio as gr
6
  from langchain_core.documents import Document
7
  from langchain_text_splitters import RecursiveCharacterTextSplitter
8
  from langchain_community.vectorstores import FAISS
9
- from langchain_openai import OpenAIEmbeddings, ChatOpenAI
10
 
11
- from load_documents import load_documents # <- giữ như hiện tại
12
  from speech_io import transcribe_audio, synthesize_speech
13
 
14
 
15
- # =============================
16
- # 1. Lade & indexiere Dokumente
17
- # =============================
18
-
19
  print("🔹 Lade Dokumente aus Supabase …")
20
  docs: List[Document] = load_documents()
21
- print(f"✔ DOCUMENTS LOADED: {len(docs)}")
22
 
23
  print("🔹 Splitte Dokumente …")
24
  text_splitter = RecursiveCharacterTextSplitter(
25
  chunk_size=800,
26
  chunk_overlap=200,
27
- separators=["\n\n", "\n", ".", "?", "!", " "],
28
  )
29
  chunks = text_splitter.split_documents(docs)
30
  print(f" - {len(chunks)} Chunks erzeugt.")
31
 
32
  print("🔹 Erzeuge VectorStore …")
33
- print(">>> Initialising embedding model for FAISS index ...")
34
  embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
35
  vectorstore = FAISS.from_documents(chunks, embeddings)
36
  retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
37
- print(">>> FAISS index built.")
38
  print(">>> Retriever ready.")
39
 
40
  print("🔹 Lade OpenAI LLM …")
@@ -44,179 +40,103 @@ llm = ChatOpenAI(
44
  )
45
 
46
 
47
- # =============================
48
- # 2. RAG-Antwortfunktion
49
- # =============================
50
-
51
  def build_context(docs: List[Document]) -> str:
52
- """Baut einen konsolidierten Kontextstring mit Quelle-Infos."""
53
  parts = []
54
- for i, d in enumerate(docs, start=1):
55
- meta = d.metadata or {}
56
- source = meta.get("source", "unbekannt")
57
- page = meta.get("page", meta.get("page_number", "?"))
58
- para = meta.get("abs_id", meta.get("paragraph_id", ""))
59
- label = f"[Quelle {i}]"
60
- if para:
61
- label += f" Abs. {para}"
62
- if page is not None and page != "?":
63
- label += f", S. {page}"
64
- parts.append(
65
- f"{label} (source={source}):\n{d.page_content.strip()}\n"
66
- )
67
  return "\n\n".join(parts)
68
 
69
- def rag_answer(user_query: str, mode: str = "Standard") -> Tuple[str, List[Document]]:
70
- """
71
- Erzeugt eine Antwort mit RAG.
72
- mode: 'Kurz', 'Standard', 'Juristisch Präzise'
73
- """
74
- retrieved = retriever.invoke(user_query)
75
- context = build_context(retrieved)
76
-
77
- if mode == "Kurz":
78
- length_instruction = "Formuliere die Antwort kurz und prägnant (max. 3 Sätze)."
79
- elif mode == "Juristisch Präzise":
80
- length_instruction = (
81
- "Formuliere die Antwort möglichst juristisch präzise, "
82
- "mit klarer Struktur (Sachverhalt, Rechtsgrundlage, Anwendung, Ergebnis)."
83
- )
84
- else:
85
- length_instruction = "Formuliere die Antwort verständlich und vollständig."
86
-
87
- system_prompt = (
88
- "Du bist ein Chatbot für Prüfungsrecht (Hochschulgesetz NRW + Prüfungsordnung). "
89
- "Du antwortest immer AUF DEUTSCH, ohne Englisch zu mischen. "
90
- "Nutze NUR die gegebenen Quellen im Kontext. "
91
- "Wenn etwas nicht eindeutig aus den Quellen hervorgeht, sage transparent, "
92
- "dass du es nicht sicher weißt.\n\n"
93
- "Ganz am Ende der Antwort liste die verwendeten Quellen in der Form "
94
- "[Quelle 1], [Quelle 2], … mit kurzer Beschreibung auf."
95
- )
96
 
97
- messages = [
98
- {"role": "system", "content": system_prompt},
99
- {
100
- "role": "user",
101
- "content": (
102
- f"FRAGE:\n{user_query}\n\n"
103
- f"KONTEXT (Auszüge aus Gesetz/Prüfungsordnung):\n{context}\n\n"
104
- f"{length_instruction}"
105
- ),
106
- },
107
- ]
108
 
109
- resp = llm.invoke(messages)
110
- answer_text = resp.content if isinstance(resp.content, str) else str(resp.content)
111
- return answer_text, retrieved
 
 
112
 
 
 
 
 
 
 
113
 
114
- # =============================
115
- # 3. Gradio-Callback-Funktionen
116
- # =============================
117
 
118
- def chatbot_text(user_input: str, history: List[Tuple[str, str]], mode: str) -> Tuple[List[Tuple[str, str]], List[Tuple[str, str]]]:
119
- if not user_input:
120
- return history, history
121
 
122
- answer, _ = rag_answer(user_input, mode=mode)
123
- history = history + [(user_input, answer)]
 
 
 
 
 
 
 
124
  return history, history
125
 
126
 
127
- def chatbot_voice(
128
- audio_file: str,
129
- history: List[Tuple[str, str]],
130
- mode: str,
131
- language_hint: str,
132
- ):
133
- """
134
- - audio_file: đường dẫn file tạm từ Gradio
135
- - history: lịch sử chat
136
- - mode: Kurz / Standard / Juristisch Präzise
137
- - language_hint: "", "de", "en", "vi", ...
138
- """
139
- if audio_file is None:
140
- return history, None, "", history
141
-
142
- # 1) Speech-to-Text
143
- lang = language_hint.strip() or None
144
- user_text = transcribe_audio(audio_file, language=lang)
145
-
146
- # 2) RAG-Antwort
147
- answer, _ = rag_answer(user_text, mode=mode)
148
-
149
- # 3) Text-to-Speech
150
- audio_out_path = synthesize_speech(answer)
151
-
152
- # 4) Update History
153
- history = history + [(user_text, answer)]
154
-
155
- return history, audio_out_path, user_text, history
156
 
 
 
 
 
 
157
 
158
- # =============================
159
- # 4. Gradio UI
160
- # =============================
161
 
162
- with gr.Blocks(title="Prüfungsrechts-Chatbot (OpenAI)") as demo:
163
- gr.Markdown("## 📚 Sprachbasierter Chatbot für Prüfungsrecht\n"
164
- "Aktuelle Prüfungsordnung + Hochschulgesetz NRW (RAG, OpenAI).")
 
165
 
166
  with gr.Tab("💬 Text-Chat"):
167
- mode_text = gr.Radio(
168
- ["Kurz", "Standard", "Juristisch Präzise"],
169
- value="Standard",
170
- label="Antwortmodus",
171
- )
172
- chatbot_t = gr.Chatbot(label="Chatverlauf")
173
- text_in = gr.Textbox(label="Text eingeben", placeholder="Frage zum Prüfungsrecht …")
174
- state_t = gr.State([]) # history
175
-
176
- btn_send = gr.Button("Senden")
177
 
178
- btn_send.click(
179
- fn=chatbot_text,
180
- inputs=[text_in, state_t, mode_text],
181
- outputs=[chatbot_t, state_t],
182
- )
183
 
184
  with gr.Tab("🎙️ Sprach-Chat"):
185
- mode_voice = gr.Radio(
186
- ["Kurz", "Standard", "Juristisch Präzise"],
187
- value="Standard",
188
- label="Antwortmodus",
189
- )
190
- language_hint = gr.Textbox(
191
- label="Sprach-Hint (optional)",
192
- placeholder="z.B. de / en / vi – leer lassen = auto-detect",
193
- value="",
194
- )
195
-
196
- chatbot_v = gr.Chatbot(label="Chatverlauf (Sprache)")
197
- audio_in = gr.Audio(
198
- label="Mikrofon",
199
- sources=["microphone"],
200
- type="filepath",
201
- )
202
- audio_out = gr.Audio(
203
- label="Antwort (TTS)",
204
- type="filepath",
205
- )
206
- transcript_box = gr.Textbox(
207
- label="Transkript deiner Frage",
208
- interactive=False,
209
- )
210
  state_v = gr.State([])
211
 
212
- btn_ask = gr.Button("Frage mit Mikrofon stellen")
 
 
 
213
 
214
- btn_ask.click(
215
- fn=chatbot_voice,
216
- inputs=[audio_in, state_v, mode_voice, language_hint],
217
- outputs=[chatbot_v, audio_out, transcript_box, state_v],
 
218
  )
219
 
220
- # Wichtig für HuggingFace Spaces
221
  if __name__ == "__main__":
222
  demo.launch()
 
1
  # app.py
2
  import os
3
+ from typing import List, Dict, Tuple
4
 
5
  import gradio as gr
6
  from langchain_core.documents import Document
7
  from langchain_text_splitters import RecursiveCharacterTextSplitter
8
  from langchain_community.vectorstores import FAISS
9
+ from langchain_openai import ChatOpenAI, OpenAIEmbeddings
10
 
11
+ from load_documents import load_documents
12
  from speech_io import transcribe_audio, synthesize_speech
13
 
14
 
15
+ # ===============================
16
+ # 1. Documents Laden
17
+ # ===============================
 
18
  print("🔹 Lade Dokumente aus Supabase …")
19
  docs: List[Document] = load_documents()
20
+ print("✔ DOCUMENTS LOADED:", len(docs))
21
 
22
  print("🔹 Splitte Dokumente …")
23
  text_splitter = RecursiveCharacterTextSplitter(
24
  chunk_size=800,
25
  chunk_overlap=200,
 
26
  )
27
  chunks = text_splitter.split_documents(docs)
28
  print(f" - {len(chunks)} Chunks erzeugt.")
29
 
30
  print("🔹 Erzeuge VectorStore …")
 
31
  embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
32
  vectorstore = FAISS.from_documents(chunks, embeddings)
33
  retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
 
34
  print(">>> Retriever ready.")
35
 
36
  print("🔹 Lade OpenAI LLM …")
 
40
  )
41
 
42
 
43
+ # ===============================
44
+ # 2. RAG Engine
45
+ # ===============================
 
46
  def build_context(docs: List[Document]) -> str:
 
47
  parts = []
48
+ for i, d in enumerate(docs, 1):
49
+ meta = d.metadata
50
+ src = meta.get("source")
51
+ page = meta.get("page")
52
+ abs_id = meta.get("abs_id")
53
+
54
+ label = f"[Quelle {i}] {src}"
55
+ if page:
56
+ label += f", Seite {page}"
57
+ if abs_id:
58
+ label += f", Abs. {abs_id}"
59
+
60
+ parts.append(f"{label}\n{d.page_content}")
61
  return "\n\n".join(parts)
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
+ def rag_answer(query: str, mode: str) -> Tuple[str, List[Document]]:
65
+ retrieved = retriever.invoke(query)
66
+ ctx = build_context(retrieved)
 
 
 
 
 
 
 
 
67
 
68
+ modes = {
69
+ "Kurz": "Antworte sehr kurz (max. 3 Sätze).",
70
+ "Standard": "Antworte ausführlich und gut verständlich.",
71
+ "Juristisch Präzise": "Antworte fachlich-präzise mit juristischer Struktur.",
72
+ }
73
 
74
+ messages = [
75
+ {"role": "system",
76
+ "content": "Du bist ein Chatbot für Prüfungsrecht. Antworte NUR auf Deutsch."},
77
+ {"role": "user",
78
+ "content": f"FRAGE:\n{query}\n\nKONTEXT:\n{ctx}\n\n{modes[mode]}"}
79
+ ]
80
 
81
+ response = llm.invoke(messages)
82
+ answer = response.content
83
+ return answer, retrieved
84
 
 
 
 
85
 
86
+ # ===============================
87
+ # 3. Chatbot Funktionen (GRADIO v6 FORMAT!)
88
+ # ===============================
89
+ def chatbot_text(user_input: str, history: List[Dict], mode: str):
90
+ answer, _ = rag_answer(user_input, mode)
91
+ history = history + [
92
+ {"role": "user", "content": user_input},
93
+ {"role": "assistant", "content": answer},
94
+ ]
95
  return history, history
96
 
97
 
98
+ def chatbot_voice(audio_file: str, history: List[Dict], mode: str, language_hint: str):
99
+ user_text = transcribe_audio(audio_file, language=language_hint or None)
100
+ answer, _ = rag_answer(user_text, mode)
101
+ audio_out = synthesize_speech(answer)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
+ history = history + [
104
+ {"role": "user", "content": user_text},
105
+ {"role": "assistant", "content": answer},
106
+ ]
107
+ return history, audio_out, user_text, history
108
 
 
 
 
109
 
110
+ # ===============================
111
+ # 4. UI
112
+ # ===============================
113
+ with gr.Blocks(title="Prüfungsrechts-Chatbot") as demo:
114
 
115
  with gr.Tab("💬 Text-Chat"):
116
+ mode = gr.Radio(["Kurz", "Standard", "Juristisch Präzise"], value="Standard")
117
+ chat = gr.Chatbot(type="messages")
118
+ state = gr.State([])
119
+ inp = gr.Textbox(label="Frage eingeben")
120
+ send = gr.Button("Senden")
 
 
 
 
 
121
 
122
+ send.click(chatbot_text, [inp, state, mode], [chat, state])
 
 
 
 
123
 
124
  with gr.Tab("🎙️ Sprach-Chat"):
125
+ mode_v = gr.Radio(["Kurz", "Standard", "Juristisch Präzise"], value="Standard")
126
+ chat_v = gr.Chatbot(type="messages")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  state_v = gr.State([])
128
 
129
+ mic = gr.Audio(sources=["microphone"], type="filepath")
130
+ lang_hint = gr.Textbox(label="Sprache (optional: de/en/vi)")
131
+ out_audio = gr.Audio(label="Antwort (TTS)")
132
+ trans_box = gr.Textbox(label="Transkript")
133
 
134
+ btn = gr.Button("Sprechen")
135
+ btn.click(
136
+ chatbot_voice,
137
+ [mic, state_v, mode_v, lang_hint],
138
+ [chat_v, out_audio, trans_box, state_v]
139
  )
140
 
 
141
  if __name__ == "__main__":
142
  demo.launch()
load_documents.py CHANGED
@@ -1,89 +1,104 @@
1
- # load_documents.py – Dokumente für RAG laden (HG NRW + Prüfungsordnung PDF)
2
 
3
  import os
4
- import requests
5
- import tempfile
6
- from supabase import create_client
 
 
 
7
  from langchain_core.documents import Document
8
- from langchain_community.document_loaders import PyPDFLoader
9
 
10
- SUPABASE_URL = os.getenv("SUPABASE_URL")
11
- SUPABASE_ANON_KEY = os.getenv("SUPABASE_ANON_KEY")
12
 
13
- if not SUPABASE_URL or not SUPABASE_ANON_KEY:
14
- raise RuntimeError("Missing SUPABASE_URL / SUPABASE_ANON_KEY in environment.")
15
 
16
- supabase = create_client(SUPABASE_URL, SUPABASE_ANON_KEY)
 
 
 
 
 
 
 
 
 
17
 
18
- PDF_FILE = "f10_bpo_ifb_tei_mif_wii_2021-01-04.pdf"
19
- PDF_URL = f"{SUPABASE_URL}/storage/v1/object/public/File%20PDF/{PDF_FILE}"
20
 
21
 
22
- def load_hg_nrw():
 
23
  print(">>> Lade Hochschulgesetz NRW (§) aus Supabase…")
24
 
25
- rows = (
26
- supabase.table("hg_nrw")
27
- .select("*")
28
- .order("order_index")
29
- .execute()
30
- ).data or []
31
-
32
- print(f" - {len(rows)} Paragraphen geladen.")
33
 
34
  docs = []
35
- for r in rows:
36
- abs_id = r["abs_id"]
37
- title = r["title"]
38
- content = r["content"]
39
-
40
- viewer_url = f"hg_view#{abs_id}"
41
-
42
- docs.append(
43
- Document(
44
- page_content=f"{title}\n{content}",
45
- metadata={
46
- "source": "Hochschulgesetz NRW",
47
- "paragraph": title,
48
- "abs_id": abs_id,
49
- "url": viewer_url,
50
- },
51
- )
52
- )
53
-
54
  return docs
55
 
56
 
57
- def load_pdf():
58
- print(">>> Lade Prüfungsordnung PDF …")
 
 
59
 
60
- resp = requests.get(PDF_URL)
61
- resp.raise_for_status()
 
62
 
63
- with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
64
- tmp.write(resp.content)
65
- pdf_path = tmp.name
66
 
67
- pages = PyPDFLoader(pdf_path).load()
 
 
 
 
68
 
69
- for i, p in enumerate(pages):
70
- p.metadata["source"] = "Prüfungsordnung (PDF)"
71
- p.metadata["page"] = i
72
- p.metadata["pdf_url"] = PDF_URL
73
 
74
- print(f" - {len(pages)} PDF-Seiten geladen.")
75
- return pages
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
 
78
- def load_documents():
 
 
79
  docs = []
80
- docs.extend(load_hg_nrw())
81
- docs.extend(load_pdf())
82
- print(f"✔ DOCUMENTS LOADED: {len(docs)}")
83
- return docs
84
 
 
 
85
 
86
- if __name__ == "__main__":
87
- docs = load_documents()
88
- print(docs[0])
89
- print("Total:", len(docs))
 
1
+ # load_documents.py
2
 
3
  import os
4
+ from io import BytesIO
5
+ from typing import List
6
+
7
+ from dotenv import load_dotenv
8
+ from supabase import create_client, Client
9
+ from pypdf import PdfReader
10
  from langchain_core.documents import Document
 
11
 
12
+ load_dotenv()
 
13
 
 
 
14
 
15
+ # ============== Supabase Init ==============
16
+ def get_supabase_client() -> Client:
17
+ url = os.getenv("SUPABASE_URL")
18
+ key = (
19
+ os.getenv("SUPABASE_SERVICE_ROLE_KEY")
20
+ or os.getenv("SUPABASE_SERVICE_ROLE")
21
+ or os.getenv("SUPABASE_KEY")
22
+ )
23
+ if not url or not key:
24
+ raise RuntimeError("Supabase ENV fehlen.")
25
 
26
+ return create_client(url, key)
 
27
 
28
 
29
+ # ============== HG NRW Paragraphen ==============
30
+ def load_hg_paragraphs(supabase: Client) -> List[Document]:
31
  print(">>> Lade Hochschulgesetz NRW (§) aus Supabase…")
32
 
33
+ table = os.getenv("HG_TABLE_NAME", "hg_nrw")
34
+ rows = supabase.table(table).select("*").order("order_index").execute().data or []
 
 
 
 
 
 
35
 
36
  docs = []
37
+ for row in rows:
38
+ text = (row.get("title", "") + "\n\n" + row.get("content", "")).strip()
39
+ if not text:
40
+ continue
41
+
42
+ docs.append(Document(
43
+ page_content=text,
44
+ metadata={
45
+ "source": "Hochschulgesetz NRW",
46
+ "abs_id": row.get("abs_id"),
47
+ "order_index": row.get("order_index"),
48
+ "url": "https://recht.nrw.de/lmi/owa/br_text_anzeigen?v_id=10000000000000000654",
49
+ "type": "law",
50
+ }
51
+ ))
52
+
53
+ print(f" - {len(docs)} Paragraphen geladen.")
 
 
54
  return docs
55
 
56
 
57
+ # ============== Prüfungsordnung PDF ==============
58
+ def load_pruefungsordnung_from_storage(supabase: Client) -> List[Document]:
59
+ bucket = os.getenv("PRUEF_BUCKET")
60
+ pdf_path = os.getenv("PRUEF_PDF_PATH")
61
 
62
+ if not bucket or not pdf_path:
63
+ print(">>> Keine Prüfungsordnung-PDF definiert.")
64
+ return []
65
 
66
+ print(">>> Lade Prüfungsordnung PDF …")
 
 
67
 
68
+ try:
69
+ data = supabase.storage.from_(bucket).download(pdf_path)
70
+ except Exception as e:
71
+ print(" Fehler beim PDF Download:", e)
72
+ return []
73
 
74
+ reader = PdfReader(BytesIO(data))
75
+ docs = []
 
 
76
 
77
+ for i, page in enumerate(reader.pages):
78
+ text = (page.extract_text() or "").strip()
79
+ if not text:
80
+ continue
81
+
82
+ docs.append(Document(
83
+ page_content=text,
84
+ metadata={
85
+ "source": "Prüfungsordnung (PDF)",
86
+ "page": i + 1,
87
+ "type": "pruefungsordnung",
88
+ }
89
+ ))
90
+
91
+ print(f" - {len(docs)} PDF-Seiten geladen.")
92
+ return docs
93
 
94
 
95
+ # ============== Main Loader ==============
96
+ def load_documents() -> List[Document]:
97
+ supabase = get_supabase_client()
98
  docs = []
 
 
 
 
99
 
100
+ docs += load_hg_paragraphs(supabase)
101
+ docs += load_pruefungsordnung_from_storage(supabase)
102
 
103
+ print(f"✔ DOCUMENTS LOADED: {len(docs)}")
104
+ return docs
 
 
speech_io.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import os
2
  from tempfile import NamedTemporaryFile
3
  from typing import Optional
@@ -6,30 +7,42 @@ from openai import OpenAI
6
  client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
7
 
8
 
 
 
 
9
  def transcribe_audio(file_path: str, language: Optional[str] = None) -> str:
 
 
 
10
  print(">>> Transkribiere Audio via OpenAI Audio API …")
11
 
12
  with open(file_path, "rb") as f:
13
  resp = client.audio.transcriptions.create(
14
  model="gpt-4o-mini-transcribe",
15
  file=f,
16
- language=language
17
  )
18
 
19
  return resp.text
20
 
21
 
 
 
 
22
  def synthesize_speech(text: str, voice: str = "alloy") -> str:
 
 
 
 
23
  print(">>> Synthesizing speech via OpenAI TTS …")
24
 
25
- # OpenAI SDK 2.x returns HttpxBinaryResponseContent
26
  response = client.audio.speech.create(
27
  model="gpt-4o-mini-tts",
28
  voice=voice,
29
- input=text
30
  )
31
 
32
- # Correct extraction method
33
  audio_bytes = response.read()
34
 
35
  tmp = NamedTemporaryFile(delete=False, suffix=".mp3")
 
1
+ # speech_io.py
2
  import os
3
  from tempfile import NamedTemporaryFile
4
  from typing import Optional
 
7
  client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
8
 
9
 
10
+ # ======================
11
+ # 1. Speech-to-Text (STT)
12
+ # ======================
13
  def transcribe_audio(file_path: str, language: Optional[str] = None) -> str:
14
+ """
15
+ Transkribiert Audio via OpenAI Audio Transcription API (gpt-4o-mini-transcribe).
16
+ """
17
  print(">>> Transkribiere Audio via OpenAI Audio API …")
18
 
19
  with open(file_path, "rb") as f:
20
  resp = client.audio.transcriptions.create(
21
  model="gpt-4o-mini-transcribe",
22
  file=f,
23
+ language=language,
24
  )
25
 
26
  return resp.text
27
 
28
 
29
+ # ======================
30
+ # 2. Text-to-Speech (TTS)
31
+ # ======================
32
  def synthesize_speech(text: str, voice: str = "alloy") -> str:
33
+ """
34
+ Wandelt Text in Sprache um (OpenAI TTS - gpt-4o-mini-tts)
35
+ Speichert MP3-Datei und gibt den Pfad zurück.
36
+ """
37
  print(">>> Synthesizing speech via OpenAI TTS …")
38
 
 
39
  response = client.audio.speech.create(
40
  model="gpt-4o-mini-tts",
41
  voice=voice,
42
+ input=text,
43
  )
44
 
45
+ # HF Spaces + OpenAI SDK v2.x → raw bytes
46
  audio_bytes = response.read()
47
 
48
  tmp = NamedTemporaryFile(delete=False, suffix=".mp3")