'
def build_single_card_html(r: dict, ms_name: str, margin_bottom="10px") -> str:
st = r.get("source_type", "")
name = r.get("name", "").strip()
src = r.get("source", "")
ym = r.get("year_month", "")
loc = " › ".join(p for p in [r.get("state",""), r.get("district",""), r.get("block",""), r.get("village","")] if p and p not in ("nan","none",""))
css = {"marksheet": "card-marksheet", "legacy_db": "card-legacy", "latest_master": "card-latest", "old_master": "card-old"}.get(st, "card-old")
if ym and "(" not in src: src = f"{src} ({ym})"
icon = {"marksheet": "📄 Marksheet", "legacy_db": "📚 Old Master", "latest_master": "✅ Latest Master", "old_master": "🕐 Older Master"}.get(st, src)
if ym: icon = f"{icon} ({ym})"
badge = ""
if ms_name and st != "marksheet" and name.upper() == ms_name.upper(): badge = '✓ Same Name'
display = name if name and name.lower() not in ("nan","none","") else "Not found"
loc_html = f'
📍 {loc}
' if loc else ""
return f"""
{icon}
{display}{badge}
{loc_html}
"""
def build_empty_html():
return """
🔍 Select a location, enter a school name or UDISE code, and click Find Schools Matching schools and their historical names will appear here
"""
# ─── Event handlers ───────────────────────────────────────────────────────────
def update_udise_dropdown(state, district, block, village):
choices = get_udise_choices(state, district, block, village)
return gr.update(choices=choices, value=None)
def on_state_change(state):
if not state:
return gr.update(info="Select State"), gr.update(choices=[], value=None, interactive=False, info="Select State first")
districts = sorted(STATE_HIER.get(state, {}).keys())
return gr.update(info=""), gr.update(choices=districts, value=None, interactive=True, info="Select District")
def on_district_change(state, district):
if not state:
return gr.update(info="Select State first"), gr.update(choices=[], value=None, interactive=False, info="Select State first")
if not district:
return gr.update(info="Select District"), gr.update(choices=[], value=None, interactive=False, info="Select District first")
blocks = sorted(STATE_HIER.get(state, {}).get(district, {}).keys())
return gr.update(info=""), gr.update(choices=blocks, value=None, interactive=True, info="Select Block")
def on_block_change(state, district, block):
if not state:
return gr.update(info="Select State first"), gr.update(choices=[], value=None, interactive=False, info="Select State first")
if not district:
return gr.update(info="Select District first"), gr.update(choices=[], value=None, interactive=False, info="Select District first")
if not block:
return gr.update(info="Select Block"), gr.update(choices=[], value=None, interactive=False, info="Select Block first")
villages = sorted(STATE_HIER.get(state, {}).get(district, {}).get(block, []))
return gr.update(info=""), gr.update(choices=villages, value=None, interactive=True, info="Select Village")
def on_village_change(state, district, block, village):
choices = get_udise_choices(state, district, block, village)
if not state:
return gr.update(info="Select State first"), gr.update(choices=choices, value=None)
if not district:
return gr.update(info="Select District first"), gr.update(choices=choices, value=None)
if not block:
return gr.update(info="Select Block first"), gr.update(choices=choices, value=None)
if not village:
return gr.update(info="Select Village"), gr.update(choices=choices, value=None)
return gr.update(info=""), gr.update(choices=choices, value=None)
def on_search(query_name, state, district, block, village, udise_val):
if udise_val and str(udise_val).strip():
# User selected or typed a UDISE. Extract the code if they selected from dropdown (which is "UDISE - Name")
u_code = str(udise_val).split(" - ")[0].strip()
return [u_code]
if not query_name:
return []
matched_udises = search_by_name_fuzzy(query_name, state, district, block, village, max_results=5)
return matched_udises
def on_refresh_caches():
res = refresh_all_caches()
global STATE_HIER, STATE_CHOICES
STATE_HIER = get_state_hierarchy()
STATE_CHOICES = sorted(STATE_HIER.keys())
return res, gr.update(choices=STATE_CHOICES)
# ─── Theme ────────────────────────────────────────────────────────────────────
THEME = gr.themes.Base(
primary_hue=gr.themes.colors.violet,
secondary_hue=gr.themes.colors.blue,
neutral_hue=gr.themes.colors.slate,
)
# ─── UI ───────────────────────────────────────────────────────────────────────
with gr.Blocks(title="School Name Resolver") as app:
search_state = gr.State([])
render_trigger = gr.State(0)
gr.HTML("""
🏫 School Name Resolver
Cross-reference marksheet names against historical UDISE records to resolve discrepancies
""")
gr.HTML('
Step 1 — Enter Location & Name
')
with gr.Row():
state_dd = gr.Dropdown(label="State", choices=STATE_CHOICES, value=None, info="Select State")
district_dd = gr.Dropdown(label="District", choices=[], value=None, interactive=False, info="Select State first")
with gr.Row():
block_dd = gr.Dropdown(label="Block", choices=[], value=None, interactive=False, info="Select District first")
village_dd = gr.Dropdown(label="Village", choices=[], value=None, interactive=False, info="Select Block first")
with gr.Row():
school_input = gr.Textbox(label="School Name", placeholder="Enter school name from marksheet...", max_lines=1, scale=1)
udise_input = gr.Dropdown(label="Or Search by UDISE Code", choices=[], allow_custom_value=True, scale=1)
with gr.Row():
clear_btn = gr.Button("🗑️ Clear Form", elem_classes="btn-secondary", size="lg", scale=1)
search_btn = gr.Button("🔍 Find Schools", elem_classes="btn-search", size="lg", scale=3)
gr.HTML('
')
gr.HTML('
Step 2 — Search Results & Mapping
')
# ── Dynamic Results Rendering ──
@gr.render(inputs=[search_state, render_trigger])
def render_results(udises, trigger):
if not udises:
gr.HTML(build_empty_html())
return
for udise in udises:
details = search_udise(udise, "")
# Cross-reference with alias dictionary to pull in custom names
mapped_names = get_names_for_udise(udise)
detail_names_upper = {r.get("name","").upper() for r in details}
for m_info in mapped_names:
m_name = m_info.get("name", "").strip()
if m_name and m_name.upper() not in detail_names_upper:
details.append({
"source": m_info.get("source", "Saved Marksheet"),
"source_type": "marksheet",
"name": m_name,
"year_month": m_info.get("year_month", "")
})
with gr.Row(elem_classes="step-box"):
with gr.Column(scale=3):
with gr.Column(elem_classes="result-panel"):
gr.HTML(build_results_heading_html(details, udise))
ms_name = next((r["name"] for r in details if r["source_type"] == "marksheet"), "")
for r in details:
if r.get("source_type") == "marksheet":
with gr.Row(elem_classes="marksheet-row"):
with gr.Column(scale=5, min_width=100):
gr.HTML(build_single_card_html(r, ms_name, margin_bottom="0"))
with gr.Column(scale=1, min_width=60):
del_btn = gr.Button("🗑️", elem_classes="btn-delete")
def make_on_delete(u_code=udise, n=r.get("name")):
def on_delete(trigger_val):
delete_alias(u_code, n)
return trigger_val + 1
return on_delete
del_btn.click(fn=make_on_delete(), inputs=[render_trigger], outputs=[render_trigger])
else:
gr.HTML(build_single_card_html(r, ms_name))
with gr.Column(scale=1):
# We removed the separate marksheet_input, we will just read directly from school_input!
with gr.Row():
month_input = gr.Dropdown(label="Month", choices=["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], value=datetime.now().strftime("%b"), interactive=True, allow_custom_value=True)
year_input = gr.Dropdown(label="Year", choices=[str(y) for y in range(2000, datetime.now().year + 2)], value=str(datetime.now().year), interactive=True, allow_custom_value=True)
map_btn = gr.Button("Save Alias", elem_classes="btn-map")
status_text = gr.Textbox(show_label=False, interactive=False, visible=False, elem_classes="map-status", max_lines=1)
def on_map_inline(u, m, month_val, year_val, trigger_val, d=details):
m = m.strip()
if not m: return gr.update(visible=True, value="⚠️ Type a name in the search box above to save!"), trigger_val
custom_ym = f"{year_val}-{month_val}"
entries = [{"udise_code": u, "alias_name": m, "source_label": "Marksheet", "year_month": custom_ym}]
seen = {m.upper()}
for r in d:
name = str(r.get("name","")).strip()
if name and name.lower() not in ("nan","none","") and name.upper() not in seen:
entries.append({
"udise_code": u,
"alias_name": name,
"source_label": r.get("source",""),
"year_month": r.get("year_month", "")
})
seen.add(name.upper())
msg = save_aliases(entries)
return gr.update(visible=True, value=msg), trigger_val + 1
map_btn.click(
fn=on_map_inline,
inputs=[gr.State(udise), school_input, month_input, year_input, render_trigger],
outputs=[status_text, render_trigger]
)
# ── Download Master Sheet ──
gr.HTML('')
download_aliases_btn = gr.DownloadButton("📥 Download Aliases Master Sheet (Excel)", elem_classes="btn-secondary", size="lg")
def on_download_aliases():
from huggingface_hub import hf_hub_download
import os
import pandas as pd
import tempfile
token = os.getenv("HF_TOKEN")
repo = os.getenv("HF_SCRAPER_REPO")
if not token or not repo: return None
try:
path = hf_hub_download(
repo_id=repo,
filename="school_aliases.csv",
repo_type="dataset",
token=token,
force_download=True,
)
# Convert the CSV into a true Excel (.xlsx) file on the fly!
df = pd.read_csv(path)
import datetime
date_str = datetime.datetime.now().strftime("%b_%Y")
download_name = f"school_aliases_{date_str}.xlsx"
temp_dir = tempfile.mkdtemp()
out_path = os.path.join(temp_dir, download_name)
df.to_excel(out_path, index=False)
return out_path
except Exception as e:
print(f"Error downloading for UI: {e}")
return None
download_aliases_btn.click(fn=on_download_aliases, outputs=[download_aliases_btn])
# ── Settings ──
with gr.Accordion("⚙️ Settings & Cache", open=False):
refresh_cache_btn = gr.Button("🔄 Refresh Data Cache", elem_classes="btn-secondary")
cache_status = gr.Textbox(show_label=False, interactive=False, max_lines=2)
def on_clear_form():
return (
gr.update(value=None), # state
gr.update(value=None), # school_input
[] # search_state
)
clear_btn.click(
fn=on_clear_form,
outputs=[state_dd, school_input, search_state]
)
state_dd.change(fn=on_state_change, inputs=[state_dd], outputs=[state_dd, district_dd])
district_dd.change(fn=on_district_change, inputs=[state_dd, district_dd], outputs=[district_dd, block_dd])
block_dd.change(fn=on_block_change, inputs=[state_dd, district_dd, block_dd], outputs=[block_dd, village_dd])
village_dd.change(fn=on_village_change, inputs=[state_dd, district_dd, block_dd, village_dd], outputs=[village_dd, udise_input])
search_btn.click(
fn=on_search,
inputs=[school_input, state_dd, district_dd, block_dd, village_dd, udise_input],
outputs=[search_state],
)
school_input.submit(
fn=on_search,
inputs=[school_input, state_dd, district_dd, block_dd, village_dd, udise_input],
outputs=[search_state],
)
udise_input.change(
fn=on_search,
inputs=[school_input, state_dd, district_dd, block_dd, village_dd, udise_input],
outputs=[search_state],
)
refresh_cache_btn.click(fn=on_refresh_caches, outputs=[cache_status, state_dd])
def load_initial_data():
try:
global STATE_HIER, STATE_CHOICES
STATE_HIER = get_state_hierarchy()
STATE_CHOICES = sorted(STATE_HIER.keys())
return gr.update(choices=STATE_CHOICES, interactive=True, info="Select State")
except Exception as e:
print(f"Failed to load data on boot: {e}")
return gr.update(info="Failed to load data")
app.load(fn=load_initial_data, outputs=[state_dd])
# ─── Launch ───────────────────────────────────────────────────────────────────
if __name__ == "__main__":
port = int(os.getenv("PORT", 7860))
print(f"Starting Gradio app on port {port}...", flush=True)
app.launch(
server_name="0.0.0.0",
server_port=port,
theme=THEME,
css=CSS,
inbrowser=True
)