""" InfoShield — Hugging Face Space entry point (Gradio SDK, no Gradio UI). This Space is declared `sdk: gradio` (see README.md) so it builds and runs, but the user-facing surface is the InfoShield React site — there is NO Gradio interface. A minimal, hidden Gradio app is mounted only at /_gradio to satisfy the Gradio SDK runtime; nothing links to it. Flat layout note: Hugging Face's web uploader cannot preserve subfolders, so all files (the React build + cache data + this script) live in the repo root. This file serves the static site from its own directory and reads the cache from the same place — cache-first, no torch, no Gemini, no network. """ from __future__ import annotations import json import os from pathlib import Path import gradio as gr from fastapi import FastAPI from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles ROOT = Path(__file__).parent.resolve() PORT = int(os.environ.get("PORT", os.environ.get("GRADIO_SERVER_PORT", 7860))) def _load_json(name: str, default): try: return json.loads((ROOT / name).read_text(encoding="utf-8")) except Exception: return default _POSTS = _load_json("posts.json", []) _CACHE = _load_json("cache.json", {"meta": {}, "posts": {}}) _CACHE_POSTS = _CACHE.get("posts", {}) def _merged_feed(): out = [] for p in _POSTS: c = _CACHE_POSTS.get(p["id"], {}) cls = c.get("classification", {}) or {} out.append({**p, "classification": cls, "analysis": c.get("analysis"), "flagged": bool(cls.get("flagged"))}) return out def _stats(): feed = _merged_feed() flagged = [p for p in feed if p["flagged"]] techniques, confidences = {}, [] for p in flagged: if p["classification"].get("confidence") is not None: confidences.append(p["classification"]["confidence"]) for t in ((p.get("analysis") or {}).get("techniques") or []): name = t.get("name") if isinstance(t, dict) else t if name: techniques[name] = techniques.get(name, 0) + 1 avg = round(sum(confidences) / len(confidences), 4) if confidences else 0.0 return { "total_posts": len(feed), "flagged_posts": len(flagged), "clear_posts": len(feed) - len(flagged), "technique_distribution": techniques, "avg_confidence": avg, "model_meta": _CACHE.get("meta", {}), "mode": "cache-first (read-only, no model)", } _ACTIVATION = {"target_url": "", "enabled": False} app = FastAPI(title="InfoShield (cache-first, static)", docs_url=None, redoc_url=None) @app.get("/health") def health(): return {"status": "ok", "mode": "cache-first", "model_loaded": False, "device": "none (serving cache)", "posts": len(_POSTS)} @app.get("/feed") def feed(): return {"posts": _merged_feed()} @app.get("/stats") def stats(): return _stats() @app.get("/post-analysis/{post_id}") def post_analysis(post_id: str): c = _CACHE_POSTS.get(post_id) if not c: return JSONResponse({"error": "unknown post id"}, status_code=404) return {"id": post_id, "classification": c.get("classification", {}), "analysis": c.get("analysis")} @app.get("/activation-target") def get_activation_target(): return _ACTIVATION @app.post("/activation-target") def set_activation_target(payload: dict): _ACTIVATION["target_url"] = str(payload.get("target_url", "")) _ACTIVATION["enabled"] = bool(payload.get("enabled", True)) return _ACTIVATION # Hidden Gradio app — present only to satisfy the Gradio SDK runtime. Mounted # before the static catch-all so its routes resolve first. with gr.Blocks(analytics_enabled=False, title="InfoShield (internal)") as _hidden: gr.Markdown("InfoShield runs as a static site at `/`. This Gradio mount is internal.") app = gr.mount_gradio_app(app, _hidden, path="/_gradio") # Serve index.html for "/" explicitly, then mount the flat static dir LAST so the # API routes and /_gradio above take precedence over the "/" catch-all. @app.get("/") def _index(): return FileResponse(ROOT / "index.html") app.mount("/", StaticFiles(directory=str(ROOT), html=True), name="static") if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=PORT)