# ============================================ # app.py — Gradio interface for HuggingFace Spaces # Provides both a web UI and automatic API endpoints # ============================================ import gradio as gr from bias_detector import BiasDetector # Load models once at startup bd = BiasDetector() # Session-level sustainability log (accumulated across all requests in one session) _session_log: list[dict] = [] def _format_sustainability(s: dict) -> str: """Format a sustainability dict into a human-readable string.""" return ( f"⚡ Energy: {s['energy_kwh']:.6f} kWh\n" f"💨 CO₂eq: {s['co2_grams']:.4f} gCO₂e\n" f"💧 Water: {s['water_liters']:.6f} L" ) def _session_totals() -> dict: """Sum all sustainability metrics recorded so far this session.""" return { "energy_kwh": sum(r["energy_kwh"] for r in _session_log), "co2_grams": sum(r["co2_grams"] for r in _session_log), "water_liters": sum(r["water_liters"] for r in _session_log), } def _build_history_table() -> str: """Return a plain-text table of all requests so far.""" if not _session_log: return "No requests yet this session." lines = [ f"{'#':<4} {'Type':<20} {'Energy (kWh)':<16} {'CO₂ (gCO₂e)':<16} {'Water (L)':<12}", "-" * 72, ] for i, r in enumerate(_session_log, 1): lines.append( f"{i:<4} {r['type']:<20} " f"{r['energy_kwh']:<16.6f} " f"{r['co2_grams']:<16.4f} " f"{r['water_liters']:<12.6f}" ) totals = _session_totals() lines.append("-" * 72) lines.append( f"{'TOTAL':<24} " f"{totals['energy_kwh']:<16.6f} " f"{totals['co2_grams']:<16.4f} " f"{totals['water_liters']:<12.6f}" ) return "\n".join(lines) # ---- Endpoint functions ---- def bias_detection(text): """Endpoint: analyze + rewrite a job posting.""" rewrite = bd.rewrite_job_posting(text) result = rewrite["analysis"] summary = ( f"Overall: {result['overall_label']}\n" f"Bias Score: {result['bias_score']}\n" f"ML Model: {result['ml_model_result']['label']} " f"({round(result['ml_model_result']['score'], 3)})\n" f"Masculine-coded words: {result['masculine_count']}\n" f"Feminine-coded words: {result['feminine_count']}" ) flagged_summary = "" for f in result["flagged_words"]: flagged_summary += f"• \"{f['word']}\" → {f['category']}\n" if not flagged_summary: flagged_summary = "No gendered words detected by lexicon." # Log sustainability s = rewrite["sustainability"] _session_log.append({"type": "Job posting rewrite", **s}) totals = _session_totals() return ( summary, flagged_summary, rewrite["fully_rewritten"], _format_sustainability(s), _build_history_table(), _format_sustainability(totals), ) def anonymize(text): """Endpoint: anonymize a CV/letter.""" result = bd.anonymize_document(text) # Log sustainability s = result["sustainability"] _session_log.append({"type": "CV anonymization", **s}) totals = _session_totals() return ( result["surface_anonymized"], result["fully_anonymized"], _format_sustainability(s), _build_history_table(), _format_sustainability(totals), ) def refresh_sustainability(): """Refresh the sustainability tab manually.""" totals = _session_totals() return _build_history_table(), _format_sustainability(totals) # ---- Build Gradio UI ---- with gr.Blocks(title="Nubias: Gender Bias Detector & CV Anonymizer") as demo: gr.Markdown("# 🌍 Nubias: Gender Bias Detector & CV Anonymizer") gr.Markdown( "Detecting gender bias in job postings and anonymizing CVs/letters " "to promote fair hiring practices — aligned with **SDG 5: Gender Equality**." ) # Shared sustainability components (declared here, rendered inside the tab below) history_box = gr.Textbox(label="Request History", lines=12, interactive=False, visible=False) totals_box = gr.Textbox(label="Session Totals", lines=4, interactive=False, visible=False) with gr.Tab("Job Posting Analyzer"): gr.Markdown("### Analyze and rewrite a job posting for gendered language") input_posting = gr.Textbox( label="Paste job posting here", lines=8, placeholder="e.g. We are looking for an aggressive self-starter who can dominate the market...", ) btn_analyze = gr.Button("Analyze & Rewrite", variant="primary") output_summary = gr.Textbox(label="Bias Analysis", lines=5) output_flagged = gr.Textbox(label="Flagged Words", lines=5) output_rewrite = gr.Textbox(label="Gender-Neutral Rewrite", lines=8) output_sustain_j = gr.Textbox(label="♻️ This Request — Sustainability", lines=4) with gr.Tab("CV / Letter Anonymizer"): gr.Markdown("### Anonymize a CV, cover letter, or recommendation letter") input_cv = gr.Textbox( label="Paste document text here", lines=8, placeholder="e.g. Sarah Johnson (sarah@email.com) is an exceptionally warm leader...", ) btn_anonymize = gr.Button("Anonymize", variant="primary") output_surface = gr.Textbox( label="Surface Anonymized (names, emails, pronouns, titles removed)", lines=6 ) output_full = gr.Textbox( label="Fully Anonymized (gendered style words neutralized)", lines=6 ) output_sustain_c = gr.Textbox(label="♻️ This Request — Sustainability", lines=4) with gr.Tab("♻️ Sustainability"): gr.Markdown( "### Environmental impact of Nubias this session\n" "Estimates are based on measured CPU energy draw (via **CodeCarbon**) " "and a data-centre Water Usage Effectiveness (WUE) of **1.6 L / kWh**.\n\n" "Carbon intensity uses CodeCarbon's location-aware grid data. " "All figures are per-request and cumulative for the current session." ) tab_history = gr.Textbox(label="Request History", lines=12, interactive=False) tab_totals = gr.Textbox(label="Session Totals", lines=4, interactive=False) btn_refresh = gr.Button("🔄 Refresh", variant="secondary") btn_refresh.click( refresh_sustainability, inputs=[], outputs=[tab_history, tab_totals], ) # Wire action buttons — both update the sustainability tab directly btn_analyze.click( bias_detection, inputs=input_posting, outputs=[output_summary, output_flagged, output_rewrite, output_sustain_j, tab_history, tab_totals], ) btn_anonymize.click( anonymize, inputs=input_cv, outputs=[output_surface, output_full, output_sustain_c, tab_history, tab_totals], ) demo.launch(server_name="0.0.0.0", server_port=7860)