| import gradio as gr |
| from google_play_scraper import Sort, reviews |
| from transformers import pipeline |
| from collections import Counter |
| import matplotlib |
| import threading |
| import base64 |
| from io import BytesIO |
| import torch |
|
|
| |
| |
| |
| |
| matplotlib.use('Agg') |
| import matplotlib.pyplot as plt |
|
|
| |
| plot_lock = threading.Lock() |
|
|
| |
| |
| |
| device = 0 if torch.cuda.is_available() else -1 |
| MODEL_NAME = "Aardiiiiy/indobertweet-base-Indonesian-sentiment-analysis" |
|
|
| print("β³ Sedang memuat model... Harap tunggu sebentar.") |
| try: |
| sentiment_model = pipeline( |
| "text-classification", |
| model=MODEL_NAME, |
| tokenizer=MODEL_NAME, |
| device=device |
| ) |
| print("β
Model siap digunakan!") |
| except Exception as e: |
| print(f"Error loading model: {e}") |
|
|
| |
| |
| |
| def scrape_reviews(app_id, max_reviews, language): |
| try: |
| result, _ = reviews( |
| app_id, |
| lang=language, |
| country="id", |
| sort=Sort.NEWEST, |
| count=max_reviews |
| ) |
| return [r["content"].strip() for r in result if r.get("content")] |
| except Exception as e: |
| print(f"Error scraping: {e}") |
| return [] |
|
|
| def analyze_sentiment_batch(texts): |
| if not texts: |
| return Counter(), 0 |
| |
| try: |
| results = sentiment_model(texts, batch_size=16, truncation=True, max_length=512) |
| labels = [res["label"].lower() for res in results] |
| return Counter(labels), len(labels) |
| except Exception as e: |
| print(f"Error analyzing: {e}") |
| return Counter(), 0 |
|
|
| def generate_plot(pos, neu, neg, total): |
| |
| with plot_lock: |
| try: |
| colors = ['#4ade80', '#fbbf24', '#f87171'] |
| labels = ["Positif", "Netral", "Negatif"] |
| sizes = [pos, neu, neg] |
|
|
| fig = plt.figure(figsize=(6, 4)) |
| fig.patch.set_alpha(0.0) |
| |
| plt.pie( |
| sizes, |
| labels=labels, |
| autopct="%1.1f%%", |
| startangle=140, |
| colors=colors, |
| |
| textprops={'color':"#000000", 'weight':'bold', 'fontsize': 10}, |
| wedgeprops={'edgecolor': 'white', 'linewidth': 2} |
| ) |
| |
| plt.axis('equal') |
| plt.tight_layout() |
|
|
| buf = BytesIO() |
| plt.savefig(buf, format="png", bbox_inches='tight', transparent=True) |
| |
| |
| plt.close(fig) |
| plt.close('all') |
| |
| return base64.b64encode(buf.getvalue()).decode() |
| except Exception as e: |
| print(f"Error generating plot: {e}") |
| return "" |
|
|
| def generate_html_dashboard(pos, neu, neg, total, img_base64): |
| |
| html = f""" |
| <div style=" |
| font-family: 'Segoe UI', sans-serif; |
| background-color: #ffffff; |
| border-radius: 12px; |
| padding: 20px; |
| color: #000000 !important; |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); |
| "> |
| <h3 style="text-align:center; color:#000000 !important; margin-top:0; font-weight:bold;"> |
| π Hasil Analisis Sentimen |
| </h3> |
| <p style="text-align:center; color:#333333 !important; font-size:14px;"> |
| Berdasarkan <b>{total}</b> ulasan terbaru |
| </p> |
| |
| <div style="display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 20px;"> |
| <div style="flex: 1; min-width: 250px;"> |
| <table style="width:100%; color: #000000 !important;"> |
| <tr style="border-bottom: 1px solid #eee;"> |
| <td style="padding: 8px 0; color: #000000 !important;">π’ Positif</td> |
| <td style="text-align:right; font-weight:bold; color:#15803d !important;">{pos}</td> |
| </tr> |
| <tr style="border-bottom: 1px solid #eee;"> |
| <td style="padding: 8px 0; color: #000000 !important;">π‘ Netral</td> |
| <td style="text-align:right; font-weight:bold; color:#b45309 !important;">{neu}</td> |
| </tr> |
| <tr style="border-bottom: 1px solid #eee;"> |
| <td style="padding: 8px 0; color: #000000 !important;">π΄ Negatif</td> |
| <td style="text-align:right; font-weight:bold; color:#b91c1c !important;">{neg}</td> |
| </tr> |
| <tr style="background-color: #f8f9fa;"> |
| <td style="padding: 8px 0; font-weight:bold; color: #000000 !important;">Total Data</td> |
| <td style="text-align:right; font-weight:bold; color: #000000 !important;">{total}</td> |
| </tr> |
| </table> |
| </div> |
| <div style="flex: 1; min-width: 250px; text-align: center;"> |
| <img src="data:image/png;base64,{img_base64}" style="max-width: 100%; height: auto;" /> |
| </div> |
| </div> |
| </div> |
| """ |
| return html |
|
|
| def run_pipeline(app_id, max_reviews, language): |
| if not app_id: |
| return "β οΈ Masukkan Package Name dulu.", "<div></div>" |
|
|
| texts = scrape_reviews(app_id, max_reviews, language) |
| if not texts: |
| return "β Tidak ditemukan review.", "<div style='color:white'>Data Tidak Ditemukan</div>" |
|
|
| raw_output = "\n".join(f"[{i+1}] {t}" for i, t in enumerate(texts)) |
| counter, total = analyze_sentiment_batch(texts) |
|
|
| pos = counter.get("positive", 0) |
| neu = counter.get("neutral", 0) |
| neg = counter.get("negative", 0) |
|
|
| img_base64 = generate_plot(pos, neu, neg, total) |
| html_result = generate_html_dashboard(pos, neu, neg, total, img_base64) |
|
|
| return raw_output, html_result |
|
|
| |
| |
| |
| theme = gr.themes.Soft( |
| primary_hue="blue", |
| secondary_hue="slate", |
| ).set( |
| body_background_fill="#0f172a", |
| block_background_fill="#1e293b", |
| block_label_text_color="#e2e8f0", |
| body_text_color="#e2e8f0" |
| ) |
|
|
| with gr.Blocks(theme=theme, title="Play Store Analyzer") as demo: |
| with gr.Row(): |
| gr.Markdown("# π± Google Play Review Analyzer") |
|
|
| with gr.Group(): |
| with gr.Row(): |
| app_id_input = gr.Textbox(label="Package Name", placeholder="com.whatsapp", scale=2) |
| language_input = gr.Dropdown(choices=["id", "en"], value="id", label="Bahasa", scale=1) |
| max_reviews_input = gr.Slider(minimum=10, maximum=500, step=10, value=50, label="Jumlah", scale=1) |
| |
| analyze_btn = gr.Button("π Analisis", variant="primary") |
|
|
| with gr.Row(): |
| with gr.Column(scale=3): |
| analysis_box = gr.HTML(label="Dashboard") |
| with gr.Column(scale=2): |
| raw_box = gr.Textbox(label="Data Mentah", lines=18) |
|
|
| analyze_btn.click( |
| run_pipeline, |
| inputs=[app_id_input, max_reviews_input, language_input], |
| outputs=[raw_box, analysis_box] |
| ) |
|
|
| if __name__ == "__main__": |
| |
| demo.launch(ssr_mode=False) |