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 # Set up logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Enhanced model list with descriptions 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 for 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)}") # Model-specific label mappings MODEL_LABEL_MAPPINGS = { # wangchanberta-att-spm-uncased-sentiment - 3 classes ( negative, neutral, positive) "ZombitX64/wangchanberta-att-spm-uncased-sentiment": { "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248, 113, 113, 0.2)", "description": "เชิงลบ"}, "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250, 204, 21, 0.2)", "description": "เป็นกลาง"}, "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52, 211, 153, 0.2)", "description": "เชิงบวก"}, }, # MultiSent E5 Pro - 4 classes (question, negative, neutral, positive) "ZombitX64/MultiSent-E5-Pro": { "LABEL_0": {"code": 0, "name": "question", "emoji": "🤔", "color": "#60a5fa", "bg": "rgba(96, 165, 250, 0.2)", "description": "คำถาม"}, "LABEL_1": {"code": 1, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248, 113, 113, 0.2)", "description": "เชิงลบ"}, "LABEL_2": {"code": 2, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250, 204, 21, 0.2)", "description": "เป็นกลาง"}, "LABEL_3": {"code": 3, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52, 211, 153, 0.2)", "description": "เชิงบวก"}, }, # Thai Sentiment E5 - 3 classes (negative, neutral, positive) "ZombitX64/Thai-sentiment-e5": { "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248, 113, 113, 0.2)", "description": "เชิงลบ"}, "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250, 204, 21, 0.2)", "description": "เป็นกลาง"}, "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52, 211, 153, 0.2)", "description": "เชิงบวก"}, }, # WangchanBERTa - usually neg/neu/pos "poom-sci/WangchanBERTa-finetuned-sentiment": { "neg": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248, 113, 113, 0.2)", "description": "เชิงลบ"}, "neu": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250, 204, 21, 0.2)", "description": "เป็นกลาง"}, "pos": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52, 211, 153, 0.2)", "description": "เชิงบวก"}, }, # Sandbox Thai - 3 classes "SandboxBhh/sentiment-thai-text-model": { "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248, 113, 113, 0.2)", "description": "เชิงลบ"}, "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250, 204, 21, 0.2)", "description": "เป็นกลาง"}, "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52, 211, 153, 0.2)", "description": "เชิงบวก"}, }, # MultiSent E5 - 3 classes "ZombitX64/MultiSent-E5": { "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248, 113, 113, 0.2)", "description": "เชิงลบ"}, "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250, 204, 21, 0.2)", "description": "เป็นกลาง"}, "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52, 211, 153, 0.2)", "description": "เชิงบวก"}, }, # WangchanBERTa Hyperopt "Thaweewat/wangchanberta-hyperopt-sentiment-01": { "neg": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248, 113, 113, 0.2)", "description": "เชิงลบ"}, "neu": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250, 204, 21, 0.2)", "description": "เป็นกลาง"}, "pos": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52, 211, 153, 0.2)", "description": "เชิงบวก"}, }, # Twitter XLM-RoBERTa - NEGATIVE/NEUTRAL/POSITIVE "cardiffnlp/twitter-xlm-roberta-base-sentiment": { "NEGATIVE": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248, 113, 113, 0.2)", "description": "เชิงลบ"}, "NEUTRAL": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250, 204, 21, 0.2)", "description": "เป็นกลาง"}, "POSITIVE": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52, 211, 153, 0.2)", "description": "เชิงบวก"}, }, # Wangchan Mobile "phoner45/wangchan-sentiment-thai-text-model": { "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248, 113, 113, 0.2)", "description": "เชิงลบ"}, "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250, 204, 21, 0.2)", "description": "เป็นกลาง"}, "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52, 211, 153, 0.2)", "description": "เชิงบวก"}, }, # ZombitX64 Sentiment models - 3 classes "ZombitX64/Sentiment-01": { "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248, 113, 113, 0.2)", "description": "เชิงลบ"}, "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250, 204, 21, 0.2)", "description": "เป็นกลาง"}, "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52, 211, 153, 0.2)", "description": "เชิงบวก"}, }, "ZombitX64/Sentiment-02": { "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248, 113, 113, 0.2)", "description": "เชิงลบ"}, "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250, 204, 21, 0.2)", "description": "เป็นกลาง"}, "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52, 211, 153, 0.2)", "description": "เชิงบวก"}, }, "ZombitX64/Sentiment-03": { "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248, 113, 113, 0.2)", "description": "เชิงลบ"}, "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250, 204, 21, 0.2)", "description": "เป็นกลาง"}, "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52, 211, 153, 0.2)", "description": "เชิงบวก"}, }, "ZombitX64/sentiment-103": { "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248, 113, 113, 0.2)", "description": "เชิงลบ"}, "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250, 204, 21, 0.2)", "description": "เป็นกลาง"}, "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52, 211, 153, 0.2)", "description": "เชิงบวก"}, }, "ZombitX64/sentimentSumdata-v1": { "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248, 113, 113, 0.2)", "description": "เชิงลบ"}, "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250, 204, 21, 0.2)", "description": "เป็นกลาง"}, "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52, 211, 153, 0.2)", "description": "เชิงบวก"}, }, } def get_label_info(label: str, model_name: str) -> Dict: """Get label information for specific model with fallback for unknown labels""" model_mappings = MODEL_LABEL_MAPPINGS.get(model_name, {}) if label in model_mappings: return model_mappings[label] # Fallback for unknown labels return { "code": -1, "name": label.lower(), "emoji": "🔍", "color": "#64748b", "bg": "rgba(100, 116, 139, 0.2)", "description": f"ไม่ทราบ ({label})" } def split_sentences(text: str) -> List[str]: """Enhanced sentence splitting with better Thai support""" 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: """Create a modern confidence visualization""" percentage = int(score * 100) return f"""
{percentage}%
""" def analyze_text(text: str, model_name: str) -> str: """Enhanced text analysis with modern HTML formatting""" if not text or not text.strip(): return """
⚠️ กรุณาใส่ข้อความที่ต้องการวิเคราะห์
""" sentences = split_sentences(text) if not sentences: return """
⚠️ ไม่พบประโยคที่สามารถวิเคราะห์ได้ กรุณาใส่ข้อความที่ยาวกว่านี้
""" try: nlp = get_nlp(model_name) except Exception as e: return f"""
เกิดข้อผิดพลาดในการโหลดโมเดล: {str(e)}
""" # Header html_parts = [f"""

🧠 ผลการวิเคราะห์ความรู้สึก

โมเดล: {model_name.split('/')[-1]}

"""] sentiment_counts = {"positive": 0, "negative": 0, "neutral": 0, "question": 0, "other": 0} total_confidence = 0 sentence_results = [] # Analyze each sentence for i, sentence in enumerate(sentences, 1): try: result = nlp(sentence)[0] label = result['label'] score = result['score'] label_info = get_label_info(label, model_name) label_name = label_info["name"] if label_name in sentiment_counts: sentiment_counts[label_name] += 1 else: sentiment_counts["other"] += 1 total_confidence += score # Store result for display sentence_results.append({ 'sentence': sentence, 'label_info': label_info, 'score': score, 'index': i, 'original_label': label }) except Exception as e: logger.error(f"Error analyzing sentence {i}: {e}") sentence_results.append({ 'sentence': sentence, 'error': str(e), 'index': i }) # Results container html_parts.append("""
""") # Individual sentence results for result in sentence_results: if 'error' in result: html_parts.append(f"""
เกิดข้อผิดพลาดในการวิเคราะห์ประโยคที่ {result['index']}

{result['error']}

""") else: label_info = result['label_info'] confidence_bar = create_confidence_bar(result['score']) html_parts.append(f"""
{label_info['emoji']}
{label_info['description']} {result['original_label']} ประโยคที่ {result['index']}

"{result['sentence'][:150]}{'...' if len(result['sentence']) > 150 else ''}"

ความมั่นใจ:
{confidence_bar}
""") # Summary section total_sentences = len(sentences) avg_confidence = total_confidence / total_sentences if total_sentences > 0 else 0 # Create chart data for summary chart_items = [] colors = {"positive": "#34d399", "negative": "#f87171", "neutral": "#facc15", "question": "#60a5fa", "other": "#64748b"} emojis = {"positive": "😊", "negative": "😢", "neutral": "😐", "question": "🤔", "other": "🔍"} for sentiment, count in sentiment_counts.items(): if count > 0: percentage = (count / total_sentences) * 100 chart_items.append(f"""
{emojis.get(sentiment, '🔍')}
{sentiment}
{count} ประโยค ({percentage:.1f}%)
""") html_parts.append(f"""

📊 สรุปผลการวิเคราะห์

{total_sentences}
ประโยคทั้งหมด
{avg_confidence*100:.0f}%
ความมั่นใจเฉลี่ย
{"".join(chart_items)}
""") html_parts.append("
") return "".join(html_parts) # Modern CSS with dark blue theme CUSTOM_CSS = """ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap'); * { font-family: 'Inter', 'Noto Sans Thai', sans-serif !important; } body, .gradio-container { background: linear-gradient(135deg, #181f2a 0%, #232e3c 100%) !important; min-height: 100vh; } .main-uxui-card { background: #232e3c !important; border-radius: 20px; box-shadow: 0 6px 32px rgba(0,0,0,0.22); border: 1.5px solid #2d3a4d; padding: 32px 28px 28px 28px; margin: 0 0 32px 0; color: #e3e8ef !important; transition: box-shadow 0.2s; } .main-uxui-card:hover { box-shadow: 0 12px 36px rgba(0,0,0,0.28); } .main-uxui-header { text-align: center; margin-bottom: 32px; } .main-uxui-header h1 { font-size: 2.8em; color: #e3e8ef; font-weight: 800; margin-bottom: 0.2em; letter-spacing: 0.5px; } .main-uxui-header p { color: #7da2e3; font-size: 1.25em; margin-top: 0; margin-bottom: 0.5em; } .main-uxui-section-title { font-size: 1.18em; color: #7da2e3; font-weight: 700; margin-bottom: 12px; letter-spacing: 0.2px; display: flex; align-items: center; gap: 8px; } .main-uxui-btn { font-size: 1.13em; padding: 0.9em 2.7em; border-radius: 13px; font-weight: 600; background: linear-gradient(90deg, #2563eb 0%, #1e293b 100%); color: #f8fafc !important; border: none; box-shadow: 0 2px 8px #1e253355; transition: all 0.2s; } .main-uxui-btn:hover { filter: brightness(1.08); box-shadow: 0 6px 18px #1e253377; transform: translateY(-2px) scale(1.03); } .main-uxui-btn.secondary { background: #232e3c; color: #7da2e3 !important; border: 1.5px solid #2d3a4d; } .main-uxui-input, .main-uxui-dropdown { font-size: 1.13em; border-radius: 10px; border: 1.5px solid #2d3a4d; background: #1e2533; color: #e3e8ef; padding: 14px; margin-bottom: 10px; } .main-uxui-dropdown { min-width: 220px; } .main-uxui-output { background: #1e2533; border-radius: 14px; border: 1.5px solid #2d3a4d; color: #e3e8ef; padding: 22px 18px; font-size: 1.08em; min-height: 180px; margin-bottom: 0; } .main-uxui-legend { background: #232e3c; border-radius: 16px; border: 1.5px solid #2d3a4d; color: #7da2e3; padding: 24px 18px; margin-top: 32px; font-size: 1.05em; } .main-uxui-legend .legend-row { display: flex; gap: 24px; flex-wrap: wrap; margin-top: 12px; } .main-uxui-legend .legend-item { flex: 1 1 180px; background: #1e2533; border-radius: 10px; padding: 16px 10px; margin-bottom: 10px; text-align: center; border: 1px solid #2d3a4d; } .main-uxui-legend .legend-item strong { color: #e3e8ef; font-size: 1.08em; } .main-uxui-legend .legend-item small { color: #7da2e3; } @media (max-width: 900px) { .main-uxui-card { padding: 16px 6px; } .main-uxui-header h1 { font-size: 2em; } .main-uxui-section-title { font-size: 1em; } } """ # Gradio Blocks app definition with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Base()) as demo: with gr.Column(elem_classes="main-uxui-card"): with gr.Row(): gr.HTML("""

Sentiment Analysis

วิเคราะห์ความรู้สึกภาษาไทย และอื่นๆ etc. รองรับหลายโมเดล

""") with gr.Row(): model_dropdown = gr.Dropdown( choices=[(desc, name) for name, desc in MODEL_LIST], value=MODEL_LIST[0][0], label="เลือกโมเดล (Model)", elem_classes="main-uxui-dropdown" ) with gr.Row(): input_box = gr.Textbox( lines=4, 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 secondary") with gr.Row(): output_html = gr.HTML(label="ผลลัพธ์", elem_classes="main-uxui-output") with gr.Row(): gr.Examples([ ["วันนี้อากาศดีมากๆ รู้สึกสดชื่นและมีความสุขมาก!"], ["เศร้ามากเลยวันนี้ งานเยอะเกินไป"], ["อาหารอร่อยดี แต่บริการช้ามาก"], ["คุณคิดอย่างไรกับเศรษฐกิจไทย?"], ["I love this product! It's amazing."], ["이 제품에는 매우 나쁩니다."], ["इस उत्पाद के लिए बहुत बुरा.."], ["This is the worst experience I've ever had."] ], inputs=input_box, label="ตัวอย่างข้อความ", ) with gr.Row(): gr.HTML("""
🗂️ คำอธิบายผลลัพธ์
😊 เชิงบวก
Positive
😢 เชิงลบ
Negative
😐 เป็นกลาง
Neutral
🤔 คำถาม
Question
""") def on_analyze(text, model): return analyze_text(text, model) analyze_btn.click(on_analyze, [input_box, model_dropdown], output_html) input_box.submit(on_analyze, [input_box, model_dropdown], output_html) model_dropdown.change(on_analyze, [input_box, model_dropdown], output_html) clear_btn.click(lambda: (""), None, output_html) # Launch configuration 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, favicon_path=None, ssl_verify=False, app_kwargs={ "docs_url": None, "redoc_url": None, } )