Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 222 additions & 0 deletions examples/browser/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OPF in the browser</title>
<style>
:root { color-scheme: light dark; }
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 820px;
margin: 2rem auto;
padding: 0 1rem;
line-height: 1.5;
}
textarea {
width: 100%;
min-height: 8rem;
font: inherit;
padding: 0.6rem;
box-sizing: border-box;
}
button {
padding: 0.5rem 1rem;
font: inherit;
cursor: pointer;
}
button[disabled] { cursor: wait; opacity: 0.6; }
#status { color: #888; margin-left: 0.75rem; font-size: 0.9rem; }
#progress-wrap { margin: 0.75rem 0; }
#progress-wrap[hidden] { display: none; }
#progress-bar {
width: 100%;
height: 10px;
background: #8884;
border-radius: 5px;
overflow: hidden;
}
#progress-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
transition: width 120ms ease-out;
}
#progress-label {
font-size: 0.8rem;
color: #888;
margin-top: 0.25rem;
font-family: ui-monospace, Menlo, monospace;
}
#redacted {
white-space: pre-wrap;
padding: 0.75rem;
border: 1px solid #8884;
border-radius: 6px;
min-height: 2rem;
}
.tag {
background: #ffd5d5;
color: #8a0000;
padding: 0 0.25rem;
border-radius: 3px;
font-weight: 600;
}
@media (prefers-color-scheme: dark) {
.tag { background: #5a1d1d; color: #ffb5b5; }
}
table { border-collapse: collapse; margin-top: 1rem; width: 100%; }
th, td { border-bottom: 1px solid #8884; padding: 0.35rem 0.5rem; text-align: left; font-size: 0.9rem; }
th { font-weight: 600; }
code { font-family: ui-monospace, Menlo, monospace; font-size: 0.85em; }
</style>
</head>
<body>
<h1>OpenAI Privacy Filter — browser demo</h1>
<p>
Runs <code>openai/privacy-filter</code> entirely in your browser via
<a href="https://huggingface.co/docs/transformers.js">Transformers.js</a>,
preferring WebGPU and falling back to WASM.
The first run downloads the <code>q4f16</code>-quantized ONNX weights (~810 MB)
and caches them in the browser's Cache Storage; later loads are instant.
</p>

<textarea id="input">Hi, I'm Alice Johnson, DOB 1990-01-02, email alice@example.com, phone +1 415 555 0134. Ship to 1600 Amphitheatre Pkwy, Mountain View, CA.</textarea>

<p>
<button id="run" disabled>Redact</button>
<span id="status">loading model…</span>
</p>

<div id="progress-wrap">
<div id="progress-bar"><div id="progress-fill"></div></div>
<div id="progress-label"></div>
</div>

<h3>Redacted output</h3>
<div id="redacted"></div>

<h3>Detected spans</h3>
<table id="spans">
<thead><tr><th>type</th><th>text</th><th>score</th></tr></thead>
<tbody></tbody>
</table>

<script type="module">
import { pipeline, env } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0";

env.allowLocalModels = false;
env.useBrowserCache = true;

const MODEL_REVISION = "7ffa9a043d54d1be65afb281eddf0ffbe629385b";

const inputEl = document.getElementById("input");
const runBtn = document.getElementById("run");
const statusEl = document.getElementById("status");
const redactedEl = document.getElementById("redacted");
const spansBody = document.querySelector("#spans tbody");
const progressWrap = document.getElementById("progress-wrap");
const progressFill = document.getElementById("progress-fill");
const progressLabel = document.getElementById("progress-label");

const fileProgress = new Map();

function formatMB(bytes) {
return (bytes / 1024 / 1024).toFixed(1) + " MB";
}

function onProgress(event) {
if (event.status === "progress" || event.status === "download") {
const key = event.file ?? event.name;
if (event.total) {
fileProgress.set(key, { loaded: event.loaded ?? 0, total: event.total });
}
} else if (event.status === "done") {
const key = event.file ?? event.name;
const entry = fileProgress.get(key);
if (entry) fileProgress.set(key, { loaded: entry.total, total: entry.total });
}

let loaded = 0;
let total = 0;
for (const { loaded: l, total: t } of fileProgress.values()) {
loaded += l;
total += t;
}

if (total === 0) return;
const pct = Math.min(100, (loaded / total) * 100);
progressFill.style.width = pct.toFixed(1) + "%";
progressLabel.textContent = `${formatMB(loaded)} / ${formatMB(total)} (${pct.toFixed(0)}%)`;
}

const device = "gpu" in navigator ? "webgpu" : "wasm";
statusEl.textContent = `loading model on ${device}…`;

const classifier = await pipeline(
"token-classification",
"openai/privacy-filter",
{
device,
dtype: "q4f16",
revision: MODEL_REVISION,
progress_callback: onProgress,
},
);

statusEl.textContent = `ready (${device})`;
progressWrap.hidden = true;
runBtn.disabled = false;

runBtn.addEventListener("click", async () => {
runBtn.disabled = true;
statusEl.textContent = "running…";
const text = inputEl.value;

const t0 = performance.now();
const spans = await classifier(text, { aggregation_strategy: "simple" });
const ms = Math.round(performance.now() - t0);

renderRedacted(text, spans);
renderSpans(spans);

statusEl.textContent = `done in ${ms} ms`;
runBtn.disabled = false;
});

function renderRedacted(text, spans) {
const sorted = [...spans].sort((a, b) => a.start - b.start);
const parts = [];
let cursor = 0;
for (const s of sorted) {
if (s.start > cursor) parts.push(document.createTextNode(text.slice(cursor, s.start)));
const tag = document.createElement("span");
tag.className = "tag";
tag.textContent = `[${s.entity_group}]`;
tag.title = `${text.slice(s.start, s.end)} (${s.score.toFixed(3)})`;
parts.push(tag);
cursor = s.end;
}
if (cursor < text.length) parts.push(document.createTextNode(text.slice(cursor)));

redactedEl.replaceChildren(...parts);
}

function renderSpans(spans) {
spansBody.replaceChildren(
...spans.map((s) => {
const tr = document.createElement("tr");
const type = document.createElement("td");
type.textContent = s.entity_group;
const word = document.createElement("td");
word.textContent = s.word;
const score = document.createElement("td");
score.textContent = s.score.toFixed(4);
tr.append(type, word, score);
return tr;
}),
);
}
</script>
</body>
</html>
11 changes: 11 additions & 0 deletions examples/browser/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "browser",
"version": "1.0.0",
"description": "",
"license": "ISC",
"author": "",
"type": "commonjs",
"scripts": {
"start": "npx --yes serve ."
}
}