"""Entry point for the FREE Hugging Face Spaces path (SDK: gradio + ZeroGPU hardware) -- see this folder's README.md for the why and setup. This is a genuine Gradio app, not a FastAPI app with Gradio bolted on -- that was the first design here, and it turned out to be a dead end: Hugging Face confirmed directly ("ZeroGPU Spaces are exclusively compatible with the Gradio SDK") that ZeroGPU's @spaces.GPU detection only works for functions actually invoked through Gradio's own `launch()`/event system, not a custom `uvicorn.run()` -- even one with Gradio mounted onto it. Real errors seen on an actual deploy before this rewrite: "No @spaces.GPU function detected during startup", and a port 7860 bind conflict (Gradio's own launch process trying to start alongside our manual uvicorn.run() call). What's unchanged despite this rewrite: `redactor/` -- the actual detection and redaction logic -- is untouched and imported directly, exactly as scripts/redact.py does. `api/` (the FastAPI backend used by every other deploy path) isn't used here at all; this file is its own self-contained UI calling the same underlying engine. One real, unavoidable tradeoff, same as before this rewrite: this sets REDACTOR_SPACY_MODEL to en_core_web_lg -- a lower-accuracy model than en_core_web_trf (see EVALUATION.md) -- because en_core_web_trf needs transformers, which needs huggingface-hub<1.0, which conflicts with the gradio version Hugging Face's own build pipeline force-installs here (needs huggingface-hub>=1.16). Confirmed via a real ResolutionImpossible on an actual build, not inferred. """ import os os.environ.setdefault("REDACTOR_SPACY_MODEL", "en_core_web_lg") import tempfile from pathlib import Path import gradio as gr import spaces from redactor.docx_io import redact_docx from redactor.engine import RedactionEngine # Built once at import time, not per upload -- loading the spaCy model is # the expensive part; every upload reuses this same engine, same as # api/main.py's lifespan does for the other deploy paths. print("Loading RedactionEngine...") _engine = RedactionEngine() print("RedactionEngine ready.") @spaces.GPU def _zerogpu_eligibility_placeholder(): """Exists only so this Space, on ZeroGPU hardware, has at least one @spaces.GPU function genuinely reachable through Gradio's own event system (wired to demo.load() below) -- a hard ZeroGPU platform requirement, confirmed via a real failed deploy, not decorative. The actual redaction work in redact_uploaded_file() below is deliberately NOT decorated with @spaces.GPU: it's CPU-only (en_core_web_lg needs no GPU at all), and decorating it would spend ZeroGPU's very small daily quota (3.5 minutes/day, 3 requests/day) on every single upload for zero benefit -- the app would become unusable beyond a couple of test runs per day for no reason. """ return None def redact_uploaded_file(file_path: str, progress: gr.Progress = gr.Progress()): """The actual product: takes an uploaded .docx, returns a redacted one plus a short summary. Runs on plain CPU, not GPU-decorated -- see _zerogpu_eligibility_placeholder's docstring for why that's deliberate, not an oversight.""" if file_path is None: raise gr.Error("Upload a .docx file first.") output_path = str(Path(tempfile.mkdtemp()) / "redacted.docx") def on_progress(units_done: int, units_total: int): fraction = units_done / units_total if units_total else 0 progress(fraction, desc=f"Redacting... {units_done}/{units_total} sections") try: replacement_log = redact_docx(file_path, output_path, engine=_engine, on_unit_done=on_progress) except Exception as e: raise gr.Error(f"Redaction failed: {e}") summary = f"{len(replacement_log)} value(s) redacted." return output_path, summary with gr.Blocks(title="BetterRedactionTool") as demo: gr.Markdown( "# BetterRedactionTool\n" "Upload a `.docx` file. PII is detected and swapped for consistent, " "realistic fake values -- the same real name/email/etc. always " "becomes the same fake one everywhere in the document." ) file_input = gr.File(label="Upload .docx", file_types=[".docx"], file_count="single", type="filepath") redact_button = gr.Button("Redact", variant="primary") summary_output = gr.Textbox(label="Result", interactive=False) file_output = gr.File(label="Download redacted.docx") redact_button.click( fn=redact_uploaded_file, inputs=file_input, outputs=[file_output, summary_output], ) # Registers _zerogpu_eligibility_placeholder as a real Gradio event # handler (fires once, when the UI loads) -- this is what makes it # "genuinely reachable through Gradio's own event system" rather # than just a decorated function sitting unused in the file. demo.load(fn=_zerogpu_eligibility_placeholder, inputs=None, outputs=None) if __name__ == "__main__": # No explicit host/port: Hugging Face's Gradio-SDK runtime sets # GRADIO_SERVER_NAME/GRADIO_SERVER_PORT, and demo.launch() reads # them automatically. Hardcoding our own here is what caused the # port-7860 bind conflict in the previous (non-Gradio-native) design. demo.launch()