# JustDial-themed Category -> Vertical & Sub-vertical Classifier (Gradio 6.x safe) try: import spaces # ZeroGPU runtime on Hugging Face except ImportError: # local / CPU fallback class spaces: @staticmethod def GPU(fn=None, **kw): return (lambda f: f)(fn) if fn else (lambda f: f) import gradio as gr from inference import classify, VERT_THR, SUB_THR import os, tempfile import pandas as pd JD_BLUE, JD_ORANGE, GREEN, RED = "#00457C", "#F58220", "#16a34a", "#dc2626" def conf_color(c): return GREEN if c >= 0.80 else (JD_ORANGE if c >= 0.60 else RED) def bar(pct, color): return (f'
') def render(r): vc, sc = r["vertical_confidence"], r["sub_vertical_confidence"] v_flag = ('⚠ Review · low confidence' if r["vertical_review"] else '✓ Confident') v_card = (f'
VERTICAL' f'{v_flag}
{r["vertical"]}
' f'{bar(vc*100, conf_color(vc))}
{vc*100:.1f}% confidence
') if r["propose_new_sub_vertical"]: s_flag = '✨ New sub-vertical suggested' s_extra = (f'
Suggested name: ' f'{r["suggested_sub_vertical"]}
') else: s_flag = '✓ Fits existing' s_extra = '' s_card = (f'
SUB-VERTICAL' f'{s_flag}
{r["sub_vertical"]}
' f'{bar(sc*100, conf_color(sc))}
{sc*100:.1f}% confidence
{s_extra}
') rows = "" for v, c in r["vertical_top3"]: rows += (f'
{v}
' f'
{bar(c*100, JD_BLUE)}
' f'
{c*100:.1f}%
') top = f'
TOP 3 VERTICALS
{rows}
' return f'
{v_card}{s_card}{top}
' EMPTY_HTML = ('
Enter a category name above and click ' 'Classify to see its vertical, sub-vertical and confidence.
') def _run(name, vthr, sthr): if not name or not name.strip(): return EMPTY_HTML return render(classify(name, vert_thr=vthr, sub_thr=sthr)) run = spaces.GPU(_run) # ZeroGPU-detectable; also works on CPU def process_file(file, vthr, sthr, progress=gr.Progress()): if file is None: return None, ('
Upload an Excel/CSV file with a ' 'category_name column, then click Process file.
') path = file if isinstance(file, str) else getattr(file, "name", None) try: df = pd.read_csv(path, dtype=str) if str(path).lower().endswith(".csv") \ else pd.read_excel(path, dtype=str) except Exception as e: return None, f'
Could not read the file: {e}
' cols = {c.strip().lower(): c for c in df.columns} if "category_name" not in cols: return None, ('
No category_name column found. ' f'Columns present: {", ".join(map(str, df.columns))}
') names = df[cols["category_name"]].fillna("").astype(str) R = [classify(n, vert_thr=vthr, sub_thr=sthr) for n in progress.tqdm(names, desc="Classifying")] # keep every original column, then append the classification columns out = df.copy() out["vertical"] = [r["vertical"] for r in R] out["vertical_confidence_%"] = [round(r["vertical_confidence"]*100, 1) for r in R] out["vertical_flag"] = ["review" if r["vertical_review"] else "ok" for r in R] out["sub_vertical"] = [r["sub_vertical"] for r in R] out["sub_vertical_confidence_%"] = [round(r["sub_vertical_confidence"]*100, 1) for r in R] out["sub_vertical_status"] = ["new-suggested" if r["propose_new_sub_vertical"] else "fits-existing" for r in R] out["suggested_new_sub_vertical"] = [r["suggested_sub_vertical"] or "" for r in R] out_path = os.path.join(tempfile.gettempdir(), "classified_results.xlsx") out.to_excel(out_path, index=False) n = len(out); rev = int((out["vertical_flag"] == "review").sum()) new = int((out["sub_vertical_status"] == "new-suggested").sum()) status = (f'
Processed {n} rows  ·  ' f'{rev} flagged for vertical review  ·  {new} with a suggested new ' f'sub-vertical.
Download the results file below.
') return out_path, status HEADER = '''
Justdial
AI Category Classifier
● LIVE
Verticals
21
Sub-verticals
122
Vertical accuracy
97.1%
Exact match
97.6%
Type a category name to get its vertical (1 of 21) and sub-vertical, each with a confidence score. Low-confidence verticals are flagged for review; when no existing sub-vertical fits, a new one is proposed.
''' CSS = """ gradio-app, body {background:#ffffff !important;} .gradio-container, .gradio-container.dark, body { --body-background-fill:#ffffff; --background-fill-primary:#ffffff; --background-fill-secondary:#f7f8fa; --block-background-fill:#ffffff; --block-border-color:#eef0f2; --block-label-text-color:#6b7280; --body-text-color:#1f2937; --body-text-color-subdued:#6b7280; --input-background-fill:#ffffff; --input-border-color:#dfe3e8; --border-color-primary:#eef0f2; --color-accent:#F58220; --link-text-color:#00457C; --neutral-50:#f7f8fa; color-scheme:light !important;} .jd-blue{color:#00457C !important;} .jd-dial{color:#F58220 !important;} .jd-just{color:#00457C !important;} .jd-orange{color:#F58220 !important;} .gradio-container {background:#ffffff !important; max-width:960px !important; margin:0 auto !important; font-family:-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif !important;} .gradio-container::before {content:""; position:fixed; top:0; left:0; right:0; height:5px; z-index:50; background:linear-gradient(90deg,#ff5f6d,#ff8a5b,#ffc371);} footer {display:none !important;} .jd-header {display:flex; align-items:center; gap:14px; padding:26px 4px 6px;} .jd-brand {font-size:30px; font-weight:800; letter-spacing:-.5px;} .jd-just {color:#00457C;} .jd-dial {color:#F58220;} .jd-divider {width:1px; height:26px; background:#e5e7eb;} .jd-apptitle {font-size:20px; font-weight:600; color:#1f2937;} .jd-live {margin-left:auto; color:#16a34a; font-weight:700; font-size:12px; letter-spacing:.5px; background:#eafaf0; border:1px solid #bbe9ca; padding:4px 10px; border-radius:20px;} .jd-stats {display:grid; grid-template-columns:repeat(4,1fr); gap:14px; margin:18px 4px 6px;} .jd-stat {background:#fff; border:1px solid #eef0f2; border-radius:12px; padding:14px 16px; box-shadow:0 1px 2px rgba(0,0,0,.03);} .jd-stat-label {font-size:12px; color:#6b7280; margin-bottom:6px;} .jd-stat-num {font-size:26px; font-weight:800; color:#1f2937;} .jd-sub {color:#6b7280; font-size:14px; line-height:1.5; margin:12px 4px 8px;} #jd-input textarea {border:1px solid #dfe3e8 !important; border-radius:10px !important; font-size:16px !important;} #jd-input textarea:focus {border-color:#F58220 !important; box-shadow:0 0 0 3px rgba(245,130,32,.15) !important;} #jd-btn {background:#F58220 !important; color:#fff !important; border:none !important; font-weight:700 !important; border-radius:10px !important; box-shadow:0 2px 6px rgba(245,130,32,.25) !important;} #jd-btn:hover {background:#e2710f !important;} .jd-results {display:flex; flex-direction:column; gap:14px; margin-top:6px;} .jd-card {background:#fff; border:1px solid #eef0f2; border-radius:14px; padding:18px 20px; box-shadow:0 1px 3px rgba(0,0,0,.04);} .jd-card-top {display:flex; align-items:center; justify-content:space-between; margin-bottom:8px;} .jd-eyebrow {font-size:11px; font-weight:700; letter-spacing:1px; color:#9aa2ad;} .jd-value {font-size:24px; font-weight:800; margin:2px 0 12px; text-transform:capitalize;} .jd-blue {color:#00457C;} .jd-orange {color:#F58220;} .jd-track {height:9px; background:#f0f2f5; border-radius:6px; overflow:hidden;} .jd-fill {height:100%; border-radius:6px;} .jd-pct {font-size:12px; color:#6b7280; margin-top:6px;} .jd-badge {font-size:12px; font-weight:700; padding:4px 10px; border-radius:20px;} .jd-badge-ok {background:#eafaf0; color:#16a34a; border:1px solid #bbe9ca;} .jd-badge-warn {background:#fff5e6; color:#b45309; border:1px solid #fcd9a3;} .jd-badge-new {background:#eef4ff; color:#00457C; border:1px solid #c3d7f7;} .jd-suggest {margin-top:12px; font-size:13px; color:#374151;} .jd-chip {background:#fff5e6; color:#b45309; border:1px solid #fcd9a3; padding:3px 10px; border-radius:8px; font-weight:700;} .jd-top-row {display:flex; align-items:center; gap:12px; margin:9px 0;} .jd-top-name {width:150px; font-size:13px; color:#374151; text-transform:capitalize;} .jd-top-bar {flex:1;} .jd-top-pct {width:52px; text-align:right; font-size:12px; color:#6b7280; font-weight:600;} .jd-empty {background:#fafbfc; border:1px dashed #dfe3e8; border-radius:14px; padding:26px; text-align:center; color:#6b7280; font-size:14px;} .jd-section {font-size:16px; font-weight:800; color:#1f2937; margin:26px 4px 2px; padding-top:10px; border-top:1px solid #eef0f2;} #jd-btn2 {background:#00457C !important; color:#fff !important; border:none !important; font-weight:700 !important; border-radius:10px !important; box-shadow:0 2px 6px rgba(0,69,124,.2) !important;} #jd-btn2:hover {background:#013a68 !important;} .jd-status {font-size:13px; border-radius:10px; padding:12px 14px; margin:8px 4px;} .jd-status-info {background:#f7f8fa; border:1px solid #eef0f2; color:#374151;} .jd-status-ok {background:#eff8f1; border:1px solid #bbe9ca; color:#166534;} .jd-status-err {background:#fef2f2; border:1px solid #f3c0c0; color:#b91c1c;} #jd-file, #jd-file *, #jd-fileout, #jd-fileout * {background:#ffffff !important; color:#374151 !important; -webkit-text-fill-color:#374151 !important;} #jd-file, #jd-fileout {border:1px solid #dfe3e8 !important; border-radius:10px !important;} #jd-file a, #jd-fileout a {color:#00457C !important; -webkit-text-fill-color:#00457C !important;} #jd-file svg, #jd-fileout svg {color:#6b7280 !important; opacity:1 !important;} /* --- force native components to light (input text, labels, examples) --- */ #jd-input textarea, #jd-input input {background:#ffffff !important; color:#1f2937 !important; -webkit-text-fill-color:#1f2937 !important; caret-color:#F58220 !important;} .gradio-container label, .gradio-container .label-wrap, .gradio-container .label-wrap > span, .gradio-container span[data-testid="block-info"], .gradio-container .head { color:#374151 !important; opacity:1 !important;} .gradio-container .label-wrap svg, .gradio-container .icon {color:#374151 !important; opacity:1 !important;} .gradio-container [class*="sample"] {background:#ffffff !important; color:#1f2937 !important; -webkit-text-fill-color:#1f2937 !important; border:1px solid #dfe3e8 !important;} .gradio-container [class*="sample"]:hover {background:#fff5e6 !important; color:#b45309 !important; -webkit-text-fill-color:#b45309 !important; border-color:#fcd9a3 !important;} .gradio-container .accordion, .gradio-container .accordion * {opacity:1 !important;} """ FORCE_LIGHT = """ () => { const u = new URL(window.location.href); if (u.searchParams.get('__theme') !== 'light') { u.searchParams.set('__theme', 'light'); window.location.replace(u.href); } } """ with gr.Blocks(title="JustDial AI Category Classifier", js=FORCE_LIGHT) as demo: gr.HTML(HEADER) with gr.Row(): inp = gr.Textbox(show_label=False, placeholder="e.g. Bridal Makeup Artists", elem_id="jd-input", scale=5, lines=1) btn = gr.Button("Classify", elem_id="jd-btn", scale=1) with gr.Accordion("Thresholds (advanced)", open=False): vthr = gr.Slider(0.0, 1.0, value=VERT_THR, step=0.05, label="Vertical review threshold") sthr = gr.Slider(0.0, 1.0, value=SUB_THR, step=0.05, label="New sub-vertical threshold") out = gr.HTML(EMPTY_HTML) btn.click(run, [inp, vthr, sthr], out) inp.submit(run, [inp, vthr, sthr], out) gr.HTML('
Bulk classify — upload an Excel/CSV
' '
File must have a category_name column. ' 'You get back vertical, sub-vertical and both confidence scores as a downloadable Excel.
') with gr.Row(): fin = gr.File(label="Upload .xlsx / .csv", file_types=[".xlsx", ".xls", ".csv"], type="filepath", elem_id="jd-file", scale=4) pbtn = gr.Button("Process file", elem_id="jd-btn2", scale=1) fstatus = gr.HTML('
Upload a file with a ' 'category_name column, then click Process file.
') fout = gr.File(label="Download results (.xlsx)", elem_id="jd-fileout") pbtn.click(process_file, [fin, vthr, sthr], [fout, fstatus]) if __name__ == "__main__": demo.launch(css=CSS)