rrsanjabi commited on
Commit
db8cb48
·
verified ·
1 Parent(s): e507822

Upload 2 files

Browse files
Files changed (2) hide show
  1. server/app.py +149 -0
  2. server/requirements.txt +6 -0
server/app.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Local demo server: static site + EDI-PHI redaction API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from contextlib import asynccontextmanager
7
+ from pathlib import Path
8
+
9
+ from fastapi import FastAPI, HTTPException
10
+ from fastapi.middleware.cors import CORSMiddleware
11
+ from fastapi.staticfiles import StaticFiles
12
+ from pydantic import BaseModel, Field
13
+
14
+ ROOT = Path(__file__).resolve().parents[1]
15
+ MODEL_DIR = ROOT / "models" / "sentedel-edi-phi-v1"
16
+
17
+ _classifier = None
18
+ _load_error: str | None = None
19
+ _device_label: str | None = None
20
+
21
+
22
+ def _resolve_device() -> int | str:
23
+ """Map env / hardware to a pipeline device (auto is not valid for pipeline)."""
24
+ import torch
25
+
26
+ requested = os.environ.get("SENTEDEL_DEVICE", "auto").strip().lower()
27
+
28
+ if requested in ("cpu", "-1"):
29
+ return -1
30
+ if requested in ("cuda", "gpu", "0", "cuda:0"):
31
+ return 0 if torch.cuda.is_available() else -1
32
+ if requested.startswith("cuda:"):
33
+ return 0 if torch.cuda.is_available() else -1
34
+
35
+ # auto: prefer GPU when available
36
+ return 0 if torch.cuda.is_available() else -1
37
+
38
+
39
+ def _load_classifier():
40
+ global _classifier, _load_error, _device_label
41
+ if not (MODEL_DIR / "model.safetensors").is_file():
42
+ _load_error = f"Missing weights: {MODEL_DIR / 'model.safetensors'}"
43
+ return
44
+
45
+ try:
46
+ from transformers import pipeline
47
+
48
+ device = _resolve_device()
49
+ _device_label = "cuda" if device == 0 else "cpu"
50
+ _classifier = pipeline(
51
+ "token-classification",
52
+ model=str(MODEL_DIR),
53
+ aggregation_strategy="simple",
54
+ device=device,
55
+ )
56
+ _load_error = None
57
+ except Exception as exc: # noqa: BLE001
58
+ _load_error = str(exc)
59
+ _classifier = None
60
+ _device_label = None
61
+
62
+
63
+ @asynccontextmanager
64
+ async def lifespan(_app: FastAPI):
65
+ _load_classifier()
66
+ yield
67
+
68
+
69
+ app = FastAPI(title="Sentedel EDI-PHI Demo", lifespan=lifespan)
70
+
71
+ app.add_middleware(
72
+ CORSMiddleware,
73
+ allow_origins=["*"],
74
+ allow_methods=["*"],
75
+ allow_headers=["*"],
76
+ )
77
+
78
+
79
+ class RedactRequest(BaseModel):
80
+ text: str = Field(min_length=1, max_length=200_000)
81
+
82
+
83
+ class Entity(BaseModel):
84
+ label: str
85
+ start: int
86
+ end: int
87
+ score: float
88
+
89
+
90
+ class RedactResponse(BaseModel):
91
+ redacted: str
92
+ entities: list[Entity]
93
+ latency_ms: int
94
+
95
+
96
+ def redact_text(text: str, entities: list[dict]) -> str:
97
+ sorted_entities = sorted(entities, key=lambda e: e.get("start", 0), reverse=True)
98
+ out = text
99
+ for ent in sorted_entities:
100
+ start, end = ent.get("start"), ent.get("end")
101
+ if start is None or end is None:
102
+ continue
103
+ out = out[:start] + "[REDACTED]" + out[end:]
104
+ return out
105
+
106
+
107
+ @app.get("/api/health")
108
+ def health():
109
+ return {
110
+ "ok": _classifier is not None,
111
+ "model_dir": str(MODEL_DIR),
112
+ "device": _device_label,
113
+ "error": _load_error,
114
+ }
115
+
116
+
117
+ @app.post("/api/redact", response_model=RedactResponse)
118
+ def redact(body: RedactRequest):
119
+ if _classifier is None:
120
+ raise HTTPException(
121
+ status_code=503,
122
+ detail=_load_error or "Model is not loaded.",
123
+ )
124
+
125
+ import time
126
+
127
+ t0 = time.perf_counter()
128
+ raw = _classifier(body.text)
129
+ latency_ms = int((time.perf_counter() - t0) * 1000)
130
+
131
+ entities = [
132
+ Entity(
133
+ label=str(item.get("entity_group") or item.get("label") or "unknown"),
134
+ start=int(item["start"]),
135
+ end=int(item["end"]),
136
+ score=float(item.get("score", 0.0)),
137
+ )
138
+ for item in raw
139
+ if item.get("start") is not None and item.get("end") is not None
140
+ ]
141
+
142
+ return RedactResponse(
143
+ redacted=redact_text(body.text, raw),
144
+ entities=entities,
145
+ latency_ms=latency_ms,
146
+ )
147
+
148
+
149
+ app.mount("/", StaticFiles(directory=ROOT, html=True), name="site")
server/requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi>=0.115.0
2
+ uvicorn[standard]>=0.32.0
3
+ torch>=2.2.0
4
+ safetensors>=0.4.0
5
+ # Privacy Filter architecture (openai_privacy_filter)
6
+ transformers @ git+https://github.com/huggingface/transformers.git