MikeDoes commited on
Commit
95a61ac
·
verified ·
1 Parent(s): 0315cc8

Upload 2 files

Browse files
Files changed (2) hide show
  1. ai4privacy-logo.png +0 -0
  2. index.html +347 -19
ai4privacy-logo.png ADDED
index.html CHANGED
@@ -1,19 +1,347 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Short Text and Open Source: Anonymiser</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js"></script>
9
+ <style>
10
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap');
11
+ * {
12
+ font-family: 'Inter', sans-serif;
13
+ }
14
+ textarea, #privacyMask {
15
+ transition: all 0.2s ease-in-out;
16
+ }
17
+ ::-webkit-scrollbar {
18
+ width: 6px;
19
+ }
20
+ ::-webkit-scrollbar-track {
21
+ background: #2d2d2d;
22
+ }
23
+ ::-webkit-scrollbar-thumb {
24
+ background: #4a4a4a;
25
+ border-radius: 3px;
26
+ }
27
+ .entity-tile {
28
+ transition: transform 0.2s, box-shadow 0.2s;
29
+ }
30
+ .entity-tile:hover {
31
+ transform: translateY(-2px);
32
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
33
+ }
34
+ </style>
35
+ </head>
36
+ <body class="bg-gray-900 min-h-screen">
37
+ <!-- Branding Header -->
38
+ <div class="bg-black/30 py-4 border-b border-white/10">
39
+ <div class="max-w-7xl mx-auto px-4 flex items-center justify-between">
40
+ <div class="flex items-center space-x-3">
41
+ <img src="ai4privacy-logo.png" alt="Logo" class="h-8 w-8">
42
+ <div>
43
+ <span class="text-xl font-bold text-white">Ai4Privacy</span>
44
+ <span class="block text-xs text-white/60">Short Text Anonymization Locally in Your Browser</span>
45
+ </div>
46
+ </div>
47
+ <!-- Settings Button -->
48
+ <button id="settingsButton" class="text-white/60 hover:text-white transition-colors">
49
+ <span class="iconify" data-icon="mdi:cog" data-width="24"></span>
50
+ </button>
51
+ </div>
52
+ </div>
53
+
54
+ <!-- Settings Panel -->
55
+ <div id="settingsPanel" class="hidden absolute right-4 top-20 bg-gray-800 border border-white/10 rounded-xl p-4 w-64 space-y-4 z-50">
56
+ <div>
57
+ <label class="block text-sm text-white/80 mb-2">Detection Threshold</label>
58
+ <input type="number" id="thresholdInput" step="0.001" min="0" max="1" value="0.01"
59
+ class="w-full bg-gray-700 border border-white/10 rounded-lg px-3 py-2 text-white">
60
+ </div>
61
+ <div>
62
+ <label class="block text-sm text-white/80 mb-2">Language Model</label>
63
+ <select id="modelSelect" class="w-full bg-gray-700 border border-white/10 rounded-lg px-3 py-2 text-white">
64
+ <option value="english">English - ai4privacy/llama-ai4privacy-english-anonymiser-openpii</option>
65
+ </select>
66
+ </div>
67
+ </div>
68
+
69
+ <div class="max-w-7xl mx-auto px-4 py-8">
70
+ <div class="flex flex-col lg:flex-row gap-8">
71
+ <!-- Input/Output Section -->
72
+ <div class="flex-1 space-y-6">
73
+ <div>
74
+ <label class="block text-sm font-medium text-white/80 mb-2">Input Text</label>
75
+ <textarea
76
+ id="inputText"
77
+ class="w-full p-4 bg-gray-800 border border-white/10 rounded-xl text-white placeholder-white/30 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/30 resize-none"
78
+ rows="6"
79
+ placeholder="Enter sensitive text to anonymize..."
80
+ ></textarea>
81
+ </div>
82
+
83
+ <div>
84
+ <label class="block text-sm font-medium text-white/80 mb-2">Anonymized Output</label>
85
+ <textarea
86
+ id="outputText"
87
+ class="w-full p-4 bg-gray-800 border border-white/10 rounded-xl text-white/80 resize-none"
88
+ rows="6"
89
+ readonly
90
+ ></textarea>
91
+ </div>
92
+ </div>
93
+
94
+ <!-- Privacy Mask Panel -->
95
+ <div class="lg:w-96">
96
+ <div class="sticky top-8">
97
+ <label class="block text-sm font-medium text-white/80 mb-2">Detected Entities</label>
98
+ <div class="bg-gray-800 border border-white/10 rounded-xl p-4">
99
+ <div class="mb-4">
100
+ <span id="processingStatus" class="text-xs text-white/40">Ready</span>
101
+ </div>
102
+ <div
103
+ id="privacyMask"
104
+ class="h-96 bg-gray-850 rounded-lg p-3 overflow-y-auto text-sm space-y-2"
105
+ >
106
+ <div class="text-center text-white/40 py-4">Processing results will appear here</div>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+ <!-- Branding Footer -->
115
+ <div class="fixed bottom-0 left-0 right-0 bg-black/30 border-t border-white/10 py-3">
116
+ <div class="max-w-7xl mx-auto px-4">
117
+ <div class="flex items-center justify-between">
118
+ <div class="text-sm text-white/50">© 2025 Ai4Privacy. All rights reserved.</div>
119
+ <div class="flex items-center space-x-4">
120
+ <span class="text-sm text-white/50">v2.1.0</span>
121
+ <div class="w-px h-4 bg-white/10"></div>
122
+ <img src="ai4privacy-logo.png" alt="Logo" class="h-6 w-6 opacity-70">
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <script type="module">
129
+ import { AutoModel, AutoTokenizer } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.4.0';
130
+
131
+ // Initialize variables
132
+ let tokenizer, model;
133
+ let isModelLoaded = false;
134
+ let currentInput = "";
135
+
136
+ // DOM Elements
137
+ const inputText = document.getElementById('inputText');
138
+ const outputText = document.getElementById('outputText');
139
+ const statusElement = document.getElementById('processingStatus');
140
+
141
+ // Add debounce to input handler
142
+ let timeout;
143
+ inputText.addEventListener('input', (event) => {
144
+ currentInput = event.target.value;
145
+ statusElement.textContent = 'Processing...';
146
+ clearTimeout(timeout);
147
+ timeout = setTimeout(updateOutput, 300);
148
+ });
149
+
150
+ async function loadModel() {
151
+ try {
152
+ tokenizer = await AutoTokenizer.from_pretrained('ai4privacy/llama-ai4privacy-english-anonymiser-openpii');
153
+ model = await AutoModel.from_pretrained('ai4privacy/llama-ai4privacy-english-anonymiser-openpii', { dtype: "q8" });
154
+ isModelLoaded = true;
155
+ statusElement.textContent = 'Model loaded';
156
+ updateOutput();
157
+ } catch (err) {
158
+ console.error("Error loading model:", err);
159
+ statusElement.textContent = 'Error loading model';
160
+ outputText.value = "Error loading model.";
161
+ }
162
+ }
163
+
164
+ async function updateOutput() {
165
+ if (!isModelLoaded) {
166
+ statusElement.textContent = 'Loading model...';
167
+ outputText.value = "";
168
+ return;
169
+ }
170
+
171
+ try {
172
+ const processed = await processText(currentInput, tokenizer, model);
173
+ statusElement.textContent = `Processed ${currentInput.length} characters`;
174
+ outputText.value = processed.maskedText;
175
+
176
+ const privacyMaskDiv = document.getElementById('privacyMask');
177
+ privacyMaskDiv.innerHTML = '';
178
+
179
+ if (processed.replacements.length > 0) {
180
+ processed.replacements.forEach(replacement => {
181
+ const tile = document.createElement('div');
182
+ tile.className = 'entity-tile bg-gray-800 p-3 rounded-lg border border-white/10 hover:border-white/20';
183
+ tile.innerHTML = `
184
+ <div class="text-xs text-white/60 mb-1">${replacement.placeholder}</div>
185
+ <div class="text-sm text-white font-medium">${replacement.original}</div>
186
+ <div class="text-xs text-white/40 mt-1">Sensitive Information</div>
187
+ <div class="text-xs text-white/40 mt-1">Activation: ${Math.round(replacement.activation * 100)}%</div>
188
+ `;
189
+ privacyMaskDiv.appendChild(tile);
190
+ });
191
+ } else {
192
+ const emptyState = document.createElement('div');
193
+ emptyState.className = 'text-center text-white/40 py-4';
194
+ emptyState.textContent = 'No sensitive information detected.';
195
+ privacyMaskDiv.appendChild(emptyState);
196
+ }
197
+ } catch (err) {
198
+ statusElement.textContent = 'Error processing text';
199
+ console.error("Error processing text:", err);
200
+ outputText.value = "Error processing text.";
201
+ }
202
+ }
203
+
204
+ async function processText(text, tokenizer, model) {
205
+ const inputs = await tokenizer(text);
206
+ const inputTokens = inputs.input_ids.data;
207
+ const tokenStrings = Array.from(inputTokens).map(id =>
208
+ tokenizer.decode([id], { skip_special_tokens: false })
209
+ );
210
+
211
+ const { logits } = await model(inputs);
212
+ const logitsData = Array.from(logits.data);
213
+ const numTokens = tokenStrings.length;
214
+ const numClasses = 3;
215
+
216
+ const logitsPerToken = [];
217
+ for (let i = 0; i < numTokens; i++) {
218
+ logitsPerToken.push(logitsData.slice(i * numClasses, (i + 1) * numClasses));
219
+ }
220
+
221
+ function softmax(logits) {
222
+ const expLogits = logits.map(Math.exp);
223
+ const sumExp = expLogits.reduce((a, b) => a + b, 0);
224
+ return expLogits.map(exp => exp / sumExp);
225
+ }
226
+
227
+ const tokenPredictions = tokenStrings.map((token, i) => {
228
+ const probs = softmax(logitsPerToken[i]);
229
+ const maxSensitive = Math.max(probs[0], probs[1]);
230
+ return {
231
+ token: token,
232
+ start: i,
233
+ end: i + 1,
234
+ probabilities: {
235
+ "B-PRIVATE": probs[0],
236
+ "I-PRIVATE": probs[1],
237
+ "O": probs[2]
238
+ },
239
+ maxSensitiveScore: maxSensitive
240
+ };
241
+ });
242
+
243
+ const aggregated = aggregatePrivacyTokens(tokenPredictions);
244
+ const { maskedText, replacements } = maskText(tokenPredictions, aggregated);
245
+ return { maskedText, replacements };
246
+ }
247
+
248
+ function aggregatePrivacyTokens(tokenPredictions) {
249
+ const threshold = parseFloat(document.getElementById('thresholdInput').value) || 0.01;
250
+ const aggregated = [];
251
+ let i = 0;
252
+ const n = tokenPredictions.length;
253
+
254
+ while (i < n) {
255
+ const currentToken = tokenPredictions[i];
256
+ if (['[CLS]', '[SEP]'].includes(currentToken.token)) {
257
+ i++;
258
+ continue;
259
+ }
260
+ const startsWithSpace = currentToken.token.startsWith(' ');
261
+ const isFirstWord = aggregated.length === 0 && i === 0;
262
+ if (startsWithSpace || isFirstWord) {
263
+ const group = {
264
+ tokens: [currentToken],
265
+ indices: [i],
266
+ scores: [currentToken.maxSensitiveScore],
267
+ startsWithSpace: startsWithSpace
268
+ };
269
+ i++;
270
+ while (i < n &&
271
+ !tokenPredictions[i].token.startsWith(' ') &&
272
+ !['[CLS]', '[SEP]'].includes(tokenPredictions[i].token)) {
273
+ group.tokens.push(tokenPredictions[i]);
274
+ group.indices.push(i);
275
+ group.scores.push(tokenPredictions[i].maxSensitiveScore);
276
+ i++;
277
+ }
278
+ if (Math.max(...group.scores) >= threshold) {
279
+ aggregated.push(group);
280
+ }
281
+ } else {
282
+ i++;
283
+ }
284
+ }
285
+ return aggregated;
286
+ }
287
+
288
+ function maskText(tokenPredictions, aggregatedGroups) {
289
+ const maskedTokens = [];
290
+ const replacements = [];
291
+ const maskedIndices = new Set();
292
+ let redactedCounter = 1;
293
+
294
+ aggregatedGroups.forEach(group => {
295
+ group.indices.forEach(idx => maskedIndices.add(idx));
296
+ });
297
+
298
+ tokenPredictions.forEach((token, idx) => {
299
+ if (['[CLS]', '[SEP]'].includes(token.token)) return;
300
+ if (maskedIndices.has(idx)) {
301
+ const group = aggregatedGroups.find(g => g.indices[0] === idx);
302
+ if (group) {
303
+ const originalTokens = group.tokens.map(t => t.token);
304
+ const originalText = originalTokens
305
+ .map((token, i) => (i === 0 && group.startsWithSpace ? token.trimStart() : token))
306
+ .join('');
307
+ const placeholder = `[PII_${redactedCounter}]`;
308
+ replacements.push({
309
+ original: originalText,
310
+ placeholder: placeholder,
311
+ activation: Math.max(...group.scores) // Add activation score
312
+ });
313
+ redactedCounter++;
314
+ const maskWithSpace = group.startsWithSpace ? ` ${placeholder}` : placeholder;
315
+ maskedTokens.push(maskWithSpace);
316
+ }
317
+ } else {
318
+ maskedTokens.push(token.token);
319
+ }
320
+ });
321
+
322
+ return { maskedText: maskedTokens.join('').replace(/\s+/g, ' ').trim(), replacements };
323
+ }
324
+
325
+ // Load model when page loads
326
+ loadModel();
327
+
328
+ // Add settings toggle functionality
329
+ const settingsButton = document.getElementById('settingsButton');
330
+ const settingsPanel = document.getElementById('settingsPanel');
331
+ let settingsVisible = false;
332
+
333
+ settingsButton.addEventListener('click', (e) => {
334
+ settingsVisible = !settingsVisible;
335
+ settingsPanel.classList.toggle('hidden', !settingsVisible);
336
+ e.stopPropagation();
337
+ });
338
+
339
+ document.addEventListener('click', (e) => {
340
+ if (settingsVisible && !settingsPanel.contains(e.target)) {
341
+ settingsPanel.classList.add('hidden');
342
+ settingsVisible = false;
343
+ }
344
+ });
345
+ </script>
346
+ </body>
347
+ </html>