| 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 |
| ) |