pridwimnjha's picture
Update app.py
c31f724 verified
Raw
History Blame Contribute Delete
14.3 kB
# 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'<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">&#9888; Review &middot; low confidence</span>'
if r["vertical_review"] else '<span class="jd-badge jd-badge-ok">&#10003; 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">&#10024; 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">&#10003; 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&nbsp;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 &nbsp;&middot;&nbsp; '
f'{rev} flagged for vertical review &nbsp;&middot;&nbsp; {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">&#9679; 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 &mdash; 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)