import os import json import requests import uuid import datetime import tempfile try: import matplotlib.pyplot as plt import matplotlib.dates as mdates HAS_MATPLOTLIB = True except ImportError: plt = None mdates = None HAS_MATPLOTLIB = False import pandas as pd from fpdf import FPDF import gradio as gr from pathlib import Path DATA_FILE = "cases.json" HF_API_URL = "https://api-inference.huggingface.co/models" TRANSCRIBE_MODEL = "openai/whisper-large" IMAGE_CAPTION_MODEL = "nlpconnect/vit-gpt2-image-captioning" OCR_MODEL = "microsoft/trocr-base-printed" TEXT_MODEL = "gpt-3.5-mini" LOGIN_USER = "Neil67" LOGIN_PASSWORD = "AsexqweasD12" CASE_CATEGORIES = ["太陽能", "儲能", "魚電", "再生能源", "停車場", "營建"] HUGGINGFACEHUB_API_TOKEN = os.environ.get("HUGGINGFACEHUB_API_TOKEN") or os.environ.get("HF_TOKEN") APP_ACCESS_KEY = os.environ.get("APP_ACCESS_KEY") HEADERS = {"Authorization": f"Bearer {HUGGINGFACEHUB_API_TOKEN}"} if HUGGINGFACEHUB_API_TOKEN else None def ensure_data_file(): if not os.path.exists(DATA_FILE): with open(DATA_FILE, "w", encoding="utf-8") as f: json.dump([], f, ensure_ascii=False, indent=2) def load_cases(): ensure_data_file() with open(DATA_FILE, "r", encoding="utf-8") as f: return json.load(f) def save_cases(cases): with open(DATA_FILE, "w", encoding="utf-8") as f: json.dump(cases, f, ensure_ascii=False, indent=2) def create_case(site_name, location, client, stage, category, tags, bid_amount, manager_notes): cases = load_cases() new_case = { "id": str(uuid.uuid4()), "site_name": site_name, "location": location, "client": client, "stage": stage, "category": category, "tags": [t.strip() for t in tags.split(",") if t.strip()], "bid_amount": bid_amount, "manager_notes": manager_notes, "created_at": datetime.datetime.now().isoformat(), "meetings": [], "photo_history": [], "ocr_documents": [], "attachments": [] } cases.append(new_case) save_cases(cases) return cases def list_site_options(): cases = load_cases() return [f"{case['site_name']} ({case['location']}) | {case['id']}" for case in cases] def get_case_by_label(label): if not label: return None cases = load_cases() case_id = label.split("|")[-1].strip() return next((case for case in cases if case["id"] == case_id), None) def call_hf_inference(model, body=None, files=None): if HEADERS is None: raise RuntimeError("請先設定 HUGGINGFACEHUB_API_TOKEN 環境變數。") endpoint = f"{HF_API_URL}/{model}" if files: response = requests.post(endpoint, headers=HEADERS, files=files) else: response = requests.post(endpoint, headers=HEADERS, json=body) response.raise_for_status() return response.json() def transcribe_audio(audio_path): if not audio_path: return "" with open(audio_path, "rb") as audio_file: data = call_hf_inference(TRANSCRIBE_MODEL, files={"file": audio_file}) return data.get("text", "") def generate_meeting_minutes(transcript, meeting_title, participants, site_info, extra_focus): prompt = ( "你是專案經理助理。請根據以下會議內容產生:\n" "1. 會議摘要\n" "2. 決議事項與後續行動項目\n" "3. 風險與注意事項\n" "4. 案場分類與細分建議\n" "5. 標案比對重點與專案管理須關注的責任項目\n\n" f"會議標題:{meeting_title}\n" f"參與者:{participants}\n" f"案場:{site_info.get('site_name', '')} / {site_info.get('location', '')} / 客戶:{site_info.get('client', '')}\n" f"案場階段:{site_info.get('stage', '')}\n" f"案場標籤:{', '.join(site_info.get('tags', []))}\n" f"標案金額:{site_info.get('bid_amount', '')}\n" f"專案經理註記:{site_info.get('manager_notes', '')}\n" f"額外重點:{extra_focus}\n\n" f"會議內容:{transcript}\n\n" "請用中文回覆,條列式整理,並用清楚的標題區分每個部分。" ) body = { "inputs": prompt, "parameters": {"max_new_tokens": 700, "temperature": 0.3} } data = call_hf_inference(TEXT_MODEL, body=body) if isinstance(data, list) and data: return data[0].get("generated_text", "") return data.get("generated_text", "") if isinstance(data, dict) else str(data) def add_meeting(site_label, meeting_title, participants, audio, extra_focus): case = get_case_by_label(site_label) if case is None: return "請先選擇或新增案場。" transcript = transcribe_audio(audio) minutes = generate_meeting_minutes(transcript, meeting_title, participants, case, extra_focus) meeting_record = { "id": str(uuid.uuid4()), "title": meeting_title, "participants": participants, "recorded_at": datetime.datetime.now().isoformat(), "audio_path": audio, "transcript": transcript, "minutes": minutes, "focus": extra_focus } cases = load_cases() for stored_case in cases: if stored_case["id"] == case["id"]: stored_case["meetings"].append(meeting_record) break save_cases(cases) return transcript, minutes def build_case_summary(case): meeting_count = len(case.get("meetings", [])) lines = [ f"**案場名稱**:{case['site_name']}", f"**地點**:{case['location']}", f"**案場類別**:{case.get('category', '')}", f"**客戶**:{case['client']}", f"**階段**:{case['stage']}", f"**標案金額**:{case['bid_amount']}", f"**標籤**:{', '.join(case['tags'])}", f"**會議紀錄數**:{meeting_count}", f"**專案管理備註**:{case['manager_notes']}", "---" ] for meeting in case.get("meetings", []): lines.append(f"- {meeting['title']} / {meeting['recorded_at']} / 參與者:{meeting['participants']}") return "\n".join(lines) def compare_sites(): cases = load_cases() rows = [] for case in cases: rows.append( { "案場": case["site_name"], "地點": case["location"], "類別": case.get("category", ""), "客戶": case["client"], "階段": case["stage"], "標案金額": case["bid_amount"], "標籤": ", ".join(case["tags"]), "會議數": len(case.get("meetings", [])) } ) return rows def get_dashboard(case_label): case = get_case_by_label(case_label) if case is None: return "請選擇一個案場以查看專案管理儀表板。" summary = build_case_summary(case) risk = ( "- 風險 1:關鍵時程延誤可能影響標案交付。\n" "- 風險 2:客戶需求變更需同步更新管理計畫。\n" "- 風險 3:多案場資料需統一分類以避免跨場溝通斷層。\n" ) bid_focus = ( "- 比對標案條件:標的範圍、交期、報價、驗收標準。\n" "- 專案經理責任:控管成本、時程、品質、風險、變更管理。\n" "- 下一步:檢視每個案場決策、行動項目與資源配置。\n" ) return f"### 專案管理儀表板\n\n{summary}\n\n### 風險與待辦\n{risk}\n\n### 標案比對與專案經理責任\n{bid_focus}\n" def get_case_detail(case_label): case = get_case_by_label(case_label) if case is None: return "請先選擇一個案場以查看詳細資料。" lines = [ f"### 案場詳細:{case['site_name']}", f"**地點**:{case['location']}", f"**客戶**:{case['client']}", f"**類別**:{case.get('category', '')}", f"**階段**:{case.get('stage', '')}", f"**標案金額**:{case.get('bid_amount', '')}", f"**標籤**:{', '.join(case.get('tags', []))}", f"**專案經理備註**:{case.get('manager_notes', '')}", f"**建立日期**:{case.get('created_at', '')}", f"**會議數**:{len(case.get('meetings', []))}", f"**照片建檔數**:{len(case.get('photo_history', []))}", f"**OCR 文件數**:{len(case.get('ocr_documents', []))}", f"**匯入文件數**:{len(case.get('attachments', []))}", "---", ] if case.get('attachments'): lines.append("#### 匯入文件清單") for attachment in case.get('attachments', [])[:10]: lines.append(f"- {attachment.get('name', '')}") if len(case.get('attachments', [])) > 10: lines.append(f"- ...還有 {len(case.get('attachments', [])) - 10} 個文件") return "\n".join(lines) def generate_case_gantt(case_label): if not HAS_MATPLOTLIB: return None case = get_case_by_label(case_label) if case is None: return None tasks = [] if case.get('meetings'): for meeting in case.get('meetings', []): try: start = datetime.datetime.fromisoformat(meeting['recorded_at']) except Exception: start = datetime.datetime.now() end = start + datetime.timedelta(days=1) tasks.append((meeting['title'] or '會議', start, end)) else: start = datetime.datetime.fromisoformat(case.get('created_at')) if case.get('created_at') else datetime.datetime.now() tasks = [ ("立項準備", start, start + datetime.timedelta(days=7)), ("設計與規劃", start + datetime.timedelta(days=7), start + datetime.timedelta(days=21)), ("採購與動工", start + datetime.timedelta(days=21), start + datetime.timedelta(days=45)), ("施工驗收", start + datetime.timedelta(days=45), start + datetime.timedelta(days=60)), ("交付與回饋", start + datetime.timedelta(days=60), start + datetime.timedelta(days=70)), ] fig, ax = plt.subplots(figsize=(10, max(4, len(tasks) * 0.8))) for idx, (task_name, start, end) in enumerate(tasks): ax.barh(idx, mdates.date2num(end) - mdates.date2num(start), left=mdates.date2num(start), height=0.5) ax.set_yticks(range(len(tasks))) ax.set_yticklabels([task[0] for task in tasks]) ax.xaxis_date() ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d')) ax.set_xlabel('日期') ax.set_title(f"{case['site_name']} 專案進度甘特圖") plt.tight_layout() temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.png') fig.savefig(temp_file.name, bbox_inches='tight') plt.close(fig) return temp_file.name def sanitize_filename(name): return "".join(ch for ch in name if ch.isalnum() or ch in (" ", "-", "_")).rstrip() def get_case_photo_gallery(case_label): case = get_case_by_label(case_label) if case is None: return [] gallery = [] for record in case.get("photo_history", []): gallery.append([record["image_path"], record.get("caption", "")]) return gallery def search_ocr_text(search_text): if not search_text: return [] query = search_text.lower() rows = [] for case in load_cases(): for record in case.get("ocr_documents", []): ocr_text = record.get("ocr_text", "") summary = record.get("summary", "") notes = record.get("notes", "") if query in ocr_text.lower() or query in summary.lower() or query in notes.lower(): excerpt = ocr_text.replace("\n", " ")[:120] if len(ocr_text) > 120: excerpt += "..." rows.append({ "案場": case["site_name"], "時間": record["timestamp"], "檔案": record["image_path"], "OCR 摘要": summary, "文字片段": excerpt }) return rows def generate_case_report_files(case_label): case = get_case_by_label(case_label) if case is None: raise ValueError("請先選擇一個案場。") timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") safe_name = sanitize_filename(case["site_name"]) pdf_path = f"case_report_{safe_name}_{timestamp}.pdf" excel_path = f"case_report_{safe_name}_{timestamp}.xlsx" pdf = FPDF() pdf.set_auto_page_break(auto=True, margin=15) pdf.add_page() font_path = None for candidate in [ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/truetype/noto/NotoSansCJKtc-Regular.otf", "/usr/share/fonts/truetype/noto/NotoSansCJKsc-Regular.otf", "C:\\Windows\\Fonts\\msjh.ttf", "C:\\Windows\\Fonts\\msjh.ttf", "C:\\Windows\\Fonts\\mingliu.ttc" ]: if os.path.exists(candidate): font_path = candidate break if font_path: pdf.add_font("Noto", "", font_path, uni=True) pdf.set_font("Noto", "B", 16) else: pdf.set_font("Arial", "B", 16) pdf.cell(0, 10, f"案場報告:{case['site_name']}", ln=True) if font_path: pdf.set_font("Noto", size=12) else: pdf.set_font("Arial", size=12) pdf.multi_cell(0, 8, f"地點:{case['location']}\n客戶:{case['client']}\n類別:{case.get('category', '')}\n階段:{case['stage']}\n標案金額:{case['bid_amount']}\n標籤:{', '.join(case['tags'])}\n") pdf.ln(4) if font_path: pdf.set_font("Noto", "B", 14) else: pdf.set_font("Arial", "B", 14) pdf.cell(0, 8, "專案管理備註", ln=True) if font_path: pdf.set_font("Noto", size=12) else: pdf.set_font("Arial", size=12) pdf.multi_cell(0, 8, case.get("manager_notes", "")) pdf.ln(4) pdf.set_font("Arial", "B", 14) pdf.cell(0, 8, "會議紀錄", ln=True) pdf.set_font("Arial", size=12) for meeting in case.get("meetings", []): pdf.multi_cell(0, 8, f"- {meeting['title']} ({meeting['recorded_at']})") pdf.multi_cell(0, 8, f" 參與者:{meeting['participants']}") pdf.multi_cell(0, 8, f" 重點:{meeting['focus']}") pdf.ln(4) pdf.set_font("Arial", "B", 14) pdf.cell(0, 8, "照片歷史庫", ln=True) pdf.set_font("Arial", size=12) for record in case.get("photo_history", []): pdf.multi_cell(0, 8, f"- {record['timestamp']}: {record.get('caption', '')}") pdf.multi_cell(0, 8, f" 摘要:{record.get('summary', '')}") pdf.ln(4) pdf.set_font("Arial", "B", 14) pdf.cell(0, 8, "OCR 文件歷史", ln=True) pdf.set_font("Arial", size=12) for record in case.get("ocr_documents", []): pdf.multi_cell(0, 8, f"- {record['timestamp']}: {record.get('notes', '')}") pdf.multi_cell(0, 8, f" 摘要:{record.get('summary', '')}") pdf.output(pdf_path) report_rows = [] for meeting in case.get("meetings", []): report_rows.append({ "類型": "會議紀錄", "時間": meeting["recorded_at"], "內容": meeting["minutes"], "說明": meeting["focus"] }) for record in case.get("photo_history", []): report_rows.append({ "類型": "照片建檔", "時間": record["timestamp"], "內容": record["summary"], "說明": record.get("caption", "") }) for record in case.get("ocr_documents", []): report_rows.append({ "類型": "OCR 文件", "時間": record["timestamp"], "內容": record["summary"], "說明": record.get("notes", "") }) df = pd.DataFrame(report_rows) with pd.ExcelWriter(excel_path, engine="openpyxl") as writer: df.to_excel(writer, index=False, sheet_name="報告摘要") pd.DataFrame([{ "案場": case['site_name'], "地點": case['location'], "類別": case.get('category', ''), "客戶": case['client'], "階段": case['stage'], "標案金額": case['bid_amount'], "標籤": ", ".join(case['tags']), "備註": case.get('manager_notes', '') }]).to_excel(writer, index=False, sheet_name="案場資訊") return pdf_path, excel_path def verify_login(user_id, password, access_key): if user_id != LOGIN_USER or password != LOGIN_PASSWORD: return "帳號或密碼錯誤,請重新輸入。", gr.update(visible=True), gr.update(visible=False) if APP_ACCESS_KEY and access_key != APP_ACCESS_KEY: return "安全存取金鑰錯誤,請檢查環境變數 APP_ACCESS_KEY。", gr.update(visible=True), gr.update(visible=False) return "登入成功,歡迎 Neil。", gr.update(visible=False), gr.update(visible=True) def scan_photo_and_generate_summary(site_label, image_path, photo_notes): case = get_case_by_label(site_label) if case is None: return "請先選擇或新增案場。", "" if not image_path: return "請上傳照片後再掃描建檔。", "" with open(image_path, "rb") as image_file: caption_response = call_hf_inference(IMAGE_CAPTION_MODEL, files={"file": image_file}) if isinstance(caption_response, list) and caption_response: caption = caption_response[0].get("generated_text", "無法辨識照片內容。") elif isinstance(caption_response, dict): caption = caption_response.get("generated_text", "無法辨識照片內容。") else: caption = str(caption_response) prompt = ( "你是專案經理助理。請根據以下照片內容與備註建立建檔摘要,並指出重點:\n" f"照片描述:{caption}\n" f"備註:{photo_notes}\n" "請以條列式回覆,並包含建檔重點與後續專案管理建議。" ) body = { "inputs": prompt, "parameters": {"max_new_tokens": 500, "temperature": 0.2} } data = call_hf_inference(TEXT_MODEL, body=body) if isinstance(data, list) and data: summary = data[0].get("generated_text", "") else: summary = data.get("generated_text", "") if isinstance(data, dict) else str(data) photo_record = { "id": str(uuid.uuid4()), "timestamp": datetime.datetime.now().isoformat(), "image_path": image_path, "caption": caption, "summary": summary, "notes": photo_notes } cases = load_cases() for stored_case in cases: if stored_case["id"] == case["id"]: stored_case["photo_history"].append(photo_record) break save_cases(cases) return caption, summary def perform_ocr(image_path): if not image_path: return "" with open(image_path, "rb") as image_file: data = call_hf_inference(OCR_MODEL, files={"file": image_file}) if isinstance(data, dict): return data.get("generated_text", "") if isinstance(data, list) and data: return data[0].get("generated_text", "") return str(data) def save_ocr_document(case, image_path, ocr_text, ocr_summary, notes): doc_record = { "id": str(uuid.uuid4()), "timestamp": datetime.datetime.now().isoformat(), "image_path": image_path, "ocr_text": ocr_text, "summary": ocr_summary, "notes": notes } case["ocr_documents"].append(doc_record) cases = load_cases() for stored_case in cases: if stored_case["id"] == case["id"]: stored_case["ocr_documents"].append(doc_record) break save_cases(cases) def scan_document_with_ocr(site_label, image_path, doc_notes): case = get_case_by_label(site_label) if case is None: return "請先選擇或新增案場。", "", "" if not image_path: return "請上傳文件照片後再進行 OCR。", "", "" ocr_text = perform_ocr(image_path) prompt = ( "你是專案經理助理。請根據以下文件 OCR 結果與備註建立完整建檔摘要,並指出管理重點:\n" f"OCR 內容:{ocr_text}\n" f"備註:{doc_notes}\n" "請呈現為條列式摘要,並列出後續專案管理建議。" ) body = { "inputs": prompt, "parameters": {"max_new_tokens": 700, "temperature": 0.2} } data = call_hf_inference(TEXT_MODEL, body=body) if isinstance(data, list) and data: summary = data[0].get("generated_text", "") else: summary = data.get("generated_text", "") if isinstance(data, dict) else str(data) save_ocr_document(case, image_path, ocr_text, summary, doc_notes) return "OCR 完成並已存檔。", ocr_text, summary def get_case_photo_history(case_label): case = get_case_by_label(case_label) if case is None: return [] rows = [] for record in case.get("photo_history", []): rows.append({ "時間": record["timestamp"], "照片檔案": record["image_path"], "說明": record["caption"], "摘要": record["summary"] }) return rows def get_case_ocr_history(case_label): case = get_case_by_label(case_label) if case is None: return [] rows = [] for record in case.get("ocr_documents", []): rows.append({ "時間": record["timestamp"], "檔案": record["image_path"], "OCR 內容": record["ocr_text"][:100] + "...", "摘要": record["summary"] }) return rows def refresh_site_dropdown(): update = gr.Dropdown.update(choices=list_site_options()) return update, update, update, update, update, update, update, update def clear_site_inputs(): return "", "", "", "", "", "", "" with gr.Blocks(title="CH-01 錄音會議與案場管理系統") as demo: gr.HTML(""" """) with gr.Column(elem_id="login_area", elem_classes="glass-panel") as login_area: gr.Markdown("## 會員專屬登入") gr.Markdown("請設定 Hugging Face Space Secrets:`HUGGINGFACEHUB_API_TOKEN` 與 `APP_ACCESS_KEY`(若啟用資安金鑰)。") login_user = gr.Textbox(label="登入ID", placeholder="輸入你的帳號", lines=1) login_pass = gr.Textbox(label="密碼", type="password", placeholder="輸入你的密碼", lines=1) login_key = gr.Textbox(label="安全存取金鑰", type="password", placeholder="若已設定請輸入", lines=1) login_button = gr.Button("登入") login_message = gr.Textbox(label="登入狀態", interactive=False, lines=1) with gr.Column(elem_id="main_area", elem_classes="glass-panel", visible=False) as main_area: gr.Markdown("# CH-01 會議錄音與專案管理系統") gr.Markdown("此系統支援:錄音轉文字、會議紀錄生成、案場管理、拍照掃描建檔、跨案場比較與專案管理儀表板。") with gr.Tab("會議錄音與紀錄"): with gr.Row(): with gr.Column(): site_selector = gr.Dropdown(label="選擇案場", choices=list_site_options(), interactive=True) meeting_title = gr.Textbox(label="會議標題", placeholder="輸入會議名稱", lines=1) participants = gr.Textbox(label="參與者", placeholder="輸入參與者名稱,用逗號分隔", lines=1) extra_focus = gr.Textbox(label="重點/標案細分說明", placeholder="例如:標案對比、現場分類、驗收重點", lines=2) audio_input = gr.Audio(sources=["microphone"], type="filepath", label="錄音/上傳音檔") transcribe_button = gr.Button("轉錄並生成會議紀錄") with gr.Column(): transcript_output = gr.Textbox(label="會議逐字稿", interactive=False, lines=12) minutes_output = gr.Textbox(label="會議紀錄與專案管理摘要", interactive=False, lines=18) transcribe_button.click( add_meeting, inputs=[site_selector, meeting_title, participants, audio_input, extra_focus], outputs=[transcript_output, minutes_output] ) with gr.Tab("案場管理"): with gr.Row(): with gr.Column(): site_name = gr.Textbox(label="案場名稱", placeholder="輸入案場名稱") location = gr.Textbox(label="地點", placeholder="輸入案場地址/區域") client = gr.Textbox(label="客戶", placeholder="輸入客戶名稱") stage = gr.Textbox(label="階段", placeholder="例如:招標/設計/施工/驗收") category = gr.Dropdown(label="案場類別", choices=CASE_CATEGORIES, value=CASE_CATEGORIES[0], interactive=True) new_category = gr.Textbox(label="新增案場類別", placeholder="若需要新增類別請輸入", lines=1) tags = gr.Textbox(label="標籤", placeholder="輸入案場細分類,如:智慧照明, 工程招標", lines=1) bid_amount = gr.Textbox(label="標案金額", placeholder="輸入預估金額") manager_notes = gr.Textbox(label="專案經理備註", placeholder="輸入管理、品質、驗收、風險等注意事項", lines=3) add_site_btn = gr.Button("新增/儲存案場") with gr.Column(): case_summary = gr.Markdown("### 案場列表與會議摘要") case_list = gr.Dataframe(value=compare_sites(), headers=["案場", "地點", "類別", "客戶", "階段", "標案金額", "標籤", "會議數"], interactive=False) site_message = gr.Textbox(label="案場建立狀態", interactive=False, lines=1) def add_site_and_refresh(site_name, location, client, stage, category, new_category, tags, bid_amount, manager_notes): chosen_category = category if new_category and new_category.strip(): chosen_category = new_category.strip() if chosen_category not in CASE_CATEGORIES: CASE_CATEGORIES.append(chosen_category) create_case(site_name, location, client, stage, chosen_category, tags, bid_amount, manager_notes) site_update = gr.Dropdown.update(choices=list_site_options()) category_update = gr.Dropdown.update(choices=CASE_CATEGORIES, value=chosen_category) return ( compare_sites(), site_update, site_update, site_update, site_update, site_update, site_update, site_update, site_update, category_update, "新增案場已成功儲存。", ) with gr.Tab("拍照掃描建檔"): with gr.Row(): with gr.Column(): photo_site = gr.Dropdown(label="選擇案場", choices=list_site_options(), interactive=True) photo_input = gr.Image(type="filepath", label="拍照/上傳照片") photo_notes = gr.Textbox(label="備註說明", placeholder="輸入現場或文件重點備註", lines=2) scan_button = gr.Button("掃描並生成建檔摘要") with gr.Column(): scan_caption = gr.Textbox(label="照片內容辨識", interactive=False, lines=4) scan_summary = gr.Textbox(label="建檔重點摘要", interactive=False, lines=12) scan_button.click( scan_photo_and_generate_summary, inputs=[photo_site, photo_input, photo_notes], outputs=[scan_caption, scan_summary] ) with gr.Tab("OCR 文件建檔"): with gr.Row(): with gr.Column(): doc_site = gr.Dropdown(label="選擇案場", choices=list_site_options(), interactive=True) doc_image = gr.Image(type="filepath", label="上傳文件照片") doc_notes = gr.Textbox(label="文件備註", placeholder="輸入文件重點、版本或來源說明", lines=2) ocr_button = gr.Button("OCR 並生成全文建檔") with gr.Column(): ocr_status = gr.Textbox(label="OCR 狀態", interactive=False, lines=1) ocr_text = gr.Textbox(label="OCR 文字內容", interactive=False, lines=8) ocr_summary = gr.Textbox(label="文件建檔摘要", interactive=False, lines=12) ocr_button.click( scan_document_with_ocr, inputs=[doc_site, doc_image, doc_notes], outputs=[ocr_status, ocr_text, ocr_summary] ) with gr.Tab("照片歷史庫"): with gr.Row(): with gr.Column(): history_site = gr.Dropdown(label="選擇案場", choices=list_site_options(), interactive=True) refresh_history = gr.Button("更新照片歷史") with gr.Column(): photo_history_table = gr.Dataframe(headers=["時間", "照片檔案", "說明", "摘要"], interactive=False) ocr_history_table = gr.Dataframe(headers=["時間", "檔案", "OCR 內容", "摘要"], interactive=False) refresh_history.click( lambda label: (get_case_photo_history(label), get_case_ocr_history(label)), inputs=[history_site], outputs=[photo_history_table, ocr_history_table] ) with gr.Tab("手機快速掃描"): with gr.Row(): with gr.Column(): mobile_site = gr.Dropdown(label="選擇案場", choices=list_site_options(), interactive=True) mobile_photo = gr.Image(type="filepath", label="手機拍照/上傳照片") mobile_notes = gr.Textbox(label="備註", placeholder="快速輸入現場或文件說明", lines=2) mobile_scan_button = gr.Button("快速掃描建檔") with gr.Column(): mobile_caption = gr.Textbox(label="掃描結果", interactive=False, lines=4) mobile_summary = gr.Textbox(label="快速建檔摘要", interactive=False, lines=10) mobile_scan_button.click( scan_photo_and_generate_summary, inputs=[mobile_site, mobile_photo, mobile_notes], outputs=[mobile_caption, mobile_summary] ) with gr.Tab("照片瀑布流畫廊"): with gr.Row(): with gr.Column(): gallery_site = gr.Dropdown(label="選擇案場", choices=list_site_options(), interactive=True) refresh_gallery = gr.Button("更新畫廊") with gr.Column(): photo_gallery = gr.Gallery(label="案場照片瀑布流", show_label=True) refresh_gallery.click(get_case_photo_gallery, inputs=[gallery_site], outputs=[photo_gallery]) with gr.Tab("OCR 文字全文搜尋"): with gr.Row(): with gr.Column(): ocr_search = gr.Textbox(label="搜尋關鍵字", placeholder="輸入 OCR 文字搜尋字詞", lines=1) search_button = gr.Button("搜尋 OCR") with gr.Column(): search_results = gr.Dataframe(headers=["案場", "時間", "檔案", "OCR 摘要", "文字片段"], interactive=False) search_button.click(search_ocr_text, inputs=[ocr_search], outputs=[search_results]) with gr.Tab("報告匯出"): with gr.Row(): with gr.Column(): export_site = gr.Dropdown(label="選擇案場", choices=list_site_options(), interactive=True) export_button = gr.Button("匯出 PDF/Excel 報告") with gr.Column(): pdf_report = gr.File(label="下載 PDF 報告") excel_report = gr.File(label="下載 Excel 報告") export_button.click(generate_case_report_files, inputs=[export_site], outputs=[pdf_report, excel_report]) with gr.Tab("多案場比較"): compare_button = gr.Button("更新比較表") compare_result = gr.Dataframe(headers=["案場", "地點", "類別", "客戶", "階段", "標案金額", "標籤", "會議數"], interactive=False) compare_button.click(compare_sites, outputs=[compare_result]) with gr.Tab("案場詳細檢視"): detail_site = gr.Dropdown(label="選擇案場查看詳細資料", choices=list_site_options(), interactive=True) detail_button = gr.Button("查看案場詳細") detail_display = gr.Markdown() detail_button.click(get_case_detail, inputs=[detail_site], outputs=[detail_display]) with gr.Tab("專案進度甘特圖"): gantt_site = gr.Dropdown(label="選擇案場生成甘特圖", choices=list_site_options(), interactive=True) gantt_button = gr.Button("生成專案甘特圖") gantt_image = gr.Image(label="專案進度甘特圖", interactive=False) gantt_button.click(generate_case_gantt, inputs=[gantt_site], outputs=[gantt_image]) with gr.Tab("專案管理儀表板"): dashboard_site = gr.Dropdown(label="選擇案場查看儀表板", choices=list_site_options(), interactive=True) dashboard_display = gr.Markdown() dashboard_button = gr.Button("刷新儀表板") dashboard_button.click(get_dashboard, inputs=[dashboard_site], outputs=[dashboard_display]) add_site_btn.click( add_site_and_refresh, inputs=[site_name, location, client, stage, category, new_category, tags, bid_amount, manager_notes], outputs=[ case_list, site_selector, dashboard_site, photo_site, doc_site, mobile_site, history_site, gallery_site, export_site, category, site_message, ], ) with gr.Row(): reset_button = gr.Button("重新整理案場列表") reset_button.click(refresh_site_dropdown, outputs=[site_selector, dashboard_site, photo_site, doc_site, mobile_site, history_site, gallery_site, export_site]) gr.Markdown("---\n請在 Hugging Face Space 中設定 `HUGGINGFACEHUB_API_TOKEN` 為你的 HF 金鑰,然後部署此應用。") login_button.click( verify_login, inputs=[login_user, login_pass, login_key], outputs=[login_message, login_area, main_area] ) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860)