Fix tokenizer extra_special_tokens crash and stabilize transformers
Browse files- README.md +6 -6
- firefox-extension/README.md +50 -0
- firefox-extension/content.js +17 -14
- firefox-extension/popup.html +9 -0
- firefox-extension/popup.js +6 -1
- main.py +171 -111
- static/index.html +13 -61
- test_bos.py +4 -0
- test_gguf.py +21 -0
- test_load.py +10 -0
- test_model.py +9 -0
- test_model2.py +8 -0
- test_tok_issue.py +39 -0
README.md
CHANGED
|
@@ -11,13 +11,13 @@ pinned: false
|
|
| 11 |
|
| 12 |
**Accelerate reading comprehension using LLM attention vectors.**
|
| 13 |
|
| 14 |
-
FlowRead AI is an accessibility and educational tool that dynamically bolds the most semantically important words in a text. It uses the raw, internal attention vectors of the **
|
| 15 |
|
| 16 |
## The Problem
|
| 17 |
In the digital age, information overload is a massive barrier. General readers experience reduced reading speeds and poor retention when trying to skim long articles or academic papers, and individuals with ADHD, dyslexia, or cognitive fatigue face even more significant challenges when processing dense blocks of text. Existing solutions (like "Bionic Reading") simply bold the first half of *every* word, which is arbitrary and ignores the actual meaning of the sentence.
|
| 18 |
|
| 19 |
## The Solution
|
| 20 |
-
FlowRead AI solves this by extracting the mathematical "saliency" of words. By averaging the incoming attention scores across different layers of Gemma
|
| 21 |
|
| 22 |
For the general reader, this translates to significantly faster reading speeds and highly efficient skimming, as the eye is naturally drawn to the most information-dense words. For readers with attention deficits or cognitive fatigue, it creates a visually guided reading path that reduces cognitive load and improves reading comprehension. Everyone benefits from a more focused reading experience.
|
| 23 |
|
|
@@ -28,11 +28,11 @@ For the general reader, this translates to significantly faster reading speeds a
|
|
| 28 |
* **Built-in A/B User Study:** A complete embedded research tool with an SQLite backend to gather real-world empirical data on how FlowRead impacts average reading speed and comprehension accuracy.
|
| 29 |
|
| 30 |
## Why Gemma?
|
| 31 |
-
We chose **Gemma
|
| 32 |
|
| 33 |
## Technical Implementation
|
| 34 |
* **Backend:** FastAPI (Python) serving a PyTorch/Hugging Face pipeline.
|
| 35 |
-
* **Model:** `
|
| 36 |
* **Frontend:** Pure HTML/JS/CSS with a minimalist, distraction-free "academic paper" aesthetic.
|
| 37 |
* **Database:** SQLite for storing A/B test results.
|
| 38 |
|
|
@@ -49,7 +49,7 @@ Running locally allows you to use your own GPU (like Apple Silicon MPS or Nvidia
|
|
| 49 |
pip install -r requirements.txt
|
| 50 |
```
|
| 51 |
2. **Authenticate with Hugging Face:**
|
| 52 |
-
Gemma
|
| 53 |
```bash
|
| 54 |
huggingface-cli login
|
| 55 |
```
|
|
@@ -66,7 +66,7 @@ Running locally allows you to use your own GPU (like Apple Silicon MPS or Nvidia
|
|
| 66 |
This repository is already configured with a `Dockerfile` and the correct YAML frontmatter to be deployed directly as a Hugging Face Docker Space.
|
| 67 |
|
| 68 |
1. Create a new **Docker Space** on Hugging Face.
|
| 69 |
-
2. Add your Hugging Face Token as a Repository Secret named `HF_TOKEN`. This is required to download the gated Gemma
|
| 70 |
3. Push this repository to your Space.
|
| 71 |
4. The Space will automatically build the Docker image and start the FastAPI server. The SQLite database (`study.db`) is safely written to the `/tmp` or `/data` directory to avoid read-only filesystem errors.
|
| 72 |
|
|
|
|
| 11 |
|
| 12 |
**Accelerate reading comprehension using LLM attention vectors.**
|
| 13 |
|
| 14 |
+
FlowRead AI is an accessibility and educational tool that dynamically bolds the most semantically important words in a text. It uses the raw, internal attention vectors of the **Gemma 4 (E2B and 26B)** models to understand what words actually matter, creating a visually guided reading path that reduces cognitive load and improves reading comprehension.
|
| 15 |
|
| 16 |
## The Problem
|
| 17 |
In the digital age, information overload is a massive barrier. General readers experience reduced reading speeds and poor retention when trying to skim long articles or academic papers, and individuals with ADHD, dyslexia, or cognitive fatigue face even more significant challenges when processing dense blocks of text. Existing solutions (like "Bionic Reading") simply bold the first half of *every* word, which is arbitrary and ignores the actual meaning of the sentence.
|
| 18 |
|
| 19 |
## The Solution
|
| 20 |
+
FlowRead AI solves this by extracting the mathematical "saliency" of words. By averaging the incoming attention scores across different layers of Gemma 4, we determine exactly which nouns, verbs, and adjectives anchor the semantic meaning of the sentence.
|
| 21 |
|
| 22 |
For the general reader, this translates to significantly faster reading speeds and highly efficient skimming, as the eye is naturally drawn to the most information-dense words. For readers with attention deficits or cognitive fatigue, it creates a visually guided reading path that reduces cognitive load and improves reading comprehension. Everyone benefits from a more focused reading experience.
|
| 23 |
|
|
|
|
| 28 |
* **Built-in A/B User Study:** A complete embedded research tool with an SQLite backend to gather real-world empirical data on how FlowRead impacts average reading speed and comprehension accuracy.
|
| 29 |
|
| 30 |
## Why Gemma?
|
| 31 |
+
We chose the **Gemma 4** family because they offer an incredible balance of deep semantic understanding and computational efficiency. Their attention heads are remarkably accurate at capturing contextual importance, punching well above their weight class. Because it has open weights, we can directly extract the `outputs.attentions` matrices—something impossible with closed-source, API-based models. With options like the highly efficient E2B model or the quantized 4-bit 26B model, the application can run locally on consumer hardware (like Macs with MPS) or on free cloud tiers (like Hugging Face Spaces), democratizing access to this tool.
|
| 32 |
|
| 33 |
## Technical Implementation
|
| 34 |
* **Backend:** FastAPI (Python) serving a PyTorch/Hugging Face pipeline.
|
| 35 |
+
* **Model:** `unsloth/gemma-4-E2B` (and 26B variants) running in `bfloat16` or 4-bit precision.
|
| 36 |
* **Frontend:** Pure HTML/JS/CSS with a minimalist, distraction-free "academic paper" aesthetic.
|
| 37 |
* **Database:** SQLite for storing A/B test results.
|
| 38 |
|
|
|
|
| 49 |
pip install -r requirements.txt
|
| 50 |
```
|
| 51 |
2. **Authenticate with Hugging Face:**
|
| 52 |
+
Gemma models are generally gated. You must accept the terms on Hugging Face and log in:
|
| 53 |
```bash
|
| 54 |
huggingface-cli login
|
| 55 |
```
|
|
|
|
| 66 |
This repository is already configured with a `Dockerfile` and the correct YAML frontmatter to be deployed directly as a Hugging Face Docker Space.
|
| 67 |
|
| 68 |
1. Create a new **Docker Space** on Hugging Face.
|
| 69 |
+
2. Add your Hugging Face Token as a Repository Secret named `HF_TOKEN`. This is required to download the gated Gemma models.
|
| 70 |
3. Push this repository to your Space.
|
| 71 |
4. The Space will automatically build the Docker image and start the FastAPI server. The SQLite database (`study.db`) is safely written to the `/tmp` or `/data` directory to avoid read-only filesystem errors.
|
| 72 |
|
firefox-extension/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# FlowRead Firefox Extension
|
| 2 |
+
|
| 3 |
+
The FlowRead Firefox extension allows you to dynamically bold the most semantically important words on any webpage, directly in your browser. It does this by communicating with your local (or cloud-hosted) FlowRead API server powered by Gemma 4 models.
|
| 4 |
+
|
| 5 |
+
## 1. Start the Backend API Server
|
| 6 |
+
|
| 7 |
+
The extension needs a running backend server to perform the mathematical saliency extraction. The backend uses the Hugging Face `transformers` library, served via FastAPI.
|
| 8 |
+
|
| 9 |
+
**Step 1: Install Dependencies**
|
| 10 |
+
Ensure you are in the root directory of the project and install the requirements:
|
| 11 |
+
```bash
|
| 12 |
+
pip install -r requirements.txt
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
**Step 2: Authenticate with Hugging Face**
|
| 16 |
+
The Gemma 4 models are gated on Hugging Face. You must log in first:
|
| 17 |
+
```bash
|
| 18 |
+
huggingface-cli login
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
**Step 3: Start the Server**
|
| 22 |
+
Run the backend using Uvicorn:
|
| 23 |
+
```bash
|
| 24 |
+
uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
| 25 |
+
```
|
| 26 |
+
*Note: This will download and load `unsloth/gemma-4-E2B` (or the configured 26B variant) and start listening for requests on port 8000. It does not use `llama.cpp` or `llama-server`; it uses PyTorch and the Hugging Face ecosystem natively.*
|
| 27 |
+
|
| 28 |
+
## 2. Install the Firefox Extension
|
| 29 |
+
|
| 30 |
+
1. Open Firefox and navigate to `about:debugging`.
|
| 31 |
+
2. Click **"This Firefox"** on the left sidebar.
|
| 32 |
+
3. Click the **"Load Temporary Add-on..."** button.
|
| 33 |
+
4. In the file dialog, navigate to the `firefox-extension` folder in this repository and select the `manifest.json` file. (Alternatively, you can select the `flowread-extension.zip` file if you prefer to use the packaged version).
|
| 34 |
+
5. The extension will appear in your list of Temporary Extensions and its icon will appear in your toolbar.
|
| 35 |
+
|
| 36 |
+
## 3. Configure the Extension
|
| 37 |
+
|
| 38 |
+
1. Click the **FlowRead icon** in your Firefox toolbar.
|
| 39 |
+
2. In the settings popup, locate the **Backend API URL** field.
|
| 40 |
+
3. Ensure it matches where your backend server is running:
|
| 41 |
+
- If running locally: `http://127.0.0.1:8000`
|
| 42 |
+
- If using Hugging Face Spaces or another remote server: `https://your-domain.com` (do not include a trailing slash)
|
| 43 |
+
4. You can also tweak other settings like the Saliency Threshold and Gradient Mode. Click **Save Settings**.
|
| 44 |
+
|
| 45 |
+
## 4. How to Use It
|
| 46 |
+
|
| 47 |
+
1. Navigate to any article, blog post, or text-heavy website.
|
| 48 |
+
2. **Method 1 (Selection):** Highlight a paragraph of text, right-click it, and select **"FlowRead Highlight"** from the context menu.
|
| 49 |
+
3. **Method 2 (Entire Page):** Right-click anywhere on the page and select **"FlowRead Entire Page"** (or use the "FlowRead Page" button in the extension popup).
|
| 50 |
+
4. The extension will send the text to your local API, compute the attention values, and instantly transform the text on your screen!
|
firefox-extension/content.js
CHANGED
|
@@ -37,15 +37,14 @@ 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', 'saliencyMode', 'modelVersion', '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 modelVersion = settings.modelVersion || "2b";
|
| 46 |
const apiUrl = settings.apiUrl || "http://127.0.0.1:8000";
|
| 47 |
-
// Default to middle layers just like the playground
|
| 48 |
-
const checkedLayers = [4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
|
| 49 |
|
| 50 |
try {
|
| 51 |
// Start polling status
|
|
@@ -74,12 +73,12 @@ browser.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
|
|
| 74 |
const response = await fetch(`${apiUrl}/analyze/${modelVersion}`, {
|
| 75 |
method: 'POST',
|
| 76 |
headers: { 'Content-Type': 'application/json' },
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
});
|
| 84 |
isFetching = false;
|
| 85 |
|
|
@@ -99,6 +98,7 @@ browser.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
|
|
| 99 |
container.dataset.tokens = JSON.stringify(currentTokens);
|
| 100 |
container.dataset.preprompt = preprompt;
|
| 101 |
container.dataset.saliencyMode = saliencyMode;
|
|
|
|
| 102 |
container.dataset.modelVersion = modelVersion;
|
| 103 |
container.dataset.originalText = selectedText;
|
| 104 |
container.innerHTML = htmlString;
|
|
@@ -118,14 +118,14 @@ browser.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
|
|
| 118 |
});
|
| 119 |
|
| 120 |
async function processEntirePage() {
|
| 121 |
-
const settings = await browser.storage.local.get(['threshold', 'gradientMode', 'preprompt', 'saliencyMode', 'modelVersion', 'apiUrl']);
|
| 122 |
const threshold = settings.threshold !== undefined ? settings.threshold : 0.35;
|
| 123 |
const useGradient = settings.gradientMode || false;
|
| 124 |
const preprompt = settings.preprompt || "";
|
| 125 |
const saliencyMode = settings.saliencyMode || "local";
|
|
|
|
| 126 |
const modelVersion = settings.modelVersion || "2b";
|
| 127 |
const apiUrl = settings.apiUrl || "http://127.0.0.1:8000";
|
| 128 |
-
const checkedLayers = [4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
|
| 129 |
|
| 130 |
// To prevent freezing the browser or overwhelming the API, process in batches
|
| 131 |
const walkerObj = document.createTreeWalker(
|
|
@@ -243,6 +243,7 @@ async function processEntirePage() {
|
|
| 243 |
container.dataset.tokens = JSON.stringify(data.words);
|
| 244 |
container.dataset.preprompt = preprompt;
|
| 245 |
container.dataset.saliencyMode = saliencyMode;
|
|
|
|
| 246 |
container.dataset.modelVersion = modelVersion;
|
| 247 |
container.dataset.originalText = text;
|
| 248 |
container.innerHTML = htmlString;
|
|
@@ -268,9 +269,9 @@ async function updateExisting(newSettings) {
|
|
| 268 |
const useGradient = newSettings.gradientMode || false;
|
| 269 |
const preprompt = newSettings.preprompt || "";
|
| 270 |
const saliencyMode = newSettings.saliencyMode || "local";
|
|
|
|
| 271 |
const modelVersion = newSettings.modelVersion || "2b";
|
| 272 |
const apiUrl = newSettings.apiUrl || "http://127.0.0.1:8000";
|
| 273 |
-
const checkedLayers = [4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
|
| 274 |
|
| 275 |
const containers = document.querySelectorAll('.flowread-container');
|
| 276 |
if (containers.length === 0) return;
|
|
@@ -282,11 +283,12 @@ async function updateExisting(newSettings) {
|
|
| 282 |
for (const container of containers) {
|
| 283 |
const oldPreprompt = container.dataset.preprompt || "";
|
| 284 |
const oldMode = container.dataset.saliencyMode || "local";
|
|
|
|
| 285 |
const oldModelVersion = container.dataset.modelVersion || "2b";
|
| 286 |
const text = container.dataset.originalText;
|
| 287 |
if (!text) continue;
|
| 288 |
|
| 289 |
-
if (oldPreprompt !== preprompt || oldMode !== saliencyMode || oldModelVersion !== modelVersion) {
|
| 290 |
if (reFetchCount === 0) {
|
| 291 |
showToast("Updating FlowRead elements with new settings...", 0);
|
| 292 |
isFetchingStatus = true;
|
|
@@ -315,7 +317,7 @@ async function updateExisting(newSettings) {
|
|
| 315 |
text: text,
|
| 316 |
preprompt: preprompt,
|
| 317 |
saliency_mode: saliencyMode,
|
| 318 |
-
|
| 319 |
})
|
| 320 |
});
|
| 321 |
|
|
@@ -326,6 +328,7 @@ async function updateExisting(newSettings) {
|
|
| 326 |
container.dataset.tokens = JSON.stringify(data.words);
|
| 327 |
container.dataset.preprompt = preprompt;
|
| 328 |
container.dataset.saliencyMode = saliencyMode;
|
|
|
|
| 329 |
container.dataset.modelVersion = modelVersion;
|
| 330 |
const htmlString = generateFlowReadHTML(data.words, threshold, useGradient);
|
| 331 |
container.innerHTML = htmlString;
|
|
|
|
| 37 |
const range = selection.getRangeAt(0);
|
| 38 |
|
| 39 |
// Get user settings
|
| 40 |
+
const settings = await browser.storage.local.get(['threshold', 'gradientMode', 'preprompt', 'saliencyMode', 'layerPreset', 'modelVersion', '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 layerPreset = settings.layerPreset || "middle";
|
| 46 |
const modelVersion = settings.modelVersion || "2b";
|
| 47 |
const apiUrl = settings.apiUrl || "http://127.0.0.1:8000";
|
|
|
|
|
|
|
| 48 |
|
| 49 |
try {
|
| 50 |
// Start polling status
|
|
|
|
| 73 |
const response = await fetch(`${apiUrl}/analyze/${modelVersion}`, {
|
| 74 |
method: 'POST',
|
| 75 |
headers: { 'Content-Type': 'application/json' },
|
| 76 |
+
body: JSON.stringify({
|
| 77 |
+
text: text,
|
| 78 |
+
preprompt: preprompt,
|
| 79 |
+
saliency_mode: saliencyMode,
|
| 80 |
+
layer_preset: layerPreset
|
| 81 |
+
})
|
| 82 |
});
|
| 83 |
isFetching = false;
|
| 84 |
|
|
|
|
| 98 |
container.dataset.tokens = JSON.stringify(currentTokens);
|
| 99 |
container.dataset.preprompt = preprompt;
|
| 100 |
container.dataset.saliencyMode = saliencyMode;
|
| 101 |
+
container.dataset.layerPreset = layerPreset;
|
| 102 |
container.dataset.modelVersion = modelVersion;
|
| 103 |
container.dataset.originalText = selectedText;
|
| 104 |
container.innerHTML = htmlString;
|
|
|
|
| 118 |
});
|
| 119 |
|
| 120 |
async function processEntirePage() {
|
| 121 |
+
const settings = await browser.storage.local.get(['threshold', 'gradientMode', 'preprompt', 'saliencyMode', 'layerPreset', 'modelVersion', 'apiUrl']);
|
| 122 |
const threshold = settings.threshold !== undefined ? settings.threshold : 0.35;
|
| 123 |
const useGradient = settings.gradientMode || false;
|
| 124 |
const preprompt = settings.preprompt || "";
|
| 125 |
const saliencyMode = settings.saliencyMode || "local";
|
| 126 |
+
const layerPreset = settings.layerPreset || "middle";
|
| 127 |
const modelVersion = settings.modelVersion || "2b";
|
| 128 |
const apiUrl = settings.apiUrl || "http://127.0.0.1:8000";
|
|
|
|
| 129 |
|
| 130 |
// To prevent freezing the browser or overwhelming the API, process in batches
|
| 131 |
const walkerObj = document.createTreeWalker(
|
|
|
|
| 243 |
container.dataset.tokens = JSON.stringify(data.words);
|
| 244 |
container.dataset.preprompt = preprompt;
|
| 245 |
container.dataset.saliencyMode = saliencyMode;
|
| 246 |
+
container.dataset.layerPreset = layerPreset;
|
| 247 |
container.dataset.modelVersion = modelVersion;
|
| 248 |
container.dataset.originalText = text;
|
| 249 |
container.innerHTML = htmlString;
|
|
|
|
| 269 |
const useGradient = newSettings.gradientMode || false;
|
| 270 |
const preprompt = newSettings.preprompt || "";
|
| 271 |
const saliencyMode = newSettings.saliencyMode || "local";
|
| 272 |
+
const layerPreset = newSettings.layerPreset || "middle";
|
| 273 |
const modelVersion = newSettings.modelVersion || "2b";
|
| 274 |
const apiUrl = newSettings.apiUrl || "http://127.0.0.1:8000";
|
|
|
|
| 275 |
|
| 276 |
const containers = document.querySelectorAll('.flowread-container');
|
| 277 |
if (containers.length === 0) return;
|
|
|
|
| 283 |
for (const container of containers) {
|
| 284 |
const oldPreprompt = container.dataset.preprompt || "";
|
| 285 |
const oldMode = container.dataset.saliencyMode || "local";
|
| 286 |
+
const oldPreset = container.dataset.layerPreset || "middle";
|
| 287 |
const oldModelVersion = container.dataset.modelVersion || "2b";
|
| 288 |
const text = container.dataset.originalText;
|
| 289 |
if (!text) continue;
|
| 290 |
|
| 291 |
+
if (oldPreprompt !== preprompt || oldMode !== saliencyMode || oldPreset !== layerPreset || oldModelVersion !== modelVersion) {
|
| 292 |
if (reFetchCount === 0) {
|
| 293 |
showToast("Updating FlowRead elements with new settings...", 0);
|
| 294 |
isFetchingStatus = true;
|
|
|
|
| 317 |
text: text,
|
| 318 |
preprompt: preprompt,
|
| 319 |
saliency_mode: saliencyMode,
|
| 320 |
+
layer_preset: layerPreset
|
| 321 |
})
|
| 322 |
});
|
| 323 |
|
|
|
|
| 328 |
container.dataset.tokens = JSON.stringify(data.words);
|
| 329 |
container.dataset.preprompt = preprompt;
|
| 330 |
container.dataset.saliencyMode = saliencyMode;
|
| 331 |
+
container.dataset.layerPreset = layerPreset;
|
| 332 |
container.dataset.modelVersion = modelVersion;
|
| 333 |
const htmlString = generateFlowReadHTML(data.words, threshold, useGradient);
|
| 334 |
container.innerHTML = htmlString;
|
firefox-extension/popup.html
CHANGED
|
@@ -98,6 +98,15 @@
|
|
| 98 |
</select>
|
| 99 |
<p class="help" style="margin-top: 0;">Global mode allows comparing importance across different paragraphs.</p>
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
<label for="model-version">Model Version</label>
|
| 102 |
<select id="model-version" style="width: 100%; margin-bottom: 15px; padding: 8px; border: 1px solid #d6d3d1; border-radius: 4px; font-family: inherit;">
|
| 103 |
<option value="2b">Gemma 4 2B (Fast / Default)</option>
|
|
|
|
| 98 |
</select>
|
| 99 |
<p class="help" style="margin-top: 0;">Global mode allows comparing importance across different paragraphs.</p>
|
| 100 |
|
| 101 |
+
<label for="layer-preset">Network Layers</label>
|
| 102 |
+
<select id="layer-preset" style="width: 100%; margin-bottom: 15px; padding: 8px; border: 1px solid #d6d3d1; border-radius: 4px; font-family: inherit;">
|
| 103 |
+
<option value="middle">Middle Layers (Semantic focus)</option>
|
| 104 |
+
<option value="first">First Few Layers (Lexical/Syntax)</option>
|
| 105 |
+
<option value="last">Last Few Layers (Output formatting)</option>
|
| 106 |
+
<option value="all">All Layers (Averaged)</option>
|
| 107 |
+
</select>
|
| 108 |
+
<p class="help" style="margin-top: 0;">Select which network layers extract attention scores.</p>
|
| 109 |
+
|
| 110 |
<label for="model-version">Model Version</label>
|
| 111 |
<select id="model-version" style="width: 100%; margin-bottom: 15px; padding: 8px; border: 1px solid #d6d3d1; border-radius: 4px; font-family: inherit;">
|
| 112 |
<option value="2b">Gemma 4 2B (Fast / Default)</option>
|
firefox-extension/popup.js
CHANGED
|
@@ -4,13 +4,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 4 |
const gradientModeInput = document.getElementById('gradient-mode');
|
| 5 |
const prepromptInput = document.getElementById('preprompt');
|
| 6 |
const saliencyModeInput = document.getElementById('saliency-mode');
|
|
|
|
| 7 |
const modelVersionInput = document.getElementById('model-version');
|
| 8 |
const apiUrlInput = document.getElementById('api-url');
|
| 9 |
const saveBtn = document.getElementById('save-btn');
|
| 10 |
const pageBtn = document.getElementById('page-btn');
|
| 11 |
|
| 12 |
// Load existing settings
|
| 13 |
-
browser.storage.local.get(['threshold', 'gradientMode', 'preprompt', 'saliencyMode', 'modelVersion', 'apiUrl'], (res) => {
|
| 14 |
if (res.threshold !== undefined) {
|
| 15 |
thresholdInput.value = res.threshold;
|
| 16 |
thresholdVal.textContent = parseFloat(res.threshold).toFixed(2);
|
|
@@ -24,6 +25,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 24 |
if (res.saliencyMode !== undefined) {
|
| 25 |
saliencyModeInput.value = res.saliencyMode;
|
| 26 |
}
|
|
|
|
|
|
|
|
|
|
| 27 |
if (res.modelVersion !== undefined) {
|
| 28 |
modelVersionInput.value = res.modelVersion;
|
| 29 |
}
|
|
@@ -42,6 +46,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 42 |
gradientMode: gradientModeInput.checked,
|
| 43 |
preprompt: prepromptInput.value.trim(),
|
| 44 |
saliencyMode: saliencyModeInput.value,
|
|
|
|
| 45 |
modelVersion: modelVersionInput.value,
|
| 46 |
apiUrl: apiUrlInput.value.trim().replace(/\/$/, '') // Remove trailing slash
|
| 47 |
};
|
|
|
|
| 4 |
const gradientModeInput = document.getElementById('gradient-mode');
|
| 5 |
const prepromptInput = document.getElementById('preprompt');
|
| 6 |
const saliencyModeInput = document.getElementById('saliency-mode');
|
| 7 |
+
const layerPresetInput = document.getElementById('layer-preset');
|
| 8 |
const modelVersionInput = document.getElementById('model-version');
|
| 9 |
const apiUrlInput = document.getElementById('api-url');
|
| 10 |
const saveBtn = document.getElementById('save-btn');
|
| 11 |
const pageBtn = document.getElementById('page-btn');
|
| 12 |
|
| 13 |
// Load existing settings
|
| 14 |
+
browser.storage.local.get(['threshold', 'gradientMode', 'preprompt', 'saliencyMode', 'layerPreset', 'modelVersion', 'apiUrl'], (res) => {
|
| 15 |
if (res.threshold !== undefined) {
|
| 16 |
thresholdInput.value = res.threshold;
|
| 17 |
thresholdVal.textContent = parseFloat(res.threshold).toFixed(2);
|
|
|
|
| 25 |
if (res.saliencyMode !== undefined) {
|
| 26 |
saliencyModeInput.value = res.saliencyMode;
|
| 27 |
}
|
| 28 |
+
if (res.layerPreset !== undefined) {
|
| 29 |
+
layerPresetInput.value = res.layerPreset;
|
| 30 |
+
}
|
| 31 |
if (res.modelVersion !== undefined) {
|
| 32 |
modelVersionInput.value = res.modelVersion;
|
| 33 |
}
|
|
|
|
| 46 |
gradientMode: gradientModeInput.checked,
|
| 47 |
preprompt: prepromptInput.value.trim(),
|
| 48 |
saliencyMode: saliencyModeInput.value,
|
| 49 |
+
layerPreset: layerPresetInput.value,
|
| 50 |
modelVersion: modelVersionInput.value,
|
| 51 |
apiUrl: apiUrlInput.value.trim().replace(/\/$/, '') // Remove trailing slash
|
| 52 |
};
|
main.py
CHANGED
|
@@ -22,20 +22,23 @@ app.add_middleware(
|
|
| 22 |
|
| 23 |
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 24 |
|
|
|
|
| 25 |
@app.get("/")
|
| 26 |
def read_root():
|
| 27 |
return RedirectResponse(url="/static/index.html")
|
| 28 |
|
|
|
|
| 29 |
# --- SQLite Database Setup ---
|
| 30 |
# Hugging Face Spaces make the /app directory read-only by default.
|
| 31 |
# We must write the database to /data (if persistent storage is enabled) or /tmp.
|
| 32 |
DB_DIR = "/data" if os.path.exists("/data") else "/tmp"
|
| 33 |
DB_FILE = os.path.join(DB_DIR, "study.db")
|
| 34 |
|
|
|
|
| 35 |
def init_db():
|
| 36 |
conn = sqlite3.connect(DB_FILE)
|
| 37 |
c = conn.cursor()
|
| 38 |
-
c.execute(
|
| 39 |
CREATE TABLE IF NOT EXISTS study_results (
|
| 40 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 41 |
user_id TEXT,
|
|
@@ -46,19 +49,20 @@ def init_db():
|
|
| 46 |
total_questions INTEGER,
|
| 47 |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 48 |
)
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
c.execute(
|
| 52 |
CREATE TABLE IF NOT EXISTS study_preferences (
|
| 53 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 54 |
user_id TEXT,
|
| 55 |
preference TEXT, -- "plain", "flowread", or "gradient"
|
| 56 |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 57 |
)
|
| 58 |
-
|
| 59 |
conn.commit()
|
| 60 |
conn.close()
|
| 61 |
|
|
|
|
| 62 |
init_db()
|
| 63 |
|
| 64 |
# --- Study Content ---
|
|
@@ -70,13 +74,8 @@ STUDY_TEXTS = [
|
|
| 70 |
"questions": [
|
| 71 |
{
|
| 72 |
"question": "Approximately how many neurons are in the human brain?",
|
| 73 |
-
"options": [
|
| 74 |
-
|
| 75 |
-
"86 billion",
|
| 76 |
-
"100 trillion",
|
| 77 |
-
"50 billion"
|
| 78 |
-
],
|
| 79 |
-
"correct": 1
|
| 80 |
},
|
| 81 |
{
|
| 82 |
"question": "What is the term for the brain's ability to reorganize itself?",
|
|
@@ -84,13 +83,13 @@ STUDY_TEXTS = [
|
|
| 84 |
"Synaptic generation",
|
| 85 |
"Neurogenesis",
|
| 86 |
"Neuroplasticity",
|
| 87 |
-
"Cognitive adaptation"
|
| 88 |
],
|
| 89 |
-
"correct": 2
|
| 90 |
-
}
|
| 91 |
],
|
| 92 |
-
"flowread_html":
|
| 93 |
-
"flowread_gradient_html":
|
| 94 |
},
|
| 95 |
{
|
| 96 |
"id": 2,
|
|
@@ -99,13 +98,8 @@ STUDY_TEXTS = [
|
|
| 99 |
"questions": [
|
| 100 |
{
|
| 101 |
"question": "Where did the Industrial Revolution begin?",
|
| 102 |
-
"options": [
|
| 103 |
-
|
| 104 |
-
"France",
|
| 105 |
-
"Germany",
|
| 106 |
-
"Britain"
|
| 107 |
-
],
|
| 108 |
-
"correct": 3
|
| 109 |
},
|
| 110 |
{
|
| 111 |
"question": "Which invention dramatically increased factory efficiency?",
|
|
@@ -113,13 +107,13 @@ STUDY_TEXTS = [
|
|
| 113 |
"The cotton gin",
|
| 114 |
"The telegraph",
|
| 115 |
"The steam engine",
|
| 116 |
-
"The assembly line"
|
| 117 |
],
|
| 118 |
-
"correct": 2
|
| 119 |
-
}
|
| 120 |
],
|
| 121 |
-
"flowread_html":
|
| 122 |
-
"flowread_gradient_html":
|
| 123 |
},
|
| 124 |
{
|
| 125 |
"id": 3,
|
|
@@ -128,13 +122,8 @@ STUDY_TEXTS = [
|
|
| 128 |
"questions": [
|
| 129 |
{
|
| 130 |
"question": "What spectrum does the James Webb Space Telescope primarily operate in?",
|
| 131 |
-
"options": [
|
| 132 |
-
|
| 133 |
-
"X-ray",
|
| 134 |
-
"Infrared",
|
| 135 |
-
"Visible light"
|
| 136 |
-
],
|
| 137 |
-
"correct": 2
|
| 138 |
},
|
| 139 |
{
|
| 140 |
"question": "Where does the telescope orbit to stay cold?",
|
|
@@ -142,19 +131,22 @@ STUDY_TEXTS = [
|
|
| 142 |
"Low Earth Orbit",
|
| 143 |
"The Moon's orbit",
|
| 144 |
"The first Lagrange point",
|
| 145 |
-
"The second Lagrange point"
|
| 146 |
],
|
| 147 |
-
"correct": 3
|
| 148 |
-
}
|
| 149 |
],
|
| 150 |
-
"flowread_html":
|
| 151 |
-
"flowread_gradient_html":
|
| 152 |
-
}
|
| 153 |
-
]# --- Study API Endpoints ---
|
|
|
|
|
|
|
| 154 |
@app.get("/api/study/texts")
|
| 155 |
def get_study_texts():
|
| 156 |
return {"texts": STUDY_TEXTS}
|
| 157 |
|
|
|
|
| 158 |
class StudySubmission(BaseModel):
|
| 159 |
user_id: str
|
| 160 |
text_id: int
|
|
@@ -163,79 +155,98 @@ class StudySubmission(BaseModel):
|
|
| 163 |
score: int
|
| 164 |
total_questions: int
|
| 165 |
|
|
|
|
| 166 |
class StudyPreference(BaseModel):
|
| 167 |
user_id: str
|
| 168 |
preference: str
|
| 169 |
|
|
|
|
| 170 |
@app.post("/api/study/preference")
|
| 171 |
def submit_study_preference(submission: StudyPreference):
|
| 172 |
conn = sqlite3.connect(DB_FILE)
|
| 173 |
c = conn.cursor()
|
| 174 |
c.execute(
|
| 175 |
"INSERT INTO study_preferences (user_id, preference) VALUES (?, ?)",
|
| 176 |
-
(submission.user_id, submission.preference)
|
| 177 |
)
|
| 178 |
conn.commit()
|
| 179 |
conn.close()
|
| 180 |
return {"status": "success"}
|
| 181 |
|
|
|
|
| 182 |
@app.post("/api/study/submit")
|
| 183 |
def submit_study_result(submission: StudySubmission):
|
| 184 |
conn = sqlite3.connect(DB_FILE)
|
| 185 |
c = conn.cursor()
|
| 186 |
c.execute(
|
| 187 |
"INSERT INTO study_results (user_id, text_id, condition, reading_time_ms, score, total_questions) VALUES (?, ?, ?, ?, ?, ?)",
|
| 188 |
-
(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
)
|
| 190 |
conn.commit()
|
| 191 |
conn.close()
|
| 192 |
return {"status": "success"}
|
| 193 |
|
|
|
|
| 194 |
@app.get("/api/study/stats")
|
| 195 |
def get_study_stats():
|
| 196 |
conn = sqlite3.connect(DB_FILE)
|
| 197 |
c = conn.cursor()
|
| 198 |
-
|
| 199 |
# Calculate stats for plain
|
| 200 |
-
c.execute(
|
|
|
|
|
|
|
| 201 |
plain_stats = c.fetchone()
|
| 202 |
-
|
| 203 |
# Calculate stats for flowread
|
| 204 |
-
c.execute(
|
|
|
|
|
|
|
| 205 |
flowread_stats = c.fetchone()
|
| 206 |
-
|
| 207 |
# Calculate stats for gradient
|
| 208 |
-
c.execute(
|
|
|
|
|
|
|
| 209 |
gradient_stats = c.fetchone()
|
| 210 |
-
|
| 211 |
# Calculate preferences
|
| 212 |
c.execute("SELECT preference, COUNT(*) FROM study_preferences GROUP BY preference")
|
| 213 |
preferences = dict(c.fetchall())
|
| 214 |
-
|
| 215 |
conn.close()
|
| 216 |
-
|
| 217 |
return {
|
| 218 |
"plain": {
|
| 219 |
"avg_reading_time_ms": plain_stats[0] or 0,
|
| 220 |
"avg_accuracy_percent": plain_stats[1] or 0,
|
| 221 |
-
"sample_size": plain_stats[2]
|
| 222 |
},
|
| 223 |
"flowread": {
|
| 224 |
"avg_reading_time_ms": flowread_stats[0] or 0,
|
| 225 |
"avg_accuracy_percent": flowread_stats[1] or 0,
|
| 226 |
-
"sample_size": flowread_stats[2]
|
| 227 |
},
|
| 228 |
"gradient": {
|
| 229 |
"avg_reading_time_ms": gradient_stats[0] or 0,
|
| 230 |
"avg_accuracy_percent": gradient_stats[1] or 0,
|
| 231 |
-
"sample_size": gradient_stats[2]
|
| 232 |
},
|
| 233 |
-
"preferences": preferences
|
| 234 |
}
|
| 235 |
|
|
|
|
| 236 |
import sys
|
| 237 |
import re
|
| 238 |
|
|
|
|
| 239 |
class StderrProgressInterceptor:
|
| 240 |
def __init__(self, original):
|
| 241 |
self.original = original
|
|
@@ -244,68 +255,83 @@ class StderrProgressInterceptor:
|
|
| 244 |
|
| 245 |
def write(self, s):
|
| 246 |
self.original.write(s)
|
| 247 |
-
match = re.search(r
|
| 248 |
if match and self.active_model:
|
| 249 |
pct = match.group(1)
|
| 250 |
self.current_progress = f"{pct}%"
|
| 251 |
# Update the global status explicitly so the API returns it immediately
|
| 252 |
model_status[self.active_model] = f"downloading: {self.current_progress}"
|
| 253 |
-
|
| 254 |
def flush(self):
|
| 255 |
self.original.flush()
|
| 256 |
|
|
|
|
| 257 |
stderr_interceptor = StderrProgressInterceptor(sys.stderr)
|
| 258 |
sys.stderr = stderr_interceptor
|
| 259 |
|
| 260 |
# --- Saliency API (Existing) ---
|
| 261 |
models = {}
|
| 262 |
tokenizers = {}
|
| 263 |
-
model_status = {
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
|
|
|
|
|
|
|
|
|
| 269 |
hf_token = os.environ.get("HF_TOKEN")
|
| 270 |
|
|
|
|
| 271 |
def load_model(model_name: str):
|
| 272 |
if model_name in models:
|
| 273 |
return models[model_name], tokenizers[model_name]
|
| 274 |
-
|
| 275 |
print(f"Loading {model_name} on {device}...")
|
| 276 |
model_status[model_name] = "downloading: 0%"
|
| 277 |
stderr_interceptor.active_model = model_name
|
| 278 |
try:
|
| 279 |
if model_name == "27b-4a":
|
| 280 |
# Use Gemma 4 26B A4B in 4-bit (requires CUDA)
|
| 281 |
-
hf_model_id = "unsloth/gemma-4-26B-A4B"
|
| 282 |
-
|
| 283 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
# BitsAndBytes for 4-bit quantization
|
| 285 |
from transformers import BitsAndBytesConfig
|
|
|
|
| 286 |
bnb_config = BitsAndBytesConfig(
|
| 287 |
-
load_in_4bit=True,
|
| 288 |
-
bnb_4bit_compute_dtype=torch.bfloat16
|
| 289 |
)
|
| 290 |
-
|
| 291 |
model = AutoModelForCausalLM.from_pretrained(
|
| 292 |
hf_model_id,
|
| 293 |
quantization_config=bnb_config,
|
| 294 |
device_map="auto",
|
| 295 |
attn_implementation="eager",
|
| 296 |
-
token=hf_token
|
| 297 |
)
|
| 298 |
else:
|
| 299 |
# Default to 2b (Gemma 4 E2B)
|
| 300 |
hf_model_id = "unsloth/gemma-4-E2B"
|
| 301 |
-
tokenizer = AutoTokenizer.from_pretrained(
|
|
|
|
|
|
|
| 302 |
model = AutoModelForCausalLM.from_pretrained(
|
| 303 |
hf_model_id,
|
| 304 |
torch_dtype=torch.bfloat16,
|
| 305 |
attn_implementation="eager",
|
| 306 |
-
token=hf_token
|
| 307 |
).to(device)
|
| 308 |
-
|
| 309 |
print(f"Model {model_name} loaded successfully.")
|
| 310 |
models[model_name] = model
|
| 311 |
tokenizers[model_name] = tokenizer
|
|
@@ -318,31 +344,37 @@ def load_model(model_name: str):
|
|
| 318 |
stderr_interceptor.active_model = None
|
| 319 |
raise e
|
| 320 |
|
|
|
|
| 321 |
# Pre-load default 2b
|
| 322 |
try:
|
| 323 |
load_model("2b")
|
| 324 |
except:
|
| 325 |
print("Could not preload 2b model.")
|
| 326 |
|
|
|
|
| 327 |
@app.get("/status")
|
| 328 |
def get_model_status():
|
| 329 |
return model_status
|
| 330 |
|
|
|
|
| 331 |
class TextRequest(BaseModel):
|
| 332 |
text: str
|
| 333 |
-
layers: Optional[List[int]] = None #
|
| 334 |
-
|
| 335 |
-
|
|
|
|
|
|
|
| 336 |
|
| 337 |
@app.post("/analyze")
|
| 338 |
def analyze_text_legacy(request: TextRequest):
|
| 339 |
return analyze_text_model("2b", request)
|
| 340 |
|
|
|
|
| 341 |
@app.post("/analyze/{model_name}")
|
| 342 |
def analyze_text_model(model_name: str, request: TextRequest):
|
| 343 |
text = request.text
|
| 344 |
preprompt = request.preprompt.strip()
|
| 345 |
-
|
| 346 |
if not text.strip():
|
| 347 |
return {"tokens": [], "scores": []}
|
| 348 |
|
|
@@ -350,51 +382,66 @@ def analyze_text_model(model_name: str, request: TextRequest):
|
|
| 350 |
model, tokenizer = load_model(model_name)
|
| 351 |
except Exception as e:
|
| 352 |
from fastapi import HTTPException
|
|
|
|
| 353 |
raise HTTPException(status_code=500, detail=str(e))
|
| 354 |
|
| 355 |
# Combine preprompt and text if preprompt exists
|
| 356 |
full_text = f"{preprompt}\n\n{text}" if preprompt else text
|
| 357 |
|
| 358 |
inputs = tokenizer(full_text, return_tensors="pt").to(model.device)
|
| 359 |
-
|
| 360 |
# Calculate how many tokens belong to the preprompt so we can strip them later
|
| 361 |
-
num_preprompt_tokens =
|
| 362 |
if preprompt:
|
| 363 |
p_toks = tokenizer(f"{preprompt}\n\n")["input_ids"]
|
| 364 |
num_preprompt_tokens = len(p_toks)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
|
| 366 |
with torch.no_grad():
|
| 367 |
# Ensure we ask the model to output attentions explicitly
|
| 368 |
outputs = model(**inputs, output_attentions=True)
|
| 369 |
-
|
| 370 |
# Check if attentions are actually returned
|
| 371 |
if not outputs.attentions:
|
| 372 |
print("Warning: Model did not return attentions.")
|
| 373 |
return {"words": []}
|
| 374 |
-
|
| 375 |
num_layers = len(outputs.attentions)
|
| 376 |
-
|
| 377 |
selected_layers = request.layers
|
| 378 |
if not selected_layers:
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
selected_layers = [l for l in selected_layers if 0 <= l < num_layers]
|
| 384 |
if not selected_layers:
|
| 385 |
selected_layers = [num_layers - 1]
|
| 386 |
-
|
| 387 |
stacked_attentions = torch.stack([outputs.attentions[l] for l in selected_layers])
|
| 388 |
-
avg_attention = stacked_attentions.mean(dim=(0, 2))[0]
|
| 389 |
-
|
| 390 |
# Calculate importance: sum of attention each token *receives* from the sequence
|
| 391 |
importance = avg_attention.sum(dim=0).cpu().float().numpy()
|
| 392 |
-
|
| 393 |
import numpy as np
|
| 394 |
|
| 395 |
if len(importance) > num_preprompt_tokens:
|
| 396 |
text_importance = importance[num_preprompt_tokens:]
|
| 397 |
-
|
| 398 |
if request.saliency_mode == "global":
|
| 399 |
# Global Mode: absolute importance across different texts
|
| 400 |
# We apply a soft root penalty so very high values don't entirely blow out the scale,
|
|
@@ -405,42 +452,55 @@ def analyze_text_model(model_name: str, request: TextRequest):
|
|
| 405 |
# Local Mode: relative importance within this specific block of text
|
| 406 |
min_score = text_importance.min()
|
| 407 |
max_score = text_importance.max()
|
| 408 |
-
|
| 409 |
if max_score > min_score:
|
| 410 |
normalized_scores = (importance - min_score) / (max_score - min_score)
|
| 411 |
else:
|
| 412 |
normalized_scores = importance - min_score
|
| 413 |
-
|
| 414 |
# Keep <bos> at max score
|
| 415 |
normalized_scores[0] = 1.0
|
| 416 |
normalized_scores = normalized_scores.clip(0, 1)
|
| 417 |
else:
|
| 418 |
normalized_scores = [1.0] * len(importance)
|
| 419 |
-
|
| 420 |
input_ids = inputs["input_ids"][0].tolist()
|
| 421 |
tokens = tokenizer.convert_ids_to_tokens(input_ids)
|
| 422 |
-
|
|
|
|
|
|
|
|
|
|
| 423 |
result = []
|
| 424 |
for i, t in enumerate(tokens):
|
| 425 |
# Decode properly
|
| 426 |
word = tokenizer.decode([input_ids[i]])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
# Special check for Gemma, decoding often removes spaces incorrectly or leaves tokens empty
|
| 428 |
# Let's clean the raw token just in case
|
| 429 |
-
raw_clean = t.replace(
|
| 430 |
-
|
| 431 |
# We will pass both decoded word and raw cleaned token to frontend to help render
|
| 432 |
-
result.append(
|
| 433 |
-
"token": raw_clean,
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
|
|
|
| 441 |
|
| 442 |
return {"words": result}
|
| 443 |
|
|
|
|
| 444 |
if __name__ == "__main__":
|
| 445 |
port = int(os.environ.get("PORT", 7860))
|
| 446 |
-
uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True)
|
|
|
|
| 22 |
|
| 23 |
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 24 |
|
| 25 |
+
|
| 26 |
@app.get("/")
|
| 27 |
def read_root():
|
| 28 |
return RedirectResponse(url="/static/index.html")
|
| 29 |
|
| 30 |
+
|
| 31 |
# --- SQLite Database Setup ---
|
| 32 |
# Hugging Face Spaces make the /app directory read-only by default.
|
| 33 |
# We must write the database to /data (if persistent storage is enabled) or /tmp.
|
| 34 |
DB_DIR = "/data" if os.path.exists("/data") else "/tmp"
|
| 35 |
DB_FILE = os.path.join(DB_DIR, "study.db")
|
| 36 |
|
| 37 |
+
|
| 38 |
def init_db():
|
| 39 |
conn = sqlite3.connect(DB_FILE)
|
| 40 |
c = conn.cursor()
|
| 41 |
+
c.execute("""
|
| 42 |
CREATE TABLE IF NOT EXISTS study_results (
|
| 43 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 44 |
user_id TEXT,
|
|
|
|
| 49 |
total_questions INTEGER,
|
| 50 |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 51 |
)
|
| 52 |
+
""")
|
| 53 |
+
|
| 54 |
+
c.execute("""
|
| 55 |
CREATE TABLE IF NOT EXISTS study_preferences (
|
| 56 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 57 |
user_id TEXT,
|
| 58 |
preference TEXT, -- "plain", "flowread", or "gradient"
|
| 59 |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 60 |
)
|
| 61 |
+
""")
|
| 62 |
conn.commit()
|
| 63 |
conn.close()
|
| 64 |
|
| 65 |
+
|
| 66 |
init_db()
|
| 67 |
|
| 68 |
# --- Study Content ---
|
|
|
|
| 74 |
"questions": [
|
| 75 |
{
|
| 76 |
"question": "Approximately how many neurons are in the human brain?",
|
| 77 |
+
"options": ["86 million", "86 billion", "100 trillion", "50 billion"],
|
| 78 |
+
"correct": 1,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
},
|
| 80 |
{
|
| 81 |
"question": "What is the term for the brain's ability to reorganize itself?",
|
|
|
|
| 83 |
"Synaptic generation",
|
| 84 |
"Neurogenesis",
|
| 85 |
"Neuroplasticity",
|
| 86 |
+
"Cognitive adaptation",
|
| 87 |
],
|
| 88 |
+
"correct": 2,
|
| 89 |
+
},
|
| 90 |
],
|
| 91 |
+
"flowread_html": '<span class="token highlighted">The</span><span class="token highlighted"> human</span><span class="token highlighted"> brain</span><span class="token highlighted"> is</span><span class="token highlighted"> a</span><span class="token highlighted"> marvel</span><span class="token"> of</span><span class="token highlighted"> biological</span><span class="token highlighted"> engineering</span><span class="token highlighted">,</span><span class="token highlighted"> containing</span><span class="token"> approximately</span><span class="token"> </span><span class="token">8</span><span class="token">6</span><span class="token highlighted"> billion</span><span class="token highlighted"> neurons</span><span class="token highlighted"> interconnected</span><span class="token highlighted"> by</span><span class="token"> tri</span><span class="token">lli</span><span class="token">ons</span><span class="token"> of</span><span class="token highlighted"> synapses</span><span class="token highlighted">.</span><span class="token highlighted"> These</span><span class="token highlighted"> neural</span><span class="token highlighted"> networks</span><span class="token"> are</span><span class="token"> responsible</span><span class="token highlighted"> for</span><span class="token"> everything</span><span class="token highlighted"> from</span><span class="token highlighted"> basic</span><span class="token highlighted"> autonomic</span><span class="token highlighted"> functions</span><span class="token highlighted">,</span><span class="token highlighted"> like</span><span class="token"> breathing</span><span class="token"> and</span><span class="token"> heart</span><span class="token"> rate</span><span class="token">,</span><span class="token highlighted"> to</span><span class="token"> complex</span><span class="token highlighted"> cognitive</span><span class="token"> processes</span><span class="token"> such</span><span class="token"> as</span><span class="token highlighted"> memory</span><span class="token highlighted">,</span><span class="token"> emotion</span><span class="token">,</span><span class="token"> and</span><span class="token highlighted"> problem</span><span class="token highlighted">-</span><span class="token highlighted">solving</span><span class="token highlighted">.</span><span class="token highlighted"> Neurop</span><span class="token highlighted">lastic</span><span class="token highlighted">ity</span><span class="token highlighted">,</span><span class="token highlighted"> the</span><span class="token highlighted"> brain</span><span class="token highlighted">\'</span><span class="token highlighted">s</span><span class="token"> ability</span><span class="token"> to</span><span class="token highlighted"> reorgan</span><span class="token highlighted">ize</span><span class="token"> itself</span><span class="token"> by</span><span class="token"> forming</span><span class="token"> new</span><span class="token"> neural</span><span class="token"> connections</span><span class="token"> throughout</span><span class="token highlighted"> life</span><span class="token highlighted">,</span><span class="token highlighted"> allows</span><span class="token highlighted"> humans</span><span class="token"> to</span><span class="token highlighted"> learn</span><span class="token"> new</span><span class="token"> skills</span><span class="token">,</span><span class="token"> recover</span><span class="token"> from</span><span class="token"> injuries</span><span class="token">,</span><span class="token"> and</span><span class="token"> adapt</span><span class="token"> to</span><span class="token"> changing</span><span class="token highlighted"> environments</span><span class="token highlighted">.</span><span class="token"> This</span><span class="token"> extraordinary</span><span class="token highlighted"> adaptability</span><span class="token"> is</span><span class="token"> what</span><span class="token"> makes</span><span class="token"> our</span><span class="token"> species</span><span class="token"> so</span><span class="token"> resilient</span><span class="token"> and</span><span class="token"> capable</span><span class="token"> of</span><span class="token"> continuous</span><span class="token"> intellectual</span><span class="token"> growth</span><span class="token">.</span>',
|
| 92 |
+
"flowread_gradient_html": '<span class="token" style="opacity: 0.53; font-weight: 489;">The</span><span class="token" style="opacity: 0.56; font-weight: 506;"> human</span><span class="token" style="opacity: 1.00; font-weight: 800;"> brain</span><span class="token" style="opacity: 0.54; font-weight: 490;"> is</span><span class="token" style="opacity: 0.51; font-weight: 472;"> a</span><span class="token" style="opacity: 0.53; font-weight: 489;"> marvel</span><span class="token" style="opacity: 0.48; font-weight: 452;"> of</span><span class="token" style="opacity: 0.53; font-weight: 484;"> biological</span><span class="token" style="opacity: 0.55; font-weight: 498;"> engineering</span><span class="token" style="opacity: 0.55; font-weight: 498;">,</span><span class="token" style="opacity: 0.56; font-weight: 506;"> containing</span><span class="token" style="opacity: 0.48; font-weight: 455;"> approximately</span><span class="token" style="opacity: 0.48; font-weight: 455;"> </span><span class="token" style="opacity: 0.43; font-weight: 421;">8</span><span class="token" style="opacity: 0.43; font-weight: 421;">6</span><span class="token" style="opacity: 0.50; font-weight: 469;"> billion</span><span class="token" style="opacity: 0.71; font-weight: 608;"> neurons</span><span class="token" style="opacity: 0.55; font-weight: 500;"> interconnected</span><span class="token" style="opacity: 0.53; font-weight: 484;"> by</span><span class="token" style="opacity: 0.43; font-weight: 419;"> tri</span><span class="token" style="opacity: 0.43; font-weight: 419;">lli</span><span class="token" style="opacity: 0.43; font-weight: 419;">ons</span><span class="token" style="opacity: 0.42; font-weight: 413;"> of</span><span class="token" style="opacity: 0.72; font-weight: 613;"> synapses</span><span class="token" style="opacity: 0.72; font-weight: 613;">.</span><span class="token" style="opacity: 0.51; font-weight: 472;"> These</span><span class="token" style="opacity: 0.57; font-weight: 516;"> neural</span><span class="token" style="opacity: 0.57; font-weight: 515;"> networks</span><span class="token" style="opacity: 0.48; font-weight: 455;"> are</span><span class="token" style="opacity: 0.48; font-weight: 452;"> responsible</span><span class="token" style="opacity: 0.52; font-weight: 482;"> for</span><span class="token" style="opacity: 0.50; font-weight: 464;"> everything</span><span class="token" style="opacity: 0.59; font-weight: 529;"> from</span><span class="token" style="opacity: 0.51; font-weight: 474;"> basic</span><span class="token" style="opacity: 0.56; font-weight: 504;"> autonomic</span><span class="token" style="opacity: 0.52; font-weight: 482;"> functions</span><span class="token" style="opacity: 0.52; font-weight: 482;">,</span><span class="token" style="opacity: 0.52; font-weight: 481;"> like</span><span class="token" style="opacity: 0.50; font-weight: 466;"> breathing</span><span class="token" style="opacity: 0.47; font-weight: 444;"> and</span><span class="token" style="opacity: 0.44; font-weight: 424;"> heart</span><span class="token" style="opacity: 0.45; font-weight: 435;"> rate</span><span class="token" style="opacity: 0.45; font-weight: 435;">,</span><span class="token" style="opacity: 0.52; font-weight: 479;"> to</span><span class="token" style="opacity: 0.49; font-weight: 462;"> complex</span><span class="token" style="opacity: 0.53; font-weight: 487;"> cognitive</span><span class="token" style="opacity: 0.48; font-weight: 451;"> processes</span><span class="token" style="opacity: 0.45; font-weight: 432;"> such</span><span class="token" style="opacity: 0.48; font-weight: 451;"> as</span><span class="token" style="opacity: 0.51; font-weight: 474;"> memory</span><span class="token" style="opacity: 0.51; font-weight: 474;">,</span><span class="token" style="opacity: 0.48; font-weight: 450;"> emotion</span><span class="token" style="opacity: 0.48; font-weight: 450;">,</span><span class="token" style="opacity: 0.44; font-weight: 428;"> and</span><span class="token" style="opacity: 0.64; font-weight: 559;"> problem</span><span class="token" style="opacity: 0.64; font-weight: 559;">-</span><span class="token" style="opacity: 0.64; font-weight: 559;">solving</span><span class="token" style="opacity: 0.64; font-weight: 559;">.</span><span class="token" style="opacity: 0.68; font-weight: 587;"> Neurop</span><span class="token" style="opacity: 0.68; font-weight: 587;">lastic</span><span class="token" style="opacity: 0.68; font-weight: 587;">ity</span><span class="token" style="opacity: 0.68; font-weight: 587;">,</span><span class="token" style="opacity: 0.50; font-weight: 469;"> the</span><span class="token" style="opacity: 0.59; font-weight: 527;"> brain</span><span class="token" style="opacity: 0.59; font-weight: 527;">\'</span><span class="token" style="opacity: 0.59; font-weight: 527;">s</span><span class="token" style="opacity: 0.45; font-weight: 434;"> ability</span><span class="token" style="opacity: 0.50; font-weight: 464;"> to</span><span class="token" style="opacity: 0.50; font-weight: 469;"> reorgan</span><span class="token" style="opacity: 0.50; font-weight: 469;">ize</span><span class="token" style="opacity: 0.47; font-weight: 444;"> itself</span><span class="token" style="opacity: 0.48; font-weight: 454;"> by</span><span class="token" style="opacity: 0.45; font-weight: 434;"> forming</span><span class="token" style="opacity: 0.45; font-weight: 431;"> new</span><span class="token" style="opacity: 0.44; font-weight: 426;"> neural</span><span class="token" style="opacity: 0.45; font-weight: 433;"> connections</span><span class="token" style="opacity: 0.44; font-weight: 426;"> throughout</span><span class="token" style="opacity: 0.52; font-weight: 480;"> life</span><span class="token" style="opacity: 0.52; font-weight: 480;">,</span><span class="token" style="opacity: 0.53; font-weight: 485;"> allows</span><span class="token" style="opacity: 0.54; font-weight: 493;"> humans</span><span class="token" style="opacity: 0.49; font-weight: 463;"> to</span><span class="token" style="opacity: 0.52; font-weight: 482;"> learn</span><span class="token" style="opacity: 0.45; font-weight: 433;"> new</span><span class="token" style="opacity: 0.48; font-weight: 454;"> skills</span><span class="token" style="opacity: 0.48; font-weight: 454;">,</span><span class="token" style="opacity: 0.47; font-weight: 449;"> recover</span><span class="token" style="opacity: 0.44; font-weight: 429;"> from</span><span class="token" style="opacity: 0.46; font-weight: 438;"> injuries</span><span class="token" style="opacity: 0.46; font-weight: 438;">,</span><span class="token" style="opacity: 0.45; font-weight: 433;"> and</span><span class="token" style="opacity: 0.45; font-weight: 436;"> adapt</span><span class="token" style="opacity: 0.43; font-weight: 421;"> to</span><span class="token" style="opacity: 0.42; font-weight: 414;"> changing</span><span class="token" style="opacity: 0.53; font-weight: 485;"> environments</span><span class="token" style="opacity: 0.53; font-weight: 485;">.</span><span class="token" style="opacity: 0.44; font-weight: 429;"> This</span><span class="token" style="opacity: 0.46; font-weight: 442;"> extraordinary</span><span class="token" style="opacity: 0.51; font-weight: 472;"> adaptability</span><span class="token" style="opacity: 0.44; font-weight: 428;"> is</span><span class="token" style="opacity: 0.45; font-weight: 435;"> what</span><span class="token" style="opacity: 0.46; font-weight: 443;"> makes</span><span class="token" style="opacity: 0.44; font-weight: 428;"> our</span><span class="token" style="opacity: 0.50; font-weight: 468;"> species</span><span class="token" style="opacity: 0.45; font-weight: 430;"> so</span><span class="token" style="opacity: 0.47; font-weight: 443;"> resilient</span><span class="token" style="opacity: 0.44; font-weight: 427;"> and</span><span class="token" style="opacity: 0.42; font-weight: 412;"> capable</span><span class="token" style="opacity: 0.43; font-weight: 422;"> of</span><span class="token" style="opacity: 0.43; font-weight: 420;"> continuous</span><span class="token" style="opacity: 0.42; font-weight: 413;"> intellectual</span><span class="token" style="opacity: 0.41; font-weight: 403;"> growth</span><span class="token" style="opacity: 0.41; font-weight: 403;">.</span>',
|
| 93 |
},
|
| 94 |
{
|
| 95 |
"id": 2,
|
|
|
|
| 98 |
"questions": [
|
| 99 |
{
|
| 100 |
"question": "Where did the Industrial Revolution begin?",
|
| 101 |
+
"options": ["United States", "France", "Germany", "Britain"],
|
| 102 |
+
"correct": 3,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
},
|
| 104 |
{
|
| 105 |
"question": "Which invention dramatically increased factory efficiency?",
|
|
|
|
| 107 |
"The cotton gin",
|
| 108 |
"The telegraph",
|
| 109 |
"The steam engine",
|
| 110 |
+
"The assembly line",
|
| 111 |
],
|
| 112 |
+
"correct": 2,
|
| 113 |
+
},
|
| 114 |
],
|
| 115 |
+
"flowread_html": '<span class="token highlighted">The</span><span class="token highlighted"> Industrial</span><span class="token highlighted"> Revolution</span><span class="token highlighted">,</span><span class="token highlighted"> which</span><span class="token highlighted"> began</span><span class="token"> in</span><span class="token highlighted"> Britain</span><span class="token"> in</span><span class="token"> the</span><span class="token"> late</span><span class="token"> </span><span class="token highlighted">1</span><span class="token highlighted">8</span><span class="token highlighted">th</span><span class="token highlighted"> century</span><span class="token highlighted">,</span><span class="token highlighted"> marked</span><span class="token"> a</span><span class="token highlighted"> profound</span><span class="token"> turning</span><span class="token highlighted"> point</span><span class="token"> in</span><span class="token highlighted"> human</span><span class="token highlighted"> history</span><span class="token highlighted">.</span><span class="token highlighted"> It</span><span class="token highlighted"> initiated</span><span class="token"> the</span><span class="token"> transition</span><span class="token highlighted"> from</span><span class="token highlighted"> agrarian</span><span class="token highlighted">,</span><span class="token"> handic</span><span class="token">raft</span><span class="token highlighted"> economies</span><span class="token highlighted"> to</span><span class="token highlighted"> industry</span><span class="token"> and</span><span class="token highlighted"> machine</span><span class="token highlighted"> manufacturing</span><span class="token highlighted">.</span><span class="token highlighted"> The</span><span class="token highlighted"> invention</span><span class="token highlighted"> of</span><span class="token"> the</span><span class="token highlighted"> steam</span><span class="token highlighted"> engine</span><span class="token highlighted">,</span><span class="token"> pioneered</span><span class="token highlighted"> by</span><span class="token"> figures</span><span class="token"> like</span><span class="token"> James</span><span class="token"> Watt</span><span class="token">,</span><span class="token highlighted"> dramatically</span><span class="token highlighted"> increased</span><span class="token"> the</span><span class="token"> efficiency</span><span class="token"> of</span><span class="token highlighted"> factories</span><span class="token"> and</span><span class="token highlighted"> transportation</span><span class="token highlighted">,</span><span class="token highlighted"> revolution</span><span class="token highlighted">izing</span><span class="token"> the</span><span class="token"> textile</span><span class="token"> industry</span><span class="token"> and</span><span class="token"> leading</span><span class="token"> to</span><span class="token"> the</span><span class="token"> expansion</span><span class="token"> of</span><span class="token highlighted"> railways</span><span class="token highlighted">.</span><span class="token"> This</span><span class="token highlighted"> era</span><span class="token highlighted"> brought</span><span class="token"> about</span><span class="token"> unprecedented</span><span class="token highlighted"> economic</span><span class="token highlighted"> growth</span><span class="token"> and</span><span class="token highlighted"> urbanization</span><span class="token highlighted">,</span><span class="token"> fundamentally</span><span class="token highlighted"> altering</span><span class="token"> social</span><span class="token"> structures</span><span class="token"> and</span><span class="token"> paving</span><span class="token"> the</span><span class="token"> way</span><span class="token"> for</span><span class="token"> the</span><span class="token"> modern</span><span class="token"> capitalist</span><span class="token"> system</span><span class="token">,</span><span class="token"> despite</span><span class="token"> also</span><span class="token"> causing</span><span class="token"> significant</span><span class="token"> social</span><span class="token"> inequalities</span><span class="token"> and</span><span class="token"> poor</span><span class="token"> working</span><span class="token"> conditions</span><span class="token"> initially</span><span class="token">.</span>',
|
| 116 |
+
"flowread_gradient_html": '<span class="token" style="opacity: 0.49; font-weight: 462;">The</span><span class="token" style="opacity: 0.59; font-weight: 524;"> Industrial</span><span class="token" style="opacity: 1.00; font-weight: 800;"> Revolution</span><span class="token" style="opacity: 1.00; font-weight: 800;">,</span><span class="token" style="opacity: 0.48; font-weight: 456;"> which</span><span class="token" style="opacity: 0.50; font-weight: 467;"> began</span><span class="token" style="opacity: 0.45; font-weight: 430;"> in</span><span class="token" style="opacity: 0.58; font-weight: 519;"> Britain</span><span class="token" style="opacity: 0.44; font-weight: 429;"> in</span><span class="token" style="opacity: 0.42; font-weight: 410;"> the</span><span class="token" style="opacity: 0.43; font-weight: 418;"> late</span><span class="token" style="opacity: 0.42; font-weight: 415;"> </span><span class="token" style="opacity: 0.49; font-weight: 457;">1</span><span class="token" style="opacity: 0.49; font-weight: 457;">8</span><span class="token" style="opacity: 0.49; font-weight: 457;">th</span><span class="token" style="opacity: 0.50; font-weight: 467;"> century</span><span class="token" style="opacity: 0.50; font-weight: 467;">,</span><span class="token" style="opacity: 0.49; font-weight: 460;"> marked</span><span class="token" style="opacity: 0.43; font-weight: 422;"> a</span><span class="token" style="opacity: 0.46; font-weight: 439;"> profound</span><span class="token" style="opacity: 0.43; font-weight: 418;"> turning</span><span class="token" style="opacity: 0.46; font-weight: 440;"> point</span><span class="token" style="opacity: 0.44; font-weight: 423;"> in</span><span class="token" style="opacity: 0.47; font-weight: 447;"> human</span><span class="token" style="opacity: 0.57; font-weight: 511;"> history</span><span class="token" style="opacity: 0.57; font-weight: 511;">.</span><span class="token" style="opacity: 0.48; font-weight: 456;"> It</span><span class="token" style="opacity: 0.48; font-weight: 450;"> initiated</span><span class="token" style="opacity: 0.45; font-weight: 431;"> the</span><span class="token" style="opacity: 0.44; font-weight: 426;"> transition</span><span class="token" style="opacity: 0.47; font-weight: 444;"> from</span><span class="token" style="opacity: 0.50; font-weight: 469;"> agrarian</span><span class="token" style="opacity: 0.50; font-weight: 469;">,</span><span class="token" style="opacity: 0.45; font-weight: 433;"> handic</span><span class="token" style="opacity: 0.45; font-weight: 433;">raft</span><span class="token" style="opacity: 0.48; font-weight: 451;"> economies</span><span class="token" style="opacity: 0.48; font-weight: 452;"> to</span><span class="token" style="opacity: 0.53; font-weight: 489;"> industry</span><span class="token" style="opacity: 0.45; font-weight: 432;"> and</span><span class="token" style="opacity: 0.47; font-weight: 444;"> machine</span><span class="token" style="opacity: 0.55; font-weight: 498;"> manufacturing</span><span class="token" style="opacity: 0.55; font-weight: 498;">.</span><span class="token" style="opacity: 0.49; font-weight: 461;"> The</span><span class="token" style="opacity: 0.46; font-weight: 442;"> invention</span><span class="token" style="opacity: 0.46; font-weight: 440;"> of</span><span class="token" style="opacity: 0.43; font-weight: 417;"> the</span><span class="token" style="opacity: 0.50; font-weight: 465;"> steam</span><span class="token" style="opacity: 0.52; font-weight: 483;"> engine</span><span class="token" style="opacity: 0.52; font-weight: 483;">,</span><span class="token" style="opacity: 0.45; font-weight: 434;"> pioneered</span><span class="token" style="opacity: 0.45; font-weight: 436;"> by</span><span class="token" style="opacity: 0.43; font-weight: 421;"> figures</span><span class="token" style="opacity: 0.44; font-weight: 426;"> like</span><span class="token" style="opacity: 0.42; font-weight: 414;"> James</span><span class="token" style="opacity: 0.44; font-weight: 428;"> Watt</span><span class="token" style="opacity: 0.44; font-weight: 428;">,</span><span class="token" style="opacity: 0.46; font-weight: 437;"> dramatically</span><span class="token" style="opacity: 0.47; font-weight: 445;"> increased</span><span class="token" style="opacity: 0.43; font-weight: 421;"> the</span><span class="token" style="opacity: 0.45; font-weight: 432;"> efficiency</span><span class="token" style="opacity: 0.43; font-weight: 421;"> of</span><span class="token" style="opacity: 0.50; font-weight: 467;"> factories</span><span class="token" style="opacity: 0.45; font-weight: 432;"> and</span><span class="token" style="opacity: 0.46; font-weight: 442;"> transportation</span><span class="token" style="opacity: 0.46; font-weight: 442;">,</span><span class="token" style="opacity: 0.47; font-weight: 446;"> revolution</span><span class="token" style="opacity: 0.47; font-weight: 446;">izing</span><span class="token" style="opacity: 0.42; font-weight: 415;"> the</span><span class="token" style="opacity: 0.45; font-weight: 436;"> textile</span><span class="token" style="opacity: 0.44; font-weight: 428;"> industry</span><span class="token" style="opacity: 0.45; font-weight: 433;"> and</span><span class="token" style="opacity: 0.43; font-weight: 416;"> leading</span><span class="token" style="opacity: 0.44; font-weight: 425;"> to</span><span class="token" style="opacity: 0.41; font-weight: 409;"> the</span><span class="token" style="opacity: 0.42; font-weight: 413;"> expansion</span><span class="token" style="opacity: 0.41; font-weight: 407;"> of</span><span class="token" style="opacity: 0.50; font-weight: 468;"> railways</span><span class="token" style="opacity: 0.50; font-weight: 468;">.</span><span class="token" style="opacity: 0.44; font-weight: 423;"> This</span><span class="token" style="opacity: 0.48; font-weight: 454;"> era</span><span class="token" style="opacity: 0.47; font-weight: 447;"> brought</span><span class="token" style="opacity: 0.43; font-weight: 418;"> about</span><span class="token" style="opacity: 0.45; font-weight: 434;"> unprecedented</span><span class="token" style="opacity: 0.46; font-weight: 438;"> economic</span><span class="token" style="opacity: 0.46; font-weight: 437;"> growth</span><span class="token" style="opacity: 0.45; font-weight: 431;"> and</span><span class="token" style="opacity: 0.47; font-weight: 449;"> urbanization</span><span class="token" style="opacity: 0.47; font-weight: 449;">,</span><span class="token" style="opacity: 0.44; font-weight: 424;"> fundamentally</span><span class="token" style="opacity: 0.46; font-weight: 441;"> altering</span><span class="token" style="opacity: 0.45; font-weight: 432;"> social</span><span class="token" style="opacity: 0.44; font-weight: 427;"> structures</span><span class="token" style="opacity: 0.45; font-weight: 432;"> and</span><span class="token" style="opacity: 0.43; font-weight: 419;"> paving</span><span class="token" style="opacity: 0.41; font-weight: 407;"> the</span><span class="token" style="opacity: 0.42; font-weight: 410;"> way</span><span class="token" style="opacity: 0.44; font-weight: 423;"> for</span><span class="token" style="opacity: 0.41; font-weight: 409;"> the</span><span class="token" style="opacity: 0.43; font-weight: 422;"> modern</span><span class="token" style="opacity: 0.44; font-weight: 429;"> capitalist</span><span class="token" style="opacity: 0.42; font-weight: 415;"> system</span><span class="token" style="opacity: 0.42; font-weight: 415;">,</span><span class="token" style="opacity: 0.45; font-weight: 432;"> despite</span><span class="token" style="opacity: 0.43; font-weight: 417;"> also</span><span class="token" style="opacity: 0.45; font-weight: 431;"> causing</span><span class="token" style="opacity: 0.43; font-weight: 419;"> significant</span><span class="token" style="opacity: 0.42; font-weight: 416;"> social</span><span class="token" style="opacity: 0.44; font-weight: 429;"> inequalities</span><span class="token" style="opacity: 0.42; font-weight: 415;"> and</span><span class="token" style="opacity: 0.42; font-weight: 414;"> poor</span><span class="token" style="opacity: 0.41; font-weight: 407;"> working</span><span class="token" style="opacity: 0.41; font-weight: 406;"> conditions</span><span class="token" style="opacity: 0.41; font-weight: 403;"> initially</span><span class="token" style="opacity: 0.41; font-weight: 403;">.</span>',
|
| 117 |
},
|
| 118 |
{
|
| 119 |
"id": 3,
|
|
|
|
| 122 |
"questions": [
|
| 123 |
{
|
| 124 |
"question": "What spectrum does the James Webb Space Telescope primarily operate in?",
|
| 125 |
+
"options": ["Ultraviolet", "X-ray", "Infrared", "Visible light"],
|
| 126 |
+
"correct": 2,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
},
|
| 128 |
{
|
| 129 |
"question": "Where does the telescope orbit to stay cold?",
|
|
|
|
| 131 |
"Low Earth Orbit",
|
| 132 |
"The Moon's orbit",
|
| 133 |
"The first Lagrange point",
|
| 134 |
+
"The second Lagrange point",
|
| 135 |
],
|
| 136 |
+
"correct": 3,
|
| 137 |
+
},
|
| 138 |
],
|
| 139 |
+
"flowread_html": '<span class="token highlighted">The</span><span class="token"> James</span><span class="token highlighted"> Webb</span><span class="token highlighted"> Space</span><span class="token highlighted"> Telescope</span><span class="token highlighted"> is</span><span class="token highlighted"> the</span><span class="token highlighted"> largest</span><span class="token"> and</span><span class="token"> most</span><span class="token highlighted"> powerful</span><span class="token highlighted"> space</span><span class="token highlighted"> telescope</span><span class="token"> ever</span><span class="token highlighted"> built</span><span class="token highlighted">.</span><span class="token highlighted"> Launched</span><span class="token"> in</span><span class="token"> </span><span class="token highlighted">2</span><span class="token highlighted">0</span><span class="token highlighted">2</span><span class="token highlighted">1</span><span class="token highlighted">,</span><span class="token highlighted"> it</span><span class="token highlighted"> operates</span><span class="token"> primarily</span><span class="token"> in</span><span class="token"> the</span><span class="token highlighted"> infrared</span><span class="token highlighted"> spectrum</span><span class="token highlighted">,</span><span class="token highlighted"> allowing</span><span class="token"> it</span><span class="token"> to</span><span class="token"> peer</span><span class="token"> through</span><span class="token"> dense</span><span class="token highlighted"> cosmic</span><span class="token"> dust</span><span class="token"> and</span><span class="token highlighted"> observe</span><span class="token"> the</span><span class="token highlighted"> universe</span><span class="token highlighted">\'</span><span class="token highlighted">s</span><span class="token"> most</span><span class="token"> distant</span><span class="token">,</span><span class="token"> early</span><span class="token highlighted"> galaxies</span><span class="token highlighted">.</span><span class="token highlighted"> Unlike</span><span class="token"> its</span><span class="token"> predecessor</span><span class="token">,</span><span class="token highlighted"> Hubble</span><span class="token highlighted">,</span><span class="token highlighted"> JW</span><span class="token highlighted">ST</span><span class="token highlighted"> orbits</span><span class="token"> the</span><span class="token highlighted"> Sun</span><span class="token highlighted"> at</span><span class="token"> the</span><span class="token"> second</span><span class="token highlighted"> Lagrange</span><span class="token highlighted"> point</span><span class="token highlighted">,</span><span class="token highlighted"> keeping</span><span class="token"> it</span><span class="token"> constantly</span><span class="token highlighted"> shielded</span><span class="token highlighted"> from</span><span class="token"> the</span><span class="token highlighted"> Sun</span><span class="token highlighted">\'</span><span class="token highlighted">s</span><span class="token"> heat</span><span class="token"> and</span><span class="token"> light</span><span class="token"> by</span><span class="token"> its</span><span class="token"> massive</span><span class="token highlighted"> sun</span><span class="token highlighted">shield</span><span class="token highlighted">.</span><span class="token"> This</span><span class="token"> incredibly</span><span class="token highlighted"> cold</span><span class="token"> environment</span><span class="token"> is</span><span class="token"> necessary</span><span class="token"> to</span><span class="token highlighted"> prevent</span><span class="token"> the</span><span class="token highlighted"> telescope</span><span class="token highlighted">\'</span><span class="token highlighted">s</span><span class="token"> own</span><span class="token"> infrared</span><span class="token"> emissions</span><span class="token"> from</span><span class="token"> interfering</span><span class="token"> with</span><span class="token"> its</span><span class="token"> highly</span><span class="token"> sensitive</span><span class="token"> observations</span><span class="token"> of</span><span class="token"> ex</span><span class="token">oplan</span><span class="token">et</span><span class="token"> atmospheres</span><span class="token"> and</span><span class="token"> star</span><span class="token"> formation</span><span class="token">.</span>',
|
| 140 |
+
"flowread_gradient_html": '<span class="token" style="opacity: 0.49; font-weight: 458;">The</span><span class="token" style="opacity: 0.45; font-weight: 435;"> James</span><span class="token" style="opacity: 1.00; font-weight: 800;"> Webb</span><span class="token" style="opacity: 0.59; font-weight: 523;"> Space</span><span class="token" style="opacity: 0.75; font-weight: 632;"> Telescope</span><span class="token" style="opacity: 0.52; font-weight: 478;"> is</span><span class="token" style="opacity: 0.47; font-weight: 447;"> the</span><span class="token" style="opacity: 0.50; font-weight: 463;"> largest</span><span class="token" style="opacity: 0.44; font-weight: 423;"> and</span><span class="token" style="opacity: 0.42; font-weight: 412;"> most</span><span class="token" style="opacity: 0.47; font-weight: 443;"> powerful</span><span class="token" style="opacity: 0.46; font-weight: 441;"> space</span><span class="token" style="opacity: 0.54; font-weight: 491;"> telescope</span><span class="token" style="opacity: 0.43; font-weight: 422;"> ever</span><span class="token" style="opacity: 0.56; font-weight: 503;"> built</span><span class="token" style="opacity: 0.56; font-weight: 503;">.</span><span class="token" style="opacity: 0.51; font-weight: 475;"> Launched</span><span class="token" style="opacity: 0.43; font-weight: 421;"> in</span><span class="token" style="opacity: 0.43; font-weight: 418;"> </span><span class="token" style="opacity: 0.50; font-weight: 463;">2</span><span class="token" style="opacity: 0.50; font-weight: 463;">0</span><span class="token" style="opacity: 0.50; font-weight: 463;">2</span><span class="token" style="opacity: 0.50; font-weight: 463;">1</span><span class="token" style="opacity: 0.50; font-weight: 463;">,</span><span class="token" style="opacity: 0.48; font-weight: 452;"> it</span><span class="token" style="opacity: 0.47; font-weight: 449;"> operates</span><span class="token" style="opacity: 0.45; font-weight: 433;"> primarily</span><span class="token" style="opacity: 0.44; font-weight: 429;"> in</span><span class="token" style="opacity: 0.43; font-weight: 418;"> the</span><span class="token" style="opacity: 0.57; font-weight: 510;"> infrared</span><span class="token" style="opacity: 0.47; font-weight: 446;"> spectrum</span><span class="token" style="opacity: 0.47; font-weight: 446;">,</span><span class="token" style="opacity: 0.48; font-weight: 453;"> allowing</span><span class="token" style="opacity: 0.43; font-weight: 420;"> it</span><span class="token" style="opacity: 0.44; font-weight: 423;"> to</span><span class="token" style="opacity: 0.46; font-weight: 437;"> peer</span><span class="token" style="opacity: 0.46; font-weight: 437;"> through</span><span class="token" style="opacity: 0.45; font-weight: 430;"> dense</span><span class="token" style="opacity: 0.46; font-weight: 441;"> cosmic</span><span class="token" style="opacity: 0.46; font-weight: 438;"> dust</span><span class="token" style="opacity: 0.45; font-weight: 431;"> and</span><span class="token" style="opacity: 0.49; font-weight: 458;"> observe</span><span class="token" style="opacity: 0.46; font-weight: 439;"> the</span><span class="token" style="opacity: 0.48; font-weight: 451;"> universe</span><span class="token" style="opacity: 0.48; font-weight: 451;">\'</span><span class="token" style="opacity: 0.48; font-weight: 451;">s</span><span class="token" style="opacity: 0.42; font-weight: 414;"> most</span><span class="token" style="opacity: 0.46; font-weight: 436;"> distant</span><span class="token" style="opacity: 0.46; font-weight: 436;">,</span><span class="token" style="opacity: 0.42; font-weight: 414;"> early</span><span class="token" style="opacity: 0.52; font-weight: 478;"> galaxies</span><span class="token" style="opacity: 0.52; font-weight: 478;">.</span><span class="token" style="opacity: 0.47; font-weight: 445;"> Unlike</span><span class="token" style="opacity: 0.43; font-weight: 419;"> its</span><span class="token" style="opacity: 0.46; font-weight: 437;"> predecessor</span><span class="token" style="opacity: 0.46; font-weight: 437;">,</span><span class="token" style="opacity: 0.50; font-weight: 467;"> Hubble</span><span class="token" style="opacity: 0.50; font-weight: 467;">,</span><span class="token" style="opacity: 0.50; font-weight: 468;"> JW</span><span class="token" style="opacity: 0.50; font-weight: 468;">ST</span><span class="token" style="opacity: 0.55; font-weight: 496;"> orbits</span><span class="token" style="opacity: 0.45; font-weight: 430;"> the</span><span class="token" style="opacity: 0.53; font-weight: 489;"> Sun</span><span class="token" style="opacity: 0.46; font-weight: 441;"> at</span><span class="token" style="opacity: 0.43; font-weight: 417;"> the</span><span class="token" style="opacity: 0.42; font-weight: 415;"> second</span><span class="token" style="opacity: 0.49; font-weight: 456;"> Lagrange</span><span class="token" style="opacity: 0.47; font-weight: 446;"> point</span><span class="token" style="opacity: 0.47; font-weight: 446;">,</span><span class="token" style="opacity: 0.47; font-weight: 444;"> keeping</span><span class="token" style="opacity: 0.45; font-weight: 431;"> it</span><span class="token" style="opacity: 0.44; font-weight: 428;"> constantly</span><span class="token" style="opacity: 0.49; font-weight: 457;"> shielded</span><span class="token" style="opacity: 0.47; font-weight: 445;"> from</span><span class="token" style="opacity: 0.43; font-weight: 419;"> the</span><span class="token" style="opacity: 0.47; font-weight: 446;"> Sun</span><span class="token" style="opacity: 0.47; font-weight: 446;">\'</span><span class="token" style="opacity: 0.47; font-weight: 446;">s</span><span class="token" style="opacity: 0.46; font-weight: 438;"> heat</span><span class="token" style="opacity: 0.44; font-weight: 423;"> and</span><span class="token" style="opacity: 0.43; font-weight: 420;"> light</span><span class="token" style="opacity: 0.44; font-weight: 429;"> by</span><span class="token" style="opacity: 0.43; font-weight: 421;"> its</span><span class="token" style="opacity: 0.43; font-weight: 418;"> massive</span><span class="token" style="opacity: 0.50; font-weight: 465;"> sun</span><span class="token" style="opacity: 0.50; font-weight: 465;">shield</span><span class="token" style="opacity: 0.50; font-weight: 465;">.</span><span class="token" style="opacity: 0.43; font-weight: 422;"> This</span><span class="token" style="opacity: 0.42; font-weight: 415;"> incredibly</span><span class="token" style="opacity: 0.46; font-weight: 440;"> cold</span><span class="token" style="opacity: 0.46; font-weight: 439;"> environment</span><span class="token" style="opacity: 0.42; font-weight: 414;"> is</span><span class="token" style="opacity: 0.43; font-weight: 422;"> necessary</span><span class="token" style="opacity: 0.45; font-weight: 431;"> to</span><span class="token" style="opacity: 0.47; font-weight: 446;"> prevent</span><span class="token" style="opacity: 0.43; font-weight: 420;"> the</span><span class="token" style="opacity: 0.47; font-weight: 445;"> telescope</span><span class="token" style="opacity: 0.47; font-weight: 445;">\'</span><span class="token" style="opacity: 0.47; font-weight: 445;">s</span><span class="token" style="opacity: 0.44; font-weight: 429;"> own</span><span class="token" style="opacity: 0.44; font-weight: 429;"> infrared</span><span class="token" style="opacity: 0.45; font-weight: 436;"> emissions</span><span class="token" style="opacity: 0.42; font-weight: 414;"> from</span><span class="token" style="opacity: 0.43; font-weight: 417;"> interfering</span><span class="token" style="opacity: 0.43; font-weight: 419;"> with</span><span class="token" style="opacity: 0.43; font-weight: 416;"> its</span><span class="token" style="opacity: 0.41; font-weight: 407;"> highly</span><span class="token" style="opacity: 0.43; font-weight: 417;"> sensitive</span><span class="token" style="opacity: 0.43; font-weight: 421;"> observations</span><span class="token" style="opacity: 0.44; font-weight: 429;"> of</span><span class="token" style="opacity: 0.45; font-weight: 430;"> ex</span><span class="token" style="opacity: 0.45; font-weight: 430;">oplan</span><span class="token" style="opacity: 0.45; font-weight: 430;">et</span><span class="token" style="opacity: 0.43; font-weight: 418;"> atmospheres</span><span class="token" style="opacity: 0.42; font-weight: 413;"> and</span><span class="token" style="opacity: 0.41; font-weight: 408;"> star</span><span class="token" style="opacity: 0.41; font-weight: 403;"> formation</span><span class="token" style="opacity: 0.41; font-weight: 403;">.</span>',
|
| 141 |
+
},
|
| 142 |
+
] # --- Study API Endpoints ---
|
| 143 |
+
|
| 144 |
+
|
| 145 |
@app.get("/api/study/texts")
|
| 146 |
def get_study_texts():
|
| 147 |
return {"texts": STUDY_TEXTS}
|
| 148 |
|
| 149 |
+
|
| 150 |
class StudySubmission(BaseModel):
|
| 151 |
user_id: str
|
| 152 |
text_id: int
|
|
|
|
| 155 |
score: int
|
| 156 |
total_questions: int
|
| 157 |
|
| 158 |
+
|
| 159 |
class StudyPreference(BaseModel):
|
| 160 |
user_id: str
|
| 161 |
preference: str
|
| 162 |
|
| 163 |
+
|
| 164 |
@app.post("/api/study/preference")
|
| 165 |
def submit_study_preference(submission: StudyPreference):
|
| 166 |
conn = sqlite3.connect(DB_FILE)
|
| 167 |
c = conn.cursor()
|
| 168 |
c.execute(
|
| 169 |
"INSERT INTO study_preferences (user_id, preference) VALUES (?, ?)",
|
| 170 |
+
(submission.user_id, submission.preference),
|
| 171 |
)
|
| 172 |
conn.commit()
|
| 173 |
conn.close()
|
| 174 |
return {"status": "success"}
|
| 175 |
|
| 176 |
+
|
| 177 |
@app.post("/api/study/submit")
|
| 178 |
def submit_study_result(submission: StudySubmission):
|
| 179 |
conn = sqlite3.connect(DB_FILE)
|
| 180 |
c = conn.cursor()
|
| 181 |
c.execute(
|
| 182 |
"INSERT INTO study_results (user_id, text_id, condition, reading_time_ms, score, total_questions) VALUES (?, ?, ?, ?, ?, ?)",
|
| 183 |
+
(
|
| 184 |
+
submission.user_id,
|
| 185 |
+
submission.text_id,
|
| 186 |
+
submission.condition,
|
| 187 |
+
submission.reading_time_ms,
|
| 188 |
+
submission.score,
|
| 189 |
+
submission.total_questions,
|
| 190 |
+
),
|
| 191 |
)
|
| 192 |
conn.commit()
|
| 193 |
conn.close()
|
| 194 |
return {"status": "success"}
|
| 195 |
|
| 196 |
+
|
| 197 |
@app.get("/api/study/stats")
|
| 198 |
def get_study_stats():
|
| 199 |
conn = sqlite3.connect(DB_FILE)
|
| 200 |
c = conn.cursor()
|
| 201 |
+
|
| 202 |
# Calculate stats for plain
|
| 203 |
+
c.execute(
|
| 204 |
+
"SELECT AVG(reading_time_ms), AVG(CAST(score AS FLOAT) / total_questions) * 100, COUNT(*) FROM study_results WHERE condition = 'plain'"
|
| 205 |
+
)
|
| 206 |
plain_stats = c.fetchone()
|
| 207 |
+
|
| 208 |
# Calculate stats for flowread
|
| 209 |
+
c.execute(
|
| 210 |
+
"SELECT AVG(reading_time_ms), AVG(CAST(score AS FLOAT) / total_questions) * 100, COUNT(*) FROM study_results WHERE condition = 'flowread'"
|
| 211 |
+
)
|
| 212 |
flowread_stats = c.fetchone()
|
| 213 |
+
|
| 214 |
# Calculate stats for gradient
|
| 215 |
+
c.execute(
|
| 216 |
+
"SELECT AVG(reading_time_ms), AVG(CAST(score AS FLOAT) / total_questions) * 100, COUNT(*) FROM study_results WHERE condition = 'gradient'"
|
| 217 |
+
)
|
| 218 |
gradient_stats = c.fetchone()
|
| 219 |
+
|
| 220 |
# Calculate preferences
|
| 221 |
c.execute("SELECT preference, COUNT(*) FROM study_preferences GROUP BY preference")
|
| 222 |
preferences = dict(c.fetchall())
|
| 223 |
+
|
| 224 |
conn.close()
|
| 225 |
+
|
| 226 |
return {
|
| 227 |
"plain": {
|
| 228 |
"avg_reading_time_ms": plain_stats[0] or 0,
|
| 229 |
"avg_accuracy_percent": plain_stats[1] or 0,
|
| 230 |
+
"sample_size": plain_stats[2],
|
| 231 |
},
|
| 232 |
"flowread": {
|
| 233 |
"avg_reading_time_ms": flowread_stats[0] or 0,
|
| 234 |
"avg_accuracy_percent": flowread_stats[1] or 0,
|
| 235 |
+
"sample_size": flowread_stats[2],
|
| 236 |
},
|
| 237 |
"gradient": {
|
| 238 |
"avg_reading_time_ms": gradient_stats[0] or 0,
|
| 239 |
"avg_accuracy_percent": gradient_stats[1] or 0,
|
| 240 |
+
"sample_size": gradient_stats[2],
|
| 241 |
},
|
| 242 |
+
"preferences": preferences,
|
| 243 |
}
|
| 244 |
|
| 245 |
+
|
| 246 |
import sys
|
| 247 |
import re
|
| 248 |
|
| 249 |
+
|
| 250 |
class StderrProgressInterceptor:
|
| 251 |
def __init__(self, original):
|
| 252 |
self.original = original
|
|
|
|
| 255 |
|
| 256 |
def write(self, s):
|
| 257 |
self.original.write(s)
|
| 258 |
+
match = re.search(r"(\d+)%\|", s)
|
| 259 |
if match and self.active_model:
|
| 260 |
pct = match.group(1)
|
| 261 |
self.current_progress = f"{pct}%"
|
| 262 |
# Update the global status explicitly so the API returns it immediately
|
| 263 |
model_status[self.active_model] = f"downloading: {self.current_progress}"
|
| 264 |
+
|
| 265 |
def flush(self):
|
| 266 |
self.original.flush()
|
| 267 |
|
| 268 |
+
|
| 269 |
stderr_interceptor = StderrProgressInterceptor(sys.stderr)
|
| 270 |
sys.stderr = stderr_interceptor
|
| 271 |
|
| 272 |
# --- Saliency API (Existing) ---
|
| 273 |
models = {}
|
| 274 |
tokenizers = {}
|
| 275 |
+
model_status = {"2b": "unloaded", "27b-4a": "unloaded"}
|
| 276 |
+
|
| 277 |
+
device = torch.device(
|
| 278 |
+
"cuda"
|
| 279 |
+
if torch.cuda.is_available()
|
| 280 |
+
else "mps"
|
| 281 |
+
if torch.backends.mps.is_available()
|
| 282 |
+
else "cpu"
|
| 283 |
+
)
|
| 284 |
hf_token = os.environ.get("HF_TOKEN")
|
| 285 |
|
| 286 |
+
|
| 287 |
def load_model(model_name: str):
|
| 288 |
if model_name in models:
|
| 289 |
return models[model_name], tokenizers[model_name]
|
| 290 |
+
|
| 291 |
print(f"Loading {model_name} on {device}...")
|
| 292 |
model_status[model_name] = "downloading: 0%"
|
| 293 |
stderr_interceptor.active_model = model_name
|
| 294 |
try:
|
| 295 |
if model_name == "27b-4a":
|
| 296 |
# Use Gemma 4 26B A4B in 4-bit (requires CUDA)
|
| 297 |
+
hf_model_id = "unsloth/gemma-4-26B-A4B-bnb-4bit"
|
| 298 |
+
try:
|
| 299 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 300 |
+
hf_model_id, token=hf_token
|
| 301 |
+
)
|
| 302 |
+
except Exception:
|
| 303 |
+
hf_model_id = "unsloth/gemma-4-26B-A4B"
|
| 304 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 305 |
+
hf_model_id, token=hf_token
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
# BitsAndBytes for 4-bit quantization
|
| 309 |
from transformers import BitsAndBytesConfig
|
| 310 |
+
|
| 311 |
bnb_config = BitsAndBytesConfig(
|
| 312 |
+
load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16
|
|
|
|
| 313 |
)
|
| 314 |
+
|
| 315 |
model = AutoModelForCausalLM.from_pretrained(
|
| 316 |
hf_model_id,
|
| 317 |
quantization_config=bnb_config,
|
| 318 |
device_map="auto",
|
| 319 |
attn_implementation="eager",
|
| 320 |
+
token=hf_token,
|
| 321 |
)
|
| 322 |
else:
|
| 323 |
# Default to 2b (Gemma 4 E2B)
|
| 324 |
hf_model_id = "unsloth/gemma-4-E2B"
|
| 325 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 326 |
+
hf_model_id, token=hf_token, extra_special_tokens={}
|
| 327 |
+
)
|
| 328 |
model = AutoModelForCausalLM.from_pretrained(
|
| 329 |
hf_model_id,
|
| 330 |
torch_dtype=torch.bfloat16,
|
| 331 |
attn_implementation="eager",
|
| 332 |
+
token=hf_token,
|
| 333 |
).to(device)
|
| 334 |
+
|
| 335 |
print(f"Model {model_name} loaded successfully.")
|
| 336 |
models[model_name] = model
|
| 337 |
tokenizers[model_name] = tokenizer
|
|
|
|
| 344 |
stderr_interceptor.active_model = None
|
| 345 |
raise e
|
| 346 |
|
| 347 |
+
|
| 348 |
# Pre-load default 2b
|
| 349 |
try:
|
| 350 |
load_model("2b")
|
| 351 |
except:
|
| 352 |
print("Could not preload 2b model.")
|
| 353 |
|
| 354 |
+
|
| 355 |
@app.get("/status")
|
| 356 |
def get_model_status():
|
| 357 |
return model_status
|
| 358 |
|
| 359 |
+
|
| 360 |
class TextRequest(BaseModel):
|
| 361 |
text: str
|
| 362 |
+
layers: Optional[List[int]] = None # Optional explicit layer selection
|
| 363 |
+
layer_preset: str = "middle" # "first", "middle", "last", "all"
|
| 364 |
+
preprompt: str = "" # Optional task-driven intent
|
| 365 |
+
saliency_mode: str = "local" # "local" or "global"
|
| 366 |
+
|
| 367 |
|
| 368 |
@app.post("/analyze")
|
| 369 |
def analyze_text_legacy(request: TextRequest):
|
| 370 |
return analyze_text_model("2b", request)
|
| 371 |
|
| 372 |
+
|
| 373 |
@app.post("/analyze/{model_name}")
|
| 374 |
def analyze_text_model(model_name: str, request: TextRequest):
|
| 375 |
text = request.text
|
| 376 |
preprompt = request.preprompt.strip()
|
| 377 |
+
|
| 378 |
if not text.strip():
|
| 379 |
return {"tokens": [], "scores": []}
|
| 380 |
|
|
|
|
| 382 |
model, tokenizer = load_model(model_name)
|
| 383 |
except Exception as e:
|
| 384 |
from fastapi import HTTPException
|
| 385 |
+
|
| 386 |
raise HTTPException(status_code=500, detail=str(e))
|
| 387 |
|
| 388 |
# Combine preprompt and text if preprompt exists
|
| 389 |
full_text = f"{preprompt}\n\n{text}" if preprompt else text
|
| 390 |
|
| 391 |
inputs = tokenizer(full_text, return_tensors="pt").to(model.device)
|
| 392 |
+
|
| 393 |
# Calculate how many tokens belong to the preprompt so we can strip them later
|
| 394 |
+
num_preprompt_tokens = 0
|
| 395 |
if preprompt:
|
| 396 |
p_toks = tokenizer(f"{preprompt}\n\n")["input_ids"]
|
| 397 |
num_preprompt_tokens = len(p_toks)
|
| 398 |
+
elif (
|
| 399 |
+
tokenizer.bos_token_id is not None
|
| 400 |
+
and len(inputs["input_ids"][0]) > 0
|
| 401 |
+
and inputs["input_ids"][0][0] == tokenizer.bos_token_id
|
| 402 |
+
):
|
| 403 |
+
num_preprompt_tokens = 1 # Just the bos token
|
| 404 |
|
| 405 |
with torch.no_grad():
|
| 406 |
# Ensure we ask the model to output attentions explicitly
|
| 407 |
outputs = model(**inputs, output_attentions=True)
|
| 408 |
+
|
| 409 |
# Check if attentions are actually returned
|
| 410 |
if not outputs.attentions:
|
| 411 |
print("Warning: Model did not return attentions.")
|
| 412 |
return {"words": []}
|
| 413 |
+
|
| 414 |
num_layers = len(outputs.attentions)
|
| 415 |
+
|
| 416 |
selected_layers = request.layers
|
| 417 |
if not selected_layers:
|
| 418 |
+
preset = request.layer_preset
|
| 419 |
+
if preset == "all":
|
| 420 |
+
selected_layers = list(range(num_layers))
|
| 421 |
+
elif preset == "first":
|
| 422 |
+
selected_layers = list(range(0, max(1, num_layers // 4)))
|
| 423 |
+
elif preset == "last":
|
| 424 |
+
selected_layers = list(range(num_layers - (num_layers // 4), num_layers))
|
| 425 |
+
else: # default to "middle"
|
| 426 |
+
start_layer = num_layers // 4
|
| 427 |
+
end_layer = num_layers - (num_layers // 4)
|
| 428 |
+
selected_layers = list(range(start_layer, end_layer))
|
| 429 |
+
|
| 430 |
selected_layers = [l for l in selected_layers if 0 <= l < num_layers]
|
| 431 |
if not selected_layers:
|
| 432 |
selected_layers = [num_layers - 1]
|
| 433 |
+
|
| 434 |
stacked_attentions = torch.stack([outputs.attentions[l] for l in selected_layers])
|
| 435 |
+
avg_attention = stacked_attentions.mean(dim=(0, 2))[0]
|
| 436 |
+
|
| 437 |
# Calculate importance: sum of attention each token *receives* from the sequence
|
| 438 |
importance = avg_attention.sum(dim=0).cpu().float().numpy()
|
| 439 |
+
|
| 440 |
import numpy as np
|
| 441 |
|
| 442 |
if len(importance) > num_preprompt_tokens:
|
| 443 |
text_importance = importance[num_preprompt_tokens:]
|
| 444 |
+
|
| 445 |
if request.saliency_mode == "global":
|
| 446 |
# Global Mode: absolute importance across different texts
|
| 447 |
# We apply a soft root penalty so very high values don't entirely blow out the scale,
|
|
|
|
| 452 |
# Local Mode: relative importance within this specific block of text
|
| 453 |
min_score = text_importance.min()
|
| 454 |
max_score = text_importance.max()
|
| 455 |
+
|
| 456 |
if max_score > min_score:
|
| 457 |
normalized_scores = (importance - min_score) / (max_score - min_score)
|
| 458 |
else:
|
| 459 |
normalized_scores = importance - min_score
|
| 460 |
+
|
| 461 |
# Keep <bos> at max score
|
| 462 |
normalized_scores[0] = 1.0
|
| 463 |
normalized_scores = normalized_scores.clip(0, 1)
|
| 464 |
else:
|
| 465 |
normalized_scores = [1.0] * len(importance)
|
| 466 |
+
|
| 467 |
input_ids = inputs["input_ids"][0].tolist()
|
| 468 |
tokens = tokenizer.convert_ids_to_tokens(input_ids)
|
| 469 |
+
|
| 470 |
+
# Check if first token is BOS
|
| 471 |
+
has_bos = (input_ids[0] == tokenizer.bos_token_id) if len(input_ids) > 0 else False
|
| 472 |
+
|
| 473 |
result = []
|
| 474 |
for i, t in enumerate(tokens):
|
| 475 |
# Decode properly
|
| 476 |
word = tokenizer.decode([input_ids[i]])
|
| 477 |
+
|
| 478 |
+
# Byte fallback handling: if the token is a byte fallback, decode the byte
|
| 479 |
+
if t.startswith("<0x") and t.endswith(">"):
|
| 480 |
+
# Just ignore printing these raw bytes out since they render ugly
|
| 481 |
+
# We'll rely on word or fallback to empty
|
| 482 |
+
word = ""
|
| 483 |
+
t = ""
|
| 484 |
+
|
| 485 |
# Special check for Gemma, decoding often removes spaces incorrectly or leaves tokens empty
|
| 486 |
# Let's clean the raw token just in case
|
| 487 |
+
raw_clean = t.replace("\u2581", " ")
|
| 488 |
+
|
| 489 |
# We will pass both decoded word and raw cleaned token to frontend to help render
|
| 490 |
+
result.append(
|
| 491 |
+
{"token": raw_clean, "word": word, "score": float(normalized_scores[i])}
|
| 492 |
+
)
|
| 493 |
+
|
| 494 |
+
# Return the actual text tokens. Keep <bos> if it exists.
|
| 495 |
+
if num_preprompt_tokens > 0 and len(result) > num_preprompt_tokens:
|
| 496 |
+
if has_bos:
|
| 497 |
+
result = [result[0]] + result[num_preprompt_tokens:]
|
| 498 |
+
else:
|
| 499 |
+
result = result[num_preprompt_tokens:]
|
| 500 |
|
| 501 |
return {"words": result}
|
| 502 |
|
| 503 |
+
|
| 504 |
if __name__ == "__main__":
|
| 505 |
port = int(os.environ.get("PORT", 7860))
|
| 506 |
+
uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True)
|
static/index.html
CHANGED
|
@@ -247,21 +247,18 @@
|
|
| 247 |
</div>
|
| 248 |
|
| 249 |
<div>
|
| 250 |
-
<label style="display:block; font-weight: 600; margin-bottom: 0.5rem; color: #44403c;">
|
| 251 |
-
Network Layers
|
| 252 |
</label>
|
| 253 |
<p style="font-size: 0.85rem; color: #78716c; margin-top: 0; margin-bottom: 0.5rem;">
|
| 254 |
-
Select which layers to
|
| 255 |
</p>
|
| 256 |
-
<
|
| 257 |
-
<
|
| 258 |
-
<
|
| 259 |
-
<
|
| 260 |
-
<
|
| 261 |
-
</
|
| 262 |
-
<div id="layer-checkboxes" style="display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;">
|
| 263 |
-
<!-- Checkboxes generated by JS -->
|
| 264 |
-
</div>
|
| 265 |
</div>
|
| 266 |
</details>
|
| 267 |
|
|
@@ -402,6 +399,7 @@
|
|
| 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');
|
|
@@ -410,48 +408,7 @@
|
|
| 410 |
const loading = document.getElementById('loading');
|
| 411 |
|
| 412 |
let currentTokens = [];
|
| 413 |
-
|
| 414 |
-
// Generate Layer Checkboxes
|
| 415 |
-
const layerContainer = document.getElementById('layer-checkboxes');
|
| 416 |
-
for(let i = 0; i < 18; i++) {
|
| 417 |
-
const label = document.createElement('label');
|
| 418 |
-
label.style.display = 'flex';
|
| 419 |
-
label.style.alignItems = 'center';
|
| 420 |
-
label.style.gap = '0.25rem';
|
| 421 |
-
label.style.fontSize = '0.85rem';
|
| 422 |
-
label.style.cursor = 'pointer';
|
| 423 |
-
|
| 424 |
-
const cb = document.createElement('input');
|
| 425 |
-
cb.type = 'checkbox';
|
| 426 |
-
cb.value = i;
|
| 427 |
-
cb.className = 'layer-cb';
|
| 428 |
-
|
| 429 |
-
label.appendChild(cb);
|
| 430 |
-
label.appendChild(document.createTextNode(`L${i}`));
|
| 431 |
-
layerContainer.appendChild(label);
|
| 432 |
-
}
|
| 433 |
-
|
| 434 |
-
// Presets Logic
|
| 435 |
-
window.setLayerPreset = function(preset) {
|
| 436 |
-
const checkboxes = document.querySelectorAll('.layer-cb');
|
| 437 |
-
checkboxes.forEach(cb => cb.checked = false);
|
| 438 |
-
|
| 439 |
-
if(preset === 'all') {
|
| 440 |
-
checkboxes.forEach(cb => cb.checked = true);
|
| 441 |
-
} else if (preset === 'first') {
|
| 442 |
-
checkboxes[0].checked = true;
|
| 443 |
-
} else if (preset === 'last') {
|
| 444 |
-
checkboxes[17].checked = true;
|
| 445 |
-
} else if (preset === 'middle') {
|
| 446 |
-
// Middle 50% for 18 layers is ~4 through 13
|
| 447 |
-
for(let i = 4; i <= 13; i++) {
|
| 448 |
-
checkboxes[i].checked = true;
|
| 449 |
-
}
|
| 450 |
-
}
|
| 451 |
-
};
|
| 452 |
-
// Set middle as default
|
| 453 |
-
setLayerPreset('middle');
|
| 454 |
-
|
| 455 |
thresholdSlider.addEventListener('input', (e) => {
|
| 456 |
thresholdVal.textContent = parseFloat(e.target.value).toFixed(2);
|
| 457 |
renderTokens();
|
|
@@ -463,15 +420,10 @@
|
|
| 463 |
const text = inputArea.value.trim();
|
| 464 |
const preprompt = prepromptInput.value.trim();
|
| 465 |
const saliencyMode = saliencyModeInput.value;
|
|
|
|
| 466 |
const modelVersion = "2b";
|
| 467 |
|
| 468 |
-
const checkedLayers = Array.from(document.querySelectorAll('.layer-cb:checked')).map(cb => parseInt(cb.value));
|
| 469 |
-
|
| 470 |
if (!text) return;
|
| 471 |
-
if (checkedLayers.length === 0) {
|
| 472 |
-
alert("Please select at least one layer!");
|
| 473 |
-
return;
|
| 474 |
-
}
|
| 475 |
|
| 476 |
analyzeBtn.disabled = true;
|
| 477 |
loading.style.display = 'block';
|
|
@@ -501,7 +453,7 @@
|
|
| 501 |
const response = await fetch(`/analyze/${modelVersion}`, {
|
| 502 |
method: 'POST',
|
| 503 |
headers: { 'Content-Type': 'application/json' },
|
| 504 |
-
body: JSON.stringify({ text, preprompt,
|
| 505 |
});
|
| 506 |
|
| 507 |
isFetching = false;
|
|
|
|
| 247 |
</div>
|
| 248 |
|
| 249 |
<div>
|
| 250 |
+
<label for="layer-preset" style="display:block; font-weight: 600; margin-bottom: 0.5rem; color: #44403c;">
|
| 251 |
+
Network Layers
|
| 252 |
</label>
|
| 253 |
<p style="font-size: 0.85rem; color: #78716c; margin-top: 0; margin-bottom: 0.5rem;">
|
| 254 |
+
Select which section of the network's layers to extract attention scores from. Middle layers generally capture the best semantic meaning.
|
| 255 |
</p>
|
| 256 |
+
<select id="layer-preset" 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;">
|
| 257 |
+
<option value="middle" selected>Middle Layers (Semantic focus)</option>
|
| 258 |
+
<option value="first">First Few Layers (Lexical/Syntax focus)</option>
|
| 259 |
+
<option value="last">Last Few Layers (Output formatting focus)</option>
|
| 260 |
+
<option value="all">All Layers (Averaged)</option>
|
| 261 |
+
</select>
|
|
|
|
|
|
|
|
|
|
| 262 |
</div>
|
| 263 |
</details>
|
| 264 |
|
|
|
|
| 399 |
const inputArea = document.getElementById('text-input');
|
| 400 |
const prepromptInput = document.getElementById('preprompt');
|
| 401 |
const saliencyModeInput = document.getElementById('saliency-mode');
|
| 402 |
+
const layerPresetInput = document.getElementById('layer-preset');
|
| 403 |
const analyzeBtn = document.getElementById('analyze-btn');
|
| 404 |
const thresholdSlider = document.getElementById('threshold');
|
| 405 |
const thresholdVal = document.getElementById('threshold-val');
|
|
|
|
| 408 |
const loading = document.getElementById('loading');
|
| 409 |
|
| 410 |
let currentTokens = [];
|
| 411 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
thresholdSlider.addEventListener('input', (e) => {
|
| 413 |
thresholdVal.textContent = parseFloat(e.target.value).toFixed(2);
|
| 414 |
renderTokens();
|
|
|
|
| 420 |
const text = inputArea.value.trim();
|
| 421 |
const preprompt = prepromptInput.value.trim();
|
| 422 |
const saliencyMode = saliencyModeInput.value;
|
| 423 |
+
const layerPreset = layerPresetInput.value;
|
| 424 |
const modelVersion = "2b";
|
| 425 |
|
|
|
|
|
|
|
| 426 |
if (!text) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
|
| 428 |
analyzeBtn.disabled = true;
|
| 429 |
loading.style.display = 'block';
|
|
|
|
| 453 |
const response = await fetch(`/analyze/${modelVersion}`, {
|
| 454 |
method: 'POST',
|
| 455 |
headers: { 'Content-Type': 'application/json' },
|
| 456 |
+
body: JSON.stringify({ text, preprompt, layer_preset: layerPreset, saliency_mode: saliencyMode })
|
| 457 |
});
|
| 458 |
|
| 459 |
isFetching = false;
|
test_bos.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from transformers import AutoTokenizer
|
| 2 |
+
tokenizer = AutoTokenizer.from_pretrained("unsloth/gemma-4-E2B", extra_special_tokens={})
|
| 3 |
+
print("add_bos_token:", tokenizer.add_bos_token)
|
| 4 |
+
print("tokens:", tokenizer("hello")["input_ids"])
|
test_gguf.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
model_id = "unsloth/gemma-4-26B-A4B-it-GGUF"
|
| 5 |
+
gguf_file = "gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf"
|
| 6 |
+
|
| 7 |
+
print("Loading tokenizer...")
|
| 8 |
+
tokenizer = AutoTokenizer.from_pretrained(model_id, gguf_file=gguf_file)
|
| 9 |
+
print("Loading model...")
|
| 10 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 11 |
+
model_id,
|
| 12 |
+
gguf_file=gguf_file,
|
| 13 |
+
device_map="auto"
|
| 14 |
+
)
|
| 15 |
+
print(f"Model loaded on device {model.device}, dtype: {model.dtype}")
|
| 16 |
+
|
| 17 |
+
inputs = tokenizer("Hello world", return_tensors="pt").to(model.device)
|
| 18 |
+
with torch.no_grad():
|
| 19 |
+
out = model(**inputs, output_attentions=True)
|
| 20 |
+
|
| 21 |
+
print("Attentions returned:", len(out.attentions) if out.attentions else "No")
|
test_load.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 2 |
+
try:
|
| 3 |
+
hf_model_id = "unsloth/gemma-4-E2B"
|
| 4 |
+
print("Loading tokenizer...")
|
| 5 |
+
tokenizer = AutoTokenizer.from_pretrained(hf_model_id)
|
| 6 |
+
print("Loading model...")
|
| 7 |
+
model = AutoModelForCausalLM.from_pretrained(hf_model_id)
|
| 8 |
+
except Exception as e:
|
| 9 |
+
import traceback
|
| 10 |
+
traceback.print_exc()
|
test_model.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import traceback
|
| 2 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 3 |
+
try:
|
| 4 |
+
print("Loading Tokenizer...")
|
| 5 |
+
AutoTokenizer.from_pretrained("unsloth/gemma-4-E2B")
|
| 6 |
+
print("Loading Model...")
|
| 7 |
+
AutoModelForCausalLM.from_pretrained("unsloth/gemma-4-E2B")
|
| 8 |
+
except Exception as e:
|
| 9 |
+
traceback.print_exc()
|
test_model2.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from transformers import AutoTokenizer
|
| 2 |
+
try:
|
| 3 |
+
print("Loading Tokenizer with empty dict override...")
|
| 4 |
+
AutoTokenizer.from_pretrained("unsloth/gemma-4-E2B", extra_special_tokens={})
|
| 5 |
+
print("Loaded!")
|
| 6 |
+
except Exception as e:
|
| 7 |
+
import traceback
|
| 8 |
+
traceback.print_exc()
|
test_tok_issue.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from transformers import AutoTokenizer
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
tokenizer = AutoTokenizer.from_pretrained("unsloth/gemma-4-E2B", extra_special_tokens={})
|
| 5 |
+
preprompt = "imagine"
|
| 6 |
+
text = "Japan's railways are the finest in the world.\xa0"
|
| 7 |
+
|
| 8 |
+
full_text = f"{preprompt}\n\n{text}" if preprompt else text
|
| 9 |
+
inputs = tokenizer(full_text, return_tensors="pt")
|
| 10 |
+
|
| 11 |
+
num_preprompt_tokens = 0
|
| 12 |
+
if preprompt:
|
| 13 |
+
p_toks = tokenizer(f"{preprompt}\n\n")["input_ids"]
|
| 14 |
+
num_preprompt_tokens = len(p_toks)
|
| 15 |
+
elif tokenizer.bos_token_id is not None and len(inputs["input_ids"][0]) > 0 and inputs["input_ids"][0][0] == tokenizer.bos_token_id:
|
| 16 |
+
num_preprompt_tokens = 1
|
| 17 |
+
|
| 18 |
+
input_ids = inputs["input_ids"][0].tolist()
|
| 19 |
+
tokens = tokenizer.convert_ids_to_tokens(input_ids)
|
| 20 |
+
has_bos = (input_ids[0] == tokenizer.bos_token_id) if len(input_ids) > 0 else False
|
| 21 |
+
|
| 22 |
+
normalized_scores = np.ones(len(tokens))
|
| 23 |
+
|
| 24 |
+
result = []
|
| 25 |
+
for i, t in enumerate(tokens):
|
| 26 |
+
word = tokenizer.decode([input_ids[i]])
|
| 27 |
+
if t.startswith('<0x') and t.endswith('>'):
|
| 28 |
+
word = ""
|
| 29 |
+
t = ""
|
| 30 |
+
raw_clean = t.replace('\u2581', ' ')
|
| 31 |
+
result.append({"token": raw_clean, "word": word})
|
| 32 |
+
|
| 33 |
+
if num_preprompt_tokens > 0 and len(result) > num_preprompt_tokens:
|
| 34 |
+
if has_bos:
|
| 35 |
+
result = [result[0]] + result[num_preprompt_tokens:]
|
| 36 |
+
else:
|
| 37 |
+
result = result[num_preprompt_tokens:]
|
| 38 |
+
|
| 39 |
+
print([r["token"] for r in result])
|