PPloychor commited on
Commit
4a247f3
·
verified ·
1 Parent(s): 3b370ef

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +470 -0
app.py ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py — Sentiment Analysis with Copy & Export (CSV/XLSX)
2
+
3
+ import gradio as gr
4
+ from transformers import pipeline
5
+ import re
6
+ from functools import lru_cache
7
+ import logging
8
+ from typing import List, Dict, Tuple
9
+ import json
10
+ import os
11
+ import tempfile
12
+
13
+ # ===== NEW: pandas สำหรับ export CSV/XLSX =====
14
+ try:
15
+ import pandas as pd
16
+ except Exception:
17
+ pd = None
18
+
19
+ # ===== Logging =====
20
+ logging.basicConfig(level=logging.INFO)
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # ===== Model list =====
24
+ MODEL_LIST = [
25
+ ("ZombitX64/MultiSent-E5-Pro", "🏆 MultiSent E5 Pro - แนะนำ (ความแม่นยำสูงสุด)"),
26
+ ("ZombitX64/Thai-sentiment-e5", "🎯 Thai Sentiment E5 - เฉพาะภาษาไทย"),
27
+ ("poom-sci/WangchanBERTa-finetuned-sentiment", "🔥 WangchanBERTa - โมเดลไทยยอดนิยม"),
28
+ ("SandboxBhh/sentiment-thai-text-model", "✨ Sandbox Thai - เร็วและแม่นยำ"),
29
+ ("ZombitX64/MultiSent-E5", "⚡ MultiSent E5 - รวดเร็ว"),
30
+ ("Thaweewat/wangchanberta-hyperopt-sentiment-01", "🧠 WangchanBERTa Hyperopt"),
31
+ ("cardiffnlp/twitter-xlm-roberta-base-sentiment", "🌐 XLM-RoBERTa - หลายภาษา"),
32
+ ("phoner45/wangchan-sentiment-thai-text-model", "📱 Wangchan Mobile"),
33
+ ("ZombitX64/Sentiment-01", "🔬 Sentiment v1"),
34
+ ("ZombitX64/Sentiment-02", "🔬 Sentiment v2"),
35
+ ("ZombitX64/Sentiment-03", "🔬 Sentiment v3"),
36
+ ("ZombitX64/sentiment-103", "🔬 Sentiment 103"),
37
+ ("ZombitX64/sentimentSumdata-v1", "🔬 sentimentSumdata-v1"),
38
+ ("ZombitX64/wangchanberta-att-spm-uncased-sentiment", "wangchanberta-att-spm-uncased-sentiment"),
39
+ ]
40
+
41
+ # ===== Cache model loading =====
42
+ @lru_cache(maxsize=3)
43
+ def get_nlp(model_name: str):
44
+ try:
45
+ return pipeline("sentiment-analysis", model=model_name)
46
+ except Exception as e:
47
+ logger.error(f"Error loading model {model_name}: {e}")
48
+ raise gr.Error(f"ไม่สามารถโหลดโมเดล {model_name} ได้: {str(e)}")
49
+
50
+ # ===== Label mappings =====
51
+ MODEL_LABEL_MAPPINGS = {
52
+ "ZombitX64/wangchanberta-att-spm-uncased-sentiment": {
53
+ "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"},
54
+ "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"},
55
+ "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"},
56
+ },
57
+ "ZombitX64/MultiSent-E5-Pro": {
58
+ "LABEL_0": {"code": 0, "name": "question", "emoji": "🤔", "color": "#60a5fa", "bg": "rgba(96,165,250,.2)", "description": "คำถาม"},
59
+ "LABEL_1": {"code": 1, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"},
60
+ "LABEL_2": {"code": 2, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"},
61
+ "LABEL_3": {"code": 3, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"},
62
+ },
63
+ "ZombitX64/Thai-sentiment-e5": {
64
+ "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"},
65
+ "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"},
66
+ "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"},
67
+ },
68
+ "poom-sci/WangchanBERTa-finetuned-sentiment": {
69
+ "neg": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"},
70
+ "neu": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"},
71
+ "pos": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"},
72
+ },
73
+ "SandboxBhh/sentiment-thai-text-model": {
74
+ "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"},
75
+ "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"},
76
+ "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"},
77
+ },
78
+ "ZombitX64/MultiSent-E5": {
79
+ "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"},
80
+ "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"},
81
+ "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"},
82
+ },
83
+ "Thaweewat/wangchanberta-hyperopt-sentiment-01": {
84
+ "neg": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"},
85
+ "neu": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"},
86
+ "pos": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"},
87
+ },
88
+ "cardiffnlp/twitter-xlm-roberta-base-sentiment": {
89
+ "NEGATIVE": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"},
90
+ "NEUTRAL": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"},
91
+ "POSITIVE": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"},
92
+ },
93
+ "phoner45/wangchan-sentiment-thai-text-model": {
94
+ "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"},
95
+ "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"},
96
+ "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"},
97
+ },
98
+ "ZombitX64/Sentiment-01": {
99
+ "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"},
100
+ "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"},
101
+ "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"},
102
+ },
103
+ "ZombitX64/Sentiment-02": {
104
+ "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"},
105
+ "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"},
106
+ "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"},
107
+ },
108
+ "ZombitX64/Sentiment-03": {
109
+ "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"},
110
+ "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"},
111
+ "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"},
112
+ },
113
+ "ZombitX64/sentiment-103": {
114
+ "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"},
115
+ "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"},
116
+ "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"},
117
+ },
118
+ "ZombitX64/sentimentSumdata-v1": {
119
+ "LABEL_0": {"code": 0, "name": "negative", "emoji": "😢", "color": "#f87171", "bg": "rgba(248,113,113,.2)", "description": "เชิงลบ"},
120
+ "LABEL_1": {"code": 1, "name": "neutral", "emoji": "😐", "color": "#facc15", "bg": "rgba(250,204,21,.2)", "description": "เป็นกลาง"},
121
+ "LABEL_2": {"code": 2, "name": "positive", "emoji": "😊", "color": "#34d399", "bg": "rgba(52,211,153,.2)", "description": "เชิงบวก"},
122
+ },
123
+ }
124
+
125
+ def get_label_info(label: str, model_name: str) -> Dict:
126
+ model_mappings = MODEL_LABEL_MAPPINGS.get(model_name, {})
127
+ if label in model_mappings:
128
+ return model_mappings[label]
129
+ return {
130
+ "code": -1, "name": label.lower(), "emoji": "🔍",
131
+ "color": "#64748b", "bg": "rgba(100,116,139,.2)",
132
+ "description": f"ไม่ทราบ ({label})"
133
+ }
134
+
135
+ # ===== Helpers =====
136
+ def split_sentences(text: str) -> List[str]:
137
+ sentences = re.split(r'[.!?।\n]+', text)
138
+ sentences = [s.strip() for s in sentences if s.strip() and len(s.strip()) > 2]
139
+ return sentences
140
+
141
+ def create_confidence_bar(score: float) -> str:
142
+ percentage = int(score * 100)
143
+ return f"""
144
+ <div style="display:flex;align-items:center;gap:10px;margin:8px 0;">
145
+ <div style="flex:1;height:8px;background:#334155;border-radius:4px;overflow:hidden;">
146
+ <div style="width:{percentage}%;height:100%;background:linear-gradient(90deg,#60a5fa,#3b82f6);"></div>
147
+ </div>
148
+ <span style="font-weight:600;color:#cbd5e1;min-width:50px;">{percentage}%</span>
149
+ </div>
150
+ """
151
+
152
+ # ===== Main analyzer (HTML) — ใช้ของเดิมได้เลย =====
153
+ def analyze_text(text: str, model_name: str) -> str:
154
+ if not text or not text.strip():
155
+ return """
156
+ <div style="padding:20px;background:rgba(248,113,113,.2);border-radius:12px;border-left:4px solid #f87171;">
157
+ <div style="color:#f87171;font-weight:600;display:flex;align-items:center;gap:8px;">
158
+ <span style="font-size:20px;">⚠️</span> กรุณาใส่ข้อความที่ต้องการวิเคราะห์
159
+ </div>
160
+ </div>
161
+ """
162
+ sentences = split_sentences(text)
163
+ if not sentences:
164
+ return """
165
+ <div style="padding:20px;background:rgba(248,113,113,.2);border-radius:12px;border-left:4px solid #f87171;">
166
+ <div style="color:#f87171;font-weight:600;display:flex;align-items:center;gap:8px;">
167
+ <span style="font-size:20px;">⚠️</span> ไม่พบประโยคที่สามารถวิเคราะห์ได้ กรุณาใส่ข้อความที่ยาวกว่านี้
168
+ </div>
169
+ </div>
170
+ """
171
+ try:
172
+ nlp = get_nlp(model_name)
173
+ except Exception as e:
174
+ return f"""
175
+ <div style="padding:20px;background:rgba(248,113,113,.2);border-radius:12px;border-left:4px solid #f87171;">
176
+ <div style="color:#f87171;font-weight:600;display:flex;align-items:center;gap:8px;">
177
+ <span style="font-size:20px;">❌</span> เกิดข้อผิดพลาดในการโหลดโมเดล: {str(e)}
178
+ </div>
179
+ </div>
180
+ """
181
+
182
+ html_parts = [f"""
183
+ <div style="background:linear-gradient(135deg,#1e3a8a 0%,#3b82f6 100%);color:#f8fafc;padding:24px;border-radius:16px 16px 0 0;margin-bottom:0;">
184
+ <h2 style="margin:0;font-size:24px;font-weight:700;display:flex;align-items:center;gap:12px;">
185
+ <span style="font-size:28px;">🧠</span> ผลการวิเคราะห์ความรู้สึก
186
+ </h2>
187
+ <p style="margin:8px 0 0 0;opacity:.9;font-size:14px;">โมเดล: {model_name.split('/')[-1]}</p>
188
+ </div>
189
+ """]
190
+
191
+ sentiment_counts = {"positive": 0, "negative": 0, "neutral": 0, "question": 0, "other": 0}
192
+ total_confidence = 0
193
+ sentence_results = []
194
+
195
+ for i, sentence in enumerate(sentences, 1):
196
+ try:
197
+ result = nlp(sentence)[0]
198
+ label = result['label']; score = float(result['score'])
199
+ label_info = get_label_info(label, model_name)
200
+ label_name = label_info["name"]
201
+ if label_name in sentiment_counts:
202
+ sentiment_counts[label_name] += 1
203
+ else:
204
+ sentiment_counts["other"] += 1
205
+ total_confidence += score
206
+ sentence_results.append({
207
+ 'sentence': sentence, 'label_info': label_info, 'score': score,
208
+ 'index': i, 'original_label': label
209
+ })
210
+ except Exception as e:
211
+ logger.error(f"Error analyzing sentence {i}: {e}")
212
+ sentence_results.append({'sentence': sentence, 'error': str(e), 'index': i})
213
+
214
+ html_parts.append("""<div style="background:#0f172a;padding:0;border-radius:0 0 16px 16px;box-shadow:0 4px 20px rgba(0,0,0,.3);overflow:hidden;">""")
215
+
216
+ for r in sentence_results:
217
+ if 'error' in r:
218
+ html_parts.append(f"""
219
+ <div style="padding:20px;border-bottom:1px solid #1e293b;">
220
+ <div style="color:#f87171;font-weight:600;display:flex;align-items:center;gap:8px;">
221
+ <span style="font-size:18px;">❌</span> เกิดข้อผิดพลาดในการวิเคราะห์ประโยคที่ {r['index']}
222
+ </div>
223
+ <p style="color:#94a3b8;margin:8px 0 0 0;font-size:14px;">{r['error']}</p>
224
+ </div>
225
+ """)
226
+ else:
227
+ li = r['label_info']; conf = create_confidence_bar(r['score'])
228
+ html_parts.append(f"""
229
+ <div style="padding:20px;border-bottom:1px solid #1e293b;transition:.2s;" onmouseover="this.style.background='#1e293b'" onmouseout="this.style.background='#0f172a'">
230
+ <div style="display:flex;align-items:flex-start;gap:16px;">
231
+ <div style="background:{li['bg']};padding:12px;border-radius:50%;min-width:48px;height:48px;display:flex;align-items:center;justify-content:center;">
232
+ <span style="font-size:20px;">{li['emoji']}</span>
233
+ </div>
234
+ <div style="flex:1;">
235
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
236
+ <span style="background:{li['color']};color:#f8fafc;padding:4px 12px;border-radius:20px;font-size:12px;font-weight:600;text-transform:uppercase;">{li['description']}</span>
237
+ <span style="color:#94a3b8;font-size:12px;background:#1e293b;padding:2px 8px;border-radius:12px;">{r['original_label']}</span>
238
+ <span style="color:#94a3b8;font-size:14px;">ประโยคที่ {r['index']}</span>
239
+ </div>
240
+ <p style="color:#f8fafc;margin:0 0 12px 0;font-size:16px;line-height:1.5;">"{r['sentence'][:150]}{'...' if len(r['sentence'])>150 else ''}"</p>
241
+ <div style="color:#94a3b8;font-size:14px;margin-bottom:8px;">ความมั่นใจ:</div>
242
+ {conf}
243
+ </div>
244
+ </div>
245
+ </div>
246
+ """)
247
+
248
+ total_sentences = len(sentences)
249
+ avg_conf = total_confidence / total_sentences if total_sentences > 0 else 0
250
+ colors = {"positive":"#34d399","negative":"#f87171","neutral":"#facc15","question":"#60a5fa","other":"#64748b"}
251
+ emojis = {"positive":"😊","negative":"😢","neutral":"😐","question":"🤔","other":"🔍"}
252
+
253
+ chart_items = []
254
+ for s, c in sentiment_counts.items():
255
+ if c > 0:
256
+ pct = (c/total_sentences)*100
257
+ chart_items.append(f"""
258
+ <div style="display:flex;align-items:center;gap:12px;padding:12px;background:rgba(59,130,246,.1);border-radius:8px;">
259
+ <span style="font-size:24px;">{emojis.get(s,'🔍')}</span>
260
+ <div style="flex:1;">
261
+ <div style="font-weight:600;color:#f8fafc;text-transform:capitalize;">{s}</div>
262
+ <div style="color:#94a3b8;font-size:14px;">{c} ประโยค ({pct:.1f}%)</div>
263
+ </div>
264
+ <div style="width:60px;height:6px;background:#334155;border-radius:3px;overflow:hidden;">
265
+ <div style="width:{pct}%;height:100%;background:{colors.get(s,'#64748b')};"></div>
266
+ </div>
267
+ </div>
268
+ """)
269
+
270
+ html_parts.append(f"""
271
+ <div style="padding:24px;background:linear-gradient(135deg,#1e293b 0%,#0f172a 100%);">
272
+ <h3 style="color:#f8fafc;margin:0 0 20px 0;font-size:20px;font-weight:700;display:flex;align-items:center;gap:8px;">
273
+ <span style="font-size:24px;">📊</span> สรุปผลการวิเคราะห์
274
+ </h3>
275
+ <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:20px;">
276
+ <div style="background:#1e293b;padding:20px;border-radius:12px;text-align:center;">
277
+ <div style="font-size:32px;font-weight:700;color:#60a5fa;margin-bottom:4px;">{total_sentences}</div>
278
+ <div style="color:#94a3b8;font-size:14px;">ประโยคทั้งหมด</div>
279
+ </div>
280
+ <div style="background:#1e293b;padding:20px;border-radius:12px;text-align:center;">
281
+ <div style="font-size:32px;font-weight:700;color:#34d399;margin-bottom:4px;">{avg_conf*100:.0f}%</div>
282
+ <div style="color:#94a3b8;font-size:14px;">ความมั่นใจเฉลี่ย</div>
283
+ </div>
284
+ </div>
285
+ <div style="display:grid;gap:8px;">{"".join(chart_items)}</div>
286
+ </div>
287
+ </div>
288
+ """)
289
+ html_parts.append("</div>")
290
+ return "".join(html_parts)
291
+
292
+ # ===== NEW: คืน HTML + JSON โครงสร้าง =====
293
+ def analyze_text_with_data(text: str, model_name: str) -> Tuple[str, str]:
294
+ html = analyze_text(text, model_name)
295
+ sentences = split_sentences(text)
296
+ if not sentences:
297
+ return html, json.dumps({"model": model_name, "items": [], "summary": {}}, ensure_ascii=False)
298
+ try:
299
+ nlp = get_nlp(model_name)
300
+ except Exception:
301
+ return html, json.dumps({"model": model_name, "items": [], "summary": {}}, ensure_ascii=False)
302
+
303
+ items = []
304
+ sentiment_counts = {"positive": 0, "negative": 0, "neutral": 0, "question": 0, "other": 0}
305
+ for i, sentence in enumerate(sentences, 1):
306
+ try:
307
+ r = nlp(sentence)[0]
308
+ raw_label = r["label"]; score = float(r["score"])
309
+ label_info = get_label_info(raw_label, model_name)
310
+ label = label_info.get("name", "other")
311
+ if label not in sentiment_counts:
312
+ label = "other"
313
+ sentiment_counts[label] += 1
314
+ items.append({
315
+ "index": i, "sentence": sentence, "label": label,
316
+ "score": score, "raw_label": raw_label
317
+ })
318
+ except Exception as e:
319
+ items.append({
320
+ "index": i, "sentence": sentence, "label": "error",
321
+ "score": 0.0, "raw_label": f"error: {e}"
322
+ })
323
+
324
+ results_json = json.dumps({"model": model_name, "items": items, "summary": sentiment_counts}, ensure_ascii=False)
325
+ return html, results_json
326
+
327
+ # ===== NEW: ข้อความรวมตาม sentiment สำหรับ Copy =====
328
+ def build_copy_texts(results_json: str) -> Tuple[str, str, str, str, str]:
329
+ try:
330
+ data = json.loads(results_json)
331
+ except Exception:
332
+ return "", "", "", "", ""
333
+ buckets = {"positive": [], "negative": [], "neutral": [], "question": [], "other": []}
334
+ for it in data.get("items", []):
335
+ lb = it.get("label", "other")
336
+ if lb not in buckets:
337
+ lb = "other"
338
+ buckets[lb].append(f"{it.get('index','')}. {it.get('sentence','')}")
339
+ j = lambda xs: "\n".join(xs) if xs else ""
340
+ return j(buckets["positive"]), j(buckets["negative"]), j(buckets["neutral"]), j(buckets["question"]), j(buckets["other"])
341
+
342
+ # ===== NEW: Export CSV/XLSX =====
343
+ def export_csv(results_json: str) -> str:
344
+ data = json.loads(results_json)
345
+ items = data.get("items", [])
346
+ if pd is None:
347
+ import csv
348
+ path = os.path.join(tempfile.gettempdir(), "sentiment_results.csv")
349
+ with open(path, "w", encoding="utf-8", newline="") as f:
350
+ w = csv.writer(f)
351
+ w.writerow(["index","sentence","label","score","raw_label"])
352
+ for it in items:
353
+ w.writerow([it.get("index",""), it.get("sentence",""), it.get("label",""), it.get("score",""), it.get("raw_label","")])
354
+ return path
355
+ df = pd.DataFrame(items, columns=["index","sentence","label","score","raw_label"])
356
+ path = os.path.join(tempfile.gettempdir(), "sentiment_results.csv")
357
+ df.to_csv(path, index=False)
358
+ return path
359
+
360
+ def export_xlsx(results_json: str) -> str:
361
+ if pd is None:
362
+ raise gr.Error("ต้องติดตั้ง pandas/openpyxl ก่อนจึงจะส่งออก .xlsx ได้")
363
+ data = json.loads(results_json)
364
+ items = data.get("items", [])
365
+ df = pd.DataFrame(items, columns=["index","sentence","label","score","raw_label"])
366
+ path = os.path.join(tempfile.gettempdir(), "sentiment_results.xlsx")
367
+ with pd.ExcelWriter(path, engine="openpyxl") as writer:
368
+ df.to_excel(writer, index=False, sheet_name="all")
369
+ for s in ["positive","negative","neutral","question","other"]:
370
+ sdf = df[df["label"] == s]
371
+ if not sdf.empty:
372
+ sdf.to_excel(writer, index=False, sheet_name=s)
373
+ return path
374
+
375
+ # ===== CSS (ย่อเพื่อความกระชับ) =====
376
+ CUSTOM_CSS = """
377
+ * { font-family: 'Inter','Noto Sans Thai',sans-serif !important; }
378
+ body, .gradio-container { background: linear-gradient(135deg,#181f2a 0%,#232e3c 100%) !important; }
379
+ .main-uxui-card { background:#232e3c;border-radius:20px;border:1.5px solid #2d3a4d;padding:24px;color:#e3e8ef; }
380
+ .main-uxui-btn { padding:.9em 2em;border-radius:12px;font-weight:600;background:linear-gradient(90deg,#2563eb 0%,#1e293b 100%);color:#f8fafc;border:none; }
381
+ .main-uxui-input, .main-uxui-dropdown { border:1.5px solid #2d3a4d;background:#1e2533;color:#e3e8ef;padding:14px;border-radius:10px; }
382
+ .main-uxui-output { background:#1e2533;border:1.5px solid #2d3a4d;border-radius:14px;padding:18px; }
383
+ """
384
+
385
+ # ===== UI =====
386
+ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Base(), title="Sentiment Analysis") as demo:
387
+ with gr.Column(elem_classes="main-uxui-card"):
388
+ gr.HTML("<h1 style='text-align:center;margin:0 0 8px 0;'>Sentiment Analysis</h1><p style='text-align:center;color:#7da2e3;margin:0;'>วิเคราะห์ความรู้สึกหลายภาษา + Export ไฟล์</p>")
389
+
390
+ with gr.Row():
391
+ model_dropdown = gr.Dropdown(
392
+ choices=[(desc, name) for name, desc in MODEL_LIST], # label, value
393
+ value=MODEL_LIST[0][0],
394
+ label="เลือกโมเดล (Model)",
395
+ elem_classes="main-uxui-dropdown"
396
+ )
397
+ with gr.Row():
398
+ input_box = gr.Textbox(
399
+ lines=5,
400
+ placeholder="พิมพ์ข้อความ (รองรับหลายประโยค แยกด้วย ., ?, ! หรือขึ้นบรรทัดใหม่)",
401
+ label="ข้อความที่ต้องการวิเคราะห์",
402
+ elem_classes="main-uxui-input"
403
+ )
404
+ with gr.Row():
405
+ analyze_btn = gr.Button("วิเคราะห์", elem_classes="main-uxui-btn")
406
+ clear_btn = gr.Button("ล้างผลลัพธ์", elem_classes="main-uxui-btn")
407
+
408
+ with gr.Tab("ผลลัพธ์"):
409
+ output_html = gr.HTML(label="ผลลัพธ์", elem_classes="main-uxui-output")
410
+
411
+ with gr.Tab("Copy ตาม Sentiment"):
412
+ gr.Markdown("**คัดลอกข้อความที่จัดกลุ่มแล้วตาม sentiment**")
413
+ pos_copy = gr.Textbox(label="😊 Positive", lines=8, show_copy_button=True)
414
+ neg_copy = gr.Textbox(label="😢 Negative", lines=8, show_copy_button=True)
415
+ neu_copy = gr.Textbox(label="😐 Neutral", lines=8, show_copy_button=True)
416
+ q_copy = gr.Textbox(label="🤔 Question", lines=6, show_copy_button=True)
417
+ other_copy = gr.Textbox(label="🔍 Other/Unknown", lines=6, show_copy_button=True)
418
+
419
+ with gr.Tab("Export"):
420
+ results_json = gr.Textbox(visible=False)
421
+ with gr.Row():
422
+ export_csv_btn = gr.Button("⬇️ Export CSV", elem_classes="main-uxui-btn")
423
+ export_xlsx_btn = gr.Button("⬇️ Export Excel (.xlsx)", elem_classes="main-uxui-btn")
424
+ export_file = gr.File(label="ดาวน์โหลดไฟล์ที่นี่", interactive=False)
425
+
426
+ gr.Examples(
427
+ examples=[
428
+ ["วันนี้อากาศดีมากๆ รู้สึกสดชื่นและมีความสุขมาก!"],
429
+ ["เศร้ามากเลยวันนี้ งานเยอะเกินไป"],
430
+ ["อาหารอร่อยดี แต่บริการช้ามาก"],
431
+ ["คุณคิดอย่างไรกับเศรษฐกิจไทย?"],
432
+ ["I love this product! It's amazing."],
433
+ ["이 제품은 별로예요. 다시는 안 살 거예요."],
434
+ ["This is the worst experience I've ever had."]
435
+ ],
436
+ inputs=input_box,
437
+ label="ตัวอย่างข้อความ",
438
+ )
439
+
440
+ # ===== Callbacks =====
441
+ def on_analyze(text, model):
442
+ html, rjson = analyze_text_with_data(text, model)
443
+ pos, neg, neu, qn, other = build_copy_texts(rjson)
444
+ return html, rjson, pos, neg, neu, qn, other
445
+
446
+ analyze_btn.click(on_analyze, [input_box, model_dropdown],
447
+ [output_html, results_json, pos_copy, neg_copy, neu_copy, q_copy, other_copy])
448
+ input_box.submit(on_analyze, [input_box, model_dropdown],
449
+ [output_html, results_json, pos_copy, neg_copy, neu_copy, q_copy, other_copy])
450
+ model_dropdown.change(on_analyze, [input_box, model_dropdown],
451
+ [output_html, results_json, pos_copy, neg_copy, neu_copy, q_copy, other_copy])
452
+
453
+ clear_btn.click(lambda: ("", "", "", "", "", "", ""), None,
454
+ [output_html, results_json, pos_copy, neg_copy, neu_copy, q_copy, other_copy])
455
+
456
+ export_csv_btn.click(export_csv, inputs=results_json, outputs=export_file)
457
+ export_xlsx_btn.click(export_xlsx, inputs=results_json, outputs=export_file)
458
+
459
+ # ===== Launch =====
460
+ if __name__ == "__main__":
461
+ demo.queue(max_size=50, default_concurrency_limit=10).launch(
462
+ server_name="0.0.0.0",
463
+ server_port=7860,
464
+ share=True,
465
+ show_error=True,
466
+ show_api=False,
467
+ quiet=False,
468
+ ssl_verify=False,
469
+ app_kwargs={"docs_url": None, "redoc_url": None},
470
+ )