Spaces:
Running on Zero
Running on Zero
use gradio server
#2
by akhaliq HF Staff - opened
- app.py +39 -159
- index.html +233 -0
app.py
CHANGED
|
@@ -11,10 +11,11 @@ from dataclasses import dataclass
|
|
| 11 |
from pathlib import Path
|
| 12 |
from typing import Final
|
| 13 |
|
| 14 |
-
import gradio as gr
|
| 15 |
import spaces
|
| 16 |
import torch
|
| 17 |
import torch.nn.functional as F
|
|
|
|
|
|
|
| 18 |
from safetensors import safe_open
|
| 19 |
|
| 20 |
import tiktoken
|
|
@@ -1229,20 +1230,6 @@ def predict_text(
|
|
| 1229 |
return source_text, detected
|
| 1230 |
|
| 1231 |
|
| 1232 |
-
@spaces.GPU
|
| 1233 |
-
def predict(text: str) -> dict[str, object]:
|
| 1234 |
-
text = text or ""
|
| 1235 |
-
if not text.strip():
|
| 1236 |
-
return EMPTY_HIGHLIGHT_PAYLOAD
|
| 1237 |
-
runtime = get_runtime()
|
| 1238 |
-
decoder = Decoder(label_info=runtime.label_info)
|
| 1239 |
-
filtered_text, spans = predict_text(runtime, text, decoder)
|
| 1240 |
-
return {
|
| 1241 |
-
"text": filtered_text,
|
| 1242 |
-
"entities": spans,
|
| 1243 |
-
}
|
| 1244 |
-
|
| 1245 |
-
|
| 1246 |
def build_redacted_text(text: str, entities: Sequence[dict[str, object]]) -> str:
|
| 1247 |
if not text or not entities:
|
| 1248 |
return text
|
|
@@ -1278,159 +1265,52 @@ def build_redacted_text(text: str, entities: Sequence[dict[str, object]]) -> str
|
|
| 1278 |
return "".join(redacted_parts)
|
| 1279 |
|
| 1280 |
|
| 1281 |
-
def summarize_entities_markdown(entities: Sequence[dict[str, object]]) -> str:
|
| 1282 |
-
if not entities:
|
| 1283 |
-
return EMPTY_SUMMARY_MARKDOWN
|
| 1284 |
|
| 1285 |
-
counts: dict[str, int] = {}
|
| 1286 |
-
for entity in entities:
|
| 1287 |
-
label = entity.get("entity")
|
| 1288 |
-
if not isinstance(label, str):
|
| 1289 |
-
continue
|
| 1290 |
-
counts[label] = counts.get(label, 0) + 1
|
| 1291 |
-
if not counts:
|
| 1292 |
-
return EMPTY_SUMMARY_MARKDOWN
|
| 1293 |
|
| 1294 |
-
|
| 1295 |
-
|
| 1296 |
-
lines.extend(f"- `{label}`: {count}" for label, count in ordered_labels)
|
| 1297 |
-
return "\n".join(lines)
|
| 1298 |
|
| 1299 |
|
|
|
|
| 1300 |
@spaces.GPU
|
| 1301 |
-
def
|
| 1302 |
-
|
| 1303 |
-
|
| 1304 |
-
|
| 1305 |
-
|
| 1306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1307 |
redacted_text = build_redacted_text(display_text, entities)
|
| 1308 |
-
summary = summarize_entities_markdown(entities)
|
| 1309 |
-
return prediction, redacted_text, summary
|
| 1310 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1311 |
|
| 1312 |
-
|
| 1313 |
-
|
| 1314 |
-
|
| 1315 |
-
|
| 1316 |
-
|
| 1317 |
-
|
| 1318 |
-
checkpoint_config,
|
| 1319 |
-
context=str(config_path),
|
| 1320 |
-
)
|
| 1321 |
-
span_class_names = SPAN_CLASS_NAMES
|
| 1322 |
-
web_color_palette = (
|
| 1323 |
-
"#e6194b",
|
| 1324 |
-
"#3cb44b",
|
| 1325 |
-
"#4363d8",
|
| 1326 |
-
"#f58231",
|
| 1327 |
-
"#911eb4",
|
| 1328 |
-
"#008080",
|
| 1329 |
-
"#9a6324",
|
| 1330 |
-
"#f032e6",
|
| 1331 |
-
"#b59f00",
|
| 1332 |
-
"#800000",
|
| 1333 |
-
"#000075",
|
| 1334 |
-
"#808080",
|
| 1335 |
-
)
|
| 1336 |
-
with gr.Blocks(
|
| 1337 |
-
**supported_kwargs(
|
| 1338 |
-
gr.Blocks,
|
| 1339 |
-
title="OpenAI Privacy Filter",
|
| 1340 |
-
fill_width=True,
|
| 1341 |
-
elem_id="privacy-filter-app",
|
| 1342 |
-
)
|
| 1343 |
-
) as demo:
|
| 1344 |
-
gr.Markdown("# OpenAI Privacy Filter Demo")
|
| 1345 |
-
gr.Markdown(
|
| 1346 |
-
"Detect and redact personal identifiers using `openai/privacy-filter`.\n\n"
|
| 1347 |
-
"This demo highlights predicted spans and generates a redacted text variant "
|
| 1348 |
-
"with label placeholders."
|
| 1349 |
-
)
|
| 1350 |
|
| 1351 |
-
with gr.Column(variant="panel"):
|
| 1352 |
-
input_text = gr.Textbox(
|
| 1353 |
-
**supported_kwargs(
|
| 1354 |
-
gr.Textbox,
|
| 1355 |
-
lines=6,
|
| 1356 |
-
label="Input text with PII",
|
| 1357 |
-
placeholder="Paste text to detect personal identifiers and generate redacted output...",
|
| 1358 |
-
container=False,
|
| 1359 |
-
)
|
| 1360 |
-
)
|
| 1361 |
-
with gr.Row():
|
| 1362 |
-
submit_button = gr.Button("Detect & Redact", variant="primary")
|
| 1363 |
-
clear_button = gr.Button("Clear")
|
| 1364 |
-
|
| 1365 |
-
with gr.Column(variant="panel"):
|
| 1366 |
-
output_text = gr.HighlightedText(
|
| 1367 |
-
**supported_kwargs(
|
| 1368 |
-
gr.HighlightedText,
|
| 1369 |
-
label="Detected entities (highlighted)",
|
| 1370 |
-
value=EMPTY_HIGHLIGHT_PAYLOAD,
|
| 1371 |
-
color_map={
|
| 1372 |
-
label: web_color_palette[idx % len(web_color_palette)]
|
| 1373 |
-
for idx, label in enumerate(
|
| 1374 |
-
label for label in span_class_names if label != BACKGROUND_CLASS_LABEL
|
| 1375 |
-
)
|
| 1376 |
-
},
|
| 1377 |
-
combine_adjacent=False,
|
| 1378 |
-
show_legend=False,
|
| 1379 |
-
container=True,
|
| 1380 |
-
)
|
| 1381 |
-
)
|
| 1382 |
-
redacted_output = gr.Textbox(
|
| 1383 |
-
**supported_kwargs(
|
| 1384 |
-
gr.Textbox,
|
| 1385 |
-
label="Redacted text output",
|
| 1386 |
-
lines=6,
|
| 1387 |
-
show_copy_button=True,
|
| 1388 |
-
interactive=False,
|
| 1389 |
-
)
|
| 1390 |
-
)
|
| 1391 |
-
entity_summary = gr.Markdown(EMPTY_SUMMARY_MARKDOWN)
|
| 1392 |
-
with gr.Accordion("How to read results", open=False):
|
| 1393 |
-
gr.Markdown(
|
| 1394 |
-
"- Detects 8 span categories: person, email, phone, address, date, URL, "
|
| 1395 |
-
"account number, and secrets.\n"
|
| 1396 |
-
"- Uses sequence decoding (BIOES + constrained Viterbi) for cleaner boundaries.\n"
|
| 1397 |
-
"- Best treated as a redaction aid, not a standalone compliance or anonymization guarantee.\n"
|
| 1398 |
-
"- Official card notes strongest support is English, with limited multilingual robustness."
|
| 1399 |
-
)
|
| 1400 |
-
submit_button.click(
|
| 1401 |
-
fn=predict_for_demo,
|
| 1402 |
-
inputs=input_text,
|
| 1403 |
-
outputs=[output_text, redacted_output, entity_summary],
|
| 1404 |
-
api_name="predict_and_redact",
|
| 1405 |
-
)
|
| 1406 |
-
input_text.submit(
|
| 1407 |
-
fn=predict_for_demo,
|
| 1408 |
-
inputs=input_text,
|
| 1409 |
-
outputs=[output_text, redacted_output, entity_summary],
|
| 1410 |
-
)
|
| 1411 |
-
clear_button.click(
|
| 1412 |
-
lambda: ("", EMPTY_HIGHLIGHT_PAYLOAD, "", EMPTY_SUMMARY_MARKDOWN),
|
| 1413 |
-
outputs=[input_text, output_text, redacted_output, entity_summary],
|
| 1414 |
-
)
|
| 1415 |
|
| 1416 |
-
|
| 1417 |
-
|
| 1418 |
-
|
| 1419 |
-
|
| 1420 |
-
|
| 1421 |
-
["Me llamo Laura Gómez y vivo en Calle de Alcalá 21, Madrid."],
|
| 1422 |
-
["Mon e-mail est jean.dupont@example.fr et mon téléphone est +33 6 12 34 56 78."],
|
| 1423 |
-
["私の名前は山田太郎です。メールはtaro.yamada@example.jpです。"],
|
| 1424 |
-
["اسمي أحمد وبريدي هو ahmed@example.com ورقم هاتفي +971501234567."],
|
| 1425 |
-
],
|
| 1426 |
-
inputs=input_text,
|
| 1427 |
-
outputs=[output_text, redacted_output, entity_summary],
|
| 1428 |
-
fn=predict_for_demo,
|
| 1429 |
-
cache_examples=False,
|
| 1430 |
-
)
|
| 1431 |
-
return demo
|
| 1432 |
|
| 1433 |
|
| 1434 |
-
|
| 1435 |
-
demo = build_demo()
|
| 1436 |
-
demo.launch()
|
|
|
|
| 11 |
from pathlib import Path
|
| 12 |
from typing import Final
|
| 13 |
|
|
|
|
| 14 |
import spaces
|
| 15 |
import torch
|
| 16 |
import torch.nn.functional as F
|
| 17 |
+
from fastapi.responses import HTMLResponse
|
| 18 |
+
from gradio import Server
|
| 19 |
from safetensors import safe_open
|
| 20 |
|
| 21 |
import tiktoken
|
|
|
|
| 1230 |
return source_text, detected
|
| 1231 |
|
| 1232 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1233 |
def build_redacted_text(text: str, entities: Sequence[dict[str, object]]) -> str:
|
| 1234 |
if not text or not entities:
|
| 1235 |
return text
|
|
|
|
| 1265 |
return "".join(redacted_parts)
|
| 1266 |
|
| 1267 |
|
|
|
|
|
|
|
|
|
|
| 1268 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1269 |
|
| 1270 |
+
|
| 1271 |
+
app = Server()
|
|
|
|
|
|
|
| 1272 |
|
| 1273 |
|
| 1274 |
+
@app.api()
|
| 1275 |
@spaces.GPU
|
| 1276 |
+
def predict_and_redact(text: str) -> dict:
|
| 1277 |
+
"""Detect PII entities in text and return highlighted entities plus redacted text."""
|
| 1278 |
+
text = text or ""
|
| 1279 |
+
if not text.strip():
|
| 1280 |
+
return {
|
| 1281 |
+
"text": "",
|
| 1282 |
+
"entities": [],
|
| 1283 |
+
"redacted": "",
|
| 1284 |
+
"summary": {},
|
| 1285 |
+
}
|
| 1286 |
+
runtime = get_runtime()
|
| 1287 |
+
decoder = Decoder(label_info=runtime.label_info)
|
| 1288 |
+
filtered_text, spans = predict_text(runtime, text, decoder)
|
| 1289 |
+
|
| 1290 |
+
entities = spans if isinstance(spans, list) else []
|
| 1291 |
+
display_text = filtered_text if isinstance(filtered_text, str) else text
|
| 1292 |
+
|
| 1293 |
redacted_text = build_redacted_text(display_text, entities)
|
|
|
|
|
|
|
| 1294 |
|
| 1295 |
+
counts: dict[str, int] = {}
|
| 1296 |
+
for entity in entities:
|
| 1297 |
+
label = entity.get("entity")
|
| 1298 |
+
if isinstance(label, str):
|
| 1299 |
+
counts[label] = counts.get(label, 0) + 1
|
| 1300 |
|
| 1301 |
+
return {
|
| 1302 |
+
"text": display_text,
|
| 1303 |
+
"entities": entities,
|
| 1304 |
+
"redacted": redacted_text,
|
| 1305 |
+
"summary": counts,
|
| 1306 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1307 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1308 |
|
| 1309 |
+
@app.get("/")
|
| 1310 |
+
async def homepage():
|
| 1311 |
+
html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
|
| 1312 |
+
with open(html_path, "r", encoding="utf-8") as f:
|
| 1313 |
+
return HTMLResponse(content=f.read())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1314 |
|
| 1315 |
|
| 1316 |
+
app.launch(show_error=True)
|
|
|
|
|
|
index.html
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>OpenAI Privacy Filter</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=Söhne,ui-sans-serif,system-ui,-apple-system,Segoe+UI,Roboto,Ubuntu,Cantarell,Noto+Sans,sans-serif&display=swap" rel="stylesheet">
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 9 |
+
<style>
|
| 10 |
+
*{margin:0;padding:0;box-sizing:border-box}
|
| 11 |
+
:root{
|
| 12 |
+
--bg:#0d0d0d;--sidebar-bg:#171717;--surface:#1e1e1e;--surface-2:#2a2a2a;
|
| 13 |
+
--border:rgba(255,255,255,.08);--border-hover:rgba(255,255,255,.15);
|
| 14 |
+
--text:#e3e3e3;--text-2:#a1a1a1;--text-3:#6e6e6e;
|
| 15 |
+
--green:#10a37f;--green-dim:rgba(16,163,127,.12);--green-hover:#0d8a6a;
|
| 16 |
+
--white:#fff;--red:#ef4146;
|
| 17 |
+
}
|
| 18 |
+
html,body{height:100%;font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased}
|
| 19 |
+
|
| 20 |
+
.app{display:flex;height:100vh;overflow:hidden}
|
| 21 |
+
|
| 22 |
+
/* ── Sidebar ── */
|
| 23 |
+
.sidebar{width:260px;background:var(--sidebar-bg);display:flex;flex-direction:column;padding:8px;flex-shrink:0}
|
| 24 |
+
.s-new{display:flex;align-items:center;gap:8px;padding:10px 12px;border-radius:10px;border:none;background:transparent;color:var(--text);cursor:pointer;font-size:14px;width:100%;transition:.15s}
|
| 25 |
+
.s-new:hover{background:var(--surface-2)}
|
| 26 |
+
.s-new svg{opacity:.7}
|
| 27 |
+
.s-label{font-size:12px;font-weight:500;color:var(--text-3);padding:16px 12px 6px;text-transform:uppercase;letter-spacing:.04em}
|
| 28 |
+
.s-list{flex:1;overflow-y:auto}
|
| 29 |
+
.s-item{padding:8px 12px;border-radius:8px;font-size:13px;color:var(--text-2);cursor:pointer;transition:.15s;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
| 30 |
+
.s-item:hover{background:var(--surface-2);color:var(--text)}
|
| 31 |
+
.s-bottom{border-top:1px solid var(--border);padding-top:8px;margin-top:8px}
|
| 32 |
+
.s-bottom a{display:flex;align-items:center;gap:8px;padding:8px 12px;border-radius:8px;font-size:13px;color:var(--text-2);text-decoration:none;transition:.15s}
|
| 33 |
+
.s-bottom a:hover{background:var(--surface-2);color:var(--text)}
|
| 34 |
+
|
| 35 |
+
/* ── Main ── */
|
| 36 |
+
.main{flex:1;display:flex;flex-direction:column;position:relative;min-width:0}
|
| 37 |
+
.topbar{height:44px;display:flex;align-items:center;padding:0 16px;gap:8px}
|
| 38 |
+
.topbar span{font-size:15px;font-weight:600;color:var(--text)}
|
| 39 |
+
.badge{font-size:10px;font-weight:600;background:var(--green);color:var(--white);padding:2px 7px;border-radius:5px}
|
| 40 |
+
|
| 41 |
+
/* ── Chat ── */
|
| 42 |
+
.chat{flex:1;overflow-y:auto;display:flex;flex-direction:column;align-items:center}
|
| 43 |
+
.chat-inner{max-width:680px;width:100%;padding:16px 20px 140px}
|
| 44 |
+
|
| 45 |
+
/* Welcome */
|
| 46 |
+
.welcome{display:flex;flex-direction:column;align-items:center;padding:80px 20px 40px;text-align:center}
|
| 47 |
+
.w-logo{width:48px;height:48px;border-radius:50%;background:var(--green);display:grid;place-items:center;margin-bottom:20px}
|
| 48 |
+
.w-logo svg{width:22px;height:22px;fill:var(--white)}
|
| 49 |
+
.welcome h1{font-size:22px;font-weight:600;margin-bottom:6px;color:var(--white)}
|
| 50 |
+
.welcome p{font-size:14px;color:var(--text-2);max-width:440px;line-height:1.6;margin-bottom:28px}
|
| 51 |
+
.examples{display:grid;grid-template-columns:1fr 1fr;gap:8px;width:100%;max-width:520px}
|
| 52 |
+
.ex-btn{padding:12px 14px;background:var(--surface);border:1px solid var(--border);border-radius:10px;color:var(--text-2);font-size:13px;line-height:1.45;cursor:pointer;text-align:left;transition:.2s}
|
| 53 |
+
.ex-btn:hover{border-color:var(--border-hover);color:var(--text);background:var(--surface-2)}
|
| 54 |
+
|
| 55 |
+
/* Messages */
|
| 56 |
+
.msg{padding:18px 0;animation:fadeUp .25s ease}
|
| 57 |
+
@keyframes fadeUp{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
|
| 58 |
+
.msg-row{display:flex;gap:14px}
|
| 59 |
+
.ava{width:28px;height:28px;border-radius:50%;display:grid;place-items:center;flex-shrink:0;font-size:12px;font-weight:600}
|
| 60 |
+
.ava.u{background:var(--surface-2);color:var(--text-2)}
|
| 61 |
+
.ava.a{background:var(--green);color:var(--white)}
|
| 62 |
+
.ava.a svg{width:14px;height:14px;fill:var(--white)}
|
| 63 |
+
.msg-c{flex:1;min-width:0}
|
| 64 |
+
.msg-c .role{font-size:13px;font-weight:600;margin-bottom:4px;color:var(--white)}
|
| 65 |
+
.msg-c .body{font-size:14px;line-height:1.7;color:var(--text)}
|
| 66 |
+
|
| 67 |
+
/* Entity tags */
|
| 68 |
+
.ent{padding:1px 5px;border-radius:3px;font-weight:500;font-size:13px;position:relative;cursor:default;border-bottom:2px solid}
|
| 69 |
+
.ent:hover::after{content:attr(data-t);position:absolute;bottom:calc(100% + 4px);left:50%;transform:translateX(-50%);background:var(--white);color:#000;font-size:10px;font-weight:600;padding:2px 7px;border-radius:4px;white-space:nowrap;z-index:9}
|
| 70 |
+
.ent-private_person{background:rgba(239,68,68,.12);color:#fca5a5;border-color:rgba(239,68,68,.5)}
|
| 71 |
+
.ent-private_email{background:rgba(99,102,241,.12);color:#a5b4fc;border-color:rgba(99,102,241,.5)}
|
| 72 |
+
.ent-private_phone{background:rgba(168,85,247,.12);color:#d8b4fe;border-color:rgba(168,85,247,.5)}
|
| 73 |
+
.ent-private_address{background:rgba(245,158,11,.12);color:#fcd34d;border-color:rgba(245,158,11,.5)}
|
| 74 |
+
.ent-private_date{background:rgba(20,184,166,.12);color:#5eead4;border-color:rgba(20,184,166,.5)}
|
| 75 |
+
.ent-private_url{background:rgba(249,115,22,.12);color:#fdba74;border-color:rgba(249,115,22,.5)}
|
| 76 |
+
.ent-account_number{background:rgba(236,72,153,.12);color:#f9a8d4;border-color:rgba(236,72,153,.5)}
|
| 77 |
+
.ent-secret{background:rgba(220,38,38,.15);color:#fca5a5;border-color:rgba(220,38,38,.6)}
|
| 78 |
+
|
| 79 |
+
/* Redacted card */
|
| 80 |
+
.r-card{margin-top:14px;border:1px solid var(--border);border-radius:10px;overflow:hidden;background:var(--surface)}
|
| 81 |
+
.r-head{display:flex;align-items:center;justify-content:space-between;padding:8px 14px;border-bottom:1px solid var(--border)}
|
| 82 |
+
.r-head span{font-size:11px;font-weight:600;color:var(--text-3);text-transform:uppercase;letter-spacing:.04em}
|
| 83 |
+
.cp-btn{padding:3px 8px;border:1px solid var(--border);border-radius:5px;background:transparent;color:var(--text-2);font-size:11px;cursor:pointer;transition:.15s}
|
| 84 |
+
.cp-btn:hover{background:var(--surface-2);color:var(--text)}
|
| 85 |
+
.r-body{padding:12px 14px;font-size:13px;line-height:1.7;color:var(--text-2);white-space:pre-wrap;word-break:break-word}
|
| 86 |
+
.r-body .tag{color:var(--green);font-weight:600}
|
| 87 |
+
|
| 88 |
+
/* Summary */
|
| 89 |
+
.pills{display:flex;flex-wrap:wrap;gap:5px;margin-top:10px}
|
| 90 |
+
.pill{padding:3px 9px;border:1px solid var(--border);border-radius:16px;font-size:11px;color:var(--text-2);background:var(--surface)}
|
| 91 |
+
.pill b{color:var(--green);margin-right:3px}
|
| 92 |
+
|
| 93 |
+
/* Dots */
|
| 94 |
+
.dots{display:flex;gap:4px;padding:2px 0}
|
| 95 |
+
.dots i{width:6px;height:6px;background:var(--text-3);border-radius:50%;animation:pulse .6s infinite alternate;font-style:normal}
|
| 96 |
+
.dots i:nth-child(2){animation-delay:.15s}
|
| 97 |
+
.dots i:nth-child(3){animation-delay:.3s}
|
| 98 |
+
@keyframes pulse{to{opacity:.25;transform:translateY(-3px)}}
|
| 99 |
+
|
| 100 |
+
/* ── Input bar ── */
|
| 101 |
+
.input-wrap{position:fixed;bottom:0;left:260px;right:0;padding:12px 20px 20px;background:linear-gradient(transparent,var(--bg) 40%)}
|
| 102 |
+
.input-inner{max-width:680px;margin:0 auto}
|
| 103 |
+
.input-box{display:flex;align-items:flex-end;gap:8px;background:var(--surface);border:1px solid var(--border);border-radius:20px;padding:6px 6px 6px 18px;transition:.2s}
|
| 104 |
+
.input-box:focus-within{border-color:rgba(255,255,255,.2)}
|
| 105 |
+
.input-box textarea{flex:1;background:none;border:none;outline:none;color:var(--text);font-size:14px;font-family:inherit;resize:none;max-height:140px;min-height:22px;line-height:1.5;padding:6px 0}
|
| 106 |
+
.input-box textarea::placeholder{color:var(--text-3)}
|
| 107 |
+
.send{width:32px;height:32px;border-radius:50%;border:none;cursor:pointer;display:grid;place-items:center;transition:.15s;flex-shrink:0;background:var(--white)}
|
| 108 |
+
.send:hover{opacity:.85;transform:scale(1.04)}
|
| 109 |
+
.send:disabled{background:var(--surface-2);cursor:default;transform:none;opacity:.5}
|
| 110 |
+
.send svg{width:14px;height:14px;fill:#000}
|
| 111 |
+
.foot{text-align:center;font-size:11px;color:var(--text-3);padding-top:6px}
|
| 112 |
+
|
| 113 |
+
::-webkit-scrollbar{width:5px}
|
| 114 |
+
::-webkit-scrollbar-thumb{background:rgba(255,255,255,.1);border-radius:3px}
|
| 115 |
+
@media(max-width:768px){.sidebar{display:none}.input-wrap{left:0}.examples{grid-template-columns:1fr}}
|
| 116 |
+
</style>
|
| 117 |
+
</head>
|
| 118 |
+
<body>
|
| 119 |
+
<div class="app">
|
| 120 |
+
<div class="sidebar">
|
| 121 |
+
<button class="s-new" onclick="resetChat()">
|
| 122 |
+
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
| 123 |
+
New analysis
|
| 124 |
+
</button>
|
| 125 |
+
<div class="s-label">Recent</div>
|
| 126 |
+
<div class="s-list" id="hist"></div>
|
| 127 |
+
<div class="s-bottom">
|
| 128 |
+
<a href="https://huggingface.co/openai/privacy-filter" target="_blank">
|
| 129 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>
|
| 130 |
+
Model Card
|
| 131 |
+
</a>
|
| 132 |
+
</div>
|
| 133 |
+
</div>
|
| 134 |
+
|
| 135 |
+
<div class="main">
|
| 136 |
+
<div class="topbar">
|
| 137 |
+
<span>Privacy Filter</span>
|
| 138 |
+
<span class="badge">openai</span>
|
| 139 |
+
</div>
|
| 140 |
+
|
| 141 |
+
<div class="chat" id="chat">
|
| 142 |
+
<div class="chat-inner" id="chatInner">
|
| 143 |
+
<div class="welcome" id="welcome">
|
| 144 |
+
<div class="w-logo"><svg viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1s3.1 1.39 3.1 3.1v2z"/></svg></div>
|
| 145 |
+
<h1>Privacy Filter</h1>
|
| 146 |
+
<p>Detect and redact PII using OpenAI's privacy-filter model. Supports names, emails, phones, addresses, dates, URLs, account numbers, and secrets.</p>
|
| 147 |
+
<div class="examples">
|
| 148 |
+
<button class="ex-btn" onclick="useEx(this)">Alice was born on 1990-01-02 and lives at 1 Main St.</button>
|
| 149 |
+
<button class="ex-btn" onclick="useEx(this)">Email me at alice@example.com or call 415-555-0101.</button>
|
| 150 |
+
<button class="ex-btn" onclick="useEx(this)">Me llamo Laura Gómez y vivo en Calle de Alcalá 21, Madrid.</button>
|
| 151 |
+
<button class="ex-btn" onclick="useEx(this)">Mon e-mail est jean.dupont@example.fr, tél +33 6 12 34 56 78.</button>
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
<div id="msgs"></div>
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
|
| 158 |
+
<div class="input-wrap">
|
| 159 |
+
<div class="input-inner">
|
| 160 |
+
<div class="input-box">
|
| 161 |
+
<textarea id="inp" rows="1" placeholder="Paste text to scan for PII…" oninput="this.style.height='auto';this.style.height=Math.min(this.scrollHeight,140)+'px'"></textarea>
|
| 162 |
+
<button class="send" id="sendBtn" onclick="send()"><svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg></button>
|
| 163 |
+
</div>
|
| 164 |
+
<div class="foot">8 PII categories · BIOES + Viterbi decoding</div>
|
| 165 |
+
</div>
|
| 166 |
+
</div>
|
| 167 |
+
</div>
|
| 168 |
+
</div>
|
| 169 |
+
|
| 170 |
+
<script type="module">
|
| 171 |
+
import{Client}from"https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
|
| 172 |
+
let C=null,busy=false;
|
| 173 |
+
(async()=>{try{C=await Client.connect(location.origin)}catch(e){console.error(e)}})();
|
| 174 |
+
|
| 175 |
+
const $=id=>document.getElementById(id);
|
| 176 |
+
const esc=s=>{const d=document.createElement('div');d.textContent=s;return d.innerHTML};
|
| 177 |
+
|
| 178 |
+
function addMsg(role,html){
|
| 179 |
+
$('welcome').style.display='none';
|
| 180 |
+
const d=document.createElement('div');d.className='msg';
|
| 181 |
+
const isA=role==='a';
|
| 182 |
+
const av=isA?'<svg viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1s3.1 1.39 3.1 3.1v2z"/></svg>':'Y';
|
| 183 |
+
d.innerHTML=`<div class="msg-row"><div class="ava ${isA?'a':'u'}">${av}</div><div class="msg-c"><div class="role">${isA?'Privacy Filter':'You'}</div><div class="body">${html}</div></div></div>`;
|
| 184 |
+
$('msgs').appendChild(d);
|
| 185 |
+
$('chat').scrollTop=$('chat').scrollHeight;
|
| 186 |
+
return d;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
function highlight(text,ents){
|
| 190 |
+
if(!ents||!ents.length)return esc(text);
|
| 191 |
+
const s=[...ents].sort((a,b)=>a.start-b.start);
|
| 192 |
+
let h='',c=0;
|
| 193 |
+
for(const e of s){if(e.start<c)continue;h+=esc(text.slice(c,e.start));h+=`<span class="ent ent-${e.entity}" data-t="${e.entity}">${esc(text.slice(e.start,e.end))}</span>`;c=e.end;}
|
| 194 |
+
return h+esc(text.slice(c));
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
function fmtRedact(r){return esc(r).replace(/\[(PERSON|EMAIL|PHONE|ADDRESS|DATE|URL|ACCOUNT_NUMBER|SECRET|REDACTED)\]/g,'<span class="tag">[$1]</span>')}
|
| 198 |
+
|
| 199 |
+
function result(d){
|
| 200 |
+
const{text,entities,redacted,summary}=d;
|
| 201 |
+
let h='<div style="margin-bottom:2px"><strong>Detected Entities</strong></div>';
|
| 202 |
+
h+='<div style="line-height:2">'+highlight(text,entities)+'</div>';
|
| 203 |
+
if(entities&&entities.length){
|
| 204 |
+
h+='<div class="pills">';
|
| 205 |
+
for(const[k,v]of Object.entries(summary||{}))h+=`<div class="pill"><b>${v}</b>${k.replace('private_','')}</div>`;
|
| 206 |
+
h+='</div>';
|
| 207 |
+
const rid='r'+Date.now();
|
| 208 |
+
h+=`<div class="r-card"><div class="r-head"><span>Redacted Output</span><button class="cp-btn" onclick="navigator.clipboard.writeText($('${rid}').innerText).then(()=>{this.textContent='Copied!';setTimeout(()=>this.textContent='Copy',1200)})">Copy</button></div><div class="r-body" id="${rid}">${fmtRedact(redacted)}</div></div>`;
|
| 209 |
+
}else{h+='<div style="margin-top:10px;color:var(--green);font-size:13px">✓ No personal identifiers detected.</div>';}
|
| 210 |
+
return h;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
window.send=async function(){
|
| 214 |
+
const inp=$('inp'),text=inp.value.trim();
|
| 215 |
+
if(!text||busy)return;
|
| 216 |
+
busy=true;inp.value='';inp.style.height='auto';$('sendBtn').disabled=true;
|
| 217 |
+
addMsg('u',esc(text));
|
| 218 |
+
const ld=addMsg('a','<div class="dots"><i></i><i></i><i></i></div>');
|
| 219 |
+
// history
|
| 220 |
+
const hi=document.createElement('div');hi.className='s-item';hi.textContent=text.slice(0,36)+(text.length>36?'…':'');$('hist').prepend(hi);
|
| 221 |
+
try{
|
| 222 |
+
if(!C)C=await Client.connect(location.origin);
|
| 223 |
+
const r=await C.predict("/predict_and_redact",{text});
|
| 224 |
+
ld.remove();addMsg('a',result(r.data[0]));
|
| 225 |
+
}catch(e){ld.remove();addMsg('a',`<span style="color:var(--red)">Error: ${esc(e.message||'Request failed')}</span>`);}
|
| 226 |
+
busy=false;$('sendBtn').disabled=false;inp.focus();
|
| 227 |
+
};
|
| 228 |
+
window.useEx=function(b){$('inp').value=b.textContent;send()};
|
| 229 |
+
window.resetChat=function(){$('msgs').innerHTML='';$('welcome').style.display='';$('inp').value='';};
|
| 230 |
+
$('inp').addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send();}});
|
| 231 |
+
</script>
|
| 232 |
+
</body>
|
| 233 |
+
</html>
|