jobbler commited on
Commit
6d8e506
·
1 Parent(s): 5dae651

feat: Add Global Saliency Calculation Mode

Browse files
firefox-extension/content.js CHANGED
@@ -37,10 +37,11 @@ browser.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
37
  const range = selection.getRangeAt(0);
38
 
39
  // Get user settings
40
- const settings = await browser.storage.local.get(['threshold', 'gradientMode', 'preprompt', 'apiUrl']);
41
  const threshold = settings.threshold !== undefined ? settings.threshold : 0.35;
42
  const useGradient = settings.gradientMode || false;
43
  const preprompt = settings.preprompt || "";
 
44
  const apiUrl = settings.apiUrl || "http://127.0.0.1:8000";
45
  // Default to middle layers just like the playground
46
  const checkedLayers = [4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
@@ -52,7 +53,8 @@ browser.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
52
  headers: { 'Content-Type': 'application/json' },
53
  body: JSON.stringify({
54
  text: selectedText,
55
- preprompt: preprompt,
 
56
  layers: checkedLayers
57
  })
58
  });
@@ -72,6 +74,7 @@ browser.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
72
  container.className = 'flowread-container';
73
  container.dataset.tokens = JSON.stringify(currentTokens);
74
  container.dataset.preprompt = preprompt;
 
75
  container.dataset.originalText = selectedText;
76
  container.innerHTML = htmlString;
77
  range.insertNode(container);
@@ -90,10 +93,11 @@ browser.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
90
  });
91
 
92
  async function processEntirePage() {
93
- const settings = await browser.storage.local.get(['threshold', 'gradientMode', 'preprompt', 'apiUrl']);
94
  const threshold = settings.threshold !== undefined ? settings.threshold : 0.35;
95
  const useGradient = settings.gradientMode || false;
96
  const preprompt = settings.preprompt || "";
 
97
  const apiUrl = settings.apiUrl || "http://127.0.0.1:8000";
98
  const checkedLayers = [4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
99
 
@@ -166,7 +170,8 @@ async function processEntirePage() {
166
  headers: { 'Content-Type': 'application/json' },
167
  body: JSON.stringify({
168
  text: text,
169
- preprompt: preprompt,
 
170
  layers: checkedLayers
171
  })
172
  });
@@ -181,6 +186,7 @@ async function processEntirePage() {
181
  container.className = 'flowread-container';
182
  container.dataset.tokens = JSON.stringify(data.words);
183
  container.dataset.preprompt = preprompt;
 
184
  container.dataset.originalText = text;
185
  container.innerHTML = htmlString;
186
 
@@ -203,6 +209,7 @@ async function updateExisting(newSettings) {
203
  const threshold = newSettings.threshold !== undefined ? newSettings.threshold : 0.35;
204
  const useGradient = newSettings.gradientMode || false;
205
  const preprompt = newSettings.preprompt || "";
 
206
  const apiUrl = newSettings.apiUrl || "http://127.0.0.1:8000";
207
  const checkedLayers = [4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
208
 
@@ -214,12 +221,13 @@ async function updateExisting(newSettings) {
214
 
215
  for (const container of containers) {
216
  const oldPreprompt = container.dataset.preprompt || "";
 
217
  const text = container.dataset.originalText;
218
  if (!text) continue;
219
 
220
- if (oldPreprompt !== preprompt) {
221
  if (reFetchCount === 0) {
222
- showToast("Updating FlowRead elements with new intent...", 0);
223
  }
224
  try {
225
  const response = await fetch(`${apiUrl}/analyze`, {
@@ -227,7 +235,8 @@ async function updateExisting(newSettings) {
227
  headers: { 'Content-Type': 'application/json' },
228
  body: JSON.stringify({
229
  text: text,
230
- preprompt: preprompt,
 
231
  layers: checkedLayers
232
  })
233
  });
@@ -238,6 +247,7 @@ async function updateExisting(newSettings) {
238
 
239
  container.dataset.tokens = JSON.stringify(data.words);
240
  container.dataset.preprompt = preprompt;
 
241
  const htmlString = generateFlowReadHTML(data.words, threshold, useGradient);
242
  container.innerHTML = htmlString;
243
  reFetchCount++;
 
37
  const range = selection.getRangeAt(0);
38
 
39
  // Get user settings
40
+ const settings = await browser.storage.local.get(['threshold', 'gradientMode', 'preprompt', 'saliencyMode', 'apiUrl']);
41
  const threshold = settings.threshold !== undefined ? settings.threshold : 0.35;
42
  const useGradient = settings.gradientMode || false;
43
  const preprompt = settings.preprompt || "";
44
+ const saliencyMode = settings.saliencyMode || "local";
45
  const apiUrl = settings.apiUrl || "http://127.0.0.1:8000";
46
  // Default to middle layers just like the playground
47
  const checkedLayers = [4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
 
53
  headers: { 'Content-Type': 'application/json' },
54
  body: JSON.stringify({
55
  text: selectedText,
56
+ preprompt: preprompt,
57
+ saliency_mode: saliencyMode,
58
  layers: checkedLayers
59
  })
60
  });
 
74
  container.className = 'flowread-container';
75
  container.dataset.tokens = JSON.stringify(currentTokens);
76
  container.dataset.preprompt = preprompt;
77
+ container.dataset.saliencyMode = saliencyMode;
78
  container.dataset.originalText = selectedText;
79
  container.innerHTML = htmlString;
80
  range.insertNode(container);
 
93
  });
94
 
95
  async function processEntirePage() {
96
+ const settings = await browser.storage.local.get(['threshold', 'gradientMode', 'preprompt', 'saliencyMode', 'apiUrl']);
97
  const threshold = settings.threshold !== undefined ? settings.threshold : 0.35;
98
  const useGradient = settings.gradientMode || false;
99
  const preprompt = settings.preprompt || "";
100
+ const saliencyMode = settings.saliencyMode || "local";
101
  const apiUrl = settings.apiUrl || "http://127.0.0.1:8000";
102
  const checkedLayers = [4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
103
 
 
170
  headers: { 'Content-Type': 'application/json' },
171
  body: JSON.stringify({
172
  text: text,
173
+ preprompt: preprompt,
174
+ saliency_mode: saliencyMode,
175
  layers: checkedLayers
176
  })
177
  });
 
186
  container.className = 'flowread-container';
187
  container.dataset.tokens = JSON.stringify(data.words);
188
  container.dataset.preprompt = preprompt;
189
+ container.dataset.saliencyMode = saliencyMode;
190
  container.dataset.originalText = text;
191
  container.innerHTML = htmlString;
192
 
 
209
  const threshold = newSettings.threshold !== undefined ? newSettings.threshold : 0.35;
210
  const useGradient = newSettings.gradientMode || false;
211
  const preprompt = newSettings.preprompt || "";
212
+ const saliencyMode = newSettings.saliencyMode || "local";
213
  const apiUrl = newSettings.apiUrl || "http://127.0.0.1:8000";
214
  const checkedLayers = [4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
215
 
 
221
 
222
  for (const container of containers) {
223
  const oldPreprompt = container.dataset.preprompt || "";
224
+ const oldMode = container.dataset.saliencyMode || "local";
225
  const text = container.dataset.originalText;
226
  if (!text) continue;
227
 
228
+ if (oldPreprompt !== preprompt || oldMode !== saliencyMode) {
229
  if (reFetchCount === 0) {
230
+ showToast("Updating FlowRead elements with new settings...", 0);
231
  }
232
  try {
233
  const response = await fetch(`${apiUrl}/analyze`, {
 
235
  headers: { 'Content-Type': 'application/json' },
236
  body: JSON.stringify({
237
  text: text,
238
+ preprompt: preprompt,
239
+ saliency_mode: saliencyMode,
240
  layers: checkedLayers
241
  })
242
  });
 
247
 
248
  container.dataset.tokens = JSON.stringify(data.words);
249
  container.dataset.preprompt = preprompt;
250
+ container.dataset.saliencyMode = saliencyMode;
251
  const htmlString = generateFlowReadHTML(data.words, threshold, useGradient);
252
  container.innerHTML = htmlString;
253
  reFetchCount++;
firefox-extension/popup.html CHANGED
@@ -91,6 +91,13 @@
91
  <input type="text" id="preprompt" placeholder="e.g., Focus on numbers and dates">
92
  <p class="help">Instruct the AI what to focus on before generating saliency.</p>
93
 
 
 
 
 
 
 
 
94
  <label for="api-url">Backend API URL</label>
95
  <input type="text" id="api-url" placeholder="http://127.0.0.1:8000">
96
  <p class="help">URL of your FlowRead backend (Local or Hugging Face).</p>
 
91
  <input type="text" id="preprompt" placeholder="e.g., Focus on numbers and dates">
92
  <p class="help">Instruct the AI what to focus on before generating saliency.</p>
93
 
94
+ <label for="saliency-mode">Saliency Mode</label>
95
+ <select id="saliency-mode" style="width: 100%; margin-bottom: 15px; padding: 8px; border: 1px solid #d6d3d1; border-radius: 4px; font-family: inherit;">
96
+ <option value="local">Local (Relative to text block)</option>
97
+ <option value="global">Global (Absolute importance)</option>
98
+ </select>
99
+ <p class="help" style="margin-top: 0;">Global mode allows comparing importance across different paragraphs.</p>
100
+
101
  <label for="api-url">Backend API URL</label>
102
  <input type="text" id="api-url" placeholder="http://127.0.0.1:8000">
103
  <p class="help">URL of your FlowRead backend (Local or Hugging Face).</p>
firefox-extension/popup.js CHANGED
@@ -3,12 +3,13 @@ document.addEventListener('DOMContentLoaded', () => {
3
  const thresholdVal = document.getElementById('threshold-val');
4
  const gradientModeInput = document.getElementById('gradient-mode');
5
  const prepromptInput = document.getElementById('preprompt');
 
6
  const apiUrlInput = document.getElementById('api-url');
7
  const saveBtn = document.getElementById('save-btn');
8
  const pageBtn = document.getElementById('page-btn');
9
 
10
  // Load existing settings
11
- browser.storage.local.get(['threshold', 'gradientMode', 'preprompt', 'apiUrl'], (res) => {
12
  if (res.threshold !== undefined) {
13
  thresholdInput.value = res.threshold;
14
  thresholdVal.textContent = parseFloat(res.threshold).toFixed(2);
@@ -19,6 +20,9 @@ document.addEventListener('DOMContentLoaded', () => {
19
  if (res.preprompt !== undefined) {
20
  prepromptInput.value = res.preprompt;
21
  }
 
 
 
22
  apiUrlInput.value = res.apiUrl || "http://127.0.0.1:8000";
23
  });
24
 
@@ -33,6 +37,7 @@ document.addEventListener('DOMContentLoaded', () => {
33
  threshold: parseFloat(thresholdInput.value),
34
  gradientMode: gradientModeInput.checked,
35
  preprompt: prepromptInput.value.trim(),
 
36
  apiUrl: apiUrlInput.value.trim().replace(/\/$/, '') // Remove trailing slash
37
  };
38
 
 
3
  const thresholdVal = document.getElementById('threshold-val');
4
  const gradientModeInput = document.getElementById('gradient-mode');
5
  const prepromptInput = document.getElementById('preprompt');
6
+ const saliencyModeInput = document.getElementById('saliency-mode');
7
  const apiUrlInput = document.getElementById('api-url');
8
  const saveBtn = document.getElementById('save-btn');
9
  const pageBtn = document.getElementById('page-btn');
10
 
11
  // Load existing settings
12
+ browser.storage.local.get(['threshold', 'gradientMode', 'preprompt', 'saliencyMode', 'apiUrl'], (res) => {
13
  if (res.threshold !== undefined) {
14
  thresholdInput.value = res.threshold;
15
  thresholdVal.textContent = parseFloat(res.threshold).toFixed(2);
 
20
  if (res.preprompt !== undefined) {
21
  prepromptInput.value = res.preprompt;
22
  }
23
+ if (res.saliencyMode !== undefined) {
24
+ saliencyModeInput.value = res.saliencyMode;
25
+ }
26
  apiUrlInput.value = res.apiUrl || "http://127.0.0.1:8000";
27
  });
28
 
 
37
  threshold: parseFloat(thresholdInput.value),
38
  gradientMode: gradientModeInput.checked,
39
  preprompt: prepromptInput.value.trim(),
40
+ saliencyMode: saliencyModeInput.value,
41
  apiUrl: apiUrlInput.value.trim().replace(/\/$/, '') // Remove trailing slash
42
  };
43
 
flowread-extension.zip CHANGED
Binary files a/flowread-extension.zip and b/flowread-extension.zip differ
 
main.py CHANGED
@@ -260,6 +260,7 @@ class TextRequest(BaseModel):
260
  text: str
261
  layers: Optional[List[int]] = None # List of layer indices to average
262
  preprompt: str = "" # Optional task-driven intent
 
263
 
264
  @app.post("/analyze")
265
  async def analyze_text(request: TextRequest):
@@ -307,18 +308,26 @@ async def analyze_text(request: TextRequest):
307
  # Calculate importance: sum of attention each token *receives* from the sequence
308
  importance = avg_attention.sum(dim=0).cpu().float().numpy()
309
 
 
 
310
  if len(importance) > num_preprompt_tokens:
311
- # Normalize to 0-1, excluding the preprompt and <bos> from max/min calculation
312
- # as they often have very high attention, skewing the rest
313
  text_importance = importance[num_preprompt_tokens:]
314
- min_score = text_importance.min()
315
- max_score = text_importance.max()
316
 
317
- # Avoid division by zero
318
- if max_score > min_score:
319
- normalized_scores = (importance - min_score) / (max_score - min_score)
 
 
 
320
  else:
321
- normalized_scores = importance - min_score
 
 
 
 
 
 
 
322
 
323
  # Keep <bos> at max score
324
  normalized_scores[0] = 1.0
 
260
  text: str
261
  layers: Optional[List[int]] = None # List of layer indices to average
262
  preprompt: str = "" # Optional task-driven intent
263
+ saliency_mode: str = "local" # "local" or "global"
264
 
265
  @app.post("/analyze")
266
  async def analyze_text(request: TextRequest):
 
308
  # Calculate importance: sum of attention each token *receives* from the sequence
309
  importance = avg_attention.sum(dim=0).cpu().float().numpy()
310
 
311
+ import numpy as np
312
+
313
  if len(importance) > num_preprompt_tokens:
 
 
314
  text_importance = importance[num_preprompt_tokens:]
 
 
315
 
316
+ if request.saliency_mode == "global":
317
+ # Global Mode: absolute importance across different texts
318
+ # We apply a soft root penalty so very high values don't entirely blow out the scale,
319
+ # but high-density blocks will still look visibly darker/bolder than simple blocks.
320
+ # An importance sum of 1.0 (average) maps to ~0.46, an importance of 3.0+ maps to 1.0
321
+ normalized_scores = np.clip((importance / 3.0) ** 0.7, 0, 1.0)
322
  else:
323
+ # Local Mode: relative importance within this specific block of text
324
+ min_score = text_importance.min()
325
+ max_score = text_importance.max()
326
+
327
+ if max_score > min_score:
328
+ normalized_scores = (importance - min_score) / (max_score - min_score)
329
+ else:
330
+ normalized_scores = importance - min_score
331
 
332
  # Keep <bos> at max score
333
  normalized_scores[0] = 1.0
static/index.html CHANGED
@@ -223,6 +223,19 @@
223
  <details style="margin-bottom: 1.5rem; border: 1px solid #d6d3d1; border-radius: 0.375rem; padding: 1rem; background: #fdfbf7;">
224
  <summary style="cursor: pointer; font-weight: bold; color: #57534e;">Advanced Saliency Settings</summary>
225
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  <div style="margin-top: 1rem;">
227
  <label for="preprompt" style="display:block; font-weight: 600; margin-bottom: 0.5rem; color: #44403c;">
228
  Intent-Driven Reading (Preprompt)
@@ -388,6 +401,7 @@
388
  // ==========================================
389
  const inputArea = document.getElementById('text-input');
390
  const prepromptInput = document.getElementById('preprompt');
 
391
  const analyzeBtn = document.getElementById('analyze-btn');
392
  const thresholdSlider = document.getElementById('threshold');
393
  const thresholdVal = document.getElementById('threshold-val');
@@ -448,6 +462,7 @@
448
  analyzeBtn.addEventListener('click', async () => {
449
  const text = inputArea.value.trim();
450
  const preprompt = prepromptInput.value.trim();
 
451
 
452
  const checkedLayers = Array.from(document.querySelectorAll('.layer-cb:checked')).map(cb => parseInt(cb.value));
453
 
@@ -465,7 +480,7 @@
465
  const response = await fetch('/analyze', {
466
  method: 'POST',
467
  headers: { 'Content-Type': 'application/json' },
468
- body: JSON.stringify({ text, preprompt, layers: checkedLayers })
469
  });
470
 
471
  if (!response.ok) throw new Error('Network response was not ok');
 
223
  <details style="margin-bottom: 1.5rem; border: 1px solid #d6d3d1; border-radius: 0.375rem; padding: 1rem; background: #fdfbf7;">
224
  <summary style="cursor: pointer; font-weight: bold; color: #57534e;">Advanced Saliency Settings</summary>
225
 
226
+ <div style="margin-top: 1rem; margin-bottom: 1rem;">
227
+ <label for="saliency-mode" style="display:block; font-weight: 600; margin-bottom: 0.5rem; color: #44403c;">
228
+ Saliency Calculation Mode
229
+ </label>
230
+ <p style="font-size: 0.85rem; color: #78716c; margin-top: 0; margin-bottom: 0.5rem;">
231
+ Choose whether importance is calculated relative to this text alone, or absolutely across all texts.
232
+ </p>
233
+ <select id="saliency-mode" style="width: 100%; padding: 0.5rem; border: 1px solid #d6d3d1; border-radius: 0.25rem; font-size: 0.9rem; box-sizing: border-box; font-family: inherit;">
234
+ <option value="local">Local Mode (Relative Min/Max)</option>
235
+ <option value="global">Global Mode (Absolute Values with Penalty)</option>
236
+ </select>
237
+ </div>
238
+
239
  <div style="margin-top: 1rem;">
240
  <label for="preprompt" style="display:block; font-weight: 600; margin-bottom: 0.5rem; color: #44403c;">
241
  Intent-Driven Reading (Preprompt)
 
401
  // ==========================================
402
  const inputArea = document.getElementById('text-input');
403
  const prepromptInput = document.getElementById('preprompt');
404
+ const saliencyModeInput = document.getElementById('saliency-mode');
405
  const analyzeBtn = document.getElementById('analyze-btn');
406
  const thresholdSlider = document.getElementById('threshold');
407
  const thresholdVal = document.getElementById('threshold-val');
 
462
  analyzeBtn.addEventListener('click', async () => {
463
  const text = inputArea.value.trim();
464
  const preprompt = prepromptInput.value.trim();
465
+ const saliencyMode = saliencyModeInput.value;
466
 
467
  const checkedLayers = Array.from(document.querySelectorAll('.layer-cb:checked')).map(cb => parseInt(cb.value));
468
 
 
480
  const response = await fetch('/analyze', {
481
  method: 'POST',
482
  headers: { 'Content-Type': 'application/json' },
483
+ body: JSON.stringify({ text, preprompt, layers: checkedLayers, saliency_mode: saliencyMode })
484
  });
485
 
486
  if (!response.ok) throw new Error('Network response was not ok');