Dusit-P commited on
Commit
f1f96f6
·
verified ·
1 Parent(s): d65ba09

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +188 -414
app.py CHANGED
@@ -1,56 +1,26 @@
1
- # app.py — Thai Sentiment (WangchanBERTa Variants) - ปรับปรุง UI และเพิ่ม Shop Analysis
2
  import os, json, importlib.util, traceback, re, math, tempfile, datetime
3
  import gradio as gr
4
  import torch, pandas as pd
5
  import torch.nn.functional as F
6
  import plotly.graph_objects as go
7
- from plotly.subplots import make_subplots
8
  from huggingface_hub import hf_hub_download
9
  from safetensors.torch import load_file
10
  from transformers import AutoTokenizer
11
 
12
  # ================= Settings =================
13
  REPO_ID = os.getenv("REPO_ID", "Dusit-P/thai-sentiment")
14
- DEFAULT_MODEL = os.getenv("DEFAULT_MODEL", "WCB")
15
  HF_TOKEN = os.getenv("HF_TOKEN", None)
16
 
17
  # เลือกเฉพาะโมเดลที่ให้ผลดีที่สุด
18
  AVAILABLE_CHOICES = ["WCB", "WCB_BiLSTM"]
19
- # โมเดลที่ซ่อนไว้ (uncomment เพื่อเปิดใช้):
20
- # AVAILABLE_CHOICES = ["WCB", "WCB_BiLSTM", "WCB_CNN_BiLSTM", "WCB_4Layer_BiLSTM"]
21
-
22
- if DEFAULT_MODEL not in AVAILABLE_CHOICES:
23
- DEFAULT_MODEL = "WCB_BiLSTM" # เปลี่ยน default เป็นตัวที่ดีที่สุด
24
 
25
  NEG_COLOR = "#F87171"
26
  POS_COLOR = "#34D399"
27
- NEUTRAL_COLOR = "#94A3B8"
28
  TEMPLATE = "plotly_white"
29
  CACHE = {}
30
 
31
- # ================= Date Presets (แก้ไขให้ทำงานถูกต้อง) =================
32
- DATE_PRESETS = {
33
- "ทั้งหมด": None,
34
- "7 วันล่าสุด": 7,
35
- "15 วันล่าสุด": 15,
36
- "30 วันล่าสุด": 30,
37
- "90 วันล่าสุด": 90
38
- }
39
-
40
- def apply_date_preset(df, date_col, preset_key):
41
- """กรองข้อมูลตาม preset ที่เลือก"""
42
- if preset_key == "ทั้งหมด" or preset_key not in DATE_PRESETS:
43
- return df
44
-
45
- days = DATE_PRESETS[preset_key]
46
- if days is None:
47
- return df
48
-
49
- now = pd.Timestamp.now()
50
- cutoff = now - pd.Timedelta(days=days)
51
-
52
- return df[df[date_col] >= cutoff]
53
-
54
  # ================= Loader =================
55
  def _import_models():
56
  if "models_module" in CACHE:
@@ -101,8 +71,8 @@ def _is_substantive_text(s, min_chars=2):
101
  def _format_pct(x): return f"{x*100:.2f}%"
102
  def _to_datetime_safe(s): return pd.to_datetime(s, errors="coerce", infer_datetime_format=True, utc=False)
103
 
104
- LIKELY_TEXT_COLS = ["text","review","message","comment","content","sentence","body","ข้อความ","รีวิว"]
105
- LIKELY_DATE_COLS = ["date","created_at","time","timestamp","datetime","วันที่","วันเวลา","เวลา"]
106
  LIKELY_SHOP_COLS = ["shop","store","branch","ร้าน","สาขา","ชื่อร้าน"]
107
 
108
  def detect_columns(df):
@@ -136,19 +106,40 @@ def detect_columns(df):
136
  if c.lower() in LIKELY_SHOP_COLS:
137
  shop_candidates.append(c)
138
  continue
139
- # ตรวจว่ามีค่าซ้ำพอสมควร (เหมือนเป็น categorical)
140
  if df[c].dtype == object:
141
  unique_ratio = df[c].nunique() / len(df)
142
- if 0.01 <= unique_ratio <= 0.5: # 1-50% ของข้อมูลเป็นค่าซ้ำ
143
  shop_candidates.append(c)
144
  shop_candidates = list(dict.fromkeys(shop_candidates))
145
  shop_col = shop_candidates[0] if len(shop_candidates) > 0 else None
146
 
147
  return text_col, date_candidates, date_col, shop_candidates, shop_col
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  # ================= Charts =================
150
- def make_summary_chart(df, chart_type="pie"):
151
- """สร้างกราฟสรุปแบบเดียว (ไม่ซ้ำซ้อน)"""
152
  total = len(df)
153
  neg_count = len(df[df["label"]=="negative"])
154
  pos_count = len(df[df["label"]=="positive"])
@@ -156,101 +147,39 @@ def make_summary_chart(df, chart_type="pie"):
156
  neg_avg = pd.to_numeric(df["negative(%)"].str.rstrip("%"), errors="coerce").mean()
157
  pos_avg = pd.to_numeric(df["positive(%)"].str.rstrip("%"), errors="coerce").mean()
158
 
159
- info = (f"**📊 สรุปผลการวิเคราะห์**\n\n"
160
- f"- 📝 ทั้งหมด: **{total:,}** รีวิว\n"
161
- f"- 😞 เชิงลบ: **{neg_count:,}** ({neg_count/total*100:.1f}%)\n"
162
- f"- 😊 เชิงบวก: **{pos_count:,}** ({pos_count/total*100:.1f}%)\n"
163
- f"- 📈 ค่าเฉลี่ยความมั่นใจ:\n"
164
- f" - เชิงลบ: {neg_avg:.2f}%\n"
165
- f" - เชิงบวก: {pos_avg:.2f}%")
166
-
167
- if chart_type == "pie":
168
- fig = go.Figure(go.Pie(
169
- labels=["😞 เชิงลบ","😊 เชิงบวก"],
170
- values=[neg_count, pos_count],
171
- hole=0.4,
172
- marker=dict(colors=[NEG_COLOR, POS_COLOR]),
173
- textinfo='label+percent',
174
- textfont_size=14
175
- ))
176
- fig.update_layout(
177
- title="สัดส่วนรีวิวเชิงบวก vs เชิงลบ",
178
- template=TEMPLATE,
179
- height=400
180
- )
181
- else: # bar
182
- fig = go.Figure()
183
- fig.add_bar(
184
- x=["เชิงลบ","เชิงบวก"],
185
- y=[neg_count, pos_count],
186
- marker_color=[NEG_COLOR, POS_COLOR],
187
- text=[neg_count, pos_count],
188
- textposition='auto'
189
- )
190
- fig.update_layout(
191
- title="จำนวนรีวิวแยกตามความรู้สึก",
192
- template=TEMPLATE,
193
- yaxis_title="จำนวน (รีวิว)",
194
- height=400
195
- )
196
-
197
- return fig, info
198
-
199
- def _resample_counts(df, date_col, freq="auto"):
200
- """รวมข้อมูลตามช่วงเวลา - auto-detect frequency"""
201
- if freq == "auto":
202
- # Auto-detect ตามช่วงเวลาของข้อมูล
203
- date_range = (df[date_col].max() - df[date_col].min()).days
204
- if date_range <= 30:
205
- freq = "D" # รายวัน
206
- elif date_range <= 90:
207
- freq = "W" # รายสัปดาห์
208
- else:
209
- freq = "M" # รายเดือน
210
-
211
- g = df.groupby([pd.Grouper(key=date_col, freq=freq),"label"]).size().unstack(fill_value=0)
212
- for c in ["negative","positive"]:
213
- if c not in g.columns: g[c]=0
214
- return g[["negative","positive"]].sort_index(), freq
215
-
216
- def make_time_chart(df, date_col, freq="auto"):
217
- """กราฟแนวโน้มตามเวลา"""
218
- ts, actual_freq = _resample_counts(df, date_col, freq)
219
-
220
- fig = go.Figure()
221
- fig.add_scatter(
222
- x=ts.index, y=ts["negative"],
223
- mode="lines+markers",
224
- name="😞 เชิงลบ",
225
- line=dict(color=NEG_COLOR, width=2),
226
- marker=dict(size=6)
227
- )
228
- fig.add_scatter(
229
- x=ts.index, y=ts["positive"],
230
- mode="lines+markers",
231
- name="😊 เชิงบวก",
232
- line=dict(color=POS_COLOR, width=2),
233
- marker=dict(size=6)
234
- )
235
-
236
- freq_map = {"D": "รายวัน", "W": "รายสัปดาห์", "M": "รายเดือน"}
237
-
238
  fig.update_layout(
239
- title=f"📈 แนวโน้มรีวิวตามเวลา ({freq_map[actual_freq]})",
240
  template=TEMPLATE,
241
- xaxis_title="วันที่",
242
- yaxis_title="จำนวนรีวิว",
243
- hovermode='x unified',
244
- height=450
245
  )
246
 
247
- return fig
 
 
 
 
 
 
248
 
249
- def make_shop_analysis(df, shop_col, date_col=None, freq="auto"):
250
- """วิเคราะห์แยกตามร้าน/สาขา"""
 
 
 
 
 
251
 
252
- # 1. สรุปภาพรวมแต่ละร้าน
253
- shop_summary = []
254
  for shop in df[shop_col].unique():
255
  if pd.isna(shop):
256
  continue
@@ -258,126 +187,84 @@ def make_shop_analysis(df, shop_col, date_col=None, freq="auto"):
258
  neg = len(shop_df[shop_df["label"]=="negative"])
259
  pos = len(shop_df[shop_df["label"]=="positive"])
260
  total = len(shop_df)
261
- pos_ratio = pos / total * 100 if total > 0 else 0
262
-
263
- shop_summary.append({
264
- "ร้าน/สาขา": shop,
265
- "รีวิวทั้งหมด": total,
266
- "😞 เชิงลบ": neg,
267
- "😊 เชิงบวก": pos,
268
- "% เชิงบวก": f"{pos_ratio:.1f}%"
269
  })
270
 
271
- summary_df = pd.DataFrame(shop_summary).sort_values("รีวิวทั้งหมด", ascending=False)
272
-
273
- # 2. กราฟเปรียบเทียบร้าน
274
- fig_compare = go.Figure()
275
 
276
- shops = summary_df["ร้าน/สาขา"].tolist()
277
- negs = summary_df["😞 เชิงลบ"].tolist()
278
- poss = summary_df["😊 เชิงบวก"].tolist()
 
 
 
 
 
 
 
 
 
 
 
279
 
280
- fig_compare.add_bar(name="😞 เชิงลบ", x=shops, y=negs, marker_color=NEG_COLOR)
281
- fig_compare.add_bar(name="😊 เชิงบวก", x=shops, y=poss, marker_color=POS_COLOR)
 
282
 
283
- fig_compare.update_layout(
284
- title="🏪 เปรียบเทียบรีวิวแต่ละร้าน/สาขา",
285
  barmode='stack',
286
  template=TEMPLATE,
287
  xaxis_title="ร้าน/สาขา",
288
  yaxis_title="จำนวนรีวิว",
289
- height=450
 
290
  )
291
 
292
- # 3. Stacked bar แสดง Shop ตามช่วงเวลา (ถ้ามี date_col)
293
- fig_time_shop = None
294
- if date_col and date_col in df.columns:
295
- # Auto-detect frequency
296
- if freq == "auto":
297
- date_range = (df[date_col].max() - df[date_col].min()).days
298
- if date_range <= 30:
299
- freq = "D"
300
- elif date_range <= 90:
301
- freq = "W"
302
- else:
303
- freq = "M"
304
-
305
- fig_time_shop = go.Figure()
306
-
307
- # เตรียมข้อมูลแยกตามร้าน
308
- for shop in shops:
309
- shop_df = df[df[shop_col] == shop]
310
- # รวมตามเวลา
311
- ts = shop_df.groupby(pd.Grouper(key=date_col, freq=freq)).size()
312
-
313
- fig_time_shop.add_bar(
314
- x=ts.index,
315
- y=ts.values,
316
- name=shop
317
- )
318
-
319
- freq_map = {"D": "รายวัน", "W": "รายสัปดาห์", "M": "รายเดือน"}
320
- fig_time_shop.update_layout(
321
- title=f"📊 จำนวนรีวิวแต่ละร้านตามเวลา ({freq_map[freq]})",
322
- barmode='stack',
323
- template=TEMPLATE,
324
- xaxis_title="วันที่",
325
- yaxis_title="จำนวนรีวิว",
326
- hovermode='x unified',
327
- height=450
328
- )
329
 
330
- return summary_df, fig_compare, fig_time_shop
331
-
332
- # ================= Core Predict =================
333
- def _predict_batch(texts, model_name, batch_size=32):
334
- model,tok,cfg=load_model(model_name)
335
- results=[]
336
- for i in range(0,len(texts),batch_size):
337
- chunk=texts[i:i+batch_size]
338
- enc=tok(chunk,padding=True,truncation=True,
339
- max_length=cfg.get("max_length",128),return_tensors="pt")
340
- with torch.no_grad():
341
- logits=model(enc["input_ids"],enc["attention_mask"])
342
- probs=F.softmax(logits,dim=1).cpu().numpy()
343
- for txt,p in zip(chunk,probs):
344
- neg,pos=float(p[0]),float(p[1])
345
- label="positive" if pos>=neg else "negative"
346
- results.append({
347
- "review":txt,
348
- "negative(%)":_format_pct(neg),
349
- "positive(%)":_format_pct(pos),
350
- "label":label
351
- })
352
- return results
353
 
354
- # ================= Tab 1: วิเคราะห์หลายรีวิว =================
355
- def predict_many(text_block, model_choice, chart_type):
356
  try:
357
  raw = (text_block or "").splitlines()
358
  norm = [_norm_text(t) for t in raw]
359
  clean = [t for t in norm if _is_substantive_text(t)]
360
 
361
  if not clean:
362
- return pd.DataFrame(), go.Figure(), "❌ ไม่พบข้อความที่สามารถวิเคราะห์ได้\n\nกรุณาป้อนข้อความที่มีความยาวอย่างน้อย 2 ตัวอักษร"
363
 
364
  results = _predict_batch(clean, model_choice)
365
  df = pd.DataFrame(results)
366
 
367
- fig, info = make_summary_chart(df, chart_type)
368
 
369
  return df, fig, info
370
 
371
  except Exception as e:
372
- return pd.DataFrame(), go.Figure(), f"❌ เกิดข้อผิดพลาด:\n\n{traceback.format_exc()}"
373
 
374
  # ================= Tab 2: อัปโหลด CSV =================
375
  def on_file_change(file_obj):
376
- """เมื่ออัปโหลดไฟล์ - ตรวจหา columns อัตโนมัติ"""
377
  if file_obj is None:
378
- return (gr.update(choices=[],value=None),
379
- gr.update(choices=[],value=None),
380
- gr.update(choices=[],value=None),
381
  gr.update(visible=False),
382
  "⚠️ กรุณาอัปโหลดไฟล์ CSV")
383
 
@@ -385,44 +272,40 @@ def on_file_change(file_obj):
385
  df = pd.read_csv(file_obj.name)
386
  text_col, date_candidates, date_col, shop_candidates, shop_col = detect_columns(df)
387
 
388
- has_date = date_col is not None
389
  has_shop = shop_col is not None
390
 
391
- note = f"✅ **ตรวจพบคอลัมน์:**\n"
392
  note += f"- 📝 ข้อความ: **{text_col}**\n"
393
 
394
- if has_date:
395
  note += f"- 📅 วันที่: **{date_col}**\n"
396
- else:
397
- note += f"- 📅 วันที่: _ไม่พบ_\n"
398
 
399
  if has_shop:
400
- note += f"- 🏪 ร้าน/สาขา: **{shop_col}** (พบ {df[shop_col].nunique()} ร้าน)\n"
401
  else:
402
  note += f"- 🏪 ร้าน/สาขา: _ไม่พบ_\n"
403
 
404
- note += f"\n_หากไม่ถูกต้อง สามารถเลือกใหม่ได้จากเมนูด้านบน_"
405
 
406
  return (gr.update(choices=list(df.columns), value=text_col),
407
  gr.update(choices=date_candidates if date_candidates else ["ไม่มี"], value=date_col),
408
  gr.update(choices=shop_candidates if shop_candidates else ["ไม่มี"], value=shop_col),
409
- gr.update(visible=has_date),
410
  note)
411
 
412
  except Exception as e:
413
- return (gr.update(choices=[],value=None),
414
- gr.update(choices=[],value=None),
415
- gr.update(choices=[],value=None),
416
  gr.update(visible=False),
417
- f"❌ ไม่สามารถอ่านไฟล์ได้:\n{str(e)}")
418
 
419
- def predict_csv(file_obj, model_choice, text_col, date_col, shop_col, date_preset, chart_type):
420
- """วิเคราะห์รีวิวจากไฟล์ CSV"""
421
  if file_obj is None:
422
- return (pd.DataFrame(), go.Figure(), go.Figure(),
423
- gr.update(visible=False), gr.update(visible=False),
424
- pd.DataFrame(), gr.update(visible=False),
425
- "❌ กรุณาอัปโหลดไฟล์ CSV", None)
426
 
427
  try:
428
  df_raw = pd.read_csv(file_obj.name)
@@ -433,70 +316,57 @@ def predict_csv(file_obj, model_choice, text_col, date_col, shop_col, date_prese
433
  if text_col not in cols:
434
  text_col, _, _, _, _ = detect_columns(df_raw)
435
 
436
- # ดึงข้อความและทำนาย
437
  texts = [_norm_text(v) for v in df_raw[text_col].tolist()]
438
  texts_clean = [t for t in texts if _is_substantive_text(t)]
439
  skipped = total_rows - len(texts_clean)
440
 
441
  if not texts_clean:
442
- return (pd.DataFrame(), go.Figure(), go.Figure(),
443
- gr.update(visible=False), gr.update(visible=False),
444
- pd.DataFrame(), gr.update(visible=False),
445
- "❌ ไม่พบข้อความที่สามารถวิเคราะห์ได้ในไฟล์", None)
446
 
 
447
  results = _predict_batch(texts_clean, model_choice)
448
  df_out = pd.DataFrame(results)
449
 
450
- # กราฟสรุปหลัก
451
- fig_main, info = make_summary_chart(df_out, chart_type)
452
 
453
- # เพิ่มข้อมูลแถวที่ข้าม
454
  if skipped > 0:
455
- info += f"\n\n⚠️ **ข้ามแถวที่ไม่มีข้อความ:** {skipped} แถว (ใช้ {len(texts_clean)}/{total_rows} แถว)"
456
-
457
- # กราฟตามเวลา
458
- fig_time = go.Figure()
459
- show_time = False
460
- df_time_filtered = None
461
-
462
- if date_col and date_col in cols and date_col != "ไม่มี":
463
- dts = _to_datetime_safe(df_raw[date_col])
464
- if dts.notna().any():
465
- df_time = df_out.copy()
466
- df_time["__dt__"] = dts
467
- df_time = df_time.dropna(subset=["__dt__"])
468
-
469
- # ใช้ date preset - แก้ bug ตรงนี้!
470
- df_time_filtered = apply_date_preset(df_time, "__dt__", date_preset)
471
-
472
- if len(df_time_filtered) > 0:
473
- fig_time = make_time_chart(df_time_filtered, "__dt__")
474
- show_time = True
475
-
476
- # แสดงข้อมูลช่วงเวลาที่กรอง
477
- if date_preset != "ทั้งหมด":
478
- info += f"\n\n📅 **ช่วงเวลาที่แสดง:** {date_preset} ({len(df_time_filtered)} รีวิว)"
479
-
480
- # วิเคราะห์ตาม Shop
481
- shop_summary_df = pd.DataFrame()
482
  fig_shop = go.Figure()
483
- fig_shop_time = None
484
  show_shop = False
485
 
486
  if shop_col and shop_col in cols and shop_col != "ไม่มี":
487
- df_with_shop = df_out.copy()
488
- df_with_shop[shop_col] = df_raw[shop_col]
 
489
 
490
- # ถ้ามี date ใช้ข้อมูลที่ filter แล้ว
491
- if df_time_filtered is not None and len(df_time_filtered) > 0:
492
- df_with_shop["__dt__"] = df_time_filtered["__dt__"]
493
- df_with_shop = df_with_shop.dropna(subset=["__dt__"])
 
 
 
 
 
 
 
 
 
 
494
 
495
- shop_summary_df, fig_shop, fig_shop_time = make_shop_analysis(
496
- df_with_shop, shop_col, "__dt__"
497
- )
 
498
  else:
499
- shop_summary_df, fig_shop, _ = make_shop_analysis(df_with_shop, shop_col)
500
 
501
  show_shop = True
502
 
@@ -505,18 +375,15 @@ def predict_csv(file_obj, model_choice, text_col, date_col, shop_col, date_prese
505
  os.close(fd)
506
  df_out.to_csv(path, index=False, encoding="utf-8-sig")
507
 
508
- return (df_out, fig_main, fig_time,
509
- gr.update(visible=show_time, value=fig_time),
510
  gr.update(visible=show_shop, value=fig_shop),
511
- shop_summary_df,
512
- gr.update(visible=show_shop and fig_shop_time is not None, value=fig_shop_time),
513
  info, path)
514
 
515
  except Exception as e:
516
- return (pd.DataFrame(), go.Figure(), go.Figure(),
517
- gr.update(visible=False), gr.update(visible=False),
518
- pd.DataFrame(), gr.update(visible=False),
519
- f"❌ เกิดข้อผิดพลาด:\n\n{traceback.format_exc()}", None)
520
 
521
  # ================= Gradio UI =================
522
  with gr.Blocks(title="Thai Sentiment Analysis", theme=gr.themes.Soft()) as demo:
@@ -532,35 +399,19 @@ with gr.Blocks(title="Thai Sentiment Analysis", theme=gr.themes.Soft()) as demo:
532
  info="WCB = เร็ว | WCB_BiLSTM = แม่นยำสูงสุด (แนะนำ)"
533
  )
534
 
535
- # =================== Tab 1: วิเคราะห์หลายรีวิว ===================
536
- with gr.Tab("📝 วิเคราะห์หลายรีวิว"):
537
- gr.Markdown("""
538
- **วิธีใช้:** ป้อนรีวิวหลายรายการ (แต่ละบรรทัด = 1 รีวิว) แล้วกด "เริ่มวิเคราะห์"
539
-
540
- **ตัวอย่าง:**
541
- ```
542
- อาหารอร่อยมาก บริการดีค่ะ
543
- ของแพงไป รสชาติก็ธรรมดา
544
- บรรยากาศดี แต่รอนาน
545
- ```
546
- """)
547
 
548
  text_input = gr.Textbox(
549
- lines=10,
550
- label="📄 ข้อความรีวิว (บรรทัดละ 1 รีวิว)",
551
- placeholder="ป้อนรีวิวที่ต้องการวิเคราะห์...\nแต่ละบรรทัด = 1 รีวิว"
552
  )
553
 
554
- with gr.Row():
555
- chart_type_1 = gr.Radio(
556
- choices=["pie", "bar"],
557
- value="pie",
558
- label="📊 รูปแบบกราฟ",
559
- info="Pie = วงกลม, Bar = แท่ง"
560
- )
561
- predict_btn_1 = gr.Button("🚀 เริ่มวิเคราะห์", variant="primary", size="lg")
562
 
563
- result_df_1 = gr.Dataframe(label="📋 ผลการวิเคราะห์ทั้งหมด")
564
 
565
  with gr.Row():
566
  with gr.Column(scale=1):
@@ -570,91 +421,32 @@ with gr.Blocks(title="Thai Sentiment Analysis", theme=gr.themes.Soft()) as demo:
570
 
571
  predict_btn_1.click(
572
  predict_many,
573
- [text_input, model_radio, chart_type_1],
574
  [result_df_1, result_chart_1, result_info_1]
575
  )
576
 
577
- # =================== Tab 2: อัปโหลด CSV ===================
578
  with gr.Tab("📤 อัปโหลด CSV"):
579
- gr.Markdown("""
580
- **วิธีใช้:** อัปโหลดไฟล์ CSV ที่มีคอลัมน์รีวิว (และอาจมีวันที่/ร้านด้วย)
581
 
582
- **คอลัมน์ที่ต้องมี:**
583
- - ✅ คอลัมน์ข้อความรีวิว (เช่น "text", "review", "รีวิว")
584
- - ⭐ คอลัมน์วันที่ (optional - สำหรับวิเคราะห์แนวโน้ม)
585
- - ⭐ คอลัมน์ร้าน/สาขา (optional - สำหรับเปรียบเทียบร้าน)
586
- """)
587
-
588
- with gr.Row():
589
- file_input = gr.File(
590
- file_types=[".csv"],
591
- label="📁 อัปโหลดไฟล์ CSV"
592
- )
593
 
594
  detect_note = gr.Markdown("⬆️ อัปโหลดไฟล์เพื่อเริ่มต้น")
595
 
596
  with gr.Row():
597
- text_col_dd = gr.Dropdown(
598
- label="📝 คอลัมน์ข้อความรีวิว",
599
- info="เลือกคอลัมน์ที่มีเนื้อหารีวิว"
600
- )
601
- date_col_dd = gr.Dropdown(
602
- label="📅 คอลัมน์วันที่ (ถ้าไม่มีเว้นว่าง)",
603
- info="สำหรับวิเคราะห์แนวโน้มตามเวลา"
604
- )
605
- shop_col_dd = gr.Dropdown(
606
- label="🏪 คอลัมน์ร้าน/สาขา (ถ้าไม่มีเว้นว่าง)",
607
- info="สำหรับเปรียบเทียบแต่ละร้าน"
608
- )
609
-
610
- gr.Markdown("### ⚙️ ตั้งค่าการวิเคราะห์")
611
-
612
- with gr.Row():
613
- date_preset = gr.Radio(
614
- choices=list(DATE_PRESETS.keys()),
615
- value="ทั้งหมด",
616
- label="📆 ช่วงเวลาที่ต้องการวิเคราะห์",
617
- info="เลือกช่วงเวลาที่ต้องการดูข้อมูล",
618
- visible=False
619
- )
620
-
621
- chart_type_2 = gr.Radio(
622
- choices=[("วงกลม", "pie"), ("แท่ง", "bar")],
623
- value="pie",
624
- label="📊 รูปแบบกราฟสรุป"
625
- )(DATE_PRESETS.keys()),
626
- value="ทั้งหมด",
627
- label="📆 ช่วงเวลาที่ต้องการวิเคราะห์",
628
- visible=False
629
- )
630
-
631
- freq = gr.Radio(
632
- choices=[("รายวัน", "D"), ("รายสัปดาห์", "W"), ("รายเดือน", "M")],
633
- value="D",
634
- label="📊 ความละเอียดของกราฟ",
635
- visible=False
636
- )
637
-
638
- with gr.Row():
639
- use_smooth = gr.Checkbox(
640
- value=True,
641
- label="✨ ปรับกราฟให้เรียบ (Moving Average)",
642
- info="ช่วยให้เห็นแนวโน้มชัดเจนขึ้น",
643
- visible=False
644
- )
645
-
646
- chart_type_2 = gr.Radio(
647
- choices=[("วงกลม", "pie"), ("แท่ง", "bar")],
648
- value="pie",
649
- label="📊 รูปแบบกราฟสรุป"
650
- )
651
-
652
- shop_analysis_row = gr.Row(visible=False)
653
- shop_trend_row = gr.Row(visible=False)
654
-
655
- predict_btn_2 = gr.Button("🚀 เริ่มวิเคราะห์ CSV", variant="primary", size="lg")
656
 
657
- gr.Markdown("### 📊 ผลการวิเคราะห์")
658
 
659
  result_df_2 = gr.Dataframe(label="📋 ผลการวิเคราะห์ทั้งหมด")
660
 
@@ -664,49 +456,31 @@ with gr.Blocks(title="Thai Sentiment Analysis", theme=gr.themes.Soft()) as demo:
664
  with gr.Column(scale=1):
665
  result_info_2 = gr.Markdown()
666
 
667
- result_time = gr.Plot(label="📈 กราฟแนวโน้มตามเวลา", visible=False)
668
-
669
- gr.Markdown("### 🏪 วิเคราะห์แยกตามร้าน/สาขา")
670
-
671
  shop_summary = gr.Dataframe(label="📊 สรุปแต่ละร้าน", visible=False)
672
- result_shop = gr.Plot(label="🏪 เปรียบเทียบรีวิวแต่ละร้าน", visible=False)
673
- result_shop_trend = gr.Plot(label="📊 รีวิวแต่ละร้านตามช่วงเวลา (Stacked Bar)", visible=False)
674
 
675
- download_file = gr.File(label="💾 ดาวน์โหลดผลลัพธ์ (CSV)")
676
 
677
- # Event handlers
678
  file_input.change(
679
  on_file_change,
680
  [file_input],
681
- [text_col_dd, date_col_dd, shop_col_dd,
682
- date_preset, detect_note]
683
  )
684
 
685
  predict_btn_2.click(
686
  predict_csv,
687
- [file_input, model_radio, text_col_dd, date_col_dd, shop_col_dd,
688
- date_preset, chart_type_2],
689
- [result_df_2, result_chart_2, result_time,
690
- result_time, result_shop,
691
- shop_summary, result_shop_trend,
692
- result_info_2, download_file]
693
  )
694
 
695
  gr.Markdown("""
696
  ---
697
  ### 💡 เกี่ยวกับโมเดล
 
 
698
 
699
- **WangchanBERTa Variants** - โมเดล BERT ภาษาไทยที่ได้รับการฝึกสำหรับงานวิเคราะห์ความรู้สึก
700
-
701
- - **WCB**: รุ่นพื้นฐาน - เร็ว เหมาะกับงานทั่วไป
702
- - **WCB_BiLSTM**: เพิ่ม BiLSTM layer - **แม่นยำสูงสุด (แนะนำ)** ⭐
703
-
704
- <!-- โมเดลอื่นๆ ที่ซ่อนไว้:
705
- - **WCB_CNN_BiLSTM**: ใช้ CNN + BiLSTM เพิ่มประสิทธิภาพ
706
- - **WCB_4Layer_BiLSTM**: BiLSTM 4 ชั้น (ช้ากว่า)
707
- -->
708
-
709
- 📌 **หมายเหตุ:** โมเดลวิเคราะห์เฉพาะ **เชิงบวก/เชิงลบ** เท่านั้น (ไม่มี neutral)
710
  """)
711
 
712
  if __name__ == "__main__":
 
1
+ # app.py — Thai Sentiment Analysis (เรียบง่าย + Shop Analysis)
2
  import os, json, importlib.util, traceback, re, math, tempfile, datetime
3
  import gradio as gr
4
  import torch, pandas as pd
5
  import torch.nn.functional as F
6
  import plotly.graph_objects as go
 
7
  from huggingface_hub import hf_hub_download
8
  from safetensors.torch import load_file
9
  from transformers import AutoTokenizer
10
 
11
  # ================= Settings =================
12
  REPO_ID = os.getenv("REPO_ID", "Dusit-P/thai-sentiment")
13
+ DEFAULT_MODEL = os.getenv("DEFAULT_MODEL", "WCB_BiLSTM")
14
  HF_TOKEN = os.getenv("HF_TOKEN", None)
15
 
16
  # เลือกเฉพาะโมเดลที่ให้ผลดีที่สุด
17
  AVAILABLE_CHOICES = ["WCB", "WCB_BiLSTM"]
 
 
 
 
 
18
 
19
  NEG_COLOR = "#F87171"
20
  POS_COLOR = "#34D399"
 
21
  TEMPLATE = "plotly_white"
22
  CACHE = {}
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  # ================= Loader =================
25
  def _import_models():
26
  if "models_module" in CACHE:
 
71
  def _format_pct(x): return f"{x*100:.2f}%"
72
  def _to_datetime_safe(s): return pd.to_datetime(s, errors="coerce", infer_datetime_format=True, utc=False)
73
 
74
+ LIKELY_TEXT_COLS = ["text","review","message","comment","content","ข้อความ","รีวิว"]
75
+ LIKELY_DATE_COLS = ["date","created_at","time","timestamp","datetime","วันที่","เวลา"]
76
  LIKELY_SHOP_COLS = ["shop","store","branch","ร้าน","สาขา","ชื่อร้าน"]
77
 
78
  def detect_columns(df):
 
106
  if c.lower() in LIKELY_SHOP_COLS:
107
  shop_candidates.append(c)
108
  continue
 
109
  if df[c].dtype == object:
110
  unique_ratio = df[c].nunique() / len(df)
111
+ if 0.01 <= unique_ratio <= 0.5:
112
  shop_candidates.append(c)
113
  shop_candidates = list(dict.fromkeys(shop_candidates))
114
  shop_col = shop_candidates[0] if len(shop_candidates) > 0 else None
115
 
116
  return text_col, date_candidates, date_col, shop_candidates, shop_col
117
 
118
+ # ================= Core Predict =================
119
+ def _predict_batch(texts, model_name, batch_size=32):
120
+ model, tok, cfg = load_model(model_name)
121
+ results = []
122
+ for i in range(0, len(texts), batch_size):
123
+ chunk = texts[i:i+batch_size]
124
+ enc = tok(chunk, padding=True, truncation=True,
125
+ max_length=cfg.get("max_length",128), return_tensors="pt")
126
+ with torch.no_grad():
127
+ logits = model(enc["input_ids"], enc["attention_mask"])
128
+ probs = F.softmax(logits, dim=1).cpu().numpy()
129
+ for txt, p in zip(chunk, probs):
130
+ neg, pos = float(p[0]), float(p[1])
131
+ label = "positive" if pos >= neg else "negative"
132
+ results.append({
133
+ "review": txt,
134
+ "negative(%)": _format_pct(neg),
135
+ "positive(%)": _format_pct(pos),
136
+ "label": label
137
+ })
138
+ return results
139
+
140
  # ================= Charts =================
141
+ def make_summary_chart(df):
142
+ """สร้างกราฟสรุปแบบ Pie"""
143
  total = len(df)
144
  neg_count = len(df[df["label"]=="negative"])
145
  pos_count = len(df[df["label"]=="positive"])
 
147
  neg_avg = pd.to_numeric(df["negative(%)"].str.rstrip("%"), errors="coerce").mean()
148
  pos_avg = pd.to_numeric(df["positive(%)"].str.rstrip("%"), errors="coerce").mean()
149
 
150
+ # Pie chart
151
+ fig = go.Figure(go.Pie(
152
+ labels=["😞 เชิงลบ", "😊 เชิงบวก"],
153
+ values=[neg_count, pos_count],
154
+ hole=0.4,
155
+ marker=dict(colors=[NEG_COLOR, POS_COLOR]),
156
+ textinfo='label+percent',
157
+ textfont_size=14
158
+ ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  fig.update_layout(
160
+ title="สัดส่วนรีวิว",
161
  template=TEMPLATE,
162
+ height=400
 
 
 
163
  )
164
 
165
+ # Summary text
166
+ info = (f"**📊 สรุปผล**\n\n"
167
+ f"- ทั้งหมด: **{total:,}** รีวิว\n"
168
+ f"- เชิงลบ: **{neg_count:,}** ({neg_count/total*100:.1f}%)\n"
169
+ f"- เชิงบวก: **{pos_count:,}** ({pos_count/total*100:.1f}%)")
170
+
171
+ return fig, info
172
 
173
+ def make_shop_chart(df, shop_col, date_col=None, days_filter=None):
174
+ """กราฟแสดงรีวิวแต่ละร้าน - เรียบง่าย"""
175
+
176
+ # กรองตาม���ันที่ถ้าต้องการ
177
+ if date_col and days_filter:
178
+ cutoff = pd.Timestamp.now() - pd.Timedelta(days=days_filter)
179
+ df = df[df[date_col] >= cutoff]
180
 
181
+ # สรุปแต่ละร้าน
182
+ shop_data = []
183
  for shop in df[shop_col].unique():
184
  if pd.isna(shop):
185
  continue
 
187
  neg = len(shop_df[shop_df["label"]=="negative"])
188
  pos = len(shop_df[shop_df["label"]=="positive"])
189
  total = len(shop_df)
190
+
191
+ shop_data.append({
192
+ "shop": str(shop),
193
+ "negative": neg,
194
+ "positive": pos,
195
+ "total": total,
196
+ "pos_pct": pos/total*100 if total > 0 else 0
 
197
  })
198
 
199
+ shop_df = pd.DataFrame(shop_data).sort_values("total", ascending=False)
 
 
 
200
 
201
+ # กราฟแท่ง Stacked
202
+ fig = go.Figure()
203
+ fig.add_bar(
204
+ name="😞 เชิงลบ",
205
+ x=shop_df["shop"],
206
+ y=shop_df["negative"],
207
+ marker_color=NEG_COLOR
208
+ )
209
+ fig.add_bar(
210
+ name="😊 เชิงบวก",
211
+ x=shop_df["shop"],
212
+ y=shop_df["positive"],
213
+ marker_color=POS_COLOR
214
+ )
215
 
216
+ title = "🏪 รีวิวแต่ละร้าน/สาขา"
217
+ if days_filter:
218
+ title += f" ({days_filter} วันล่าสุด)"
219
 
220
+ fig.update_layout(
221
+ title=title,
222
  barmode='stack',
223
  template=TEMPLATE,
224
  xaxis_title="ร้าน/สาขา",
225
  yaxis_title="จำนวนรีวิว",
226
+ height=450,
227
+ showlegend=True
228
  )
229
 
230
+ # ตารางสรุป
231
+ summary_df = pd.DataFrame({
232
+ "ร้าน/สาขา": shop_df["shop"],
233
+ "รีวิวทั้งหมด": shop_df["total"],
234
+ "😞 เชิงลบ": shop_df["negative"],
235
+ "😊 เชิงบวก": shop_df["positive"],
236
+ "% เชิงบวก": shop_df["pos_pct"].apply(lambda x: f"{x:.1f}%")
237
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
 
239
+ return fig, summary_df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
 
241
+ # ================= Tab 1: วิเคราะห์ข้อความ =================
242
+ def predict_many(text_block, model_choice):
243
  try:
244
  raw = (text_block or "").splitlines()
245
  norm = [_norm_text(t) for t in raw]
246
  clean = [t for t in norm if _is_substantive_text(t)]
247
 
248
  if not clean:
249
+ return pd.DataFrame(), go.Figure(), "❌ ไม่พบข้อความที่วิเคราะห์ได้"
250
 
251
  results = _predict_batch(clean, model_choice)
252
  df = pd.DataFrame(results)
253
 
254
+ fig, info = make_summary_chart(df)
255
 
256
  return df, fig, info
257
 
258
  except Exception as e:
259
+ return pd.DataFrame(), go.Figure(), f"❌ เกิดข้อผิดพลาด:\n{traceback.format_exc()}"
260
 
261
  # ================= Tab 2: อัปโหลด CSV =================
262
  def on_file_change(file_obj):
263
+ """ตรวจหา columns เมื่ออัปโหลดไฟล์"""
264
  if file_obj is None:
265
+ return (gr.update(choices=[], value=None),
266
+ gr.update(choices=[], value=None),
267
+ gr.update(choices=[], value=None),
268
  gr.update(visible=False),
269
  "⚠️ กรุณาอัปโหลดไฟล์ CSV")
270
 
 
272
  df = pd.read_csv(file_obj.name)
273
  text_col, date_candidates, date_col, shop_candidates, shop_col = detect_columns(df)
274
 
 
275
  has_shop = shop_col is not None
276
 
277
+ note = f"✅ **ตรวจพบคอลัมน์**\n"
278
  note += f"- 📝 ข้อความ: **{text_col}**\n"
279
 
280
+ if date_col:
281
  note += f"- 📅 วันที่: **{date_col}**\n"
 
 
282
 
283
  if has_shop:
284
+ note += f"- 🏪 ร้าน/สาขา: **{shop_col}** ({df[shop_col].nunique()} ร้าน)\n"
285
  else:
286
  note += f"- 🏪 ร้าน/สาขา: _ไม่พบ_\n"
287
 
288
+ note += f"\n_หากไม่ถูกต้อง เลือกใหม่ได้ด้านบน_"
289
 
290
  return (gr.update(choices=list(df.columns), value=text_col),
291
  gr.update(choices=date_candidates if date_candidates else ["ไม่มี"], value=date_col),
292
  gr.update(choices=shop_candidates if shop_candidates else ["ไม่มี"], value=shop_col),
293
+ gr.update(visible=has_shop),
294
  note)
295
 
296
  except Exception as e:
297
+ return (gr.update(choices=[], value=None),
298
+ gr.update(choices=[], value=None),
299
+ gr.update(choices=[], value=None),
300
  gr.update(visible=False),
301
+ f"❌ ไม่สามารถอ่านไฟล์ได้: {str(e)}")
302
 
303
+ def predict_csv(file_obj, model_choice, text_col, date_col, shop_col, days_filter):
304
+ """วิเคราะห์ CSV"""
305
  if file_obj is None:
306
+ return (pd.DataFrame(), go.Figure(),
307
+ gr.update(visible=False), pd.DataFrame(),
308
+ "❌ กรุณาอัปโหลดไฟล์", None)
 
309
 
310
  try:
311
  df_raw = pd.read_csv(file_obj.name)
 
316
  if text_col not in cols:
317
  text_col, _, _, _, _ = detect_columns(df_raw)
318
 
319
+ # ดึงข้อความ
320
  texts = [_norm_text(v) for v in df_raw[text_col].tolist()]
321
  texts_clean = [t for t in texts if _is_substantive_text(t)]
322
  skipped = total_rows - len(texts_clean)
323
 
324
  if not texts_clean:
325
+ return (pd.DataFrame(), go.Figure(),
326
+ gr.update(visible=False), pd.DataFrame(),
327
+ "❌ ไม่พบข้อความที่วิเคราะห์ได้", None)
 
328
 
329
+ # ทำนาย
330
  results = _predict_batch(texts_clean, model_choice)
331
  df_out = pd.DataFrame(results)
332
 
333
+ # กราฟสรุป
334
+ fig_main, info = make_summary_chart(df_out)
335
 
 
336
  if skipped > 0:
337
+ info += f"\n\n⚠️ ข้ามแถวว่าง: {skipped} แถว"
338
+
339
+ # วิเคราะห์ Shop (ถ้ามี)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  fig_shop = go.Figure()
341
+ shop_summary = pd.DataFrame()
342
  show_shop = False
343
 
344
  if shop_col and shop_col in cols and shop_col != "ไม่มี":
345
+ # เตรียมข้อมูล
346
+ df_shop = df_out.copy()
347
+ df_shop[shop_col] = df_raw[shop_col].iloc[:len(df_out)]
348
 
349
+ # เพิ่ม date ถ้ามี
350
+ if date_col and date_col in cols and date_col != "ไม่มี":
351
+ dts = _to_datetime_safe(df_raw[date_col])
352
+ df_shop[date_col] = dts.iloc[:len(df_out)]
353
+ df_shop = df_shop.dropna(subset=[date_col])
354
+
355
+ # แปลง days_filter
356
+ days = None
357
+ if days_filter == "7 วันล่าสุด":
358
+ days = 7
359
+ elif days_filter == "15 วันล่าสุด":
360
+ days = 15
361
+ elif days_filter == "30 วันล่าสุด":
362
+ days = 30
363
 
364
+ fig_shop, shop_summary = make_shop_chart(df_shop, shop_col, date_col, days)
365
+
366
+ if days:
367
+ info += f"\n\n📅 แสดงข้อมูล: {days_filter}"
368
  else:
369
+ fig_shop, shop_summary = make_shop_chart(df_shop, shop_col)
370
 
371
  show_shop = True
372
 
 
375
  os.close(fd)
376
  df_out.to_csv(path, index=False, encoding="utf-8-sig")
377
 
378
+ return (df_out, fig_main,
 
379
  gr.update(visible=show_shop, value=fig_shop),
380
+ shop_summary,
 
381
  info, path)
382
 
383
  except Exception as e:
384
+ return (pd.DataFrame(), go.Figure(),
385
+ gr.update(visible=False), pd.DataFrame(),
386
+ f"❌ เกิดข้อผิดพลาด:\n{traceback.format_exc()}", None)
 
387
 
388
  # ================= Gradio UI =================
389
  with gr.Blocks(title="Thai Sentiment Analysis", theme=gr.themes.Soft()) as demo:
 
399
  info="WCB = เร็ว | WCB_BiLSTM = แม่นยำสูงสุด (แนะนำ)"
400
  )
401
 
402
+ # =================== Tab 1 ===================
403
+ with gr.Tab("📝 วิเคราะห์ข้อความ"):
404
+ gr.Markdown("**วิธีใช้:** ป้อนรีวิวหลายรายการ (แต่ละบรรทัด = 1 รีวิว)")
 
 
 
 
 
 
 
 
 
405
 
406
  text_input = gr.Textbox(
407
+ lines=8,
408
+ label="📄 ข้อความรีวิว",
409
+ placeholder="ป้อนรีวิว แต่ละบรรทัด = 1 รีวิว\n\nตัวอย่าง:\nอาหารอร่อยมาก บริการดี\nของแพง รสชาติธรรมดา"
410
  )
411
 
412
+ predict_btn_1 = gr.Button("🚀 เริ่มวิเคราะห์", variant="primary", size="lg")
 
 
 
 
 
 
 
413
 
414
+ result_df_1 = gr.Dataframe(label="📋 ผลการวิเคราะห์")
415
 
416
  with gr.Row():
417
  with gr.Column(scale=1):
 
421
 
422
  predict_btn_1.click(
423
  predict_many,
424
+ [text_input, model_radio],
425
  [result_df_1, result_chart_1, result_info_1]
426
  )
427
 
428
+ # =================== Tab 2 ===================
429
  with gr.Tab("📤 อัปโหลด CSV"):
430
+ gr.Markdown("**วิธีใช้:** อัปโหลดไฟล์ CSV ที่มีคอลัมน์รีวิว (และอาจมีวันที่/ร้านด้วย)")
 
431
 
432
+ file_input = gr.File(file_types=[".csv"], label="📁 อัปโหลดไฟล์ CSV")
 
 
 
 
 
 
 
 
 
 
433
 
434
  detect_note = gr.Markdown("⬆️ อัปโหลดไฟล์เพื่อเริ่มต้น")
435
 
436
  with gr.Row():
437
+ text_col_dd = gr.Dropdown(label="📝 คอลัมน์ข้อความรีวิว")
438
+ date_col_dd = gr.Dropdown(label="📅 คอลัมน์วันที่ (ถ้ามี)")
439
+ shop_col_dd = gr.Dropdown(label="🏪 คอลัมน์ร้าน/สาขา (ถ้ามี)")
440
+
441
+ days_filter = gr.Radio(
442
+ choices=["ทั้งหมด", "7 วันล่าสุด", "15 วันล่าสุด", "30 วันล่าสุด"],
443
+ value="ทั้งหมด",
444
+ label="📆 ช่วงเวลา",
445
+ info="ใช้กรองข้อมูลเฉพาะกราฟร้าน (ถ้ามีวันที่)",
446
+ visible=False
447
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
 
449
+ predict_btn_2 = gr.Button("🚀 เริ่มวิเคราะห์", variant="primary", size="lg")
450
 
451
  result_df_2 = gr.Dataframe(label="📋 ผลการวิเคราะห์ทั้งหมด")
452
 
 
456
  with gr.Column(scale=1):
457
  result_info_2 = gr.Markdown()
458
 
459
+ result_shop = gr.Plot(label="🏪 รีวิวแต่ละร้าน/สาขา", visible=False)
 
 
 
460
  shop_summary = gr.Dataframe(label="📊 สรุปแต่ละร้าน", visible=False)
 
 
461
 
462
+ download_file = gr.File(label="💾 ดาวน์โหลดผลลัพธ์")
463
 
464
+ # Events
465
  file_input.change(
466
  on_file_change,
467
  [file_input],
468
+ [text_col_dd, date_col_dd, shop_col_dd, days_filter, detect_note]
 
469
  )
470
 
471
  predict_btn_2.click(
472
  predict_csv,
473
+ [file_input, model_radio, text_col_dd, date_col_dd, shop_col_dd, days_filter],
474
+ [result_df_2, result_chart_2, result_shop, shop_summary, result_info_2, download_file]
 
 
 
 
475
  )
476
 
477
  gr.Markdown("""
478
  ---
479
  ### 💡 เกี่ยวกับโมเดล
480
+ - **WCB**: เร็ว เหมาะงานทั่วไป
481
+ - **WCB_BiLSTM**: แม่นยำสูงสุด ⭐ (แนะนำ)
482
 
483
+ 📌 วิเคราะห์เฉพาะ **เชิงบวก/เชิงลบ** เท่านั้น
 
 
 
 
 
 
 
 
 
 
484
  """)
485
 
486
  if __name__ == "__main__":