redaction_tool / app.py
Alokik089's picture
Rewrite as a genuine Gradio app (ZeroGPU requires Gradio's own launch)
b97057a
Raw History Blame Contribute Delete
5.3 kB
"""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()