Spaces:
Running on Zero
Running on Zero
| # 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: | |
| 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'<div class="jd-track"><div class="jd-fill" ' | |
| f'style="width:{pct:.1f}%;background:{color}"></div></div>') | |
| def render(r): | |
| vc, sc = r["vertical_confidence"], r["sub_vertical_confidence"] | |
| v_flag = ('<span class="jd-badge jd-badge-warn">⚠ Review · low confidence</span>' | |
| if r["vertical_review"] else '<span class="jd-badge jd-badge-ok">✓ Confident</span>') | |
| v_card = (f'<div class="jd-card"><div class="jd-card-top"><span class="jd-eyebrow">VERTICAL</span>' | |
| f'{v_flag}</div><div class="jd-value jd-blue">{r["vertical"]}</div>' | |
| f'{bar(vc*100, conf_color(vc))}<div class="jd-pct">{vc*100:.1f}% confidence</div></div>') | |
| if r["propose_new_sub_vertical"]: | |
| s_flag = '<span class="jd-badge jd-badge-new">✨ New sub-vertical suggested</span>' | |
| s_extra = (f'<div class="jd-suggest">Suggested name: ' | |
| f'<span class="jd-chip">{r["suggested_sub_vertical"]}</span></div>') | |
| else: | |
| s_flag = '<span class="jd-badge jd-badge-ok">✓ Fits existing</span>' | |
| s_extra = '' | |
| s_card = (f'<div class="jd-card"><div class="jd-card-top"><span class="jd-eyebrow">SUB-VERTICAL</span>' | |
| f'{s_flag}</div><div class="jd-value jd-orange">{r["sub_vertical"]}</div>' | |
| f'{bar(sc*100, conf_color(sc))}<div class="jd-pct">{sc*100:.1f}% confidence</div>{s_extra}</div>') | |
| rows = "" | |
| for v, c in r["vertical_top3"]: | |
| rows += (f'<div class="jd-top-row"><div class="jd-top-name">{v}</div>' | |
| f'<div class="jd-top-bar">{bar(c*100, JD_BLUE)}</div>' | |
| f'<div class="jd-top-pct">{c*100:.1f}%</div></div>') | |
| top = f'<div class="jd-card"><div class="jd-eyebrow">TOP 3 VERTICALS</div>{rows}</div>' | |
| return f'<div class="jd-results">{v_card}{s_card}{top}</div>' | |
| EMPTY_HTML = ('<div class="jd-empty">Enter a category name above and click ' | |
| '<b>Classify</b> to see its vertical, sub-vertical and confidence.</div>') | |
| 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, ('<div class="jd-status jd-status-info">Upload an Excel/CSV file with a ' | |
| '<b>category_name</b> column, then click <b>Process file</b>.</div>') | |
| 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'<div class="jd-status jd-status-err">Could not read the file: {e}</div>' | |
| cols = {c.strip().lower(): c for c in df.columns} | |
| if "category_name" not in cols: | |
| return None, ('<div class="jd-status jd-status-err">No <b>category_name</b> column found. ' | |
| f'Columns present: {", ".join(map(str, df.columns))}</div>') | |
| 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'<div class="jd-status jd-status-ok">Processed <b>{n}</b> rows · ' | |
| f'{rev} flagged for vertical review · {new} with a suggested new ' | |
| f'sub-vertical. <br>Download the results file below.</div>') | |
| return out_path, status | |
| HEADER = ''' | |
| <div class="jd-header"> | |
| <div class="jd-brand"><span class="jd-just">Just</span><span class="jd-dial">dial</span></div> | |
| <div class="jd-divider"></div> | |
| <div class="jd-apptitle">AI Category Classifier</div> | |
| <span class="jd-live">● LIVE</span> | |
| </div> | |
| <div class="jd-stats"> | |
| <div class="jd-stat"><div class="jd-stat-label">Verticals</div><div class="jd-stat-num">21</div></div> | |
| <div class="jd-stat"><div class="jd-stat-label">Sub-verticals</div><div class="jd-stat-num">122</div></div> | |
| <div class="jd-stat"><div class="jd-stat-label">Vertical accuracy</div><div class="jd-stat-num">97.1%</div></div> | |
| <div class="jd-stat"><div class="jd-stat-label">Exact match</div><div class="jd-stat-num">97.6%</div></div> | |
| </div> | |
| <div class="jd-sub">Type a category name to get its <b>vertical</b> (1 of 21) and <b>sub-vertical</b>, | |
| each with a confidence score. Low-confidence verticals are flagged for review; when no existing | |
| sub-vertical fits, a new one is proposed.</div> | |
| ''' | |
| 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('<div class="jd-section">Bulk classify — upload an Excel/CSV</div>' | |
| '<div class="jd-sub" style="margin-top:-2px">File must have a <b>category_name</b> column. ' | |
| 'You get back vertical, sub-vertical and both confidence scores as a downloadable Excel.</div>') | |
| 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('<div class="jd-status jd-status-info">Upload a file with a ' | |
| '<b>category_name</b> column, then click <b>Process file</b>.</div>') | |
| 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) |