DeepLearning101 commited on
Commit
a4e4968
·
verified ·
1 Parent(s): 19fcfd9

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +314 -0
app.py ADDED
@@ -0,0 +1,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import google.generativeai as genai
3
+ import os
4
+ import json
5
+ import pandas as pd
6
+ import tempfile
7
+ from pdf2image import convert_from_path
8
+ from pptx import Presentation
9
+ from pptx.util import Inches, Pt
10
+ from pptx.dml.color import RGBColor
11
+ from huggingface_hub import HfApi, hf_hub_download
12
+ from dotenv import load_dotenv
13
+
14
+ # --- 設定與常數 ---
15
+ load_dotenv()
16
+ PROF_SAVE_FILE = "saved_professors.json"
17
+ COMP_SAVE_FILE = "saved_companies.json"
18
+ HF_TOKEN = os.getenv("HF_TOKEN")
19
+ DATASET_REPO_ID = os.getenv("DATASET_REPO_ID")
20
+
21
+ # ==========================================
22
+ # 🧠 核心服務層 (The Logic / Chef)
23
+ # ==========================================
24
+ class UnifiedService:
25
+ def __init__(self, api_key_input=None):
26
+ self.api_key = self._get_api_key(api_key_input)
27
+ if self.api_key:
28
+ genai.configure(api_key=self.api_key)
29
+ # 使用支援 Google Search 的模型
30
+ self.model_name = "gemini-2.0-flash-exp"
31
+
32
+ def _get_api_key(self, user_key):
33
+ if user_key and user_key.strip(): return user_key.strip()
34
+ system_key = os.getenv("GEMINI_API_KEY")
35
+ if system_key: return system_key
36
+ return None # 允許初始化時無 Key,但在使用功能時會噴錯
37
+
38
+ def _check_key(self):
39
+ if not self.api_key: raise ValueError("請先輸入 API Key 或設定系統環境變數")
40
+
41
+ # --- 1. PDF 轉 PPTX ---
42
+ def analyze_pdf_to_pptx(self, pdf_file, progress):
43
+ self._check_key()
44
+ model = genai.GenerativeModel(self.model_name)
45
+ prs = Presentation()
46
+ prs.slide_width = Inches(16); prs.slide_height = Inches(9)
47
+
48
+ progress(0.1, desc="轉檔中...")
49
+ images = convert_from_path(pdf_file)
50
+
51
+ for i, img in enumerate(images):
52
+ progress(0.1 + (0.8 * (i / len(images))), desc=f"分析第 {i+1} 頁...")
53
+ slide = prs.slides.add_slide(prs.slide_layouts[6])
54
+
55
+ prompt = "Detect all text blocks. Return JSON: [{'text':..., 'box_2d':[ymin,xmin,ymax,xmax] (0-1000), 'font_size':int, 'is_bold':bool, 'color':hex}]"
56
+ try:
57
+ response = model.generate_content([prompt, img], generation_config={"response_mime_type": "application/json"})
58
+ blocks = json.loads(response.text)
59
+ for b in blocks:
60
+ box = b.get("box_2d", [0,0,0,0])
61
+ left, top = Inches((box[1]/1000)*16), Inches((box[0]/1000)*9)
62
+ width, height = Inches(((box[3]-box[1])/1000)*16), Inches(((box[2]-box[0])/1000)*9)
63
+ tx = slide.shapes.add_textbox(left, top, width, height)
64
+ p = tx.text_frame.paragraphs[0]
65
+ p.text = b.get("text",""); p.font.size = Pt(b.get("font_size", 12)); p.font.bold = b.get("is_bold", False)
66
+ try: p.font.color.rgb = RGBColor.from_string(b.get("color", "#000000").replace("#",""))
67
+ except: pass
68
+ except Exception as e: print(f"Page {i} err: {e}")
69
+
70
+ out = tempfile.mktemp(suffix=".pptx")
71
+ prs.save(out)
72
+ return out, "✅ 轉換完成"
73
+
74
+ # --- 2. 圖片去字 ---
75
+ def remove_text(self, image):
76
+ self._check_key()
77
+ model = genai.GenerativeModel(self.model_name)
78
+ prompt = "Remove all text from this image, fill background naturally. Return image only."
79
+ resp = model.generate_content([prompt, image]) # V1 SDK 通常回傳 multipart,這裡簡化處理
80
+ # 注意: Gemini V1 SDK 在 Python 直接回傳 image 比較 tricky,若失敗建議檢查 SDK 版本
81
+ # 這裡假設環境支援直接回圖,若否則需用 requests 操作 REST API
82
+ try:
83
+ return resp.parts[0].image
84
+ except:
85
+ return image # Fallback
86
+
87
+ # --- 3. 搜尋 (教授/公司) 共用邏輯 ---
88
+ def _search_with_google(self, query, prompt_template):
89
+ self._check_key()
90
+ # 這裡使用 Google Search Tool 設定
91
+ tools = [{"google_search": {}}]
92
+ model = genai.GenerativeModel(self.model_name, tools=tools)
93
+
94
+ # Step 1: Search
95
+ resp1 = model.generate_content(prompt_template.format(query=query))
96
+
97
+ # Step 2: Extract JSON (Pure Text Model)
98
+ model_extract = genai.GenerativeModel(self.model_name) # No tools for extraction
99
+ extract_prompt = f"Extract structured data from this text into JSON array: {resp1.text}"
100
+ resp2 = model_extract.generate_content(extract_prompt, generation_config={"response_mime_type": "application/json"})
101
+ try: return json.loads(resp2.text)
102
+ except: return []
103
+
104
+ def search_professors(self, query):
105
+ p = "Find 10 prominent professors in Taiwan for '{query}'. Return name, university, department."
106
+ return self._search_with_google(query, p)
107
+
108
+ def search_companies(self, query):
109
+ p = "Find 5-10 Taiwanese companies for '{query}'. Return name, industry."
110
+ return self._search_with_google(query, p)
111
+
112
+ def get_details(self, data, role):
113
+ self._check_key()
114
+ tools = [{"google_search": {}}]
115
+ model = genai.GenerativeModel(self.model_name, tools=tools)
116
+ prompt = f"Act as {role}. Investigate: {json.dumps(data)}. Report in Traditional Chinese Markdown."
117
+ resp = model.generate_content(prompt)
118
+
119
+ # 處理來源引用 (V1 SDK)
120
+ sources = []
121
+ if hasattr(resp.candidates[0], 'grounding_metadata'):
122
+ chunks = resp.candidates[0].grounding_metadata.grounding_chunks
123
+ for c in chunks:
124
+ if c.web: sources.append({"title": c.web.title, "uri": c.web.uri})
125
+
126
+ # 去重
127
+ unique_sources = list({v['uri']:v for v in sources}.values())
128
+ return {"text": resp.text, "sources": unique_sources}
129
+
130
+ def chat(self, hist, msg, context, role):
131
+ self._check_key()
132
+ model = genai.GenerativeModel(self.model_name)
133
+ chat = model.start_chat(history=[
134
+ {"role": "user" if h[0] else "model", "parts": [h[0] or h[1]]} for h in hist
135
+ ])
136
+ full_msg = f"Context: {context}\nInstruction: {role}\nUser: {msg}"
137
+ resp = chat.send_message(full_msg)
138
+ return resp.text
139
+
140
+ # ==========================================
141
+ # 💾 資料存取層 (Persistence)
142
+ # ==========================================
143
+ def load_data(filename):
144
+ if HF_TOKEN and DATASET_REPO_ID:
145
+ try: hf_hub_download(repo_id=DATASET_REPO_ID, filename=filename, repo_type="dataset", token=HF_TOKEN, local_dir=".")
146
+ except: pass
147
+ if os.path.exists(filename):
148
+ try:
149
+ with open(filename, 'r', encoding='utf-8') as f: return json.load(f)
150
+ except: pass
151
+ return []
152
+
153
+ def save_data(data, filename):
154
+ with open(filename, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2)
155
+ if HF_TOKEN and DATASET_REPO_ID:
156
+ try:
157
+ api = HfApi(token=HF_TOKEN)
158
+ api.upload_file(path_or_fileobj=filename, path_in_repo=filename, repo_id=DATASET_REPO_ID, repo_type="dataset", commit_message="Sync")
159
+ except: pass
160
+
161
+ # ==========================================
162
+ # 🖥️ 介面邏輯 (UI Helpers)
163
+ # ==========================================
164
+ def format_df(data_list, cols):
165
+ if not data_list: return pd.DataFrame(columns=cols)
166
+ res = []
167
+ for d in data_list:
168
+ icon = {'match':'✅','good':'✅','risk':'⚠️'}.get(d.get('status'),'')
169
+ res.append([f"{icon} {d.get('name')}", d.get('university') or d.get('industry'), ", ".join(d.get('tags',[]))])
170
+ return pd.DataFrame(res, columns=cols)
171
+
172
+ # ==========================================
173
+ # 🚀 主程式 (Gradio)
174
+ # ==========================================
175
+ def main_app():
176
+ # 初始化
177
+ prof_data = load_data(PROF_SAVE_FILE)
178
+ comp_data = load_data(COMP_SAVE_FILE)
179
+
180
+ with gr.Blocks(title="Prof.404 x PPT.404 Ultimate", theme=gr.themes.Soft()) as demo:
181
+
182
+ # 全域 Key
183
+ with gr.Accordion("🔑 系統設定 (API Key)", open=False):
184
+ api_key = gr.Textbox(label="Google Gemini API Key", type="password", placeholder="若未填寫則使用系統預設")
185
+
186
+ gr.Markdown(
187
+ """
188
+ <div align="center">
189
+ <h1>🚀 Prof.404 Ultimate: 產學導航 & 文件工具站</h1>
190
+ <h3>整合文件視覺處理 (PPT/Img) 與 產學資源導航 (Prof/Com) 的全方位平台</h3>
191
+ </div>
192
+ """
193
+ )
194
+
195
+ with gr.Tabs():
196
+
197
+ # --- Tab 1: 工具箱 ---
198
+ with gr.Tab("🛠️ 文件工具箱 (PPT.404)"):
199
+ with gr.Row():
200
+ with gr.Column():
201
+ gr.Markdown("### 📄 PDF 轉 PPTX (含排版還原)")
202
+ pdf_file = gr.File(label="上傳 PDF")
203
+ pdf_btn = gr.Button("開始轉換", variant="primary")
204
+ ppt_out = gr.File(label="下載 PPTX")
205
+ pdf_msg = gr.Textbox(label="狀態", interactive=False)
206
+
207
+ pdf_btn.click(
208
+ lambda f, k: UnifiedService(k).analyze_pdf_to_pptx(f, gr.Progress()),
209
+ inputs=[pdf_file, api_key], outputs=[ppt_out, pdf_msg]
210
+ )
211
+
212
+ with gr.Column():
213
+ gr.Markdown("### 🎨 圖片智慧去字")
214
+ img_in = gr.Image(type="pil", label="原圖")
215
+ img_btn = gr.Button("一鍵去除", variant="primary")
216
+ img_out = gr.Image(label="結果")
217
+
218
+ img_btn.click(
219
+ lambda i, k: UnifiedService(k).remove_text(i),
220
+ inputs=[img_in, api_key], outputs=[img_out]
221
+ )
222
+
223
+ # --- Tab 2: 找教授 ---
224
+ with gr.Tab("🎓 找教授 (Prof.404)"):
225
+ p_state = gr.State(prof_data)
226
+ p_current = gr.State(None) # 當前選中的教授
227
+
228
+ with gr.Row():
229
+ p_query = gr.Textbox(label="搜尋領域", scale=4)
230
+ p_btn = gr.Button("搜尋", scale=1)
231
+
232
+ with gr.Row():
233
+ p_table = gr.Dataframe(headers=["姓名", "大學", "標籤"], interactive=False, scale=1)
234
+ with gr.Column(scale=1, visible=False) as p_detail_col:
235
+ p_md = gr.Markdown()
236
+ p_chat = gr.Chatbot(height=300)
237
+ p_msg = gr.Textbox(label="詢問關於此教授")
238
+
239
+ # Logic Wrappers
240
+ def search_p(q, k, saved):
241
+ svc = UnifiedService(k)
242
+ res = svc.search_professors(q)
243
+ return res, format_df(res, ["姓名","大學","標籤"])
244
+
245
+ def select_p(evt: gr.SelectData, res, k, saved):
246
+ svc = UnifiedService(k)
247
+ item = res[evt.index[0]]
248
+ # 取得詳細資料
249
+ det = svc.get_details(item, "Academic Consultant")
250
+ item['details'] = det['text']
251
+ # 簡易儲存邏輯 (為了Demo簡化,實際建議加上去重)
252
+ saved.append(item)
253
+ save_data(saved, PROF_SAVE_FILE)
254
+
255
+ display_text = det['text'] + "\n\n📚 來源:\n" + "\n".join([f"- {s['title']}" for s in det['sources']])
256
+ return gr.update(visible=True), display_text, [], item, saved
257
+
258
+ def chat_p(hist, msg, item, k):
259
+ svc = UnifiedService(k)
260
+ reply = svc.chat(hist, msg, item.get('details'), "Academic Consultant")
261
+ hist.append((msg, reply))
262
+ return hist, ""
263
+
264
+ p_btn.click(search_p, [p_query, api_key, p_state], [p_state, p_table])
265
+ p_table.select(select_p, [p_state, api_key, p_state], [p_detail_col, p_md, p_chat, p_current, p_state])
266
+ p_msg.submit(chat_p, [p_chat, p_msg, p_current, api_key], [p_chat, p_msg])
267
+
268
+ # --- Tab 3: 找公司 ---
269
+ with gr.Tab("🏢 找公司 (Com.404)"):
270
+ c_state = gr.State(comp_data)
271
+ c_current = gr.State(None)
272
+
273
+ with gr.Row():
274
+ c_query = gr.Textbox(label="搜尋產業/公司", scale=4)
275
+ c_btn = gr.Button("搜尋", scale=1)
276
+
277
+ with gr.Row():
278
+ c_table = gr.Dataframe(headers=["公司", "產業", "標籤"], interactive=False, scale=1)
279
+ with gr.Column(scale=1, visible=False) as c_detail_col:
280
+ c_md = gr.Markdown()
281
+ c_chat = gr.Chatbot(height=300)
282
+ c_msg = gr.Textbox(label="詢問關於此公司")
283
+
284
+ # Logic Wrappers (Similar structure)
285
+ def search_c(q, k, saved):
286
+ svc = UnifiedService(k)
287
+ res = svc.search_companies(q)
288
+ return res, format_df(res, ["公司","產業","標籤"])
289
+
290
+ def select_c(evt: gr.SelectData, res, k, saved):
291
+ svc = UnifiedService(k)
292
+ item = res[evt.index[0]]
293
+ det = svc.get_details(item, "Business Analyst")
294
+ item['details'] = det['text']
295
+ saved.append(item)
296
+ save_data(saved, COMP_SAVE_FILE)
297
+
298
+ display_text = det['text'] + "\n\n📚 來源:\n" + "\n".join([f"- {s['title']}" for s in det['sources']])
299
+ return gr.update(visible=True), display_text, [], item, saved
300
+
301
+ def chat_c(hist, msg, item, k):
302
+ svc = UnifiedService(k)
303
+ reply = svc.chat(hist, msg, item.get('details'), "Business Analyst")
304
+ hist.append((msg, reply))
305
+ return hist, ""
306
+
307
+ c_btn.click(search_c, [c_query, api_key, c_state], [c_state, c_table])
308
+ c_table.select(select_c, [c_state, api_key, c_state], [c_detail_col, c_md, c_chat, c_current, c_state])
309
+ c_msg.submit(chat_c, [c_chat, c_msg, c_current, api_key], [c_chat, c_msg])
310
+
311
+ demo.queue().launch()
312
+
313
+ if __name__ == "__main__":
314
+ main_app()