tbdavid2019 commited on
Commit
fa116c8
·
verified ·
1 Parent(s): 15dfb2c

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +241 -0
app.py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from openai import OpenAI
3
+ import os
4
+ import tempfile
5
+ from dotenv import load_dotenv
6
+ from markitdown import MarkItDown
7
+
8
+ load_dotenv()
9
+ api_key = os.getenv("OPENAI_API_KEY")
10
+ api_base = os.getenv("OPENAI_API_BASE")
11
+ # 刪除全域 client,改由 generate_questions 動態初始化
12
+
13
+ # ✅ 合併多檔案文字
14
+
15
+ def extract_text_from_files(files):
16
+ from openai import OpenAI
17
+ import os
18
+
19
+ api_key = os.getenv("OPENAI_API_KEY")
20
+ api_base = os.getenv("OPENAI_API_BASE")
21
+ client = OpenAI(api_key=api_key, base_url=api_base)
22
+
23
+ image_exts = {".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"}
24
+ merged_text = ""
25
+ for f in files:
26
+ ext = os.path.splitext(f.name)[1].lower()
27
+ if ext in image_exts:
28
+ md = MarkItDown(llm_client=client, llm_model="gpt-4.1")
29
+ else:
30
+ md = MarkItDown()
31
+ result = md.convert(f.name)
32
+ merged_text += result.text_content + "\n"
33
+ return merged_text
34
+
35
+ # ✅ 產出題目與答案(根據語言與題型)
36
+
37
+ def generate_questions(files, question_types, num_questions, lang, llm_key, baseurl, model=None):
38
+ try:
39
+ text = extract_text_from_files(files)
40
+ trimmed_text = text[:200000]
41
+
42
+ # 優先使用 .env,否則用 UI 傳入值
43
+ key = os.getenv("OPENAI_API_KEY") or llm_key
44
+ base = os.getenv("OPENAI_API_BASE") or baseurl
45
+ model_name = model or "gpt-4.1"
46
+ if not key or not base:
47
+ return "⚠️ 請輸入 LLM key 與 baseurl", ""
48
+ client = OpenAI(api_key=key, base_url=base)
49
+
50
+ type_map = {
51
+ "單選選擇題": {
52
+ "zh-Hant": "單選選擇題(每題四個選項)",
53
+ "zh-Hans": "单选选择题(每题四个选项)",
54
+ "en": "single choice question (4 options)",
55
+ "ja": "四択問題"
56
+ },
57
+ "多選選擇題": {
58
+ "zh-Hant": "多選選擇題(每題四到五個選項)",
59
+ "zh-Hans": "多选选择题(每题四到五个选项)",
60
+ "en": "multiple choice question (4-5 options)",
61
+ "ja": "複数選択問題"
62
+ },
63
+ "問答題": {
64
+ "zh-Hant": "簡答題",
65
+ "zh-Hans": "简答题",
66
+ "en": "short answer",
67
+ "ja": "短答式問題"
68
+ },
69
+ "申論題": {
70
+ "zh-Hant": "申論題",
71
+ "zh-Hans": "申论题",
72
+ "en": "essay question",
73
+ "ja": "記述式問題"
74
+ }
75
+ }
76
+
77
+ prompt_map = {
78
+ "繁體中文": "你是一位專業的出題者,請根據以下內容,設計 {n} 題以下類型的題目:{types}。每題後面請標註【答案】。內容如下:\n{text}",
79
+ "簡體中文": "你是一位专业的出题者,请根据以下内容,设计 {n} 题以下类型的题目:{types}。每题后面请标注【答案】。内容如下:\n{text}",
80
+ "English": "You are a professional exam writer. Based on the following content, generate {n} questions of types: {types}. Please mark the answer after each question using [Answer:]. Content:\n{text}",
81
+ "日本語": "あなたはプロの出題者です。以下の内容に基づいて、{types}を含む{n}問の問題を作成してください。各問題の後に【答え】を付けてください。内容:\n{text}"
82
+ }
83
+
84
+ lang_key_map = {
85
+ "繁體中文": "zh-Hant",
86
+ "簡體中文": "zh-Hans",
87
+ "English": "en",
88
+ "日本語": "ja"
89
+ }
90
+
91
+ lang_key = lang_key_map[lang]
92
+ types_str = "、".join([type_map[t][lang_key] for t in question_types])
93
+ prompt = prompt_map[lang].format(n=num_questions, types=types_str, text=trimmed_text)
94
+
95
+ response = client.chat.completions.create(
96
+ model=model_name,
97
+ messages=[{"role": "user", "content": prompt}]
98
+ )
99
+ content = response.choices[0].message.content
100
+
101
+ questions, answers = [], []
102
+ for line in content.strip().split("\n"):
103
+ if not line.strip():
104
+ continue
105
+ try:
106
+ if "【答案】" in line:
107
+ q, a = line.split("【答案】", 1)
108
+ elif "[Answer:" in line:
109
+ q, a = line.split("[Answer:", 1)
110
+ a = a.rstrip("]")
111
+ elif "【答え】" in line:
112
+ q, a = line.split("【答え】", 1)
113
+ else:
114
+ questions.append(line.strip())
115
+ answers.append("")
116
+ continue
117
+ questions.append(q.strip())
118
+ answers.append(a.strip())
119
+ except Exception:
120
+ questions.append(line.strip())
121
+ answers.append("")
122
+
123
+ if not questions:
124
+ return "⚠️ 無法解析 AI 回傳內容,請��查輸入內容或稍後再試。", ""
125
+
126
+ return "\n\n".join(questions), "\n\n".join(answers)
127
+ except Exception as e:
128
+ return f"⚠️ 發生錯誤:{str(e)}", ""
129
+
130
+ # ✅ 匯出 Markdown, Quizlet(TSV)
131
+
132
+ def export_files(questions_text, answers_text):
133
+ md_path = tempfile.NamedTemporaryFile(delete=False, suffix=".md").name
134
+ with open(md_path, "w", encoding="utf-8") as f:
135
+ f.write("# 📘 題目 Questions\n\n" + questions_text + "\n\n# ✅ 解答 Answers\n\n" + answers_text)
136
+
137
+ quizlet_path = tempfile.NamedTemporaryFile(delete=False, suffix=".tsv").name
138
+ with open(quizlet_path, "w", encoding="utf-8") as f:
139
+ for q, a in zip(questions_text.split("\n\n"), answers_text.split("\n\n")):
140
+ q_clean = q.replace("\n", " ").replace("\r", " ")
141
+ a_clean = a.replace("\n", " ").replace("\r", " ")
142
+ f.write(f"{q_clean}\t{a_clean}\n")
143
+
144
+ return md_path, quizlet_path
145
+
146
+ # ✅ Gradio UI
147
+
148
+ # --- FastAPI + Gradio 整合 ---
149
+ from fastapi import FastAPI, UploadFile, File, Form
150
+ from fastapi.responses import JSONResponse
151
+ from typing import List, Optional
152
+ import uvicorn
153
+
154
+ def build_gradio_blocks():
155
+ with gr.Blocks() as demo:
156
+ gr.Markdown("# 📄 通用 AI 出題系統(支援多檔、多語、匯出格式)")
157
+
158
+ with gr.Row():
159
+ with gr.Column():
160
+ file_input = gr.File(
161
+ label="上傳文件(可多檔)",
162
+ file_types=[
163
+ ".pdf", ".ppt", ".pptx", ".doc", ".docx", ".xls", ".xlsx", ".csv",
164
+ ".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp",
165
+ ".mp3", ".wav", ".m4a", ".flac", ".ogg", ".aac", ".amr", ".wma", ".opus",
166
+ ".html", ".htm", ".json", ".xml", ".txt", ".md", ".rtf", ".log",
167
+ ".zip", ".epub"
168
+ ],
169
+ file_count="multiple"
170
+ )
171
+ lang = gr.Dropdown(["繁體中文", "簡體中文", "English", "日本語"], value="繁體中文", label="語言 Language")
172
+ question_types = gr.CheckboxGroup(["單選選擇題", "多選選擇題", "問答題", "申論題"],
173
+ label="選擇題型(可複選)",
174
+ value=["單選選擇題"])
175
+ num_questions = gr.Slider(1, 20, value=10, step=1, label="題目數量")
176
+ llm_key = gr.Textbox(label="LLM Key (不會儲存)", type="password", placeholder="請輸入你的 OpenAI API Key")
177
+ baseurl = gr.Textbox(label="Base URL (如 https://api.openai.com/v1)",value="https://api.openai.com/v1", placeholder="請輸入 API Base URL")
178
+ model_box = gr.Textbox(label="Model 名稱", value="gpt-4.1", placeholder="如 gpt-4.1, gpt-3.5-turbo, ...")
179
+ generate_btn = gr.Button("✏️ 開始出題")
180
+
181
+ with gr.Column():
182
+ qbox = gr.Textbox(label="📘 題目 Questions", lines=15)
183
+ abox = gr.Textbox(label="✅ 解答 Answers", lines=15)
184
+ export_btn = gr.Button("📤 匯出 Markdown / Quizlet")
185
+ md_out = gr.File(label="📝 Markdown 檔下載")
186
+ quizlet_out = gr.File(label="📋 Quizlet (TSV) 檔下載")
187
+
188
+
189
+ generate_btn.click(fn=generate_questions,
190
+ inputs=[file_input, question_types, num_questions, lang, llm_key, baseurl, model_box],
191
+ outputs=[qbox, abox])
192
+
193
+ export_btn.click(fn=export_files,
194
+ inputs=[qbox, abox],
195
+ outputs=[md_out, quizlet_out])
196
+ return demo
197
+
198
+ if __name__ == "__main__":
199
+ demo = build_gradio_blocks()
200
+ demo.launch()
201
+
202
+ # --- FastAPI API 介面 ---
203
+ from fastapi import FastAPI, UploadFile, File, Form
204
+ from fastapi.responses import JSONResponse
205
+ from typing import List, Optional
206
+ import uvicorn
207
+
208
+ api_app = FastAPI(title="AI 出題系統 API")
209
+
210
+ @api_app.post("/api/generate")
211
+ async def api_generate(
212
+ files: List[UploadFile] = File(...),
213
+ question_types: List[str] = Form(...),
214
+ num_questions: int = Form(...),
215
+ lang: str = Form(...),
216
+ llm_key: Optional[str] = Form(None),
217
+ baseurl: Optional[str] = Form(None),
218
+ model: Optional[str] = Form(None)
219
+ ):
220
+ # 將 UploadFile 轉為臨時檔案物件,與 Gradio 行為一致
221
+ temp_files = []
222
+ for f in files:
223
+ temp = tempfile.NamedTemporaryFile(delete=False)
224
+ temp.write(await f.read())
225
+ temp.flush()
226
+ temp_files.append(temp)
227
+ temp.name = temp.name # 保持介面一致
228
+
229
+ # 呼叫原本的出題邏輯
230
+ questions, answers = generate_questions(
231
+ temp_files, question_types, num_questions, lang, llm_key, baseurl, model
232
+ )
233
+
234
+ # 關閉臨時檔案
235
+ for temp in temp_files:
236
+ temp.close()
237
+
238
+ return JSONResponse({"questions": questions, "answers": answers})
239
+
240
+ # 若要啟動 API 伺服器,請執行:
241
+ # uvicorn app:api_app --host 0.0.0.0 --port 7861