|
import gradio as gr |
|
from transformers import pipeline |
|
import numpy as np |
|
import plotly.express as px |
|
import plotly.graph_objects as go |
|
|
|
|
|
pipe = pipeline("text-classification", model="vanhai123/phobert-vi-comment-4class", tokenizer="vanhai123/phobert-vi-comment-4class") |
|
|
|
|
|
label_map = { |
|
"LABEL_0": {"name": "Tích cực", "emoji": "😊", "color": "#22c55e"}, |
|
"LABEL_1": {"name": "Tiêu cực", "emoji": "😞", "color": "#ef4444"}, |
|
"LABEL_2": {"name": "Trung lập", "emoji": "😐", "color": "#64748b"}, |
|
"LABEL_3": {"name": "Độc hại", "emoji": "😡", "color": "#dc2626"} |
|
} |
|
|
|
def classify_comment(comment): |
|
if not comment.strip(): |
|
return "Vui lòng nhập bình luận để phân tích!", None, None |
|
|
|
|
|
results = pipe(comment) |
|
if isinstance(results, list): |
|
results = results[0] if results else {} |
|
|
|
|
|
all_scores = pipe(comment, return_all_scores=True) |
|
if isinstance(all_scores, list): |
|
all_scores = all_scores[0] if all_scores else [] |
|
|
|
|
|
main_label = results.get('label', 'UNKNOWN') |
|
main_score = results.get('score', 0) |
|
|
|
if main_label in label_map: |
|
label_info = label_map[main_label] |
|
main_result = f""" |
|
<div style=" |
|
background: linear-gradient(135deg, {label_info['color']}22, {label_info['color']}11); |
|
border: 2px solid {label_info['color']}; |
|
border-radius: 15px; |
|
padding: 20px; |
|
text-align: center; |
|
margin: 10px 0; |
|
box-shadow: 0 4px 15px rgba(0,0,0,0.1); |
|
"> |
|
<div style="font-size: 48px; margin-bottom: 10px;">{label_info['emoji']}</div> |
|
<div style="font-size: 24px; font-weight: bold; color: {label_info['color']}; margin-bottom: 5px;"> |
|
{label_info['name']} |
|
</div> |
|
<div style="font-size: 18px; color: var(--text-secondary);"> |
|
Độ tin cậy: <strong>{round(main_score*100, 1)}%</strong> |
|
</div> |
|
</div> |
|
""" |
|
else: |
|
main_result = f"Không xác định được nhãn: {main_label}" |
|
|
|
|
|
if all_scores: |
|
labels = [] |
|
scores = [] |
|
colors = [] |
|
|
|
for item in all_scores: |
|
label_key = item['label'] |
|
if label_key in label_map: |
|
labels.append(label_map[label_key]['name']) |
|
scores.append(item['score']) |
|
colors.append(label_map[label_key]['color']) |
|
|
|
|
|
fig = go.Figure(data=[ |
|
go.Bar( |
|
y=labels, |
|
x=scores, |
|
orientation='h', |
|
marker_color=colors, |
|
text=[f"{s:.1%}" for s in scores], |
|
textposition='inside', |
|
textfont=dict(color='white', size=12, family='Inter') |
|
) |
|
]) |
|
|
|
fig.update_layout( |
|
title={ |
|
'text': 'Phân phối điểm số dự đoán', |
|
'x': 0.5, |
|
'font': {'size': 16, 'family': 'Inter', 'color': 'var(--text-primary)'} |
|
}, |
|
xaxis_title="Điểm số", |
|
yaxis_title="Loại bình luận", |
|
height=300, |
|
margin=dict(l=20, r=20, t=50, b=20), |
|
plot_bgcolor='rgba(0,0,0,0)', |
|
paper_bgcolor='rgba(0,0,0,0)', |
|
font=dict(family="Inter", size=12, color='var(--text-primary)'), |
|
xaxis=dict( |
|
showgrid=True, |
|
gridwidth=1, |
|
gridcolor='var(--border-light)', |
|
range=[0, 1], |
|
color='var(--text-primary)' |
|
), |
|
yaxis=dict( |
|
showgrid=False, |
|
color='var(--text-primary)' |
|
) |
|
) |
|
|
|
|
|
details = "<div style='margin-top: 15px;'>" |
|
details += "<h4 style='color: var(--text-primary); margin-bottom: 10px;'>📊 Chi tiết điểm số:</h4>" |
|
for item in sorted(all_scores, key=lambda x: x['score'], reverse=True): |
|
label_key = item['label'] |
|
if label_key in label_map: |
|
info = label_map[label_key] |
|
percentage = item['score'] * 100 |
|
bar_width = int(item['score'] * 100) |
|
details += f""" |
|
<div style="margin-bottom: 8px;"> |
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 3px;"> |
|
<span style="font-weight: 500; color: var(--text-primary);">{info['emoji']} {info['name']}</span> |
|
<span style="font-weight: bold; color: {info['color']};">{percentage:.1f}%</span> |
|
</div> |
|
<div style="background: var(--progress-bg); border-radius: 10px; height: 8px; overflow: hidden;"> |
|
<div style="background: {info['color']}; height: 100%; width: {bar_width}%; border-radius: 10px;"></div> |
|
</div> |
|
</div> |
|
""" |
|
details += "</div>" |
|
|
|
return main_result, fig, details |
|
|
|
return main_result, None, None |
|
|
|
|
|
custom_css = """ |
|
/* Import Google Fonts */ |
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); |
|
|
|
/* CSS Variables cho Light Mode */ |
|
:root { |
|
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
--secondary-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); |
|
|
|
/* Light mode colors */ |
|
--bg-primary: #ffffff; |
|
--bg-secondary: #f8fafc; |
|
--bg-tertiary: #f1f5f9; |
|
--bg-header: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
|
--text-primary: #1e293b; |
|
--text-secondary: #64748b; |
|
--text-accent: #475569; |
|
--text-on-primary: #ffffff; |
|
|
|
--border-light: #e2e8f0; |
|
--border-medium: #cbd5e1; |
|
--border-strong: #94a3b8; |
|
|
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); |
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); |
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); |
|
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1); |
|
|
|
--progress-bg: #e2e8f0; |
|
--input-bg: #ffffff; |
|
--card-bg: #ffffff; |
|
} |
|
|
|
/* Dark Mode */ |
|
@media (prefers-color-scheme: dark) { |
|
:root { |
|
--bg-primary: #0f172a; |
|
--bg-secondary: #1e293b; |
|
--bg-tertiary: #334155; |
|
--bg-header: linear-gradient(135deg, #4338ca 0%, #7c3aed 100%); |
|
|
|
--text-primary: #f1f5f9; |
|
--text-secondary: #94a3b8; |
|
--text-accent: #cbd5e1; |
|
--text-on-primary: #ffffff; |
|
|
|
--border-light: #334155; |
|
--border-medium: #475569; |
|
--border-strong: #64748b; |
|
|
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); |
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4); |
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5); |
|
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.6); |
|
|
|
--progress-bg: #334155; |
|
--input-bg: #1e293b; |
|
--card-bg: #1e293b; |
|
} |
|
} |
|
|
|
/* Manual dark mode class override */ |
|
.dark { |
|
--bg-primary: #0f172a; |
|
--bg-secondary: #1e293b; |
|
--bg-tertiary: #334155; |
|
--bg-header: linear-gradient(135deg, #4338ca 0%, #7c3aed 100%); |
|
|
|
--text-primary: #f1f5f9; |
|
--text-secondary: #94a3b8; |
|
--text-accent: #cbd5e1; |
|
--text-on-primary: #ffffff; |
|
|
|
--border-light: #334155; |
|
--border-medium: #475569; |
|
--border-strong: #64748b; |
|
|
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); |
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4); |
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5); |
|
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.6); |
|
|
|
--progress-bg: #334155; |
|
--input-bg: #1e293b; |
|
--card-bg: #1e293b; |
|
} |
|
|
|
/* Global styles */ |
|
* { |
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; |
|
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease !important; |
|
} |
|
|
|
/* Main container styling */ |
|
.gradio-container { |
|
background: var(--bg-primary) !important; |
|
min-height: 100vh; |
|
color: var(--text-primary) !important; |
|
} |
|
|
|
/* Block container */ |
|
.block { |
|
background: var(--card-bg) !important; |
|
border: 1px solid var(--border-light) !important; |
|
border-radius: 16px !important; |
|
box-shadow: var(--shadow-md) !important; |
|
} |
|
|
|
/* Header styling */ |
|
.app-title { |
|
background: var(--bg-header); |
|
color: var(--text-on-primary); |
|
padding: 40px 30px; |
|
text-align: center; |
|
border-radius: 20px 20px 0 0; |
|
margin: -20px -20px 30px -20px; |
|
position: relative; |
|
overflow: hidden; |
|
} |
|
|
|
.app-title::before { |
|
content: ''; |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
right: 0; |
|
bottom: 0; |
|
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 20"><defs><radialGradient id="a" cx="50%" cy="0%" r="100%"><stop offset="0%" stop-color="white" stop-opacity="0.1"/><stop offset="100%" stop-color="white" stop-opacity="0"/></radialGradient></defs><rect width="100" height="20" fill="url(%23a)"/></svg>'); |
|
pointer-events: none; |
|
} |
|
|
|
.app-title h1 { |
|
font-size: clamp(1.8rem, 4vw, 2.5rem); |
|
font-weight: 700; |
|
margin-bottom: 10px; |
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.2); |
|
position: relative; |
|
z-index: 1; |
|
} |
|
|
|
.app-title p { |
|
font-size: clamp(1rem, 2.5vw, 1.1rem); |
|
opacity: 0.95; |
|
font-weight: 400; |
|
position: relative; |
|
z-index: 1; |
|
max-width: 600px; |
|
margin: 0 auto; |
|
line-height: 1.6; |
|
} |
|
|
|
/* Section headers */ |
|
h3 { |
|
color: var(--text-primary) !important; |
|
font-weight: 600 !important; |
|
margin-bottom: 15px !important; |
|
font-size: 1.25rem !important; |
|
} |
|
|
|
/* Input styling */ |
|
.input-container textarea, |
|
textarea { |
|
background: var(--input-bg) !important; |
|
border: 2px solid var(--border-light) !important; |
|
border-radius: 12px !important; |
|
padding: 16px !important; |
|
font-size: 16px !important; |
|
color: var(--text-primary) !important; |
|
transition: all 0.3s ease !important; |
|
resize: vertical !important; |
|
line-height: 1.5 !important; |
|
} |
|
|
|
.input-container textarea:focus, |
|
textarea:focus { |
|
border-color: #667eea !important; |
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15) !important; |
|
outline: none !important; |
|
background: var(--input-bg) !important; |
|
} |
|
|
|
.input-container textarea::placeholder, |
|
textarea::placeholder { |
|
color: var(--text-secondary) !important; |
|
opacity: 0.8 !important; |
|
} |
|
|
|
/* Button styling */ |
|
button.primary, |
|
.gr-button-primary { |
|
background: var(--primary-gradient) !important; |
|
border: none !important; |
|
border-radius: 12px !important; |
|
padding: 14px 32px !important; |
|
font-weight: 600 !important; |
|
font-size: 16px !important; |
|
color: var(--text-on-primary) !important; |
|
text-transform: none !important; |
|
letter-spacing: 0.025em !important; |
|
transition: all 0.3s ease !important; |
|
box-shadow: var(--shadow-md) !important; |
|
cursor: pointer !important; |
|
} |
|
|
|
button.primary:hover, |
|
.gr-button-primary:hover { |
|
transform: translateY(-2px) !important; |
|
box-shadow: var(--shadow-lg) !important; |
|
filter: brightness(1.05) !important; |
|
} |
|
|
|
button.primary:active, |
|
.gr-button-primary:active { |
|
transform: translateY(0) !important; |
|
box-shadow: var(--shadow-md) !important; |
|
} |
|
|
|
/* Examples styling */ |
|
.examples-container { |
|
background: var(--bg-secondary); |
|
border: 1px solid var(--border-light); |
|
border-radius: 12px; |
|
padding: 20px; |
|
margin-top: 20px; |
|
} |
|
|
|
.examples-container h3 { |
|
color: var(--text-primary); |
|
margin-bottom: 15px; |
|
font-weight: 600; |
|
font-size: 1.1rem; |
|
} |
|
|
|
.examples-container p { |
|
color: var(--text-secondary); |
|
margin-bottom: 15px; |
|
line-height: 1.5; |
|
} |
|
|
|
/* Example buttons */ |
|
.gr-examples .gr-button { |
|
background: var(--bg-tertiary) !important; |
|
border: 1px solid var(--border-light) !important; |
|
border-radius: 8px !important; |
|
color: var(--text-primary) !important; |
|
padding: 12px 16px !important; |
|
margin: 4px !important; |
|
font-size: 14px !important; |
|
line-height: 1.4 !important; |
|
transition: all 0.2s ease !important; |
|
text-align: left !important; |
|
} |
|
|
|
.gr-examples .gr-button:hover { |
|
background: var(--bg-secondary) !important; |
|
border-color: var(--border-medium) !important; |
|
transform: translateY(-1px) !important; |
|
box-shadow: var(--shadow-sm) !important; |
|
} |
|
|
|
/* Tab styling */ |
|
.tab-nav button, |
|
.gr-tab-nav button { |
|
border-radius: 8px 8px 0 0 !important; |
|
font-weight: 500 !important; |
|
background: var(--bg-tertiary) !important; |
|
color: var(--text-secondary) !important; |
|
border: 1px solid var(--border-light) !important; |
|
border-bottom: none !important; |
|
padding: 12px 20px !important; |
|
transition: all 0.2s ease !important; |
|
} |
|
|
|
.tab-nav button:hover, |
|
.gr-tab-nav button:hover { |
|
background: var(--bg-secondary) !important; |
|
color: var(--text-primary) !important; |
|
} |
|
|
|
.tab-nav button.selected, |
|
.gr-tab-nav button.selected { |
|
background: var(--primary-gradient) !important; |
|
color: var(--text-on-primary) !important; |
|
border-color: #667eea !important; |
|
} |
|
|
|
/* Tab content */ |
|
.tabitem, |
|
.gr-tabitem { |
|
background: var(--card-bg) !important; |
|
border: 1px solid var(--border-light) !important; |
|
border-top: none !important; |
|
border-radius: 0 0 12px 12px !important; |
|
padding: 20px !important; |
|
} |
|
|
|
/* Output containers */ |
|
.output-container { |
|
background: var(--card-bg); |
|
border-radius: 12px; |
|
border: 1px solid var(--border-light); |
|
margin-top: 20px; |
|
overflow: hidden; |
|
} |
|
|
|
/* Plot containers */ |
|
.plotly-graph-div { |
|
background: var(--card-bg) !important; |
|
border-radius: 8px !important; |
|
} |
|
|
|
/* Footer */ |
|
.footer { |
|
text-align: center; |
|
padding: 30px 20px; |
|
color: var(--text-secondary); |
|
font-size: 14px; |
|
border-top: 1px solid var(--border-light); |
|
margin-top: 40px; |
|
background: var(--bg-secondary); |
|
border-radius: 0 0 16px 16px; |
|
line-height: 1.6; |
|
} |
|
|
|
.footer strong { |
|
color: var(--text-primary); |
|
font-weight: 600; |
|
} |
|
|
|
.footer em { |
|
color: var(--text-accent); |
|
font-style: normal; |
|
} |
|
|
|
/* Responsive design */ |
|
@media (max-width: 768px) { |
|
.app-title { |
|
padding: 30px 20px; |
|
} |
|
|
|
.app-title h1 { |
|
font-size: 2rem; |
|
} |
|
|
|
.app-title p { |
|
font-size: 1rem; |
|
} |
|
|
|
button.primary, |
|
.gr-button-primary { |
|
padding: 12px 24px !important; |
|
font-size: 15px !important; |
|
} |
|
|
|
.examples-container { |
|
padding: 16px; |
|
} |
|
|
|
.footer { |
|
padding: 20px 15px; |
|
font-size: 13px; |
|
} |
|
} |
|
|
|
/* Smooth transitions for theme changes */ |
|
* { |
|
transition: background-color 0.3s ease, |
|
color 0.3s ease, |
|
border-color 0.3s ease, |
|
box-shadow 0.3s ease !important; |
|
} |
|
|
|
/* Custom scrollbar */ |
|
::-webkit-scrollbar { |
|
width: 8px; |
|
height: 8px; |
|
} |
|
|
|
::-webkit-scrollbar-track { |
|
background: var(--bg-secondary); |
|
border-radius: 4px; |
|
} |
|
|
|
::-webkit-scrollbar-thumb { |
|
background: var(--border-strong); |
|
border-radius: 4px; |
|
} |
|
|
|
::-webkit-scrollbar-thumb:hover { |
|
background: var(--text-secondary); |
|
} |
|
|
|
/* Loading states */ |
|
.loading { |
|
opacity: 0.7; |
|
pointer-events: none; |
|
} |
|
|
|
/* Focus indicators for accessibility */ |
|
button:focus-visible, |
|
textarea:focus-visible { |
|
outline: 2px solid #667eea !important; |
|
outline-offset: 2px !important; |
|
} |
|
|
|
/* High contrast mode support */ |
|
@media (prefers-contrast: high) { |
|
:root { |
|
--border-light: var(--border-strong); |
|
--text-secondary: var(--text-primary); |
|
} |
|
} |
|
|
|
/* Reduced motion support */ |
|
@media (prefers-reduced-motion: reduce) { |
|
* { |
|
transition: none !important; |
|
animation: none !important; |
|
} |
|
} |
|
""" |
|
|
|
|
|
theme_script = """ |
|
<script> |
|
// Auto-detect system theme and apply appropriate class |
|
function updateTheme() { |
|
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; |
|
const root = document.documentElement; |
|
|
|
if (isDark) { |
|
root.classList.add('dark'); |
|
} else { |
|
root.classList.remove('dark'); |
|
} |
|
} |
|
|
|
// Initial theme setup |
|
updateTheme(); |
|
|
|
// Listen for system theme changes |
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme); |
|
|
|
// Manual theme toggle function (có thể được gọi từ button nếu cần) |
|
window.toggleTheme = function() { |
|
const root = document.documentElement; |
|
root.classList.toggle('dark'); |
|
} |
|
</script> |
|
""" |
|
|
|
|
|
with gr.Blocks(css=custom_css, title="Phân loại bình luận tiếng Việt - PhoBERT", theme=gr.themes.Soft()) as demo: |
|
|
|
gr.HTML(theme_script) |
|
|
|
|
|
gr.HTML(""" |
|
<div class="app-title"> |
|
<h1>🧠 Phân loại bình luận tiếng Việt</h1> |
|
<p>Sử dụng mô hình PhoBERT để phân tích cảm xúc và độc hại trong bình luận mạng xã hội với giao diện thích ứng light/dark mode</p> |
|
</div> |
|
""") |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=1): |
|
|
|
gr.HTML("<h3>📝 Nhập bình luận cần phân tích</h3>") |
|
|
|
input_text = gr.Textbox( |
|
lines=4, |
|
placeholder="Nhập bình luận tiếng Việt để phân tích cảm xúc và độ độc hại...\n\nVí dụ: 'Sản phẩm này thật tuyệt vời, tôi rất hài lòng!'", |
|
label="", |
|
elem_classes=["input-container"] |
|
) |
|
|
|
submit_btn = gr.Button( |
|
"🔍 Phân tích bình luận", |
|
variant="primary", |
|
size="lg" |
|
) |
|
|
|
|
|
gr.HTML(""" |
|
<div class="examples-container"> |
|
<h3>💡 Ví dụ mẫu</h3> |
|
<p>Nhấp vào các ví dụ bên dưới để thử nghiệm:</p> |
|
</div> |
|
""") |
|
|
|
gr.Examples( |
|
examples=[ |
|
"Bạn làm tốt lắm, cảm ơn nhiều! Tôi rất hài lòng với sản phẩm này.", |
|
"Sản phẩm quá tệ, không đáng tiền. Chất lượng kém quá!", |
|
"Tôi không có ý kiến gì đặc biệt về vấn đề này.", |
|
"Mày bị điên à, nói chuyện như vậy mà cũng được?", |
|
"Dịch vụ khách hàng rất tốt, nhân viên nhiệt tình hỗ trợ.", |
|
"Giao hàng chậm quá, đã 1 tuần rồi mà chưa nhận được.", |
|
"Thông tin này khá hữu ích, cảm ơn bạn đã chia sẻ.", |
|
"Đồ rác, ai mua là ngu! Tiền bỏ ra sông bỏ ra bể." |
|
], |
|
inputs=input_text |
|
) |
|
|
|
with gr.Column(scale=1): |
|
|
|
gr.HTML("<h3>📊 Kết quả phân tích</h3>") |
|
|
|
with gr.Tabs(): |
|
with gr.TabItem("🎯 Kết quả chính", elem_id="main_result_tab"): |
|
result_output = gr.HTML( |
|
value="<div style='text-align: center; padding: 40px; color: var(--text-secondary);'>Nhập bình luận và nhấn 'Phân tích' để xem kết quả</div>" |
|
) |
|
|
|
with gr.TabItem("📈 Biểu đồ phân phối", elem_id="chart_tab"): |
|
chart_output = gr.Plot() |
|
|
|
with gr.TabItem("📋 Chi tiết điểm số", elem_id="details_tab"): |
|
details_output = gr.HTML() |
|
|
|
|
|
submit_btn.click( |
|
fn=classify_comment, |
|
inputs=input_text, |
|
outputs=[result_output, chart_output, details_output] |
|
) |
|
|
|
input_text.submit( |
|
fn=classify_comment, |
|
inputs=input_text, |
|
outputs=[result_output, chart_output, details_output] |
|
) |
|
|
|
|
|
gr.HTML(""" |
|
<div class="footer"> |
|
<p> |
|
<strong>Mô hình:</strong> PhoBERT fine-tuned cho phân loại bình luận tiếng Việt<br> |
|
<strong>Các nhãn:</strong> Tích cực • Tiêu cực • Trung lập • Độc hại<br> |
|
<em>Được xây bởi Hà Văn Hải</em> |
|
</p> |
|
</div> |
|
""") |
|
|
|
|
|
demo.launch( |
|
share=True, |
|
server_name="0.0.0.0", |
|
server_port=7860, |
|
show_error=True, |
|
quiet=False |
|
) |