Spaces:
Running
Running
Load the decision fine-tune (ONNX served from the Space), trained prompt format and scale, Egg check tab
Browse files- .gitattributes +5 -0
- README.md +68 -65
- index.html +2 -3
- models/egemma2-decide/onnx/model_q4.onnx +3 -0
- models/egemma2-decide/onnx/model_q4.onnx_data +3 -0
- models/egemma2-decide/onnx/model_q4f16.onnx +3 -0
- models/egemma2-decide/onnx/model_q4f16.onnx_data +3 -0
- models/egemma2-decide/onnx/vision_encoder_q4.onnx +3 -0
- models/egemma2-decide/onnx/vision_encoder_q4.onnx_data +3 -0
- models/egemma2-decide/onnx/vision_encoder_q4f16.onnx +3 -0
- models/egemma2-decide/onnx/vision_encoder_q4f16.onnx_data +3 -0
- models/egemma2-decide/preprocessor_config.json +22 -0
- models/egemma2-decide/processor_config.json +78 -0
- models/egemma2-decide/tokenizer.json +3 -0
- models/egemma2-decide/tokenizer_config.json +57 -0
- style.css +14 -0
- worker.js +211 -200
.gitattributes
CHANGED
|
@@ -42,3 +42,8 @@ examples/textvqa-mc.jpg filter=lfs diff=lfs merge=lfs -text
|
|
| 42 |
examples/vqav2-yesno.jpg filter=lfs diff=lfs merge=lfs -text
|
| 43 |
models/egemma2-decide/onnx/audio_encoder_q4.onnx_data filter=lfs diff=lfs merge=lfs -text
|
| 44 |
models/egemma2-decide/onnx/audio_encoder_q4f16.onnx_data filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
examples/vqav2-yesno.jpg filter=lfs diff=lfs merge=lfs -text
|
| 43 |
models/egemma2-decide/onnx/audio_encoder_q4.onnx_data filter=lfs diff=lfs merge=lfs -text
|
| 44 |
models/egemma2-decide/onnx/audio_encoder_q4f16.onnx_data filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
models/egemma2-decide/onnx/model_q4.onnx_data filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
models/egemma2-decide/onnx/model_q4f16.onnx_data filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
models/egemma2-decide/onnx/vision_encoder_q4.onnx_data filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
models/egemma2-decide/onnx/vision_encoder_q4f16.onnx_data filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
models/egemma2-decide/tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,65 +1,68 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Decide Live
|
| 3 |
-
emoji: 🎯
|
| 4 |
-
colorFrom: green
|
| 5 |
-
colorTo: gray
|
| 6 |
-
sdk: static
|
| 7 |
-
pinned: false
|
| 8 |
-
license: apache-2.0
|
| 9 |
-
short_description: On-device decisions from camera and microphone
|
| 10 |
-
models:
|
| 11 |
-
- google/embeddinggemma-2
|
| 12 |
-
- FINAL-Bench/EmbeddingGemma2-ZTC-ONNX
|
| 13 |
-
---
|
| 14 |
-
|
| 15 |
-
# Decide Live: on-device decisions from camera and microphone
|
| 16 |
-
|
| 17 |
-
A static page that makes zero-token decisions in the browser. A multimodal embedding model,
|
| 18 |
-
[EmbeddingGemma 2](https://huggingface.co/google/embeddinggemma-2) (Apache-2.0) fine-tuned for decisions,
|
| 19 |
-
runs locally with [transformers.js](https://github.com/huggingface/transformers.js) 4.3.1 on WebGPU,
|
| 20 |
-
or on the CPU (WASM) when WebGPU is not available (
|
| 21 |
-
|
| 22 |
-
The ONNX files (from the private model repo `FINAL-Bench/EmbeddingGemma2-ZTC-ONNX`, same file layout as
|
| 23 |
-
[onnx-community/embeddinggemma-2-ONNX](https://huggingface.co/onnx-community/embeddinggemma-2-ONNX)) are served
|
| 24 |
-
from this Space under `models/egemma2-decide/`, so the page needs no access to any other repository.
|
| 25 |
-
There is no server and no API call per decision. The weights are downloaded once and cached by the browser.
|
| 26 |
-
Camera frames, microphone audio, images and text are processed on the device and are never uploaded.
|
| 27 |
-
|
| 28 |
-
## What it does
|
| 29 |
-
|
| 30 |
-
- **Live**: embeds camera frames (or a demo feed made from the sample images) and scores up to 4 decision
|
| 31 |
-
cards. By default each card embeds the frame together with its own question, in the trained format
|
| 32 |
-
(one pass per card). The "frame alone" setting embeds the frame once for all cards (faster, not the
|
| 33 |
-
trained format). Each card shows probability bars, the current decision, and a confidence gate.
|
| 34 |
-
- **Microphone / sound file**: the same cards for 2 second sound windows (16 kHz mono). Windows
|
| 35 |
-
quieter than -55 dBFS are not sent to the model. The audio encoder is the unchanged base one.
|
| 36 |
-
- **Single input**: one image, text, audio clip or short video, a question, an optional context,
|
| 37 |
-
and 2 to 8 options written as `key: description` (or a yes / no question).
|
| 38 |
-
- **Egg check**: 8 held-out candling photos (not used in training) and your own photo, decided with
|
| 39 |
-
the trained egg prompt.
|
| 40 |
-
- **Sample tests**: 18 image tests with gold answers, run in the browser with an accuracy report,
|
| 41 |
-
plus a comparison of prompt formats.
|
| 42 |
-
|
| 43 |
-
## How a decision is computed (trained format)
|
| 44 |
-
|
| 45 |
-
1. Query: the media placeholders first, then `task: classification | query: {question}`, then
|
| 46 |
-
`\nContext: {context}` when a context is given. The model embeds media and text together.
|
| 47 |
-
2. Each option: `title: none | text: {description}`, embedded once and cached.
|
| 48 |
-
3. Embedding: mean pooling over the attention mask, then L2 normalization (768 numbers).
|
| 49 |
-
4. P(option) = softmax(scale * cosine). The scale was learned in training: 19.96
|
| 50 |
-
(temperature 1 / scale, about 0.0501), stored in `models/egemma2-decide/decision.json`.
|
| 51 |
-
|
| 52 |
-
Yes / no questions keep the question in the query and score the options `Yes` and `No`.
|
| 53 |
-
The older prompt formats of the first version of this page remain selectable for comparison.
|
| 54 |
-
|
| 55 |
-
## Files
|
| 56 |
-
|
| 57 |
-
- `index.html`, `style.css`, `app.js`: the page.
|
| 58 |
-
- `worker.js`: loads the model in a Web Worker and returns embeddings.
|
| 59 |
-
- `models/egemma2-decide/`: ONNX model
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Decide Live
|
| 3 |
+
emoji: 🎯
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: gray
|
| 6 |
+
sdk: static
|
| 7 |
+
pinned: false
|
| 8 |
+
license: apache-2.0
|
| 9 |
+
short_description: On-device decisions from camera and microphone
|
| 10 |
+
models:
|
| 11 |
+
- google/embeddinggemma-2
|
| 12 |
+
- FINAL-Bench/EmbeddingGemma2-ZTC-ONNX
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
# Decide Live: on-device decisions from camera and microphone
|
| 16 |
+
|
| 17 |
+
A static page that makes zero-token decisions in the browser. A multimodal embedding model,
|
| 18 |
+
[EmbeddingGemma 2](https://huggingface.co/google/embeddinggemma-2) (Apache-2.0) fine-tuned for decisions,
|
| 19 |
+
runs locally with [transformers.js](https://github.com/huggingface/transformers.js) 4.3.1 on WebGPU,
|
| 20 |
+
or on the CPU (WASM) when WebGPU is not available (much slower).
|
| 21 |
+
|
| 22 |
+
The ONNX files (from the private model repo `FINAL-Bench/EmbeddingGemma2-ZTC-ONNX`, same file layout as
|
| 23 |
+
[onnx-community/embeddinggemma-2-ONNX](https://huggingface.co/onnx-community/embeddinggemma-2-ONNX)) are served
|
| 24 |
+
from this Space under `models/egemma2-decide/`, so the page needs no access to any other repository.
|
| 25 |
+
There is no server and no API call per decision. The weights are downloaded once and cached by the browser.
|
| 26 |
+
Camera frames, microphone audio, images and text are processed on the device and are never uploaded.
|
| 27 |
+
|
| 28 |
+
## What it does
|
| 29 |
+
|
| 30 |
+
- **Live**: embeds camera frames (or a demo feed made from the sample images) and scores up to 4 decision
|
| 31 |
+
cards. By default each card embeds the frame together with its own question, in the trained format
|
| 32 |
+
(one pass per card). The "frame alone" setting embeds the frame once for all cards (faster, not the
|
| 33 |
+
trained format). Each card shows probability bars, the current decision, and a confidence gate.
|
| 34 |
+
- **Microphone / sound file**: the same cards for 2 second sound windows (16 kHz mono). Windows
|
| 35 |
+
quieter than -55 dBFS are not sent to the model. The audio encoder is the unchanged base one.
|
| 36 |
+
- **Single input**: one image, text, audio clip or short video, a question, an optional context,
|
| 37 |
+
and 2 to 8 options written as `key: description` (or a yes / no question).
|
| 38 |
+
- **Egg check**: 8 held-out candling photos (not used in training) and your own photo, decided with
|
| 39 |
+
the trained egg prompt.
|
| 40 |
+
- **Sample tests**: 18 image tests with gold answers, run in the browser with an accuracy report,
|
| 41 |
+
plus a comparison of prompt formats.
|
| 42 |
+
|
| 43 |
+
## How a decision is computed (trained format)
|
| 44 |
+
|
| 45 |
+
1. Query: the media placeholders first, then `task: classification | query: {question}`, then
|
| 46 |
+
`\nContext: {context}` when a context is given. The model embeds media and text together.
|
| 47 |
+
2. Each option: `title: none | text: {description}`, embedded once and cached.
|
| 48 |
+
3. Embedding: mean pooling over the attention mask, then L2 normalization (768 numbers).
|
| 49 |
+
4. P(option) = softmax(scale * cosine). The scale was learned in training: 19.96
|
| 50 |
+
(temperature 1 / scale, about 0.0501), stored in `models/egemma2-decide/decision.json`.
|
| 51 |
+
|
| 52 |
+
Yes / no questions keep the question in the query and score the options `Yes` and `No`.
|
| 53 |
+
The older prompt formats of the first version of this page remain selectable for comparison.
|
| 54 |
+
|
| 55 |
+
## Files
|
| 56 |
+
|
| 57 |
+
- `index.html`, `style.css`, `app.js`: the page.
|
| 58 |
+
- `worker.js`: loads the model in a Web Worker and returns embeddings.
|
| 59 |
+
- `models/egemma2-decide/`: ONNX model, config, processor and tokenizer files, and `decision.json`
|
| 60 |
+
(scale and prompt formats). Text and vision graphs in `q4f16` (as in the model repo) and in a portable
|
| 61 |
+
`q4` (the model repo's `onnx_portable/` files: 4-bit weights, with plain 8-bit gathers for the embedding
|
| 62 |
+
and position tables, so the same files also run on the WASM runtime, which lacks GatherBlockQuantized),
|
| 63 |
+
plus the 4-bit audio encoder. fp16 and fp32 files are not copied here because Space storage is limited to 1 GB.
|
| 64 |
+
- `examples/`: sample images and `samples.json` (name, files, state, type, question, options, gold);
|
| 65 |
+
`examples/egg-test/`: held-out egg candling photos.
|
| 66 |
+
- `CREDITS.md`: sources and licenses of the sample images.
|
| 67 |
+
|
| 68 |
+
Code: Apache-2.0. Model: derived from google/embeddinggemma-2 (Apache-2.0).
|
index.html
CHANGED
|
@@ -184,9 +184,8 @@
|
|
| 184 |
<label class="col">Precision (applies on next load)
|
| 185 |
<select id="dtype">
|
| 186 |
<option value="auto">Auto: 4-bit (q4f16 on WebGPU with fp16 support, else q4)</option>
|
| 187 |
-
<option value="q4">q4 (4-bit, works everywhere)</option>
|
| 188 |
<option value="q4f16">q4f16 (4-bit weights, fp16 math; WebGPU with fp16 only)</option>
|
| 189 |
-
<option value="fp16">fp16 (largest; WebGPU only)</option>
|
| 190 |
</select>
|
| 191 |
</label>
|
| 192 |
<label class="col">Backend (applies on next load)
|
|
@@ -270,7 +269,7 @@
|
|
| 270 |
<ul>
|
| 271 |
<li>An embedding model compares meanings; it does not read, count or reason step by step. Questions about exact counts, small text or negation are hard for it.</li>
|
| 272 |
<li>Probabilities depend on the temperature. They are comparable between options of one card, not calibrated against ground truth.</li>
|
| 273 |
-
<li>WebGPU needs a recent Chrome, Edge or Safari. Without it the page falls back to the CPU (WASM): it works
|
| 274 |
<li>The fine-tuning targeted image decisions (including egg candling). The audio encoder is the unchanged base one, so sound decisions are not tuned.</li>
|
| 275 |
</ul>
|
| 276 |
<p class="note">Sample image credits: <a href="./CREDITS.md" target="_blank">CREDITS.md</a>. Code: Apache-2.0.</p>
|
|
|
|
| 184 |
<label class="col">Precision (applies on next load)
|
| 185 |
<select id="dtype">
|
| 186 |
<option value="auto">Auto: 4-bit (q4f16 on WebGPU with fp16 support, else q4)</option>
|
| 187 |
+
<option value="q4">q4 (4-bit, works everywhere, also on the CPU)</option>
|
| 188 |
<option value="q4f16">q4f16 (4-bit weights, fp16 math; WebGPU with fp16 only)</option>
|
|
|
|
| 189 |
</select>
|
| 190 |
</label>
|
| 191 |
<label class="col">Backend (applies on next load)
|
|
|
|
| 269 |
<ul>
|
| 270 |
<li>An embedding model compares meanings; it does not read, count or reason step by step. Questions about exact counts, small text or negation are hard for it.</li>
|
| 271 |
<li>Probabilities depend on the temperature. They are comparable between options of one card, not calibrated against ground truth.</li>
|
| 272 |
+
<li>WebGPU needs a recent Chrome, Edge or Safari. Without it the page falls back to the CPU (WASM): it works with the same 4-bit files (about 340 MB), but it takes several seconds per image (about 6 s at 70 image tokens, about 30 s at 280 in our test), so the live rate drops to a frame every few seconds.</li>
|
| 273 |
<li>The fine-tuning targeted image decisions (including egg candling). The audio encoder is the unchanged base one, so sound decisions are not tuned.</li>
|
| 274 |
</ul>
|
| 275 |
<p class="note">Sample image credits: <a href="./CREDITS.md" target="_blank">CREDITS.md</a>. Code: Apache-2.0.</p>
|
models/egemma2-decide/onnx/model_q4.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:eb6b3c8cae2c7a65272c6211313f34f70941df1d40869d4021be22ec27aabdee
|
| 3 |
+
size 490555
|
models/egemma2-decide/onnx/model_q4.onnx_data
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0bdc17b2daccb303a367a306f914d9de3781ba58ac1d2c44f3ba7d0a626851e5
|
| 3 |
+
size 224360448
|
models/egemma2-decide/onnx/model_q4f16.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:53feeced79582d661e30adeaa9829cea90d92b1c78347e26fd123773580f71d0
|
| 3 |
+
size 495298
|
models/egemma2-decide/onnx/model_q4f16.onnx_data
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0e293b62bf40b405a9f0e88985dc4a25a5bb05572ca314da4baad9400be3eae8
|
| 3 |
+
size 156862464
|
models/egemma2-decide/onnx/vision_encoder_q4.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:daea777fe94c02d19749c286fdd0f52d2cea56697a0f093c9b7f4652e9646a75
|
| 3 |
+
size 164811
|
models/egemma2-decide/onnx/vision_encoder_q4.onnx_data
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6823b98325ae0b4e8108fb28f995f9dfb5496b43c8432c65c8c7d881eeb39fb3
|
| 3 |
+
size 114774016
|
models/egemma2-decide/onnx/vision_encoder_q4f16.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:572258f9700e2a23edaa478a717d4da29618206e24ccd4fb34c822181ab49e27
|
| 3 |
+
size 152866
|
models/egemma2-decide/onnx/vision_encoder_q4f16.onnx_data
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:27a86792e1d3572b4667cf09cb59165e54a18e3e9bc59bf22f2c35b845ad052a
|
| 3 |
+
size 97614336
|
models/egemma2-decide/preprocessor_config.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"dither": 0.0,
|
| 3 |
+
"feature_extractor_type": "Gemma4AudioFeatureExtractor",
|
| 4 |
+
"feature_size": 128,
|
| 5 |
+
"fft_length": 512,
|
| 6 |
+
"fft_overdrive": false,
|
| 7 |
+
"frame_length": 320,
|
| 8 |
+
"hop_length": 160,
|
| 9 |
+
"input_scale_factor": 1.0,
|
| 10 |
+
"max_frequency": 8000.0,
|
| 11 |
+
"mel_floor": 0.001,
|
| 12 |
+
"min_frequency": 0.0,
|
| 13 |
+
"padding_side": "right",
|
| 14 |
+
"padding_value": 0.0,
|
| 15 |
+
"per_bin_mean": null,
|
| 16 |
+
"per_bin_stddev": null,
|
| 17 |
+
"preemphasis": 0.0,
|
| 18 |
+
"preemphasis_htk_flavor": true,
|
| 19 |
+
"return_attention_mask": true,
|
| 20 |
+
"sampling_rate": 16000,
|
| 21 |
+
"processor_class": "EmbeddingGemma2Processor"
|
| 22 |
+
}
|
models/egemma2-decide/processor_config.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"audio_ms_per_token": 40,
|
| 3 |
+
"audio_seq_length": 280,
|
| 4 |
+
"feature_extractor": {
|
| 5 |
+
"dither": 0.0,
|
| 6 |
+
"feature_extractor_type": "Gemma4AudioFeatureExtractor",
|
| 7 |
+
"feature_size": 128,
|
| 8 |
+
"fft_length": 512,
|
| 9 |
+
"fft_overdrive": false,
|
| 10 |
+
"frame_length": 320,
|
| 11 |
+
"hop_length": 160,
|
| 12 |
+
"input_scale_factor": 1.0,
|
| 13 |
+
"max_frequency": 8000.0,
|
| 14 |
+
"mel_floor": 0.001,
|
| 15 |
+
"min_frequency": 0.0,
|
| 16 |
+
"padding_side": "right",
|
| 17 |
+
"padding_value": 0.0,
|
| 18 |
+
"per_bin_mean": null,
|
| 19 |
+
"per_bin_stddev": null,
|
| 20 |
+
"preemphasis": 0.0,
|
| 21 |
+
"preemphasis_htk_flavor": true,
|
| 22 |
+
"return_attention_mask": true,
|
| 23 |
+
"sampling_rate": 16000
|
| 24 |
+
},
|
| 25 |
+
"image_processor": {
|
| 26 |
+
"do_convert_rgb": true,
|
| 27 |
+
"do_normalize": false,
|
| 28 |
+
"do_rescale": true,
|
| 29 |
+
"do_resize": true,
|
| 30 |
+
"image_mean": [
|
| 31 |
+
0.0,
|
| 32 |
+
0.0,
|
| 33 |
+
0.0
|
| 34 |
+
],
|
| 35 |
+
"image_processor_type": "Gemma4ImageProcessor",
|
| 36 |
+
"image_seq_length": 280,
|
| 37 |
+
"image_std": [
|
| 38 |
+
1.0,
|
| 39 |
+
1.0,
|
| 40 |
+
1.0
|
| 41 |
+
],
|
| 42 |
+
"max_soft_tokens": 280,
|
| 43 |
+
"patch_size": 16,
|
| 44 |
+
"pooling_kernel_size": 3,
|
| 45 |
+
"resample": 3,
|
| 46 |
+
"rescale_factor": 0.00392156862745098
|
| 47 |
+
},
|
| 48 |
+
"image_seq_length": 280,
|
| 49 |
+
"processor_class": "EmbeddingGemma2Processor",
|
| 50 |
+
"video_processor": {
|
| 51 |
+
"add_timestamps": false,
|
| 52 |
+
"do_convert_rgb": true,
|
| 53 |
+
"do_normalize": false,
|
| 54 |
+
"do_rescale": true,
|
| 55 |
+
"do_resize": true,
|
| 56 |
+
"do_sample_frames": true,
|
| 57 |
+
"fps": 1,
|
| 58 |
+
"image_mean": [
|
| 59 |
+
0.0,
|
| 60 |
+
0.0,
|
| 61 |
+
0.0
|
| 62 |
+
],
|
| 63 |
+
"image_std": [
|
| 64 |
+
1.0,
|
| 65 |
+
1.0,
|
| 66 |
+
1.0
|
| 67 |
+
],
|
| 68 |
+
"max_frames": 32,
|
| 69 |
+
"max_soft_tokens": 140,
|
| 70 |
+
"overflow_strategy": "uniform",
|
| 71 |
+
"patch_size": 16,
|
| 72 |
+
"pooling_kernel_size": 3,
|
| 73 |
+
"resample": 3,
|
| 74 |
+
"rescale_factor": 0.00392156862745098,
|
| 75 |
+
"return_metadata": false,
|
| 76 |
+
"video_processor_type": "EmbeddingGemma2VideoProcessor"
|
| 77 |
+
}
|
| 78 |
+
}
|
models/egemma2-decide/tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4d777ef5bdc1aa36227abdfb77c3e49e7b9c892d16e1b6bda41c393504828be4
|
| 3 |
+
size 32170510
|
models/egemma2-decide/tokenizer_config.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"audio_token": "<|audio|>",
|
| 3 |
+
"backend": "tokenizers",
|
| 4 |
+
"boa_token": "<|audio>",
|
| 5 |
+
"boi_token": "<|image>",
|
| 6 |
+
"bos_token": "<bos>",
|
| 7 |
+
"eoa_token": "<audio|>",
|
| 8 |
+
"eoc_token": "<channel|>",
|
| 9 |
+
"eoi_token": "<image|>",
|
| 10 |
+
"eos_token": "<eos>",
|
| 11 |
+
"eot_token": "<turn|>",
|
| 12 |
+
"escape_token": "<|\"|>",
|
| 13 |
+
"etc_token": "<tool_call|>",
|
| 14 |
+
"etd_token": "<tool|>",
|
| 15 |
+
"etr_token": "<tool_response|>",
|
| 16 |
+
"extra_special_tokens": [
|
| 17 |
+
"<|video|>"
|
| 18 |
+
],
|
| 19 |
+
"image_token": "<|image|>",
|
| 20 |
+
"is_local": true,
|
| 21 |
+
"local_files_only": false,
|
| 22 |
+
"mask_token": "<mask>",
|
| 23 |
+
"model_max_length": 1000000000000000019884624838656,
|
| 24 |
+
"model_specific_special_tokens": {
|
| 25 |
+
"audio_token": "<|audio|>",
|
| 26 |
+
"boa_token": "<|audio>",
|
| 27 |
+
"boi_token": "<|image>",
|
| 28 |
+
"eoa_token": "<audio|>",
|
| 29 |
+
"eoc_token": "<channel|>",
|
| 30 |
+
"eoi_token": "<image|>",
|
| 31 |
+
"eot_token": "<turn|>",
|
| 32 |
+
"escape_token": "<|\"|>",
|
| 33 |
+
"etc_token": "<tool_call|>",
|
| 34 |
+
"etd_token": "<tool|>",
|
| 35 |
+
"etr_token": "<tool_response|>",
|
| 36 |
+
"image_token": "<|image|>",
|
| 37 |
+
"soc_token": "<|channel>",
|
| 38 |
+
"sot_token": "<|turn>",
|
| 39 |
+
"stc_token": "<|tool_call>",
|
| 40 |
+
"std_token": "<|tool>",
|
| 41 |
+
"str_token": "<|tool_response>",
|
| 42 |
+
"think_token": "<|think|>",
|
| 43 |
+
"video_token": "<|video|>"
|
| 44 |
+
},
|
| 45 |
+
"pad_token": "<pad>",
|
| 46 |
+
"padding_side": "right",
|
| 47 |
+
"processor_class": "EmbeddingGemma2Processor",
|
| 48 |
+
"soc_token": "<|channel>",
|
| 49 |
+
"sot_token": "<|turn>",
|
| 50 |
+
"stc_token": "<|tool_call>",
|
| 51 |
+
"std_token": "<|tool>",
|
| 52 |
+
"str_token": "<|tool_response>",
|
| 53 |
+
"think_token": "<|think|>",
|
| 54 |
+
"tokenizer_class": "GemmaTokenizer",
|
| 55 |
+
"unk_token": "<unk>",
|
| 56 |
+
"video_token": "<|video|>"
|
| 57 |
+
}
|
style.css
CHANGED
|
@@ -180,3 +180,17 @@ td.no { color: var(--bad); font-weight: 700; }
|
|
| 180 |
.hud { top: 44px; left: 8px; right: auto; font-size: 11.5px; }
|
| 181 |
.overlay { left: 8px; right: 8px; bottom: 8px; gap: 10px; padding: 8px 10px; }
|
| 182 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
.hud { top: 44px; left: 8px; right: auto; font-size: 11.5px; }
|
| 181 |
.overlay { left: 8px; right: 8px; bottom: 8px; gap: 10px; padding: 8px 10px; }
|
| 182 |
}
|
| 183 |
+
|
| 184 |
+
/* egg check */
|
| 185 |
+
.egggrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 10px; margin: 12px 0; }
|
| 186 |
+
.eggthumb { padding: 6px; display: flex; flex-direction: column; gap: 4px; align-items: stretch; font-size: 12px; color: var(--muted); }
|
| 187 |
+
.eggthumb img { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 8px; background: #000; display: block; }
|
| 188 |
+
.btnrow .drop.inline { display: inline-block; margin: 0; padding: 8px 12px; }
|
| 189 |
+
.eggresult { display: grid; grid-template-columns: minmax(120px, 220px) 1fr; gap: 16px; align-items: start; margin: 12px 0; }
|
| 190 |
+
.eggresult img { width: 100%; border-radius: 10px; background: #000; }
|
| 191 |
+
@media (max-width: 560px) { .eggresult { grid-template-columns: 1fr; } }
|
| 192 |
+
.okc { color: var(--good); font-weight: 600; margin-left: 6px; }
|
| 193 |
+
.noc { color: var(--bad); font-weight: 600; margin-left: 6px; }
|
| 194 |
+
img.mini { width: 32px; height: 32px; object-fit: cover; border-radius: 4px; vertical-align: middle; margin-right: 6px; }
|
| 195 |
+
.dctx { display: block; font-size: 12px; font-weight: 400; color: var(--muted); }
|
| 196 |
+
.dedit input + input { margin-top: 6px; }
|
worker.js
CHANGED
|
@@ -1,200 +1,211 @@
|
|
| 1 |
-
// SPDX-License-Identifier: Apache-2.0
|
| 2 |
-
// Embedding worker: loads
|
| 3 |
-
//
|
| 4 |
-
//
|
| 5 |
-
//
|
| 6 |
-
import {
|
| 7 |
-
AutoConfig,
|
| 8 |
-
AutoModel,
|
| 9 |
-
AutoProcessor,
|
| 10 |
-
RawImage,
|
| 11 |
-
RawVideo,
|
| 12 |
-
RawVideoFrame,
|
| 13 |
-
env,
|
| 14 |
-
} from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.1/dist/transformers.min.js";
|
| 15 |
-
|
| 16 |
-
const MODEL_ID = "
|
| 17 |
-
env.allowLocalModels =
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
let
|
| 23 |
-
let
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
if (
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
if (
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
if (
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
const
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
const
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
return (await run(await processor([
|
| 154 |
-
}
|
| 155 |
-
if (kind === "
|
| 156 |
-
if (
|
| 157 |
-
const
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
return (await run(await processor(
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
const
|
| 166 |
-
if (!text) return (await run(await processor(null, null,
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
}
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
// Embedding worker: loads a decision-tuned EmbeddingGemma 2 (ONNX, same graph layout as
|
| 3 |
+
// onnx-community/embeddinggemma-2-ONNX) with transformers.js and returns L2-normalized
|
| 4 |
+
// 768-d embeddings (mean pooling over the attention mask, then L2 normalization).
|
| 5 |
+
// The model files are served next to this page (./models/...), so nothing is fetched from elsewhere.
|
| 6 |
+
import {
|
| 7 |
+
AutoConfig,
|
| 8 |
+
AutoModel,
|
| 9 |
+
AutoProcessor,
|
| 10 |
+
RawImage,
|
| 11 |
+
RawVideo,
|
| 12 |
+
RawVideoFrame,
|
| 13 |
+
env,
|
| 14 |
+
} from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.1/dist/transformers.min.js";
|
| 15 |
+
|
| 16 |
+
const MODEL_ID = "egemma2-decide";
|
| 17 |
+
env.allowLocalModels = true;
|
| 18 |
+
env.allowRemoteModels = false;
|
| 19 |
+
env.localModelPath = new URL("./models/", self.location.href).pathname; // same-origin path, e.g. "/models/"
|
| 20 |
+
if (env.backends?.onnx?.wasm) env.backends.onnx.wasm.numThreads = 1; // static hosting is not cross-origin isolated
|
| 21 |
+
|
| 22 |
+
let processor = null;
|
| 23 |
+
let model = null;
|
| 24 |
+
let loaded = null; // { device, dtype, audio }
|
| 25 |
+
let decision = null; // { scale } from decision.json
|
| 26 |
+
let queue = Promise.resolve();
|
| 27 |
+
|
| 28 |
+
const post = (msg, transfer) => self.postMessage(msg, transfer || []);
|
| 29 |
+
const state = (phase, label, extra = {}) => post({ type: "state", phase, label, ...extra });
|
| 30 |
+
|
| 31 |
+
let gpuF16 = false;
|
| 32 |
+
async function hasWebGPU() {
|
| 33 |
+
try {
|
| 34 |
+
if (!("gpu" in navigator)) return false;
|
| 35 |
+
const adapter = await navigator.gpu.requestAdapter();
|
| 36 |
+
gpuF16 = !!adapter?.features?.has("shader-f16");
|
| 37 |
+
return !!adapter;
|
| 38 |
+
} catch {
|
| 39 |
+
return false;
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
async function loadModel({ device = "auto", dtype = "q4", audio = false }) {
|
| 44 |
+
if (loaded && (device === "auto" || device === loaded.device) && dtype === loaded.requested && (!audio || loaded.audio)) {
|
| 45 |
+
return loaded;
|
| 46 |
+
}
|
| 47 |
+
if (model) {
|
| 48 |
+
try { await model.dispose(); } catch {}
|
| 49 |
+
model = null;
|
| 50 |
+
loaded = null;
|
| 51 |
+
}
|
| 52 |
+
let candidates;
|
| 53 |
+
if (device === "auto") candidates = (await hasWebGPU()) ? ["webgpu", "wasm"] : ["wasm"];
|
| 54 |
+
else candidates = [device];
|
| 55 |
+
|
| 56 |
+
if (!decision) {
|
| 57 |
+
try { decision = await (await fetch(`${env.localModelPath}${MODEL_ID}/decision.json`)).json(); } catch { decision = {}; }
|
| 58 |
+
}
|
| 59 |
+
if (!processor) {
|
| 60 |
+
state("loading", "Loading processor");
|
| 61 |
+
processor = await AutoProcessor.from_pretrained(MODEL_ID);
|
| 62 |
+
}
|
| 63 |
+
let lastErr = null;
|
| 64 |
+
const requested = dtype;
|
| 65 |
+
if (candidates.includes("webgpu") && device !== "auto") await hasWebGPU();
|
| 66 |
+
for (const dev of candidates) {
|
| 67 |
+
// Files shipped with this page (Space storage is limited to 1 GB):
|
| 68 |
+
// q4: 4-bit weights with plain 8-bit gathers for the embedding and position tables, so the same files
|
| 69 |
+
// run on WebGPU and on the WASM (CPU) runtime, which lacks GatherBlockQuantized;
|
| 70 |
+
// q4f16: 4-bit weights with fp16 math (WebGPU with shader-f16 only);
|
| 71 |
+
// audio: one 4-bit audio encoder used with both.
|
| 72 |
+
const d = dev === "wasm" ? "q4" : requested === "auto" ? (gpuF16 ? "q4f16" : "q4") : requested;
|
| 73 |
+
dtype = audio ? { model: d, vision_encoder: d, audio_encoder: "q4" } : d;
|
| 74 |
+
try {
|
| 75 |
+
const config = await AutoConfig.from_pretrained(MODEL_ID);
|
| 76 |
+
if (!audio) config.audio_config = null; // text + vision only (video uses the vision encoder)
|
| 77 |
+
state("loading", `Downloading model (${dev}${audio ? ", with audio encoder" : ""})`, { progress: 0, device: dev });
|
| 78 |
+
const t0 = performance.now();
|
| 79 |
+
model = await AutoModel.from_pretrained(MODEL_ID, {
|
| 80 |
+
config,
|
| 81 |
+
device: dev,
|
| 82 |
+
dtype,
|
| 83 |
+
progress_callback: (p) => {
|
| 84 |
+
if (p.status === "progress_total") state("loading", `Downloading model (${dev})`, { progress: p.progress, device: dev, loadedBytes: p.loaded, totalBytes: p.total });
|
| 85 |
+
},
|
| 86 |
+
});
|
| 87 |
+
const sessionMs = performance.now() - t0;
|
| 88 |
+
state("loading", "Warming up", { progress: 100, device: dev });
|
| 89 |
+
const tw = performance.now();
|
| 90 |
+
const warm = await processor(["task: classification | query: warm up"]);
|
| 91 |
+
const out = await model(warm);
|
| 92 |
+
out.sentence_embedding.dispose?.();
|
| 93 |
+
const warmMs = performance.now() - tw;
|
| 94 |
+
const label = typeof dtype === "string" ? dtype : Object.entries(dtype).map(([k, v]) => `${k.replace("_encoder", "")} ${v}`).join(", ");
|
| 95 |
+
loaded = { device: dev, dtype: label, requested, audio, loadMs: performance.now() - t0, sessionMs, warmMs, scale: decision?.scale || null };
|
| 96 |
+
state("ready", `Ready on ${dev.toUpperCase()} (${label})`, { device: dev, audio });
|
| 97 |
+
return loaded;
|
| 98 |
+
} catch (e) {
|
| 99 |
+
lastErr = e;
|
| 100 |
+
console.warn(`Loading on ${dev} failed`, e);
|
| 101 |
+
if (model) { try { await model.dispose(); } catch {} model = null; }
|
| 102 |
+
state("loading", `Loading on ${dev} failed, trying next backend`, { device: dev });
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
state("error", `Could not load the model: ${lastErr?.message || lastErr}`);
|
| 106 |
+
throw lastErr;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
function rows(tensor) {
|
| 110 |
+
const [n, d] = tensor.dims;
|
| 111 |
+
const data = tensor.data;
|
| 112 |
+
const out = [];
|
| 113 |
+
for (let i = 0; i < n; i++) {
|
| 114 |
+
const v = new Float32Array(d);
|
| 115 |
+
let s = 0;
|
| 116 |
+
for (let j = 0; j < d; j++) { v[j] = data[i * d + j]; s += v[j] * v[j]; }
|
| 117 |
+
s = Math.sqrt(s) || 1;
|
| 118 |
+
for (let j = 0; j < d; j++) v[j] /= s;
|
| 119 |
+
out.push(v);
|
| 120 |
+
}
|
| 121 |
+
return out;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
let lastModelMs = 0;
|
| 125 |
+
async function run(inputs) {
|
| 126 |
+
const tm = performance.now();
|
| 127 |
+
const out = await model(inputs);
|
| 128 |
+
lastModelMs = performance.now() - tm;
|
| 129 |
+
const emb = rows(out.sentence_embedding);
|
| 130 |
+
try { out.sentence_embedding.dispose?.(); } catch {}
|
| 131 |
+
return emb;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
// texts: array of strings, embedded in small batches (right padding; padded tokens are masked out)
|
| 135 |
+
async function embedTexts(texts) {
|
| 136 |
+
const res = [];
|
| 137 |
+
const B = 8;
|
| 138 |
+
for (let i = 0; i < texts.length; i += B) {
|
| 139 |
+
const batch = texts.slice(i, i + B);
|
| 140 |
+
res.push(...(await run(await processor(batch))));
|
| 141 |
+
}
|
| 142 |
+
return res;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
// One input that may interleave text with an image, audio clip or video.
|
| 146 |
+
// If the text already holds a placeholder (<|image|>, <|audio|>, <|video|>) it is used as is;
|
| 147 |
+
// otherwise the placeholder is put first, as in the trained prompt format. Empty text embeds the media alone.
|
| 148 |
+
async function embedInput(input) {
|
| 149 |
+
const { kind } = input;
|
| 150 |
+
const text = (input.text || "").trim();
|
| 151 |
+
const withPh = (ph) => (text.includes(ph) ? text : `${ph}${text}`);
|
| 152 |
+
if (kind === "text" || !kind) {
|
| 153 |
+
return (await run(await processor([text])))[0];
|
| 154 |
+
}
|
| 155 |
+
if (kind === "image" || kind === "rgba") {
|
| 156 |
+
if (processor.image_processor) processor.image_processor.max_soft_tokens = input.budget || 280;
|
| 157 |
+
const img = kind === "rgba"
|
| 158 |
+
? new RawImage(new Uint8ClampedArray(input.data), input.width, input.height, 4)
|
| 159 |
+
: await RawImage.fromBlob(input.blob);
|
| 160 |
+
if (!text) return (await run(await processor(null, img)))[0];
|
| 161 |
+
return (await run(await processor([withPh("<|image|>")], [[img]])))[0];
|
| 162 |
+
}
|
| 163 |
+
if (kind === "audio") {
|
| 164 |
+
if (!loaded?.audio) throw new Error("The audio encoder is not loaded.");
|
| 165 |
+
const audio = input.audio instanceof Float32Array ? input.audio : new Float32Array(input.audio);
|
| 166 |
+
if (!text) return (await run(await processor(null, null, audio)))[0];
|
| 167 |
+
return (await run(await processor([withPh("<|audio|>")], null, [audio])))[0];
|
| 168 |
+
}
|
| 169 |
+
if (kind === "video") {
|
| 170 |
+
if (processor.image_processor) processor.image_processor.max_soft_tokens = input.budget || 280;
|
| 171 |
+
const frames = input.frames.map((f) => new RawVideoFrame(new RawImage(new Uint8ClampedArray(f.data), f.width, f.height, 4), f.timestamp));
|
| 172 |
+
const video = new RawVideo(frames, input.duration);
|
| 173 |
+
if (!text) return (await run(await processor(null, null, null, video)))[0];
|
| 174 |
+
return (await run(await processor([withPh("<|video|>")], null, null, [video])))[0];
|
| 175 |
+
}
|
| 176 |
+
throw new Error(`Unknown input kind: ${kind}`);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
async function handle(msg) {
|
| 180 |
+
const { id, op } = msg;
|
| 181 |
+
try {
|
| 182 |
+
if (op === "load") {
|
| 183 |
+
const info = await loadModel(msg.options || {});
|
| 184 |
+
post({ type: "result", id, result: info });
|
| 185 |
+
return;
|
| 186 |
+
}
|
| 187 |
+
if (!model) throw new Error("Model is not loaded yet.");
|
| 188 |
+
const t0 = performance.now();
|
| 189 |
+
if (op === "embedTexts") {
|
| 190 |
+
const embs = await embedTexts(msg.texts);
|
| 191 |
+
post({ type: "result", id, result: { embeddings: embs, ms: performance.now() - t0 } }, embs.map((e) => e.buffer));
|
| 192 |
+
} else if (op === "embedInput") {
|
| 193 |
+
const emb = await embedInput(msg.input);
|
| 194 |
+
post({ type: "result", id, result: { embedding: emb, ms: performance.now() - t0, modelMs: lastModelMs } }, [emb.buffer]);
|
| 195 |
+
} else if (op === "embedInputs") {
|
| 196 |
+
// several prompts over the same media (one pass each); used by live cards in the trained format
|
| 197 |
+
const embs = [];
|
| 198 |
+
for (const inp of msg.inputs) embs.push(await embedInput(inp));
|
| 199 |
+
post({ type: "result", id, result: { embeddings: embs, ms: performance.now() - t0 } }, embs.map((e) => e.buffer));
|
| 200 |
+
} else {
|
| 201 |
+
throw new Error(`Unknown op ${op}`);
|
| 202 |
+
}
|
| 203 |
+
} catch (e) {
|
| 204 |
+
post({ type: "error", id, message: e?.message || String(e) });
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
self.addEventListener("message", ({ data }) => {
|
| 209 |
+
// serialize all requests: one model, one queue
|
| 210 |
+
queue = queue.then(() => handle(data));
|
| 211 |
+
});
|