Spaces:
Running on Zero
Running on Zero
File size: 14,324 Bytes
064fb44 f3d0a56 064fb44 f3d0a56 a264e34 c31f724 52716e4 a264e34 064fb44 f3d0a56 064fb44 6ae309d f3d0a56 6ae309d f3d0a56 064fb44 f3d0a56 064fb44 f3d0a56 064fb44 f3d0a56 064fb44 f3d0a56 a56caea 52716e4 a56caea 52716e4 064fb44 f3d0a56 4986b4a f3d0a56 064fb44 f3d0a56 49aa5e3 52716e4 a56caea 52716e4 49aa5e3 f3d0a56 4986b4a f3d0a56 a264e34 4986b4a f3d0a56 a264e34 064fb44 f3d0a56 52716e4 a264e34 064fb44 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | # 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">⚠ 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) |