import os, re, json, time, warnings, subprocess, signal warnings.filterwarnings("ignore") import numpy as np import gradio as gr import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import torch import torch.nn.functional as F from transformers import AutoTokenizer, AutoModelForSequenceClassification from huggingface_hub import hf_hub_download print("APP STARTED") # ── Config ──────────────────────────────────────────────────────────────────── MODEL_PATH = "" HF_MODEL_REPO = "Jaykumardas/Multilingual_News_Model" # ── Load model ──────────────────────────────────────────────────────────────── def load_model_and_labels(): model_source = HF_MODEL_REPO if HF_MODEL_REPO else MODEL_PATH print(f"[INFO] Loading from: {model_source}") try: tokenizer = AutoTokenizer.from_pretrained(model_source) print("[INFO] Tokenizer loaded OK") except Exception as e: raise RuntimeError(f"Tokenizer load failed: {e}") id2label = None lmap = os.path.join(model_source, "label_map.json") try: lmap_path = hf_hub_download( repo_id=model_source, filename="label_map.json" ) with open(lmap_path, encoding="utf-8") as f: lm = json.load(f) id2label = {int(k): v for k, v in lm["id2label"].items()} print(f"[INFO] id2label loaded from HF: {id2label}") except Exception as e: print(f"[WARN] label_map.json not found in HF repo: {e}") if id2label is None: cfg_path = os.path.join(model_source, "config.json") if os.path.isfile(cfg_path): with open(cfg_path, encoding="utf-8") as f: cfg = json.load(f) if cfg.get("id2label"): id2label = {int(k): v for k, v in cfg["id2label"].items()} print(f"[INFO] id2label from config.json: {id2label}") if id2label is None: raise RuntimeError("label_map.json not found. Re-run your save cell in Kaggle.") try: model = AutoModelForSequenceClassification.from_pretrained( model_source, num_labels=len(id2label), ignore_mismatched_sizes=True) device = "cuda" if torch.cuda.is_available() else "cpu" model.to(device).eval() print(f"[INFO] Model OK — {len(id2label)} classes — {device.upper()}") except Exception as e: raise RuntimeError(f"Model load failed: {e}") return model, tokenizer, id2label, device try: MODEL, TOKENIZER, ID2LABEL, DEVICE = load_model_and_labels() CLASS_NAMES = [ID2LABEL[i] for i in sorted(ID2LABEL)] NUM_CLASSES = len(CLASS_NAMES) MODEL_LOADED = True print(f"[INFO] Classes: {CLASS_NAMES}") except Exception as e: print(f"[ERROR] {e}") MODEL_LOADED = False CLASS_NAMES = ["Model not loaded"] NUM_CLASSES = 1 ID2LABEL = {0: "Model not loaded"} DEVICE = "cpu" # ── Icons / metrics / samples ───────────────────────────────────────────────── ICONS = { "entertainment":"🎬","sports":"🏏","state":"🗺️","national":"🇮🇳", "international":"🌏","business":"📈","technology":"💻","science":"🔬", "health":"🏥","politics":"🏛️", } ICONS.update({k.title(): v for k, v in list(ICONS.items())}) def get_icon(label): return ICONS.get(label, "📰") REAL_METRICS = { "TF-IDF + LR": {"test_acc":83.84,"test_f1":77.85,"color":"#3b82f6","train_time":"< 2 min"}, "BiLSTM": {"test_acc":79.36,"test_f1":67.16,"color":"#8b5cf6","train_time":"~14 min"}, "XLM-RoBERTa": {"test_acc":86.12,"test_f1":78.75,"color":"#10b981","train_time":"~45 min"}, } SAMPLES = { "Telugu": "హైదరాబాద్‌లో క్రికెట్ టోర్నమెంట్ ప్రారంభమైంది; జిల్లా స్థాయి జట్లు పాల్గొంటున్నాయి.", "Malayalam":"కേരളത്തിൽ ഇന്ന് കനത്ത മഴ; ഒൻപത് ജില്ലകളിൽ യെല്ലോ അലർട്ട് പ്രഖ്യാപിച്ചു.", "Marathi": "मुंबई शेअर बाजारात आज मोठी तेजी; सेन्सेक्स ५०० अंकांनी वधारला.", "Tamil": "தமிழ்நாட்டில் புதிய தொழில்நுட்ப பூங்கா திறப்பு; ஆயிரக்கணக்கான வேலை வாய்ப்புகள்.", "Gujarati": "ગુજરાત ટીમ સ્ટેટ ક્રિકેટ ચેમ્પિયનશિપ જીતી; ખેલાડીઓ ઉત્સાહિત.", } # ── Preprocessing ───────────────────────────────────────────────────────────── def clean_text(text): if not isinstance(text, str): return "" text = re.sub(r"https?://\S+|www\.\S+", " ", text) text = re.sub(r"<[^>]+>", " ", text) text = re.sub(r"[\u200b\u200c\u200d\ufeff\u00ad]", "", text) text = re.sub( r"[^\w\s\u0900-\u097F\u0C00-\u0C7F\u0D00-\u0D7F\u0B80-\u0BFF\u0A80-\u0AFF]", " ", text) return re.sub(r"\s+", " ", text).strip() # ── Inference ───────────────────────────────────────────────────────────────── def predict_text(text): if not MODEL_LOADED: return {c: 0.0 for c in CLASS_NAMES}, "Model not loaded", 0.0, 0 t_clean = clean_text(text) if not t_clean: return {c: 0.0 for c in CLASS_NAMES}, "Empty input", 0.0, 0 enc = TOKENIZER(t_clean, max_length=128, padding="max_length", truncation=True, return_tensors="pt") enc = {k: v.to(DEVICE) for k, v in enc.items()} t0 = time.time() with torch.no_grad(): logits = MODEL(**enc).logits ms = int((time.time() - t0) * 1000) probs = F.softmax(logits, dim=-1).squeeze().cpu().numpy() idx = int(np.argmax(probs)) label = ID2LABEL.get(idx, f"class_{idx}") return ({ID2LABEL.get(i, f"class_{i}"): float(probs[i]) for i in range(len(probs))}, label, float(probs[idx]), ms) # ── Charts ──────────────────────────────────────────────────────────────────── def conf_chart(probs_dict, pred_label): paired = sorted(zip(probs_dict.values(), probs_dict.keys()), reverse=True) vals = [p[0]*100 for p in paired] labs = [p[1] for p in paired] colors = ["#10b981" if l == pred_label else "#6366f1" if v > 10 else "#334155" for l, v in zip(labs, vals)] fig, ax = plt.subplots(figsize=(9, max(4, len(labs)*0.5+1))) fig.patch.set_facecolor("#0f172a"); ax.set_facecolor("#0f172a") bars = ax.barh(labs[::-1], vals[::-1], color=colors[::-1], height=0.55, edgecolor="none") for bar, v in zip(bars, vals[::-1]): ax.text(bar.get_width()+0.5, bar.get_y()+bar.get_height()/2, f"{v:.1f}%", va="center", ha="left", color="#e2e8f0", fontsize=10, fontweight="bold") ax.set_xlim(0, 115) ax.set_xlabel("Confidence (%)", color="#94a3b8", fontsize=11) ax.set_title("Prediction Confidence", color="#f1f5f9", fontsize=13, fontweight="bold", pad=12) ax.tick_params(colors="#94a3b8", labelsize=10) for s in ax.spines.values(): s.set_visible(False) ax.grid(axis="x", color="#1e293b", linewidth=0.8) plt.tight_layout(pad=1.5) return fig def metrics_chart(): models = list(REAL_METRICS.keys()) accs = [REAL_METRICS[m]["test_acc"] for m in models] f1s = [REAL_METRICS[m]["test_f1"] for m in models] cols = [REAL_METRICS[m]["color"] for m in models] x, w = np.arange(len(models)), 0.32 fig, ax = plt.subplots(figsize=(10, 5)) fig.patch.set_facecolor("#0f172a"); ax.set_facecolor("#0f172a") b1 = ax.bar(x-w/2, accs, w, label="Test Accuracy (%)", color=[c+"cc" for c in cols], edgecolor="none") b2 = ax.bar(x+w/2, f1s, w, label="Test F1 Macro (%)", color=cols, edgecolor="none", alpha=0.75) for bars in [b1, b2]: for bar in bars: h = bar.get_height() ax.text(bar.get_x()+bar.get_width()/2, h+0.5, f"{h:.1f}", ha="center", va="bottom", color="#e2e8f0", fontsize=10, fontweight="bold") ax.set_xticks(x); ax.set_xticklabels(models, color="#94a3b8", fontsize=11) ax.set_ylim(0, 105); ax.set_ylabel("Score (%)", color="#94a3b8", fontsize=11) ax.set_title("Model Comparison — Test Results", color="#f1f5f9", fontsize=13, fontweight="bold", pad=14) ax.tick_params(colors="#94a3b8") ax.legend(facecolor="#1e293b", edgecolor="none", labelcolor="#e2e8f0") for s in ax.spines.values(): s.set_visible(False) ax.grid(axis="y", color="#1e293b", linewidth=0.8) plt.tight_layout(pad=1.5) return fig _METRICS_FIG = metrics_chart() # pre-render once # ── Gradio handlers ─────────────────────────────────────────────────────────── def classify_single(text): if not text or not text.strip(): return '

Please enter a headline.

', None, None pd, label, conf, ms = predict_text(text) icon = get_icon(label) pct = conf * 100 cc = "#10b981" if pct >= 70 else "#f59e0b" if pct >= 40 else "#ef4444" html = f"""
{icon}
Predicted Category
{label.title()}
Confidence
{pct:.1f}%
Model
XLM-RoBERTa
Inference
{ms} ms

IndicGLUE  ·  5 languages  ·  {NUM_CLASSES} categories  ·  Test acc: 86.12%
""" return html, conf_chart(pd, label), pd def classify_batch(batch_text): if not batch_text or not batch_text.strip(): return '

Enter at least one headline.

', None lines = [l.strip() for l in batch_text.strip().split("\n") if l.strip()][:50] rows = "" labels_list = [] for i, line in enumerate(lines, 1): pd, label, conf, _ = predict_text(line) icon = get_icon(label); pct = conf*100 cc = "#10b981" if pct >= 70 else "#f59e0b" if pct >= 40 else "#ef4444" prev = (line[:80]+"…") if len(line) > 80 else line labels_list.append(label) rows += f""" {i} {prev} {icon} {label.title()} {pct:.1f}% """ from collections import Counter counts = Counter(labels_list) summary = " · ".join(f"{get_icon(k)} {k.title()}: {v}" for k,v in counts.most_common(5)) table = f"""
{len(lines)} headlines — {summary}
{rows}
# Headline Category Conf.
""" # Pie chart fig, ax = plt.subplots(figsize=(7, 5)) fig.patch.set_facecolor("#0f172a"); ax.set_facecolor("#0f172a") pal = ["#10b981","#6366f1","#f59e0b","#ef4444","#3b82f6","#8b5cf6","#ec4899","#14b8a6","#f97316","#84cc16"] cd = dict(counts) wedges, texts, ats = ax.pie(cd.values(), labels=[k.title() for k in cd], autopct="%1.0f%%", colors=pal[:len(cd)], startangle=140, wedgeprops={"edgecolor":"#0f172a","linewidth":2}) for t in texts: t.set_color("#94a3b8"); t.set_fontsize(10) for at in ats: at.set_color("#0f172a"); at.set_fontweight("bold"); at.set_fontsize(9) ax.set_title("Category Distribution", color="#f1f5f9", fontsize=13, fontweight="bold", pad=14) plt.tight_layout() return table, fig # ── CSS ─────────────────────────────────────────────────────────────────────── # IMPORTANT: No @import (blocked in Kaggle). No body/html background override # (breaks Kaggle iframe rendering). Only style our own named classes. CSS = """ * { box-sizing: border-box; } .gradio-container { max-width: 1100px !important; margin: 0 auto !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; } .app-header { background: linear-gradient(135deg, #0f172a, #1e1b4b 50%, #0f172a); border: 1px solid #1e293b; border-radius: 14px; padding: 32px 40px 24px; text-align: center; margin-bottom: 8px; } .header-badge { display: inline-block; background: linear-gradient(90deg, #6366f1, #8b5cf6); color: white; font-size: 10px; font-weight: 700; letter-spacing: 2.5px; text-transform: uppercase; padding: 4px 14px; border-radius: 20px; margin-bottom: 14px; } .header-title { font-size: 38px; font-weight: 800; color: #f1f5f9; line-height: 1.1; margin: 0 0 8px; } .header-title span { background: linear-gradient(90deg, #6366f1, #10b981); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .header-sub { font-size: 14px; color: #64748b; margin: 0; } .header-stats { display: flex; justify-content: center; gap: 16px; margin-top: 20px; flex-wrap: wrap; } .stat-pill { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 7px 16px; font-size: 12px; color: #94a3b8; } .stat-pill strong { color: #e2e8f0; } .tab-nav { background: #0f172a !important; border-bottom: 1px solid #1e293b !important; } .tab-nav button { color: #64748b !important; font-weight: 600 !important; font-size: 13px !important; padding: 12px 20px !important; border: none !important; border-bottom: 2px solid transparent !important; background: transparent !important; } .tab-nav button.selected { color: #6366f1 !important; border-bottom-color: #6366f1 !important; } textarea, input[type=text] { background: #1e293b !important; border: 1px solid #334155 !important; color: #e2e8f0 !important; border-radius: 10px !important; font-size: 14px !important; } label { color: #94a3b8 !important; font-size: 12px !important; text-transform: uppercase !important; } button.primary { background: linear-gradient(135deg, #6366f1, #8b5cf6) !important; color: white !important; font-weight: 700 !important; border: none !important; border-radius: 10px !important; } button.secondary { background: #1e293b !important; color: #94a3b8 !important; border: 1px solid #334155 !important; border-radius: 8px !important; } .app-footer { background: #0f172a; border: 1px solid #1e293b; border-radius: 14px; padding: 24px 40px; text-align: center; margin-top: 24px; } .footer-team { display: flex; justify-content: center; gap: 32px; flex-wrap: wrap; margin-bottom: 14px; } .footer-member { display: flex; align-items: center; gap: 10px; } .footer-avatar { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 13px; color: white; } .footer-name { font-size: 13px; color: #94a3b8; } .footer-roll { font-size: 11px; color: #475569; } .footer-copy { font-size: 12px; color: #64748b; margin-top: 10px; } footer { display: none !important; } """ HEADER = """
Generative AI Assignment · CBIT · 2025-26

Multilingual News
Classification

Chaitanya Bharathi Institute of Technology · Dept. of AI & ML

Model XLM-RoBERTa
Languages 5 Indic
Dataset IndicGLUE
Test Acc 86.12%
""" FOOTER = """ """ PROJECT_HTML = """

Problem Statement

A single unified model that reads Telugu, Malayalam, Marathi, Tamil, and Gujarati natively, classifying news headlines into up to 10 categories — no translation required.

Dataset — IndicGLUE (ai4bharat/indic_glue)

iNLTK Headlines subsets — 37,069 labeled headlines across 5 languages.
Split: Train 25,945 · Val 3,707 · Test 7,414

Results (Test Set)

TF-IDF + LR: 83.84% · F1 77.85%
BiLSTM: 79.36% · F1 67.16%
XLM-RoBERTa: 86.% · F1 78.75%

""" TEAM_HTML = """
Meet the Team
Dept. of AI & ML · CBIT · Guided by Mr. Panigrahi Srikanth
J
Jay Kumar Das
160123748035 · Phase 1 Lead

IndicGLUE data loading, Unicode-safe preprocessing, TF-IDF baseline (84.95%), EDA.

S
Siddhartha Dontula
160123748036 · Phase 2 Lead

BiLSTM design (60k vocab, GlobalMaxPool), training curves, per-class evaluation (79.36%).

P
Praneeth Reddy Ganta
160123748037 · Phase 3 Lead

XLM-RoBERTa fine-tuning, full evaluation, Gradio UI deployment (86.12%).

""" # ── Build UI ────────────────────────────────────────────────────────────────── # ONE with gr.Blocks() block. Nothing opens after it closes. No demo.load(). # The metrics chart uses gr.Plot(value=_METRICS_FIG) — renders immediately. with gr.Blocks(css=CSS, title="Multilingual News Classification") as demo: gr.HTML(HEADER) with gr.Tabs(): with gr.Tab("Classify News"): with gr.Row(): with gr.Column(scale=1): txt_in = gr.Textbox( placeholder="Paste a news headline in any of the 5 supported languages...", lines=4, label="News Headline") gr.HTML('
Load Sample
') with gr.Row(): for lang in ["Telugu", "Malayalam", "Marathi"]: b = gr.Button(lang, size="sm") b.click(fn=lambda l=lang: SAMPLES.get(l,""), outputs=txt_in) with gr.Row(): for lang in ["Tamil", "Gujarati"]: b = gr.Button(lang, size="sm") b.click(fn=lambda l=lang: SAMPLES.get(l,""), outputs=txt_in) go_btn = gr.Button("Classify", variant="primary", size="lg") with gr.Column(scale=1): res_html = gr.HTML() res_chart = gr.Plot() res_json = gr.JSON(visible=False) go_btn.click(fn=classify_single, inputs=txt_in, outputs=[res_html, res_chart, res_json]) with gr.Tab("Batch Classify"): gr.HTML('
Batch mode — one headline per line, max 50.
') with gr.Row(): with gr.Column(scale=1): batch_in = gr.Textbox(placeholder="One headline per line...", lines=12, label="Headlines") batch_btn = gr.Button("Classify All", variant="primary") with gr.Column(scale=1): batch_tbl = gr.HTML() batch_chart = gr.Plot() batch_btn.click(fn=classify_batch, inputs=batch_in, outputs=[batch_tbl, batch_chart]) with gr.Tab("Model Comparison"): gr.Plot(value=_METRICS_FIG) # pre-rendered — no event needed with gr.Row(): for mname, md in REAL_METRICS.items(): with gr.Column(): gr.HTML(f"""
{mname}
{md['test_acc']}%
Test Accuracy
{md['test_f1']}%
F1 Macro
{md['train_time']}
""") with gr.Tab("Project Details"): gr.HTML(PROJECT_HTML) with gr.Tab("Team"): gr.HTML(TEAM_HTML) gr.HTML(FOOTER) # ── Launch ──────────────────────────────────────────────────────────────────── # Kill any leftover Gradio server first (re-running a Kaggle cell leaves it alive) def _free_ports(): for port in range(7860, 7871): try: r = subprocess.run(["lsof", "-ti", f"tcp:{port}"], capture_output=True, text=True) for pid in r.stdout.strip().split("\n"): if pid: os.kill(int(pid), signal.SIGKILL) print(f"[INFO] Freed port {port} (killed PID {pid})") except Exception: pass _free_ports() try: demo.close() except Exception: pass import time as _t; _t.sleep(1) demo.launch( share=True, # Required in Kaggle — generates gradio.live public URL server_port=7860, # Kaggle proxies this port to its output iframe server_name="0.0.0.0", show_error=True, quiet=False, )