# app.py — Sentiment Analysis with Copy & Export (CSV/XLSX) import gradio as gr from transformers import pipeline import re from functools import lru_cache import logging from typing import List, Dict, Tuple import json import os import tempfile # ===== NEW: pandas สำหรับ export CSV/XLSX ===== try: import pandas as pd except Exception: pd = None # ===== Logging ===== logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # ===== Model list ===== MODEL_LIST = [ ("ZombitX64/MultiSent-E5-Pro", "🏆 MultiSent E5 Pro - แนะนำ (ความแม่นยำสูงสุด)"), ("ZombitX64/Thai-sentiment-e5", "🎯 Thai Sentiment E5 - เฉพาะภาษาไทย"), ("poom-sci/WangchanBERTa-finetuned-sentiment", "🔥 WangchanBERTa - โมเดลไทยยอดนิยม"), ("SandboxBhh/sentiment-thai-text-model", "✨ Sandbox Thai - เร็วและแม่นยำ"), ("ZombitX64/MultiSent-E5", "⚡ MultiSent E5 - รวดเร็ว"), ("Thaweewat/wangchanberta-hyperopt-sentiment-01", "🧠 WangchanBERTa Hyperopt"), ("cardiffnlp/twitter-xlm-roberta-base-sentiment", "🌐 XLM-RoBERTa - หลายภาษา"), ("phoner45/wangchan-sentiment-thai-text-model", "📱 Wangchan Mobile"), ("ZombitX64/Sentiment-01", "🔬 Sentiment v1"), ("ZombitX64/Sentiment-02", "🔬 Sentiment v2"), ("ZombitX64/Sentiment-03", "🔬 Sentiment v3"), ("ZombitX64/sentiment-103", "🔬 Sentiment 103"), ("ZombitX64/sentimentSumdata-v1", "🔬 sentimentSumdata-v1"), ("ZombitX64/wangchanberta-att-spm-uncased-sentiment", "wangchanberta-att-spm-uncased-sentiment"), ] # ===== Cache model loading ===== @lru_cache(maxsize=3) def get_nlp(model_name: str): try: return pipeline("sentiment-analysis", model=model_name) except Exception as e: logger.error(f"Error loading model {model_name}: {e}") raise gr.Error(f"ไม่สามารถโหลดโมเดล {model_name} ได้: {str(e)}") # ===== Label mappings ===== MODEL_LABEL_MAPPINGS = { "ZombitX64/wangchanberta-att-spm-uncased-sentiment": { "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"}, "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"}, "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"}, }, "ZombitX64/MultiSent-E5-Pro": { "LABEL_0": {"code": 0, "name": "question", "emoji": "🤔", "color": "#60a5fa", "bg": "rgba(96,165,250,.2)", "description": "คำถาม"}, "LABEL_1": {"code": 1, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"}, "LABEL_2": {"code": 2, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"}, "LABEL_3": {"code": 3, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"}, }, "ZombitX64/Thai-sentiment-e5": { "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"}, "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"}, "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"}, }, "poom-sci/WangchanBERTa-finetuned-sentiment": { "neg": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"}, "neu": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"}, "pos": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"}, }, "SandboxBhh/sentiment-thai-text-model": { "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"}, "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"}, "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"}, }, "ZombitX64/MultiSent-E5": { "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"}, "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"}, "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"}, }, "Thaweewat/wangchanberta-hyperopt-sentiment-01": { "neg": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"}, "neu": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"}, "pos": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"}, }, "cardiffnlp/twitter-xlm-roberta-base-sentiment": { "NEGATIVE": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"}, "NEUTRAL": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"}, "POSITIVE": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"}, }, "phoner45/wangchan-sentiment-thai-text-model": { "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"}, "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"}, "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"}, }, "ZombitX64/Sentiment-01": { "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"}, "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"}, "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"}, }, "ZombitX64/Sentiment-02": { "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"}, "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"}, "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"}, }, "ZombitX64/Sentiment-03": { "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"}, "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"}, "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"}, }, "ZombitX64/sentiment-103": { "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"}, "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"}, "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"}, }, "ZombitX64/sentimentSumdata-v1": { "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"}, "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"}, "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"}, }, } def get_label_info(label: str, model_name: str) -> Dict: model_mappings = MODEL_LABEL_MAPPINGS.get(model_name, {}) if label in model_mappings: return model_mappings[label] return { "code": -1, "name": label.lower(), "emoji": "🔍", "color": "#64748b", "bg": "rgba(100,116,139,.2)", "description": f"ไม่ทราบ ({label})" } # ===== Helpers ===== def split_sentences(text: str) -> List[str]: sentences = re.split(r'[.!?।\n]+', text) sentences = [s.strip() for s in sentences if s.strip() and len(s.strip()) > 2] return sentences def create_confidence_bar(score: float) -> str: percentage = int(score * 100) return f"""
โมเดล: {model_name.split('/')[-1]}
วิเคราะห์ความรู้สึกหลายภาษา + Export ไฟล์
") with gr.Row(): model_dropdown = gr.Dropdown( choices=[(desc, name) for name, desc in MODEL_LIST], # label, value value=MODEL_LIST[0][0], label="เลือกโมเดล (Model)", elem_classes="main-uxui-dropdown" ) with gr.Row(): input_box = gr.Textbox( lines=5, placeholder="พิมพ์ข้อความ (รองรับหลายประโยค แยกด้วย ., ?, ! หรือขึ้นบรรทัดใหม่)", label="ข้อความที่ต้องการวิเคราะห์", elem_classes="main-uxui-input" ) with gr.Row(): analyze_btn = gr.Button("วิเคราะห์", elem_classes="main-uxui-btn") clear_btn = gr.Button("ล้างผลลัพธ์", elem_classes="main-uxui-btn") with gr.Tab("ผลลัพธ์"): output_html = gr.HTML(label="ผลลัพธ์", elem_classes="main-uxui-output") with gr.Tab("Copy ตาม Sentiment"): gr.Markdown("**คัดลอกข้อความที่จัดกลุ่มแล้วตาม sentiment**") pos_copy = gr.Textbox(label="😊 Positive", lines=8, show_copy_button=True) neg_copy = gr.Textbox(label="😢 Negative", lines=8, show_copy_button=True) neu_copy = gr.Textbox(label="😐 Neutral", lines=8, show_copy_button=True) q_copy = gr.Textbox(label="🤔 Question", lines=6, show_copy_button=True) other_copy = gr.Textbox(label="🔍 Other/Unknown", lines=6, show_copy_button=True) with gr.Tab("Export"): results_json = gr.Textbox(visible=False) with gr.Row(): export_csv_btn = gr.Button("⬇️ Export CSV", elem_classes="main-uxui-btn") export_xlsx_btn = gr.Button("⬇️ Export Excel (.xlsx)", elem_classes="main-uxui-btn") export_file = gr.File(label="ดาวน์โหลดไฟล์ที่นี่", interactive=False) gr.Examples( examples=[ ["วันนี้อากาศดีมากๆ รู้สึกสดชื่นและมีความสุขมาก!"], ["เศร้ามากเลยวันนี้ งานเยอะเกินไป"], ["อาหารอร่อยดี แต่บริการช้ามาก"], ["คุณคิดอย่างไรกับเศรษฐกิจไทย?"], ["I love this product! It's amazing."], ["이 제품은 별로예요. 다시는 안 살 거예요."], ["This is the worst experience I've ever had."] ], inputs=input_box, label="ตัวอย่างข้อความ", ) # ===== Callbacks ===== def on_analyze(text, model): html, rjson = analyze_text_with_data(text, model) pos, neg, neu, qn, other = build_copy_texts(rjson) return html, rjson, pos, neg, neu, qn, other analyze_btn.click(on_analyze, [input_box, model_dropdown], [output_html, results_json, pos_copy, neg_copy, neu_copy, q_copy, other_copy]) input_box.submit(on_analyze, [input_box, model_dropdown], [output_html, results_json, pos_copy, neg_copy, neu_copy, q_copy, other_copy]) model_dropdown.change(on_analyze, [input_box, model_dropdown], [output_html, results_json, pos_copy, neg_copy, neu_copy, q_copy, other_copy]) clear_btn.click(lambda: ("", "", "", "", "", "", ""), None, [output_html, results_json, pos_copy, neg_copy, neu_copy, q_copy, other_copy]) export_csv_btn.click(export_csv, inputs=results_json, outputs=export_file) export_xlsx_btn.click(export_xlsx, inputs=results_json, outputs=export_file) # ===== Launch ===== if __name__ == "__main__": demo.queue(max_size=50, default_concurrency_limit=10).launch( server_name="0.0.0.0", server_port=7860, share=True, show_error=True, show_api=False, quiet=False, ssl_verify=False, app_kwargs={"docs_url": None, "redoc_url": None}, )