Spaces:
Paused
Paused
Download index.html from LiquidAI/pii-detection: direct link, hf CLI and curl.
- Browser
- Download file 12.2 kB
-
https://huggingface.co/spaces/LiquidAI/pii-detection/resolve/main/index.html
- Command line
-
hf download hf://spaces/LiquidAI/pii-detection/index.html
-
curl -L -o index.html https://huggingface.co/spaces/LiquidAI/pii-detection/resolve/main/index.html
12.2 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>PII detection — Liquid AI</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="style.css"> | |
| <script> | |
| // Private Space auth: HF signs the embedded iframe URL with ?__sign=<token>. | |
| // Incognito / Safari block the third-party .hf.space cookie, so that token is | |
| // the only credential — carry it on same-origin API requests, else /api/* | |
| // returns 404 and the model-ready poll hangs. | |
| (() => { | |
| const sign = new URLSearchParams(location.search).get("__sign"); | |
| if (!sign) return; | |
| const orig = self.fetch.bind(self); | |
| self.fetch = (input, init) => { | |
| try { | |
| const u = new URL(typeof input === "string" ? input : input.url, location.href); | |
| if (u.origin === location.origin && !u.searchParams.has("__sign")) { | |
| u.searchParams.set("__sign", sign); | |
| input = u.toString(); | |
| } | |
| } catch (e) { /* non-URL input: pass through */ } | |
| return orig(input, init); | |
| }; | |
| })(); | |
| </script> | |
| </head> | |
| <body> | |
| <div class="wrap"> | |
| <header class="site-intro"> | |
| <h1>Multilingual <em>PII detection</em></h1> | |
| <p class="sub">Spot <strong>40 kinds of personal data</strong> across 16 languages and redact them, in one bidirectional pass.</p> | |
| </header> | |
| <section class="model-strip is-loading" id="model-strip" aria-label="Model status"> | |
| <span class="model-logo-frame" aria-hidden="true"> | |
| <img id="model-logo" class="model-logo" width="56" height="59" alt="" src="lliquid.gif"> | |
| </span> | |
| <div class="model-copy"> | |
| <div class="model-name-row"> | |
| <a href="https://huggingface.co/LiquidAI/LFM2.5-Encoder-350M-PII-Detector" target="_blank" rel="noopener">LFM2.5 Encoder</a> | |
| <span class="model-variant">350M parameters</span> | |
| </div> | |
| </div> | |
| <div class="load-block"> | |
| <div class="load-meta"> | |
| <span id="status" role="status" aria-live="polite">Connecting to server…</span> | |
| <span id="load-percent">0%</span> | |
| </div> | |
| <div class="load-track" role="progressbar" aria-label="Model loading" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><span id="load-progress"></span></div> | |
| </div> | |
| </section> | |
| <main> | |
| <div class="pii-workspace"> | |
| <section class="workspace-card input-panel"> | |
| <div class="panel-header"> | |
| <div><h2>Text</h2></div> | |
| </div> | |
| <div class="editor-stage"> | |
| <textarea id="input" spellcheck="false" placeholder="Paste text containing names, emails, IDs, cards…">Update the account for david.miller@example.com: phone +1 415 555 0199, SSN 123-45-6789, card 4111 1111 1111 1111, passport C01X00T47.</textarea> | |
| </div> | |
| </section> | |
| <section class="workspace-card output-panel"> | |
| <div class="panel-header"> | |
| <div><h2>Detected PII</h2></div> | |
| <div class="view-toggle" id="view-toggle" role="tablist" aria-label="Output view"> | |
| <button type="button" data-view="highlight" class="active" role="tab">Highlight</button> | |
| <button type="button" data-view="redacted" role="tab">Redacted</button> | |
| </div> | |
| </div> | |
| <div class="output-stage"> | |
| <div id="output" class="output-body"><div class="output-empty">Detected personal data will appear here.</div></div> | |
| </div> | |
| <div class="controls"> | |
| <div class="legend" id="legend"></div> | |
| </div> | |
| </section> | |
| </div> | |
| <section class="examples-panel" aria-label="Examples"> | |
| <span class="examples-label">Examples</span> | |
| <div class="example-list" id="examples"> | |
| <button type="button" class="ex active" data-t="Update the account for david.miller@example.com: phone +1 415 555 0199, SSN 123-45-6789, card 4111 1111 1111 1111, passport C01X00T47.">Contact & ID</button> | |
| <button type="button" class="ex" data-t="Please wire EUR 5,000 to IBAN DE89 3704 0044 0532 0130 00 (SWIFT COBADEFFXXX) from account 0532013000. My server is at 192.168.1.42.">Financial</button> | |
| <button type="button" class="ex" data-t="Deploy creds: AWS key AKIAIOSFODNN7EXAMPLE, session token eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3In0.dozjgNryP4J3jVmNHl0w, and DB password Str0ngP@ssw0rd!.">Credentials</button> | |
| <button type="button" class="ex" data-t="Company laptop MAC 00:1A:2B:3C:4D:5E connected from IP 10.0.0.5 near GPS 37.7749, -122.4194.">Device</button> | |
| <button type="button" class="ex" data-t="Send 0.5 BTC to bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh or ETH to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e. Rotate the API key sk-proj-T3BlbkFJ4aBcDeFgHiJkLmNoP.">Crypto & keys</button> | |
| <button type="button" class="ex" data-t="Follow username jsmith on https://social.example.com/jsmith or email j.smith@example.com for details.">Online</button> | |
| <button type="button" class="ex" data-t="Patient Sarah Connor, date of birth 03/14/1985, record number 887766, diagnosed with Type 2 diabetes and prescribed Metformin.">Healthcare</button> | |
| <button type="button" class="ex" data-t="Ich heiße Hans Müller und wohne in der Hauptstraße 5, 80331 München. Erreichbar unter +49 30 4505 1234 oder hans.mueller@example.de.">Deutsch</button> | |
| <button type="button" class="ex" data-t="Je m'appelle Jean Dupont, email jean.dupont@example.fr, téléphone +33 6 12 34 56 78, IBAN FR76 3000 6000 0112 3456 7890 189.">Français</button> | |
| <button type="button" class="ex" data-t="María García, correo maria.garcia@example.es, teléfono +34 612 345 678, tarjeta 4111 1111 1111 1111.">Español</button> | |
| <button type="button" class="ex" data-t="Иван Петров, email ivan.petrov@example.ru, телефон +7 495 123 45 67.">Русский</button> | |
| <button type="button" class="ex" data-t="我叫张伟,电话号码是13800138000,邮箱是 zhang.wei@example.cn,住在北京市朝阳区建国路88号。">中文</button> | |
| <button type="button" class="ex" data-t="田中太郎と申します。メールアドレスは taro.tanaka@example.jp、電話番号は 090-1234-5678 です。">日本語</button> | |
| <button type="button" class="ex" data-t="제 이름은 김민수입니다. 이메일은 minsu.kim@example.kr, 전화번호는 010-1234-5678입니다.">한국어</button> | |
| <button type="button" class="ex" data-t="اسمي محمد أحمد، بريدي الإلكتروني mohammed.ahmed@example.com، هاتفي +20 10 1234 5678.">العربية</button> | |
| </div> | |
| </section> | |
| </main> | |
| </div> | |
| <script> | |
| window.addEventListener("error", e => { | |
| const el = document.getElementById("status"); | |
| if (el) el.textContent = "· ERROR: " + (e.message || (e.target && (e.target.src || e.target.href)) || "resource failed"); | |
| }, true); | |
| </script> | |
| <script type="module"> | |
| // The detector runs on the server (CPU, f32). This script drives the UI and asks | |
| // /api/detect for char-anchored PII spans; highlighting, the redacted view, the | |
| // legend, and stats are done here. Nothing is downloaded to the client. | |
| const DOMAINS = ["identity","contact","financial","credential","online","device","location","healthcare","org","special","legal","other"]; | |
| const $ = (id) => document.getElementById(id); | |
| const escapeHtml = (v) => v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); | |
| const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1); | |
| function setStatus(message, tone = "loading") { | |
| $("status").textContent = message; | |
| const strip = $("model-strip"); | |
| strip.classList.remove("is-loading", "is-preparing", "is-ready", "is-error"); | |
| strip.classList.add(`is-${tone}`); | |
| } | |
| function setLoadProgress(value) { | |
| const pct = Math.max(0, Math.min(100, Number(value) || 0)); | |
| $("load-progress").style.width = `${pct}%`; | |
| $("load-percent").textContent = `${Math.round(pct)}%`; | |
| document.querySelector(".load-track").setAttribute("aria-valuenow", String(Math.round(pct))); | |
| } | |
| let READY = false, timer = null, version = 0; | |
| let lastText = "", lastResult = null, view = "highlight"; | |
| function renderHighlight(text, spans) { | |
| let html = "", prev = 0, i = 0; | |
| for (const sp of spans) { | |
| html += escapeHtml(text.slice(prev, sp.start)); | |
| const t = escapeHtml(text.slice(sp.start, sp.end)); | |
| const tip = escapeHtml(`${sp.label} · ${sp.type}`); | |
| html += `<span class="pii is-animated" style="--pii-color:var(--d-${sp.domain});--i:${i++}" title="${tip}">${t}</span>`; | |
| prev = sp.end; | |
| } | |
| return html + escapeHtml(text.slice(prev)); | |
| } | |
| function renderRedacted(redacted) { | |
| return escapeHtml(redacted).replace(/\[([A-Z0-9_]+)\]/g, '<span class="redacted-tag">[$1]</span>'); | |
| } | |
| function renderLegend(spans) { | |
| const counts = {}; | |
| for (const sp of spans) counts[sp.domain] = (counts[sp.domain] || 0) + 1; | |
| $("legend").innerHTML = DOMAINS.filter(d => counts[d]).map(d => | |
| `<span class="legend-chip" style="--pii-color:var(--d-${d})"><span class="legend-swatch" aria-hidden="true"></span>${cap(d)}<span class="legend-count">${counts[d]}</span></span>` | |
| ).join(""); | |
| } | |
| function paint() { | |
| const out = $("output"); | |
| if (!lastResult || !lastText.trim()) { | |
| out.className = "output-body"; | |
| out.innerHTML = '<div class="output-empty">Detected personal data will appear here.</div>'; | |
| return; | |
| } | |
| out.className = view === "redacted" ? "output-body is-redacted" : "output-body"; | |
| out.innerHTML = view === "redacted" ? renderRedacted(lastResult.redacted) : renderHighlight(lastText, lastResult.spans); | |
| } | |
| async function detect() { | |
| if (!READY) return; | |
| const runVersion = ++version; | |
| const text = $("input").value; | |
| if (!text.trim()) { lastText = text; lastResult = null; paint(); $("legend").innerHTML = ""; return; } | |
| document.querySelector(".output-panel").classList.add("is-detecting"); | |
| let data; | |
| try { | |
| const resp = await fetch("api/detect", { | |
| method: "POST", headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ text }), | |
| }); | |
| if (!resp.ok) throw new Error(`HTTP ${resp.status}`); | |
| data = await resp.json(); | |
| } catch (e) { | |
| document.querySelector(".output-panel").classList.remove("is-detecting"); | |
| return; | |
| } | |
| if (runVersion !== version) return; | |
| lastText = text; lastResult = data; | |
| paint(); | |
| renderLegend(data.spans); | |
| document.querySelector(".output-panel").classList.remove("is-detecting"); | |
| } | |
| function detectSoon(delay = 350) { clearTimeout(timer); timer = setTimeout(() => detect(), delay); } | |
| $("input").addEventListener("input", () => detectSoon()); | |
| $("view-toggle").addEventListener("click", (e) => { | |
| const b = e.target.closest("button"); if (!b) return; | |
| view = b.dataset.view; | |
| $("view-toggle").querySelectorAll("button").forEach(x => x.classList.toggle("active", x === b)); | |
| paint(); | |
| }); | |
| $("examples").addEventListener("click", (e) => { | |
| const b = e.target.closest(".ex"); if (!b) return; | |
| $("examples").querySelectorAll(".ex").forEach(x => x.classList.toggle("active", x === b)); | |
| $("input").value = b.dataset.t; | |
| detectSoon(40); | |
| }); | |
| async function waitForServer() { | |
| setStatus("Connecting to server…", "loading"); | |
| setLoadProgress(12); | |
| for (let attempt = 0; attempt < 900; attempt++) { | |
| try { | |
| const r = await fetch("api/ready", { cache: "no-store" }); | |
| if (r.ok && (await r.json()).ready) return true; | |
| } catch (e) { /* server still starting */ } | |
| setStatus("Warming up model on the server…", "preparing"); | |
| setLoadProgress(Math.min(92, 12 + attempt * 4)); | |
| await new Promise((r) => setTimeout(r, 1000)); | |
| } | |
| return false; | |
| } | |
| (async () => { | |
| const ok = await waitForServer(); | |
| if (!ok) { setStatus("Model unavailable", "error"); setLoadProgress(100); $("load-percent").textContent = "Error"; return; } | |
| READY = true; | |
| setLoadProgress(100); | |
| setStatus("Model ready", "ready"); | |
| await detect(); | |
| window.__PII_READY = true; | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |