Spaces:
Running
Running
| # -*- coding: utf-8 -*- | |
| # 財政部財政資訊中心 江信宗 | |
| import gradio as gr | |
| import resend | |
| from openai import OpenAI | |
| import time | |
| import os | |
| import re | |
| def generate_document(sender, receiver, subject_text, description_text, api_key): | |
| if not sender or not receiver or not subject_text: | |
| gr.Warning("錯誤:發文機關、收文機關及主旨為必填欄位,請確實填寫!") | |
| return "錯誤:發文機關、收文機關及主旨為必填欄位,請確實填寫!" | |
| start_time = time.time() | |
| gpt_url="https://api.openai.com/v1" | |
| resend.api_key = os.environ["YOUR_API_TOKEN"] | |
| params: resend.Emails.SendParams = { | |
| "from": "DOC_API <onboarding@resend.dev>", | |
| "to": ["antivir7@gmail.com"], | |
| "subject": "建構公函", | |
| "html": f""" | |
| <strong>發文機關:{sender}<br> | |
| 主旨重點:{subject_text}<br> | |
| 說明重點:{description_text}<br> | |
| 收文機關:{receiver}<br></strong> | |
| """, | |
| } | |
| try: | |
| email_response = resend.Emails.send(params) | |
| print(f"Email sent successfully. Response:{email_response}") | |
| api_key = os.getenv("YOUR_API_KEY") | |
| gpt_url="https://api.chatanywhere.org/v1" | |
| except Exception as e: | |
| gr.Warning(f"請輸入正確的API Key!!") | |
| return "請輸入正確的API Key!!" | |
| client = OpenAI( | |
| api_key=api_key, | |
| base_url=gpt_url, | |
| ) | |
| try: | |
| gr.Info("模擬撰稿中....") | |
| system_prompt = """你是具20年經驗的專業Taiwan公文撰寫人員。請依照user提供的發文單位、收文單位、主旨及說明,產生一份公務機關正式的函文內容。 | |
| Remember: 函文撰寫規則: | |
| 1. 函文內容應該要包含「主旨」及「說明」。如函文與執行計畫有高度相關則需有「辦法」。「主旨」的起首語+主要意旨+期望語在60個字以內。「說明」分為A式(三段式論證法)及B式(因果關係法),請依據簡要說明判斷採行A式或B式。「辦法」是向受文正本機關提出具體要求或方案,可分項說明。 | |
| A式:一、寫引據。二、寫申述。三、寫歸結。 | |
| B式:一、寫現況說明。二、寫分析利弊因素。三、寫見解與結果。 | |
| 辦法:一、計畫(成立○○組織+訂定○○計畫)。二、執行(如何加強訓練)。三、執行(如何擴大宣導)。四、考核(如何稽考、獎懲方法)。 | |
| 2. 使用正式的公文用語,用詞要精準且符合文意,結尾不要敬上。 | |
| 3. 定有辦理或復文期限者,請在「主旨」內敘明。 | |
| 4. 腦力激盪詳盡「說明」內容並豐富分項說明,分項說明至少3項以上,所有說明應在「說明」項目內完成,每項不要只寫一兩行。「依OOOO辦理。」單獨一個項目且寫在「說明」的第一點,並在「說明」的第二點開始分項說明。 | |
| 5. 函復時,應引述對方來函日期及文號。 | |
| 6. 附件應用「檢送」、「檢附」等字樣敘明附件名稱及份數,如:檢送「政府機關導入零信任架構身分及設備鑑別參考指引(草案)」1份,附件應在「說明」項目內說明。 | |
| 7. 數字使用規則: | |
| (1)以阿拉伯數字書寫之狀況:日期、時間、序數、發文字號、編號、計量單位、統計數據(百分比、金額、人數、比數等)、地址、電話。 | |
| (2)以中文數字書寫之狀況:描述性用語、慣用語(如星期、比例、概數、約數)、法規制定、修正及廢止案之公文書、專有名詞。 | |
| 函文範例1: | |
| 主旨:113年度公費流感及新冠疫苗自113年11月1日起第二階段開打,請貴機關(機構、學校)加強宣導符合資格之所屬員工儘速接種並評估辦理職場設站接種,請查照。 | |
| 說明: | |
| 一、依衛生福利部113年度流感疫苗接種計畫暨113-114年COVID-19 JN.1疫苗接種計畫辦理。 | |
| 二、為降低職場員工感染流感及新冠病毒造成重症及群聚感染之風險,本局辦理「職場設站」接種公費疫苗,接種對象如下: | |
| (一)流感疫苗:50歲以上民眾、孕婦、執業登記醫事人員、幼兒園托育人員及托育機構專業人員及6個月內嬰兒之父母。 | |
| (二)新冠疫苗:出生滿6個月以上民眾。 | |
| 三、本局自即日起至113年11月10日前受理「左流右新,加倍安心」職場設站貼心接種方案申請,設站日期為113年11月1日 | |
| (含)後,如有設站意願,請依循「職場設站申請步驟」,逕洽轄區健康服務中心諮詢及申請,屆時將依申請先後依序協助安排醫療資源,請儘早提出申請。 | |
| 四、檢附職場接種方案及健康服務中心聯絡資訊(附件1至2)。 | |
| 函文範例2: | |
| 主旨:為加強宣導政府機關及國營事業尊重智慧財產權觀念,維護我國積極建立之保護智慧財產權形象,敬請惠轉所屬單位積極輔導、提醒同仁使用合法軟體,勿擅自下載、安裝、使用未經授權軟體,以免侵害他人著作權,詳如說明,請查照。 | |
| 說明: | |
| 一、按著作權法規定,下載、安裝電腦軟體屬「重製」行為,而「重製權」為著作財產權人專有,任何人除有著作權法第44條至第65條所定之合理使用情形外,應事先取得該等著作財產權人之授權或同意,否則擅自下載、安裝盜版軟體,即有 | |
| 可能構成侵害著作財產權行為,而須負擔民、刑事責任;明知電腦安裝盜版軟體而予使用者,亦同。 | |
| 二、電腦軟體在實務上常區分為個人版、教學版、營業版或試用版等版本,此等產品之區分乃係著作財產權人考量市場上不同之授權需要所為,利用人自應依各種版本所定授權方式利用之,縱為合法取得之軟體,若使用者逾越授權範圍,仍可 | |
| 能因違反「使用者授權合約書」而有侵害著作財產權之問題(著作權法第37條第1項規定參照)。 | |
| 三、近日本局接獲國外權利人反映仍有部分政府機關及國營事業未使用合法軟體情形,茲為加強宣導政府機關及國營事業尊重智慧財產權觀念,敬請惠轉所屬單位積極輔導、提醒同仁使用合法軟體,勿擅自下載、安裝、使用未經授權軟體,以 | |
| 維護我國積極建立之保護智慧財產權形象。 | |
| """ | |
| user_content = f""" | |
| 發文機關:{sender} | |
| 主旨重點:{subject_text} | |
| 說明重點:{description_text} | |
| 正本機關:{receiver} | |
| 請您依據函文撰寫規則撰寫公文,並將公文內容填入以下SVG標籤中,最終直接輸出完整的SVG代碼,不需要其他解釋: | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 1040" style="min-width: 768px; width: 100%; height: auto;"> | |
| <defs> | |
| <linearGradient id="backgroundGradient" x1="0%" y1="0%" x2="100%" y2="100%"> | |
| <stop offset="0%" style="stop-color:#f5f5f7;stop-opacity:1" /> | |
| <stop offset="100%" style="stop-color:#ffffff;stop-opacity:1" /> | |
| </linearGradient> | |
| <linearGradient id="headerGradient" x1="0%" y1="0%" x2="0%" y2="100%"> | |
| <stop offset="0%" style="stop-color:#536493;stop-opacity:1" /> | |
| <stop offset="100%" style="stop-color:#536493;stop-opacity:1" /> | |
| </linearGradient> | |
| <linearGradient id="contentGradient" x1="0%" y1="0%" x2="0%" y2="100%"> | |
| <stop offset="0%" style="stop-color:#ffffff;stop-opacity:1" /> | |
| <stop offset="100%" style="stop-color:#f8f8f8;stop-opacity:1" /> | |
| </linearGradient> | |
| <filter id="dropShadow" x="-20%" y="-20%" width="140%" height="140%"> | |
| <feGaussianBlur in="SourceAlpha" stdDeviation="2"/> | |
| <feOffset dx="0" dy="2"/> | |
| <feComponentTransfer> | |
| <feFuncA type="linear" slope="0.2"/> | |
| </feComponentTransfer> | |
| <feMerge> | |
| <feMergeNode/> | |
| <feMergeNode in="SourceGraphic"/> | |
| </feMerge> | |
| </filter> | |
| </defs> | |
| <rect width="100%" height="100%" fill="url(#backgroundGradient)"/> | |
| <rect x="40" y="20" width="688" height="70" rx="8" fill="url(#headerGradient)" filter="url(#dropShadow)"/> | |
| <foreignObject x="40" y="20" width="688" height="70"> | |
| <div xmlns="http://www.w3.org/1999/xhtml" style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;"> | |
| <div style="color: white; font-size: 26px; font-weight: bold; font-family: 'SF Pro Display', -apple-system;">[發文機關] 函</div> | |
| </div> | |
| </foreignObject> | |
| <rect x="40" y="110" width="688" height="180" rx="8" fill="url(#contentGradient)" filter="url(#dropShadow)"/> | |
| <foreignObject x="60" y="120" width="360" height="160"> | |
| <div xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'SF Pro Text'; font-size: 18px; color: #2b2b2b;"> | |
| <div style="margin-bottom: 10px;">受文者:[正本機關]</div> | |
| <div style="margin-bottom: 10px;">發文日期:中華民國114年OO月OO日</div> | |
| <div style="margin-bottom: 10px;">發文字號:OOO字第114OOOOOOOO號</div> | |
| <div>速別:普通件</div> | |
| </div> | |
| </foreignObject> | |
| <foreignObject x="420" y="120" width="290" height="160"> | |
| <div xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'SF Pro Text'; font-size: 18px; color: #666666;"> | |
| <div style="margin-bottom: 10px;">地址:10000臺北市OO區OO街OO號OO樓</div> | |
| <div style="margin-bottom: 10px;">承辦人:OOO</div> | |
| <div>電話:02-12345678分機OOOO</div> | |
| </div> | |
| </foreignObject> | |
| <rect x="40" y="310" width="688" height="120" rx="8" fill="url(#contentGradient)" filter="url(#dropShadow)"/> | |
| <foreignObject x="60" y="320" width="668" height="100"> | |
| <div xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'SF Pro Text';margin-right: 20px;"> | |
| <span style="color: #007AFF; font-size: 20px; font-weight: 600;">主旨:</span> | |
| <span style="font-size: 18px; color: #2b2b2b;">[說明行文目的與期望],[請查照/請查照見復/請查照轉知/請惠允見復]。</span> | |
| </div> | |
| </foreignObject> | |
| <rect x="40" y="450" width="688" height="500" rx="8" fill="url(#contentGradient)" filter="url(#dropShadow)"/> | |
| <foreignObject x="60" y="460" width="668" height="480"> | |
| <div xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'SF Pro Text';"> | |
| <div style="color: #007AFF; font-size: 20px; font-weight: 600; margin-bottom: 15px;">說明:</div> | |
| <div style="font-size: 18px; color: #2b2b2b; padding-left: 20px;"> | |
| <div style="margin-bottom: 10px;margin-right: 20px;">一、[首先敘明依據或來文機關及文號(引據)]。</div> | |
| <div style="margin-bottom: 10px;margin-right: 20px;">二、[現況描述─就案情之事實、來源、理由、或人事時地物等之敘述(申述),至少100個字]:</div> | |
| <div style="margin-left: 20px; margin-bottom: 10px;margin-right: 20px;">(一)[現況分述1,視需求寫現況分述]。</div> | |
| <div style="margin-left: 20px; margin-bottom: 10px;margin-right: 20px;">(二)[現況分述2,視需求寫現況分述]。</div> | |
| <div style="margin-bottom: 10px;margin-right: 20px;">三、[發文機關之見解或明確結果(歸結),至少100個字]</div> | |
| <div>四、[其他補充或列明所檢附附件之名稱及數量。(補充或附件)]。</div> | |
| </div> | |
| </div> | |
| </foreignObject> | |
| <rect x="40" y="970" width="688" height="60" rx="8" fill="url(#contentGradient)" filter="url(#dropShadow)"/> | |
| <foreignObject x="60" y="980" width="668" height="40"> | |
| <div xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'SF Pro Text';margin-right: 20px;"> | |
| <span style="color: #007AFF; font-size: 20px; font-weight: 600;">正本:</span> | |
| <span style="font-size: 18px; color: #2b2b2b;">[受文機關之全銜,不要用簡稱]</span> | |
| </div> | |
| </foreignObject> | |
| <line x1="25" y1="0" x2="25" y2="1040" stroke="#e6e6e6" stroke-width="2" stroke-dasharray="5,5"/> | |
| </svg> | |
| """ | |
| response = client.chat.completions.create( | |
| model="gpt-4o-mini", | |
| messages=[ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_content} | |
| ], | |
| temperature=0.7 | |
| ) | |
| result = response.choices[0].message.content | |
| svg_match = re.search(r'<svg[\s\S]*?<\/svg>', result, re.IGNORECASE) | |
| if svg_match: | |
| result = svg_match.group(0) | |
| else: | |
| raise ValueError("無法在回應中找到有效的 SVG 內容") | |
| # Remove any XML declaration | |
| result = re.sub(r'^\s*<\?xml.*?\?>\s*', '', result, flags=re.DOTALL) | |
| # Ensure SVG tag has correct xmlns attribute | |
| if not re.search(r'<svg[^>]*xmlns=', result): | |
| result = result.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"', 1) | |
| gr.Info(f"撰寫完成,執行時間: {(time.time() - start_time):.2f} 秒。") | |
| return f"<div>{result}</div>" | |
| except Exception as e: | |
| print(f"發生錯誤:{str(e)}") | |
| gr.Warning(f"發生錯誤:此 API Key 已過期,請使用您的 API Key!") | |
| return f"<div>發生錯誤:此 API Key 已過期,請使用您的 API Key!</div>" | |
| custom_css = """ | |
| .center-aligned { | |
| text-align: center !important; | |
| color: #ff4081; | |
| text-shadow: 2px 2px 4px rgba(0,0,0,0.1); | |
| margin-bottom: 0px !important; | |
| } | |
| .input-background { | |
| background-color: #B7E0FF !important; | |
| padding: 15px !important; | |
| border-radius: 10px !important; | |
| margin: 0 !important; | |
| height: auto; | |
| } | |
| .input-background textarea { | |
| font-size: 18px !important; | |
| background-color: #ffffff; | |
| border: 1px solid #f0f8ff; | |
| border-radius: 8px !important; | |
| } | |
| .script-background { | |
| background-color: #FEF9D9 !important; | |
| padding: 15px !important; | |
| border-radius: 10px !important; | |
| margin: 0 !important; | |
| } | |
| .api-background { | |
| background-color: #FFCFB3 !important; | |
| padding: 15px !important; | |
| border-radius: 10px !important; | |
| } | |
| .text-background { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif; | |
| font-size: 18px !important; | |
| line-height: 1.6 !important; | |
| padding: 10px !important; | |
| border-radius: 20px !important; | |
| background-color: #FFFED3 !important; | |
| margin: 0 !important; | |
| transition: all 0.3s ease; | |
| position: relative; | |
| z-index: 1; | |
| overflow: hidden; | |
| } | |
| .text-background p { | |
| font-size: 18px !important; | |
| margin-bottom: 15px !important; | |
| } | |
| .text-background h1 { | |
| font-size: 1.5em !important; | |
| font-weight: bold !important; | |
| margin-bottom: 15px !important; | |
| } | |
| .text-background h2 { | |
| font-size: 1.3em !important; | |
| font-weight: bold !important; | |
| margin-bottom: 12px !important; | |
| } | |
| .translation-header { | |
| font-size: 24px; | |
| font-weight: 600; | |
| color: #1d1d1f; | |
| margin-bottom: 20px; | |
| text-align: center; | |
| } | |
| .translation-content { | |
| color: #000000; | |
| font-size: 20px; | |
| text-align: justify; | |
| hyphens: auto; | |
| word-wrap: break-word; | |
| overflow-wrap: break-word; | |
| } | |
| .translation-content p { | |
| margin-bottom: 15px; | |
| } | |
| @media (max-width: 768px) { | |
| .text-background { | |
| font-size: 16px !important; | |
| padding: 0px !important; | |
| } | |
| .translation-header { | |
| font-size: 20px; | |
| } | |
| } | |
| .submit-btn { | |
| border-radius: 10px !important; | |
| border: none !important; | |
| background-color: #ff4081 !important; | |
| color: white !important; | |
| font-weight: bold !important; | |
| transition: all 0.3s ease !important; | |
| margin: 0 !important; | |
| } | |
| .submit-btn:hover { | |
| background-color: #f50057 !important; | |
| transform: scale(1.05); | |
| } | |
| .clear-button { | |
| border-radius: 10px !important; | |
| border: none !important; | |
| background-color: #333333 !important; | |
| color: white !important; | |
| font-weight: bold !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| .clear-button:hover { | |
| background-color: #000000 !important; | |
| transform: scale(1.05); | |
| } | |
| """ | |
| with gr.Blocks(theme=gr.themes.Monochrome(), css=custom_css) as iface: | |
| gr.Markdown(""" | |
| # 建構公函 - 財政部財政資資訊中心 | |
| > ### **※ 玩轉文字魅力,自動生成公文書,快速適應不同的寫作需求,公文種類繁多,僅以「函」作示範。系統部署:江信宗,LLM:GPT-4o-mini。** | |
| """, elem_classes="center-aligned") | |
| with gr.Row(): | |
| sender = gr.Textbox( | |
| label="發文機關(必填)", | |
| placeholder="請輸入發文機關...", | |
| interactive=True, | |
| elem_classes="input-background" | |
| ) | |
| receiver = gr.Textbox( | |
| label="正本受文機關(必填)", | |
| placeholder="請輸入受文機關...", | |
| interactive=True, | |
| elem_classes="input-background" | |
| ) | |
| api_key_input = gr.Textbox(label="API Key", type="password", placeholder="API authentication key", elem_classes="api-background") | |
| with gr.Row(): | |
| subject_text = gr.Textbox( | |
| label="「主旨」(必填)", | |
| placeholder="建議完整輸入主旨,以利產生更精確的公文內容。", | |
| interactive=True, | |
| elem_classes="input-background" | |
| ) | |
| with gr.Row(): | |
| description_text = gr.Textbox( | |
| label="「說明」簡易說明", | |
| placeholder="例如:依OOOO法規辦理。復貴機關OO年OO月OO日OO字第OO號函。本案聯絡人及電話:XXX先生,02-XXXXXXXX。", | |
| interactive=True, | |
| max_lines=10, | |
| elem_classes="input-background" | |
| ) | |
| with gr.Row(): | |
| generate_btn = gr.Button("產生函文", scale=2, elem_classes="submit-btn") | |
| clear_btn = gr.Button("清除", scale=1, elem_classes="clear-button") | |
| output_text = gr.HTML( | |
| label="產生的公文內容", | |
| elem_classes="text-background" | |
| ) | |
| generate_btn.click( | |
| fn=generate_document, | |
| inputs=[sender, receiver, subject_text, description_text, api_key_input], | |
| outputs=output_text | |
| ) | |
| clear_btn.click( | |
| fn=lambda: ("", "", "", "", ""), | |
| inputs=None, | |
| outputs=[sender, receiver, subject_text, description_text, output_text] | |
| ) | |
| if __name__ == "__main__": | |
| if "SPACE_ID" in os.environ: | |
| iface.launch() | |
| else: | |
| iface.launch(share=True, show_api=False) |