Spaces:
Running
on
T4
Running
on
T4
thomasht86
commited on
Commit
β’
2034346
1
Parent(s):
5d22e58
Upload folder using huggingface_hub
Browse files- README.md +2 -143
- backend/colpali.py +236 -270
- backend/stopwords.py +2 -1
- backend/vespa_app.py +3 -2
- frontend/app.py +71 -24
- frontend/layout.py +2 -1
- globals.css +65 -51
- icons.py +1 -1
- main.py +63 -73
- output.css +145 -61
- requirements.txt +1 -1
- static/.DS_Store +0 -0
README.md
CHANGED
@@ -9,152 +9,11 @@ sdk_version: 4.44.0
|
|
9 |
app_file: main.py
|
10 |
pinned: false
|
11 |
license: apache-2.0
|
|
|
12 |
models:
|
13 |
- vidore/colpaligemma-3b-pt-448-base
|
14 |
- vidore/colpali-v1.2
|
15 |
preload_from_hub:
|
16 |
- vidore/colpaligemma-3b-pt-448-base config.json,model-00001-of-00002.safetensors,model-00002-of-00002.safetensors,model.safetensors.index.json,preprocessor_config.json,special_tokens_map.json,tokenizer.json,tokenizer_config.json 12c59eb7e23bc4c26876f7be7c17760d5d3a1ffa
|
17 |
- vidore/colpali-v1.2 adapter_config.json,adapter_model.safetensors,preprocessor_config.json,special_tokens_map.json,tokenizer.json,tokenizer_config.json 9912ce6f8a462d8cf2269f5606eabbd2784e764f
|
18 |
-
---
|
19 |
-
|
20 |
-
<!-- Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
|
21 |
-
|
22 |
-
<picture>
|
23 |
-
<source media="(prefers-color-scheme: dark)" srcset="https://assets.vespa.ai/logos/Vespa-logo-green-RGB.svg">
|
24 |
-
<source media="(prefers-color-scheme: light)" srcset="https://assets.vespa.ai/logos/Vespa-logo-dark-RGB.svg">
|
25 |
-
<img alt="#Vespa" width="200" src="https://assets.vespa.ai/logos/Vespa-logo-dark-RGB.svg" style="margin-bottom: 25px;">
|
26 |
-
</picture>
|
27 |
-
|
28 |
-
# Visual Retrieval ColPali
|
29 |
-
|
30 |
-
# Prepare data and Vespa application
|
31 |
-
|
32 |
-
First, install `uv`:
|
33 |
-
|
34 |
-
```bash
|
35 |
-
curl -LsSf https://astral.sh/uv/install.sh | sh
|
36 |
-
```
|
37 |
-
|
38 |
-
Then, run:
|
39 |
-
|
40 |
-
```bash
|
41 |
-
uv sync --extra dev --extra feed
|
42 |
-
```
|
43 |
-
|
44 |
-
Convert the `prepare_feed_deploy.py` to notebook to:
|
45 |
-
|
46 |
-
```bash
|
47 |
-
jupytext --to notebook prepare_feed_deploy.py
|
48 |
-
```
|
49 |
-
|
50 |
-
And launch a Jupyter instance, see https://docs.astral.sh/uv/guides/integration/jupyter/ for recommended approach.
|
51 |
-
|
52 |
-
Open and follow the `prepare_feed_deploy.ipynb` notebook to prepare the data and deploy the Vespa application.
|
53 |
-
|
54 |
-
# Developing on the web app
|
55 |
-
|
56 |
-
|
57 |
-
Then, in this directory, run:
|
58 |
-
|
59 |
-
```bash
|
60 |
-
uv sync --extra dev
|
61 |
-
```
|
62 |
-
|
63 |
-
This will generate a virtual environment with the required dependencies at `.venv`.
|
64 |
-
|
65 |
-
To activate the virtual environment, run:
|
66 |
-
|
67 |
-
```bash
|
68 |
-
source .venv/bin/activate
|
69 |
-
```
|
70 |
-
|
71 |
-
And run development server:
|
72 |
-
|
73 |
-
```bash
|
74 |
-
python hello.py
|
75 |
-
```
|
76 |
-
|
77 |
-
## Preparation
|
78 |
-
|
79 |
-
First, set up your `.env` file by renaming `.env.example` to `.env` and filling in the required values.
|
80 |
-
(Token can be shared with 1password, `HF_TOKEN` is personal and must be created at huggingface)
|
81 |
-
|
82 |
-
### Deploying the Vespa app
|
83 |
-
|
84 |
-
To deploy the Vespa app, run:
|
85 |
-
|
86 |
-
```bash
|
87 |
-
python deploy_vespa_app.py --tenant_name mytenant --vespa_application_name myapp --token_id_write mytokenid_write --token_id_read mytokenid_read
|
88 |
-
```
|
89 |
-
|
90 |
-
You should get an output like:
|
91 |
-
|
92 |
-
```bash
|
93 |
-
Found token endpoint: https://abcde.z.vespa-app.cloud
|
94 |
-
````
|
95 |
-
|
96 |
-
### Feeding the data
|
97 |
-
|
98 |
-
#### Dependencies
|
99 |
-
|
100 |
-
In addition to the python dependencies, you also need `poppler`
|
101 |
-
On Mac:
|
102 |
-
|
103 |
-
```bash
|
104 |
-
brew install poppler
|
105 |
-
```
|
106 |
-
|
107 |
-
First, you need to create a huggingface token, after you have accepted the term to use the model
|
108 |
-
at https://huggingface.co/google/paligemma-3b-mix-448.
|
109 |
-
Add the token to your environment variables as `HF_TOKEN`:
|
110 |
-
|
111 |
-
```bash
|
112 |
-
export HF_TOKEN=yourtoken
|
113 |
-
```
|
114 |
-
|
115 |
-
To feed the data, run:
|
116 |
-
|
117 |
-
```bash
|
118 |
-
python feed_vespa.py --vespa_app_url https://myapp.z.vespa-app.cloud --vespa_cloud_secret_token mysecrettoken
|
119 |
-
```
|
120 |
-
|
121 |
-
### Starting the front-end
|
122 |
-
|
123 |
-
```bash
|
124 |
-
python main.py
|
125 |
-
```
|
126 |
-
|
127 |
-
## Deploy to huggingface π€
|
128 |
-
|
129 |
-
### Compiling dependencies
|
130 |
-
|
131 |
-
Before a deploy, make sure to run this to compile the `uv` lock file to `requirements.txt` if you have made changes to the dependencies:
|
132 |
-
|
133 |
-
```bash
|
134 |
-
uv pip compile pyproject.toml -o requirements.txt
|
135 |
-
```
|
136 |
-
|
137 |
-
### Deploying to huggingface
|
138 |
-
|
139 |
-
To deploy, run
|
140 |
-
|
141 |
-
```bash
|
142 |
-
huggingface-cli upload vespa-engine/colpali-vespa-visual-retrieval . . --repo-type=space
|
143 |
-
```
|
144 |
-
|
145 |
-
Note that you need to set `HF_TOKEN` environment variable first.
|
146 |
-
This is personal, and must be created at [huggingface](https://huggingface.co/settings/tokens).
|
147 |
-
Make sure the token has `write` access.
|
148 |
-
Be aware that this will not delete existing files, only modify or add,
|
149 |
-
see [huggingface-cli](https://huggingface.co/docs/huggingface_hub/en/guides/upload#upload-from-the-cli) for more
|
150 |
-
information.
|
151 |
-
|
152 |
-
### Making changes to CSS
|
153 |
-
|
154 |
-
To make changes to output.css apply, run
|
155 |
-
|
156 |
-
```bash
|
157 |
-
shad4fast watch # watches all files passed through the tailwind.config.js content section
|
158 |
-
|
159 |
-
shad4fast build # minifies the current output.css file to reduce bundle size in production.
|
160 |
-
```
|
|
|
9 |
app_file: main.py
|
10 |
pinned: false
|
11 |
license: apache-2.0
|
12 |
+
suggested_hardware: t4-small
|
13 |
models:
|
14 |
- vidore/colpaligemma-3b-pt-448-base
|
15 |
- vidore/colpali-v1.2
|
16 |
preload_from_hub:
|
17 |
- vidore/colpaligemma-3b-pt-448-base config.json,model-00001-of-00002.safetensors,model-00002-of-00002.safetensors,model.safetensors.index.json,preprocessor_config.json,special_tokens_map.json,tokenizer.json,tokenizer_config.json 12c59eb7e23bc4c26876f7be7c17760d5d3a1ffa
|
18 |
- vidore/colpali-v1.2 adapter_config.json,adapter_model.safetensors,preprocessor_config.json,special_tokens_map.json,tokenizer.json,tokenizer_config.json 9912ce6f8a462d8cf2269f5606eabbd2784e764f
|
19 |
+
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/colpali.py
CHANGED
@@ -1,308 +1,274 @@
|
|
1 |
-
#!/usr/bin/env python3
|
2 |
-
|
3 |
import torch
|
4 |
from PIL import Image
|
5 |
import numpy as np
|
6 |
-
from typing import
|
7 |
from pathlib import Path
|
8 |
import base64
|
9 |
from io import BytesIO
|
10 |
-
from typing import Union, Tuple, List
|
11 |
-
import matplotlib
|
12 |
-
import matplotlib.cm as cm
|
13 |
import re
|
14 |
import io
|
15 |
-
|
16 |
-
import time
|
17 |
-
import backend.testquery as testquery
|
18 |
|
19 |
from colpali_engine.models import ColPali, ColPaliProcessor
|
20 |
from colpali_engine.utils.torch_utils import get_torch_device
|
21 |
-
from einops import rearrange
|
22 |
from vidore_benchmark.interpretability.torch_utils import (
|
23 |
normalize_similarity_map_per_query_token,
|
24 |
)
|
25 |
-
from vidore_benchmark.interpretability.vit_configs import VIT_CONFIG
|
26 |
-
|
27 |
-
matplotlib.use("Agg")
|
28 |
-
# Prepare the colormap once to avoid recomputation
|
29 |
-
colormap = cm.get_cmap("viridis")
|
30 |
-
|
31 |
-
COLPALI_GEMMA_MODEL_NAME = "vidore/colpaligemma-3b-pt-448-base"
|
32 |
|
33 |
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
print(f"Using device: {device}")
|
39 |
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
|
46 |
-
device_map=device,
|
47 |
-
)
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
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 |
-
Yields:
|
87 |
-
Tuple[int, str, str]: A tuple containing the image index, the selected token, and the base64-encoded image.
|
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 |
vespa_sim_map_tensor = torch.zeros(
|
127 |
-
(
|
128 |
-
len(vespa_sim_maps),
|
129 |
-
query_embs.size(dim=1),
|
130 |
-
vit_config.n_patch_per_dim,
|
131 |
-
vit_config.n_patch_per_dim,
|
132 |
-
)
|
133 |
)
|
134 |
for idx, vespa_sim_map in enumerate(vespa_sim_maps):
|
135 |
for cell in vespa_sim_map["quantized"]["cells"]:
|
136 |
patch = int(cell["address"]["patch"])
|
137 |
-
|
138 |
-
|
139 |
-
if hasattr(processor, "image_seq_length"):
|
140 |
-
image_seq_length = processor.image_seq_length
|
141 |
else:
|
142 |
image_seq_length = 1024
|
143 |
|
144 |
if patch >= image_seq_length:
|
145 |
continue
|
146 |
-
query_token = int(cell["address"]["querytoken"])
|
147 |
-
value = cell["value"]
|
148 |
vespa_sim_map_tensor[
|
149 |
idx,
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
] = value
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
158 |
)
|
159 |
-
else:
|
160 |
-
# Preprocess inputs
|
161 |
-
print("Computing similarity maps")
|
162 |
-
start2 = time.perf_counter()
|
163 |
-
input_image_processed = processor.process_images(processed_images).to(device)
|
164 |
-
|
165 |
-
# Forward passes
|
166 |
-
with torch.no_grad():
|
167 |
-
output_image = model.forward(**input_image_processed)
|
168 |
-
|
169 |
-
# Remove the special tokens from the output
|
170 |
-
output_image = output_image[:, : processor.image_seq_length, :]
|
171 |
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
"b (h w) c -> b h w c",
|
176 |
-
h=vit_config.n_patch_per_dim,
|
177 |
-
w=vit_config.n_patch_per_dim,
|
178 |
)
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
197 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
198 |
|
199 |
-
|
200 |
-
|
201 |
-
for idx, img in enumerate(original_images):
|
202 |
-
SCALING_FACTOR = 8
|
203 |
-
sim_map_resolution = (
|
204 |
-
max(32, int(original_sizes[idx][0] / SCALING_FACTOR)),
|
205 |
-
max(32, int(original_sizes[idx][1] / SCALING_FACTOR)),
|
206 |
)
|
207 |
-
|
208 |
-
|
209 |
-
for token_idx, token in token_idx_map.items():
|
210 |
-
if should_filter_token(token):
|
211 |
-
continue
|
212 |
-
|
213 |
-
# Get the similarity map for this image and the selected token
|
214 |
-
sim_map = similarity_map_normalized[idx, token_idx, :, :] # Shape: (h, w)
|
215 |
-
|
216 |
-
# Move the similarity map to CPU, convert to float (as BFloat16 not supported by Numpy) and convert to NumPy array
|
217 |
-
sim_map_np = sim_map.cpu().float().numpy()
|
218 |
-
|
219 |
-
# Resize the similarity map to the original image size
|
220 |
-
sim_map_img = Image.fromarray(sim_map_np)
|
221 |
-
sim_map_resized = sim_map_img.resize(
|
222 |
-
sim_map_resolution, resample=Image.BICUBIC
|
223 |
-
)
|
224 |
-
|
225 |
-
# Convert the resized similarity map to a NumPy array
|
226 |
-
sim_map_resized_np = np.array(sim_map_resized, dtype=np.float32)
|
227 |
-
|
228 |
-
# Normalize the similarity map to range [0, 1]
|
229 |
-
sim_map_min = sim_map_resized_np.min()
|
230 |
-
sim_map_max = sim_map_resized_np.max()
|
231 |
-
if sim_map_max - sim_map_min > 1e-6:
|
232 |
-
sim_map_normalized = (sim_map_resized_np - sim_map_min) / (
|
233 |
-
sim_map_max - sim_map_min
|
234 |
-
)
|
235 |
-
else:
|
236 |
-
sim_map_normalized = np.zeros_like(sim_map_resized_np)
|
237 |
-
|
238 |
-
# Apply a colormap to the normalized similarity map
|
239 |
-
heatmap = colormap(sim_map_normalized) # Returns an RGBA array
|
240 |
-
|
241 |
-
# Convert the heatmap to a PIL Image
|
242 |
-
heatmap_uint8 = (heatmap * 255).astype(np.uint8)
|
243 |
-
heatmap_img = Image.fromarray(heatmap_uint8)
|
244 |
-
heatmap_img_rgba = heatmap_img.convert("RGBA")
|
245 |
-
|
246 |
-
# Save the image to a BytesIO buffer
|
247 |
-
buffer = io.BytesIO()
|
248 |
-
heatmap_img_rgba.save(buffer, format="PNG")
|
249 |
-
buffer.seek(0)
|
250 |
-
|
251 |
-
# Encode the image to base64
|
252 |
-
blended_img_base64 = base64.b64encode(buffer.read()).decode("utf-8")
|
253 |
-
|
254 |
-
# Store the base64-encoded image
|
255 |
-
result_per_image[token] = blended_img_base64
|
256 |
-
yield idx, token, token_idx, blended_img_base64
|
257 |
-
end3 = time.perf_counter()
|
258 |
-
print(f"Blending images took: {end3 - start3} s")
|
259 |
-
|
260 |
-
|
261 |
-
def get_query_embeddings_and_token_map(
|
262 |
-
processor, model, query
|
263 |
-
) -> Tuple[torch.Tensor, dict]:
|
264 |
-
if model is None: # use static test query data (saves time when testing)
|
265 |
-
return testquery.q_embs, testquery.idx_to_token
|
266 |
-
|
267 |
-
start_time = time.perf_counter()
|
268 |
-
inputs = processor.process_queries([query]).to(model.device)
|
269 |
-
with torch.no_grad():
|
270 |
-
embeddings_query = model(**inputs)
|
271 |
-
q_emb = embeddings_query.to("cpu")[0] # Extract the single embedding
|
272 |
-
# Use this cell output to choose a token using its index
|
273 |
-
query_tokens = processor.tokenizer.tokenize(processor.decode(inputs.input_ids[0]))
|
274 |
-
# reverse key, values in dictionary
|
275 |
-
print(query_tokens)
|
276 |
-
idx_to_token = {idx: val for idx, val in enumerate(query_tokens)}
|
277 |
-
end_time = time.perf_counter()
|
278 |
-
print(f"Query inference took: {end_time - start_time} s")
|
279 |
-
return q_emb, idx_to_token
|
280 |
-
|
281 |
-
|
282 |
-
def should_filter_token(token: str) -> bool:
|
283 |
-
# Pattern to match tokens that start with '<', numbers, whitespace, special characters (except β), or the string 'Question'
|
284 |
-
# Will exclude these tokens from the similarity map generation
|
285 |
-
# Does NOT match:
|
286 |
-
# 2
|
287 |
-
# 0
|
288 |
-
# 2
|
289 |
-
# 3
|
290 |
-
# β2
|
291 |
-
# βhi
|
292 |
-
#
|
293 |
-
# Do match:
|
294 |
-
# <bos>
|
295 |
-
# Question
|
296 |
-
# :
|
297 |
-
# _Percentage
|
298 |
-
# <pad>
|
299 |
-
# \n
|
300 |
-
# β
|
301 |
-
# ?
|
302 |
-
# )
|
303 |
-
# %
|
304 |
-
# /)
|
305 |
-
pattern = re.compile(r"^<.*$|^\s+$|^(?!.*\d)(?!β)\S+$|^Question$|^β$")
|
306 |
-
if pattern.match(token):
|
307 |
-
return True
|
308 |
-
return False
|
|
|
|
|
|
|
1 |
import torch
|
2 |
from PIL import Image
|
3 |
import numpy as np
|
4 |
+
from typing import Generator, Tuple, List, Union, Dict
|
5 |
from pathlib import Path
|
6 |
import base64
|
7 |
from io import BytesIO
|
|
|
|
|
|
|
8 |
import re
|
9 |
import io
|
10 |
+
import matplotlib.cm as cm
|
|
|
|
|
11 |
|
12 |
from colpali_engine.models import ColPali, ColPaliProcessor
|
13 |
from colpali_engine.utils.torch_utils import get_torch_device
|
|
|
14 |
from vidore_benchmark.interpretability.torch_utils import (
|
15 |
normalize_similarity_map_per_query_token,
|
16 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
|
19 |
+
class SimMapGenerator:
|
20 |
+
"""
|
21 |
+
Generates similarity maps based on query embeddings and image patches using the ColPali model.
|
22 |
+
"""
|
|
|
23 |
|
24 |
+
COLPALI_GEMMA_MODEL_NAME = "vidore/colpaligemma-3b-pt-448-base"
|
25 |
+
colormap = cm.get_cmap("viridis") # Preload colormap for efficiency
|
26 |
+
|
27 |
+
def __init__(self, model_name: str = "vidore/colpali-v1.2", n_patch: int = 32):
|
28 |
+
"""
|
29 |
+
Initializes the SimMapGenerator class with a specified model and patch dimension.
|
30 |
+
|
31 |
+
Args:
|
32 |
+
model_name (str): The model name for loading the ColPali model.
|
33 |
+
n_patch (int): The number of patches per dimension.
|
34 |
+
"""
|
35 |
+
self.model_name = model_name
|
36 |
+
self.n_patch = n_patch
|
37 |
+
self.device = get_torch_device("auto")
|
38 |
+
print(f"Using device: {self.device}")
|
39 |
+
self.model, self.processor = self.load_model()
|
40 |
+
|
41 |
+
def load_model(self) -> Tuple[ColPali, ColPaliProcessor]:
|
42 |
+
"""
|
43 |
+
Loads the ColPali model and processor.
|
44 |
+
|
45 |
+
Returns:
|
46 |
+
Tuple[ColPali, ColPaliProcessor]: Loaded model and processor.
|
47 |
+
"""
|
48 |
+
model = ColPali.from_pretrained(
|
49 |
+
self.model_name,
|
50 |
torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
|
51 |
+
device_map=self.device,
|
52 |
+
).eval()
|
53 |
+
|
54 |
+
processor = ColPaliProcessor.from_pretrained(self.model_name)
|
55 |
+
return model, processor
|
56 |
+
|
57 |
+
def gen_similarity_maps(
|
58 |
+
self,
|
59 |
+
query: str,
|
60 |
+
query_embs: torch.Tensor,
|
61 |
+
token_idx_map: Dict[int, str],
|
62 |
+
images: List[Union[Path, str]],
|
63 |
+
vespa_sim_maps: List[Dict],
|
64 |
+
) -> Generator[Tuple[int, str, str], None, None]:
|
65 |
+
"""
|
66 |
+
Generates similarity maps for the provided images and query, and returns base64-encoded blended images.
|
67 |
+
|
68 |
+
Args:
|
69 |
+
query (str): The query string.
|
70 |
+
query_embs (torch.Tensor): Query embeddings tensor.
|
71 |
+
token_idx_map (dict): Mapping from indices to tokens.
|
72 |
+
images (List[Union[Path, str]]): List of image paths or base64-encoded strings.
|
73 |
+
vespa_sim_maps (List[Dict]): List of Vespa similarity maps.
|
74 |
+
|
75 |
+
Yields:
|
76 |
+
Tuple[int, str, str]: A tuple containing the image index, selected token, and base64-encoded image.
|
77 |
+
"""
|
78 |
+
processed_images, original_images, original_sizes = [], [], []
|
79 |
+
for img in images:
|
80 |
+
img_pil = self._load_image(img)
|
81 |
+
original_images.append(img_pil.copy())
|
82 |
+
original_sizes.append(img_pil.size)
|
83 |
+
processed_images.append(img_pil)
|
84 |
+
|
85 |
+
vespa_sim_map_tensor = self._prepare_similarity_map_tensor(
|
86 |
+
query_embs, vespa_sim_maps
|
87 |
+
)
|
88 |
+
similarity_map_normalized = normalize_similarity_map_per_query_token(
|
89 |
+
vespa_sim_map_tensor
|
90 |
+
)
|
|
|
|
|
91 |
|
92 |
+
for idx, img in enumerate(original_images):
|
93 |
+
for token_idx, token in token_idx_map.items():
|
94 |
+
if self.should_filter_token(token):
|
95 |
+
continue
|
96 |
+
|
97 |
+
sim_map = similarity_map_normalized[idx, token_idx, :, :]
|
98 |
+
blended_img_base64 = self._blend_image(
|
99 |
+
img, sim_map, original_sizes[idx]
|
100 |
+
)
|
101 |
+
yield idx, token, token_idx, blended_img_base64
|
102 |
+
|
103 |
+
def _load_image(self, img: Union[Path, str]) -> Image:
|
104 |
+
"""
|
105 |
+
Loads an image from a file path or a base64-encoded string.
|
106 |
+
|
107 |
+
Args:
|
108 |
+
img (Union[Path, str]): The image to load.
|
109 |
+
|
110 |
+
Returns:
|
111 |
+
Image: The loaded PIL image.
|
112 |
+
"""
|
113 |
+
try:
|
114 |
+
if isinstance(img, Path):
|
115 |
+
return Image.open(img).convert("RGB")
|
116 |
+
elif isinstance(img, str):
|
117 |
+
return Image.open(BytesIO(base64.b64decode(img))).convert("RGB")
|
118 |
+
except Exception as e:
|
119 |
+
raise ValueError(f"Failed to load image: {e}")
|
120 |
+
|
121 |
+
def _prepare_similarity_map_tensor(
|
122 |
+
self, query_embs: torch.Tensor, vespa_sim_maps: List[Dict]
|
123 |
+
) -> torch.Tensor:
|
124 |
+
"""
|
125 |
+
Prepares a similarity map tensor from Vespa similarity maps.
|
126 |
+
|
127 |
+
Args:
|
128 |
+
query_embs (torch.Tensor): Query embeddings tensor.
|
129 |
+
vespa_sim_maps (List[Dict]): List of Vespa similarity maps.
|
130 |
+
|
131 |
+
Returns:
|
132 |
+
torch.Tensor: The prepared similarity map tensor.
|
133 |
+
"""
|
134 |
vespa_sim_map_tensor = torch.zeros(
|
135 |
+
(len(vespa_sim_maps), query_embs.size(1), self.n_patch, self.n_patch)
|
|
|
|
|
|
|
|
|
|
|
136 |
)
|
137 |
for idx, vespa_sim_map in enumerate(vespa_sim_maps):
|
138 |
for cell in vespa_sim_map["quantized"]["cells"]:
|
139 |
patch = int(cell["address"]["patch"])
|
140 |
+
query_token = int(cell["address"]["querytoken"])
|
141 |
+
value = cell["value"]
|
142 |
+
if hasattr(self.processor, "image_seq_length"):
|
143 |
+
image_seq_length = self.processor.image_seq_length
|
144 |
else:
|
145 |
image_seq_length = 1024
|
146 |
|
147 |
if patch >= image_seq_length:
|
148 |
continue
|
|
|
|
|
149 |
vespa_sim_map_tensor[
|
150 |
idx,
|
151 |
+
query_token,
|
152 |
+
patch // self.n_patch,
|
153 |
+
patch % self.n_patch,
|
154 |
] = value
|
155 |
+
return vespa_sim_map_tensor
|
156 |
+
|
157 |
+
def _blend_image(
|
158 |
+
self, img: Image, sim_map: torch.Tensor, original_size: Tuple[int, int]
|
159 |
+
) -> str:
|
160 |
+
"""
|
161 |
+
Blends an image with a similarity map and encodes it to base64.
|
162 |
+
|
163 |
+
Args:
|
164 |
+
img (Image): The original image.
|
165 |
+
sim_map (torch.Tensor): The similarity map tensor.
|
166 |
+
original_size (Tuple[int, int]): The original size of the image.
|
167 |
+
|
168 |
+
Returns:
|
169 |
+
str: The base64-encoded blended image.
|
170 |
+
"""
|
171 |
+
SCALING_FACTOR = 8
|
172 |
+
sim_map_resolution = (
|
173 |
+
max(32, int(original_size[0] / SCALING_FACTOR)),
|
174 |
+
max(32, int(original_size[1] / SCALING_FACTOR)),
|
175 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
176 |
|
177 |
+
sim_map_np = sim_map.cpu().float().numpy()
|
178 |
+
sim_map_img = Image.fromarray(sim_map_np).resize(
|
179 |
+
sim_map_resolution, resample=Image.BICUBIC
|
|
|
|
|
|
|
180 |
)
|
181 |
+
sim_map_resized_np = np.array(sim_map_img, dtype=np.float32)
|
182 |
+
sim_map_normalized = self._normalize_sim_map(sim_map_resized_np)
|
183 |
+
|
184 |
+
heatmap = self.colormap(sim_map_normalized)
|
185 |
+
heatmap_img = Image.fromarray((heatmap * 255).astype(np.uint8)).convert("RGBA")
|
186 |
+
|
187 |
+
buffer = io.BytesIO()
|
188 |
+
heatmap_img.save(buffer, format="PNG")
|
189 |
+
return base64.b64encode(buffer.getvalue()).decode("utf-8")
|
190 |
+
|
191 |
+
@staticmethod
|
192 |
+
def _normalize_sim_map(sim_map: np.ndarray) -> np.ndarray:
|
193 |
+
"""
|
194 |
+
Normalizes a similarity map to range [0, 1].
|
195 |
+
|
196 |
+
Args:
|
197 |
+
sim_map (np.ndarray): The similarity map.
|
198 |
+
|
199 |
+
Returns:
|
200 |
+
np.ndarray: The normalized similarity map.
|
201 |
+
"""
|
202 |
+
sim_map_min, sim_map_max = sim_map.min(), sim_map.max()
|
203 |
+
if sim_map_max - sim_map_min > 1e-6:
|
204 |
+
return (sim_map - sim_map_min) / (sim_map_max - sim_map_min)
|
205 |
+
return np.zeros_like(sim_map)
|
206 |
+
|
207 |
+
@staticmethod
|
208 |
+
def should_filter_token(token: str) -> bool:
|
209 |
+
"""
|
210 |
+
Determines if a token should be filtered out based on predefined patterns.
|
211 |
+
|
212 |
+
The function filters out tokens that:
|
213 |
+
|
214 |
+
- Start with '<' (e.g., '<bos>')
|
215 |
+
- Consist entirely of whitespace
|
216 |
+
- Are purely punctuation (excluding tokens that contain digits or start with 'β')
|
217 |
+
- Start with an underscore '_'
|
218 |
+
- Exactly match the word 'Question'
|
219 |
+
- Are exactly the single character 'β'
|
220 |
+
|
221 |
+
Output of test:
|
222 |
+
Token: '2' | False
|
223 |
+
Token: '0' | False
|
224 |
+
Token: '2' | False
|
225 |
+
Token: '3' | False
|
226 |
+
Token: 'β2' | False
|
227 |
+
Token: 'βhi' | False
|
228 |
+
Token: 'norwegian' | False
|
229 |
+
Token: 'unlisted' | False
|
230 |
+
Token: '<bos>' | True
|
231 |
+
Token: 'Question' | True
|
232 |
+
Token: ':' | True
|
233 |
+
Token: '<pad>' | True
|
234 |
+
Token: '\n' | True
|
235 |
+
Token: 'β' | True
|
236 |
+
Token: '?' | True
|
237 |
+
Token: ')' | True
|
238 |
+
Token: '%' | True
|
239 |
+
Token: '/)' | True
|
240 |
+
|
241 |
+
|
242 |
+
Args:
|
243 |
+
token (str): The token to check.
|
244 |
+
|
245 |
+
Returns:
|
246 |
+
bool: True if the token should be filtered out, False otherwise.
|
247 |
+
"""
|
248 |
+
pattern = re.compile(
|
249 |
+
r"^<.*$|^\s+$|^(?!.*\d)(?!β)[^\w\s]+$|^_.*$|^Question$|^β$"
|
250 |
)
|
251 |
+
return bool(pattern.match(token))
|
252 |
+
|
253 |
+
# TODO: Would be nice to @lru_cache this method.
|
254 |
+
def get_query_embeddings_and_token_map(
|
255 |
+
self, query: str
|
256 |
+
) -> Tuple[torch.Tensor, dict]:
|
257 |
+
"""
|
258 |
+
Retrieves query embeddings and a token index map.
|
259 |
+
|
260 |
+
Args:
|
261 |
+
query (str): The query string.
|
262 |
+
|
263 |
+
Returns:
|
264 |
+
Tuple[torch.Tensor, dict]: Query embeddings and token index map.
|
265 |
+
"""
|
266 |
+
inputs = self.processor.process_queries([query]).to(self.model.device)
|
267 |
+
with torch.no_grad():
|
268 |
+
q_emb = self.model(**inputs).to("cpu")[0]
|
269 |
|
270 |
+
query_tokens = self.processor.tokenizer.tokenize(
|
271 |
+
self.processor.decode(inputs.input_ids[0])
|
|
|
|
|
|
|
|
|
|
|
272 |
)
|
273 |
+
idx_to_token = {idx: token for idx, token in enumerate(query_tokens)}
|
274 |
+
return q_emb, idx_to_token
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/stopwords.py
CHANGED
@@ -6,6 +6,7 @@ if not spacy.util.is_package("en_core_web_sm"):
|
|
6 |
spacy.cli.download("en_core_web_sm")
|
7 |
nlp = spacy.load("en_core_web_sm")
|
8 |
|
|
|
9 |
# It would be possible to remove bolding for stopwords without removing them from the query,
|
10 |
# but that would require a java plugin which we didn't want to complicate this sample app with.
|
11 |
def filter(text):
|
@@ -14,4 +15,4 @@ def filter(text):
|
|
14 |
if len(tokens) == 0:
|
15 |
# if we remove all the words we don't have a query at all, so use the original
|
16 |
return text
|
17 |
-
return " ".join(tokens)
|
|
|
6 |
spacy.cli.download("en_core_web_sm")
|
7 |
nlp = spacy.load("en_core_web_sm")
|
8 |
|
9 |
+
|
10 |
# It would be possible to remove bolding for stopwords without removing them from the query,
|
11 |
# but that would require a java plugin which we didn't want to complicate this sample app with.
|
12 |
def filter(text):
|
|
|
15 |
if len(tokens) == 0:
|
16 |
# if we remove all the words we don't have a query at all, so use the original
|
17 |
return text
|
18 |
+
return " ".join(tokens)
|
backend/vespa_app.py
CHANGED
@@ -7,9 +7,10 @@ import torch
|
|
7 |
from dotenv import load_dotenv
|
8 |
from vespa.application import Vespa
|
9 |
from vespa.io import VespaQueryResponse
|
10 |
-
from .colpali import
|
11 |
import backend.stopwords
|
12 |
|
|
|
13 |
class VespaQueryClient:
|
14 |
MAX_QUERY_TERMS = 64
|
15 |
VESPA_SCHEMA_NAME = "pdf_page"
|
@@ -364,7 +365,7 @@ class VespaQueryClient:
|
|
364 |
fields_to_add = [
|
365 |
f"sim_map_{token}_{idx}"
|
366 |
for idx, token in idx_to_token.items()
|
367 |
-
if not should_filter_token(token)
|
368 |
]
|
369 |
for child in result["root"]["children"]:
|
370 |
for sim_map_key in fields_to_add:
|
|
|
7 |
from dotenv import load_dotenv
|
8 |
from vespa.application import Vespa
|
9 |
from vespa.io import VespaQueryResponse
|
10 |
+
from .colpali import SimMapGenerator
|
11 |
import backend.stopwords
|
12 |
|
13 |
+
|
14 |
class VespaQueryClient:
|
15 |
MAX_QUERY_TERMS = 64
|
16 |
VESPA_SCHEMA_NAME = "pdf_page"
|
|
|
365 |
fields_to_add = [
|
366 |
f"sim_map_{token}_{idx}"
|
367 |
for idx, token in idx_to_token.items()
|
368 |
+
if not SimMapGenerator.should_filter_token(token)
|
369 |
]
|
370 |
for child in result["root"]["children"]:
|
371 |
for sim_map_key in fields_to_add:
|
frontend/app.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
from typing import Optional
|
2 |
from urllib.parse import quote_plus
|
3 |
|
4 |
-
from fasthtml.components import H1, H2, Div, Form, Img, NotStr, P, Span
|
5 |
from fasthtml.xtend import A, Script
|
6 |
from lucide_fasthtml import Lucide
|
7 |
from shad4fast import Badge, Button, Input, Label, RadioGroup, RadioGroupItem, Separator
|
@@ -154,7 +154,7 @@ def SearchBox(with_border=False, query_value="", ranking_value="nn+colpali"):
|
|
154 |
name="query",
|
155 |
value=query_value,
|
156 |
id="search-input",
|
157 |
-
cls="text-base pl-10 border-transparent ring-offset-transparent ring-0 focus-visible:ring-transparent awesomplete",
|
158 |
data_list="#suggestions",
|
159 |
style="font-size: 1rem",
|
160 |
autofocus=True,
|
@@ -366,7 +366,23 @@ def SimMapButtonPoll(query_id, idx, token, token_idx):
|
|
366 |
)
|
367 |
|
368 |
|
369 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
370 |
if not results:
|
371 |
return Div(
|
372 |
P(
|
@@ -376,10 +392,13 @@ def SearchResult(results: list, query_id: Optional[str] = None):
|
|
376 |
cls="grid p-10",
|
377 |
)
|
378 |
|
|
|
379 |
# Otherwise, display the search results
|
380 |
result_items = []
|
381 |
for idx, result in enumerate(results):
|
382 |
fields = result["fields"] # Extract the 'fields' part of each result
|
|
|
|
|
383 |
blur_image_base64 = f"data:image/jpeg;base64,{fields['blur_image']}"
|
384 |
|
385 |
sim_map_fields = {
|
@@ -472,7 +491,7 @@ def SearchResult(results: list, query_id: Optional[str] = None):
|
|
472 |
Div(
|
473 |
Img(
|
474 |
src=blur_image_base64,
|
475 |
-
hx_get=f"/full_image?
|
476 |
style="backdrop-filter: blur(5px);",
|
477 |
hx_trigger="load",
|
478 |
hx_swap="outerHTML",
|
@@ -493,9 +512,12 @@ def SearchResult(results: list, query_id: Optional[str] = None):
|
|
493 |
),
|
494 |
Div(
|
495 |
Div(
|
496 |
-
|
497 |
-
"
|
498 |
-
|
|
|
|
|
|
|
499 |
),
|
500 |
cls="flex items-center justify-end",
|
501 |
),
|
@@ -504,7 +526,10 @@ def SearchResult(results: list, query_id: Optional[str] = None):
|
|
504 |
Div(
|
505 |
Div(
|
506 |
Div(
|
507 |
-
H3(
|
|
|
|
|
|
|
508 |
P(
|
509 |
NotStr(fields.get("snippet", "")),
|
510 |
cls="text-highlight text-muted-foreground",
|
@@ -517,23 +542,28 @@ def SearchResult(results: list, query_id: Optional[str] = None):
|
|
517 |
Div(
|
518 |
Div(
|
519 |
Div(
|
520 |
-
H3(
|
|
|
|
|
|
|
521 |
Div(
|
522 |
P(
|
523 |
NotStr(fields.get("text", "")),
|
524 |
cls="text-highlight text-muted-foreground",
|
525 |
),
|
526 |
-
Br()
|
527 |
),
|
528 |
cls="grid grid-rows-[auto_0px] content-start gap-y-3",
|
529 |
),
|
530 |
id=f"result-text-full-{idx}",
|
531 |
cls="grid gap-y-3 p-8 border border-dashed",
|
532 |
),
|
533 |
-
Div(
|
534 |
-
|
|
|
|
|
535 |
),
|
536 |
-
cls="grid grid-rows-[1fr_1fr] gap-y-8 p-8 text-sm",
|
537 |
),
|
538 |
cls="grid bg-background",
|
539 |
),
|
@@ -545,11 +575,13 @@ def SearchResult(results: list, query_id: Optional[str] = None):
|
|
545 |
id=f"image-text-columns-{idx}",
|
546 |
cls="relative grid grid-cols-1 border-t grid-image-text-columns",
|
547 |
),
|
548 |
-
cls="grid grid-cols-1 grid-rows-[
|
549 |
),
|
550 |
)
|
551 |
|
552 |
-
return
|
|
|
|
|
553 |
*result_items,
|
554 |
image_swapping,
|
555 |
toggle_text_content,
|
@@ -559,22 +591,37 @@ def SearchResult(results: list, query_id: Optional[str] = None):
|
|
559 |
)
|
560 |
|
561 |
|
562 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
563 |
return Div(
|
564 |
Div("AI-response (Gemini-8B)", cls="text-xl font-semibold p-5"),
|
565 |
Div(
|
566 |
Div(
|
567 |
-
|
568 |
-
LoadingSkeleton(),
|
569 |
-
hx_ext="sse",
|
570 |
-
sse_connect=f"/get-message?query_id={query_id}&query={quote_plus(query)}",
|
571 |
-
sse_swap="message",
|
572 |
-
sse_close="close",
|
573 |
-
hx_swap="innerHTML",
|
574 |
-
),
|
575 |
),
|
576 |
id="chat-messages",
|
577 |
cls="overflow-auto min-h-0 grid items-end px-5",
|
578 |
),
|
|
|
579 |
cls="h-full grid grid-rows-[auto_1fr_auto] min-h-0 gap-3",
|
580 |
)
|
|
|
1 |
from typing import Optional
|
2 |
from urllib.parse import quote_plus
|
3 |
|
4 |
+
from fasthtml.components import H1, H2, H3, Br, Div, Form, Img, NotStr, P, Span
|
5 |
from fasthtml.xtend import A, Script
|
6 |
from lucide_fasthtml import Lucide
|
7 |
from shad4fast import Badge, Button, Input, Label, RadioGroup, RadioGroupItem, Separator
|
|
|
154 |
name="query",
|
155 |
value=query_value,
|
156 |
id="search-input",
|
157 |
+
cls="text-base pl-10 border-transparent ring-offset-transparent ring-0 focus-visible:ring-transparent bg-white dark:bg-background awesomplete",
|
158 |
data_list="#suggestions",
|
159 |
style="font-size: 1rem",
|
160 |
autofocus=True,
|
|
|
366 |
)
|
367 |
|
368 |
|
369 |
+
def SearchInfo(search_time, total_count):
|
370 |
+
return (
|
371 |
+
Div(
|
372 |
+
NotStr(
|
373 |
+
f"<span>Found <strong>{total_count}</strong> results in <strong>{search_time}</strong> seconds.</span>"
|
374 |
+
),
|
375 |
+
cls="grid bg-background border-t text-sm text-center p-3",
|
376 |
+
),
|
377 |
+
)
|
378 |
+
|
379 |
+
|
380 |
+
def SearchResult(
|
381 |
+
results: list,
|
382 |
+
query: str, query_id: Optional[str] = None,
|
383 |
+
search_time: float = 0,
|
384 |
+
total_count: int = 0,
|
385 |
+
):
|
386 |
if not results:
|
387 |
return Div(
|
388 |
P(
|
|
|
392 |
cls="grid p-10",
|
393 |
)
|
394 |
|
395 |
+
doc_ids = []
|
396 |
# Otherwise, display the search results
|
397 |
result_items = []
|
398 |
for idx, result in enumerate(results):
|
399 |
fields = result["fields"] # Extract the 'fields' part of each result
|
400 |
+
doc_id = fields["id"]
|
401 |
+
doc_ids.append(doc_id)
|
402 |
blur_image_base64 = f"data:image/jpeg;base64,{fields['blur_image']}"
|
403 |
|
404 |
sim_map_fields = {
|
|
|
491 |
Div(
|
492 |
Img(
|
493 |
src=blur_image_base64,
|
494 |
+
hx_get=f"/full_image?doc_id={doc_id}",
|
495 |
style="backdrop-filter: blur(5px);",
|
496 |
hx_trigger="load",
|
497 |
hx_swap="outerHTML",
|
|
|
512 |
),
|
513 |
Div(
|
514 |
Div(
|
515 |
+
A(
|
516 |
+
Lucide(icon="external-link", size="18"),
|
517 |
+
f"PDF Source (Page {fields['page_number']})",
|
518 |
+
href=f"{fields['url']}#page={fields['page_number'] + 1}",
|
519 |
+
target="_blank",
|
520 |
+
cls="flex items-center gap-1.5 font-mono bold text-sm",
|
521 |
),
|
522 |
cls="flex items-center justify-end",
|
523 |
),
|
|
|
526 |
Div(
|
527 |
Div(
|
528 |
Div(
|
529 |
+
H3(
|
530 |
+
"Dynamic summary",
|
531 |
+
cls="text-base font-semibold",
|
532 |
+
),
|
533 |
P(
|
534 |
NotStr(fields.get("snippet", "")),
|
535 |
cls="text-highlight text-muted-foreground",
|
|
|
542 |
Div(
|
543 |
Div(
|
544 |
Div(
|
545 |
+
H3(
|
546 |
+
"Full text",
|
547 |
+
cls="text-base font-semibold",
|
548 |
+
),
|
549 |
Div(
|
550 |
P(
|
551 |
NotStr(fields.get("text", "")),
|
552 |
cls="text-highlight text-muted-foreground",
|
553 |
),
|
554 |
+
Br(),
|
555 |
),
|
556 |
cls="grid grid-rows-[auto_0px] content-start gap-y-3",
|
557 |
),
|
558 |
id=f"result-text-full-{idx}",
|
559 |
cls="grid gap-y-3 p-8 border border-dashed",
|
560 |
),
|
561 |
+
Div(
|
562 |
+
cls="absolute inset-x-0 bottom-0 bg-gradient-to-t from-[#fcfcfd] dark:from-[#1c2024] pt-[7%]"
|
563 |
+
),
|
564 |
+
cls="relative grid",
|
565 |
),
|
566 |
+
cls="grid grid-rows-[1fr_1fr] xl:grid-rows-[1fr_2fr] gap-y-8 p-8 text-sm",
|
567 |
),
|
568 |
cls="grid bg-background",
|
569 |
),
|
|
|
575 |
id=f"image-text-columns-{idx}",
|
576 |
cls="relative grid grid-cols-1 border-t grid-image-text-columns",
|
577 |
),
|
578 |
+
cls="grid grid-cols-1 grid-rows-[auto_auto_1fr]",
|
579 |
),
|
580 |
)
|
581 |
|
582 |
+
return [
|
583 |
+
Div(
|
584 |
+
SearchInfo(search_time, total_count),
|
585 |
*result_items,
|
586 |
image_swapping,
|
587 |
toggle_text_content,
|
|
|
591 |
)
|
592 |
|
593 |
|
594 |
+
,
|
595 |
+
Div(
|
596 |
+
ChatResult(query_id=query_id, query=query, doc_ids=doc_ids),
|
597 |
+
hx_swap_oob="true",
|
598 |
+
id="chat_messages",
|
599 |
+
),
|
600 |
+
]
|
601 |
+
|
602 |
+
|
603 |
+
def ChatResult(query_id: str, query: str, doc_ids: Optional[list] = None):
|
604 |
+
messages = Div(LoadingSkeleton())
|
605 |
+
|
606 |
+
if doc_ids:
|
607 |
+
messages = Div(
|
608 |
+
LoadingSkeleton(),
|
609 |
+
hx_ext="sse",
|
610 |
+
sse_connect=f"/get-message?query_id={query_id}&doc_ids={','.join(doc_ids)}&query={quote_plus(query)}",
|
611 |
+
sse_swap="message",
|
612 |
+
sse_close="close",
|
613 |
+
hx_swap="innerHTML",
|
614 |
+
)
|
615 |
+
|
616 |
return Div(
|
617 |
Div("AI-response (Gemini-8B)", cls="text-xl font-semibold p-5"),
|
618 |
Div(
|
619 |
Div(
|
620 |
+
messages,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
621 |
),
|
622 |
id="chat-messages",
|
623 |
cls="overflow-auto min-h-0 grid items-end px-5",
|
624 |
),
|
625 |
+
id="chat_messages",
|
626 |
cls="h-full grid grid-rows-[auto_1fr_auto] min-h-0 gap-3",
|
627 |
)
|
frontend/layout.py
CHANGED
@@ -151,7 +151,7 @@ def Links():
|
|
151 |
)
|
152 |
|
153 |
|
154 |
-
def Layout(*c, **kwargs):
|
155 |
return (
|
156 |
Title("Visual Retrieval ColPali"),
|
157 |
Body(
|
@@ -162,6 +162,7 @@ def Layout(*c, **kwargs):
|
|
162 |
),
|
163 |
*c,
|
164 |
**kwargs,
|
|
|
165 |
cls="grid grid-rows-[minmax(0,55px)_minmax(0,1fr)] min-h-0",
|
166 |
),
|
167 |
layout_script,
|
|
|
151 |
)
|
152 |
|
153 |
|
154 |
+
def Layout(*c, is_home=False, **kwargs):
|
155 |
return (
|
156 |
Title("Visual Retrieval ColPali"),
|
157 |
Body(
|
|
|
162 |
),
|
163 |
*c,
|
164 |
**kwargs,
|
165 |
+
data_is_home=str(is_home).lower(),
|
166 |
cls="grid grid-rows-[minmax(0,55px)_minmax(0,1fr)] min-h-0",
|
167 |
),
|
168 |
layout_script,
|
globals.css
CHANGED
@@ -5,58 +5,57 @@
|
|
5 |
|
6 |
@layer base {
|
7 |
:root {
|
8 |
-
--background:
|
9 |
-
--foreground:
|
10 |
-
--card:
|
11 |
-
--card-foreground:
|
12 |
-
--popover:
|
13 |
-
--popover-foreground:
|
14 |
-
--primary:
|
15 |
-
--primary-foreground:
|
16 |
-
--secondary:
|
17 |
-
--secondary-foreground:
|
18 |
-
--muted:
|
19 |
-
--muted-foreground:
|
20 |
-
--accent:
|
21 |
-
--accent-foreground:
|
22 |
-
--destructive:
|
23 |
-
--destructive-foreground:
|
24 |
-
--border:
|
25 |
-
--input:
|
26 |
-
--ring:
|
27 |
-
--
|
28 |
-
--chart-
|
29 |
-
--chart-
|
30 |
-
--chart-
|
31 |
-
--chart-
|
32 |
-
--chart-5: 27 87% 67%;
|
33 |
}
|
34 |
|
35 |
.dark {
|
36 |
-
--background:
|
37 |
-
--foreground:
|
38 |
-
--card:
|
39 |
-
--card-foreground:
|
40 |
-
--popover:
|
41 |
-
--popover-foreground:
|
42 |
-
--primary:
|
43 |
-
--primary-foreground:
|
44 |
-
--secondary:
|
45 |
-
--secondary-foreground:
|
46 |
-
--muted:
|
47 |
-
--muted-foreground:
|
48 |
-
--accent:
|
49 |
-
--accent-foreground:
|
50 |
-
--destructive:
|
51 |
-
--destructive-foreground:
|
52 |
-
--border:
|
53 |
-
--input:
|
54 |
-
--ring:
|
55 |
-
--chart-1:
|
56 |
-
--chart-2:
|
57 |
-
--chart-3:
|
58 |
-
--chart-4:
|
59 |
-
--chart-5:
|
60 |
}
|
61 |
}
|
62 |
|
@@ -193,6 +192,16 @@ header {
|
|
193 |
grid-column: 1/-1;
|
194 |
}
|
195 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
196 |
main {
|
197 |
overflow: auto;
|
198 |
}
|
@@ -236,14 +245,19 @@ aside {
|
|
236 |
}
|
237 |
|
238 |
.awesomplete > ul {
|
239 |
-
@apply text-sm space-y-
|
240 |
margin: 0;
|
241 |
border-top: none;
|
242 |
border-left: 1px solid hsl(var(--input));
|
243 |
border-right: 1px solid hsl(var(--input));
|
244 |
border-bottom: 1px solid hsl(var(--input));
|
245 |
border-radius: 0 0 calc(var(--radius) - 2px) calc(var(--radius) - 2px);
|
246 |
-
background:
|
|
|
|
|
|
|
|
|
|
|
247 |
box-shadow: none;
|
248 |
text-shadow: none;
|
249 |
}
|
|
|
5 |
|
6 |
@layer base {
|
7 |
:root {
|
8 |
+
--background: 240 20% 99%; /* 1 */
|
9 |
+
--foreground: 210 13% 13%; /* 12 */
|
10 |
+
--card: 240 20% 99%; /* 1 */
|
11 |
+
--card-foreground: 210 13% 13%; /* 12 */
|
12 |
+
--popover: 240 20% 99%; /* 1 */
|
13 |
+
--popover-foreground: 210 13% 13%; /* 12 */
|
14 |
+
--primary: 210 13% 13%; /* 12 */
|
15 |
+
--primary-foreground: 240 20% 98%; /* 2 */
|
16 |
+
--secondary: 240 11% 95%; /* 3 */
|
17 |
+
--secondary-foreground: 210 13% 13%; /* 12 */
|
18 |
+
--muted: 240 11% 95%; /* 3 */
|
19 |
+
--muted-foreground: 220 6% 40%; /* 11 */
|
20 |
+
--accent: 240 11% 95%; /* 3 */
|
21 |
+
--accent-foreground: 210 13% 13%; /* 12 */
|
22 |
+
--destructive: 358 75% 59%; /* 9 - red */
|
23 |
+
--destructive-foreground: 240 20% 98%; /* 2 */
|
24 |
+
--border: 240 10% 86%; /* 6 */
|
25 |
+
--input: 240 10% 86%; /* 6 */
|
26 |
+
--ring: 210 13% 13%; /* 12 */
|
27 |
+
--chart-1: 10 78% 54%; /* 9 - tomato */
|
28 |
+
--chart-2: 173 80% 36%; /* 9 - teal */
|
29 |
+
--chart-3: 206 100% 50%; /* 9 - blue */
|
30 |
+
--chart-4: 42 100% 62%; /* 9 - amber */
|
31 |
+
--chart-5: 23 93% 53%; /* 9 - orange */
|
|
|
32 |
}
|
33 |
|
34 |
.dark {
|
35 |
+
--background: 240 6% 7%; /* 1 */
|
36 |
+
--foreground: 220 9% 94%; /* 12 */
|
37 |
+
--card: 240 6% 7%; /* 1 */
|
38 |
+
--card-foreground: 220 9% 94%; /* 12 */
|
39 |
+
--popover: 240 6% 7%; /* 1 */
|
40 |
+
--popover-foreground: 220 9% 94%; /* 12 */
|
41 |
+
--primary: 220 9% 94%; /* 12 */
|
42 |
+
--primary-foreground: 220 6% 10%; /* 2 */
|
43 |
+
--secondary: 225 6% 14%; /* 3 */
|
44 |
+
--secondary-foreground: 220 9% 94%; /* 12 */
|
45 |
+
--muted: 225 6% 14%; /* 3 */
|
46 |
+
--muted-foreground: 216 7% 71%; /* 11 */
|
47 |
+
--accent: 225 6% 14%; /* 3 */
|
48 |
+
--accent-foreground: 220 9% 94%; /* 12 */
|
49 |
+
--destructive: 358 75% 59%; /* 9 - red */
|
50 |
+
--destructive-foreground: 220 9% 94%; /* 12 */
|
51 |
+
--border: 213 8% 23%; /* 6 */
|
52 |
+
--input: 213 8% 23%; /* 6 */
|
53 |
+
--ring: 220 9% 94%; /* 12 */
|
54 |
+
--chart-1: 10 78% 54%; /* 9 - tomato */
|
55 |
+
--chart-2: 173 80% 36%; /* 9 - teal */
|
56 |
+
--chart-3: 206 100% 50%; /* 9 - blue */
|
57 |
+
--chart-4: 42 100% 62%; /* 9 - amber */
|
58 |
+
--chart-5: 23 93% 53%; /* 9 - orange */
|
59 |
}
|
60 |
}
|
61 |
|
|
|
192 |
grid-column: 1/-1;
|
193 |
}
|
194 |
|
195 |
+
body {
|
196 |
+
&[data-is-home="true"] {
|
197 |
+
background: radial-gradient(circle at 50% 100%, #fcfcfd, #fcfcfd, #fdfdfe, #fdfdfe, #fefefe, #fefefe, #ffffff, #ffffff);
|
198 |
+
|
199 |
+
.dark & {
|
200 |
+
background: radial-gradient(circle at 50% 50%, #272a2d, #242629, #212326, #1e1f22, #1b1c1e, #18181b, #151517, #111113);
|
201 |
+
}
|
202 |
+
}
|
203 |
+
}
|
204 |
+
|
205 |
main {
|
206 |
overflow: auto;
|
207 |
}
|
|
|
245 |
}
|
246 |
|
247 |
.awesomplete > ul {
|
248 |
+
@apply text-sm space-y-1;
|
249 |
margin: 0;
|
250 |
border-top: none;
|
251 |
border-left: 1px solid hsl(var(--input));
|
252 |
border-right: 1px solid hsl(var(--input));
|
253 |
border-bottom: 1px solid hsl(var(--input));
|
254 |
border-radius: 0 0 calc(var(--radius) - 2px) calc(var(--radius) - 2px);
|
255 |
+
background: white;
|
256 |
+
|
257 |
+
.dark & {
|
258 |
+
background: hsl(var(--background));
|
259 |
+
}
|
260 |
+
|
261 |
box-shadow: none;
|
262 |
text-shadow: none;
|
263 |
}
|
icons.py
CHANGED
@@ -1 +1 @@
|
|
1 |
-
ICONS = {"chevrons-right": "<path d=\"m6 17 5-5-5-5\"></path><path d=\"m13 17 5-5-5-5\"></path>", "moon": "<path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\"></path>", "sun": "<circle cx=\"12\" cy=\"12\" r=\"4\"></circle><path d=\"M12 2v2\"></path><path d=\"M12 20v2\"></path><path d=\"m4.93 4.93 1.41 1.41\"></path><path d=\"m17.66 17.66 1.41 1.41\"></path><path d=\"M2 12h2\"></path><path d=\"M20 12h2\"></path><path d=\"m6.34 17.66-1.41 1.41\"></path><path d=\"m19.07 4.93-1.41 1.41\"></path>", "github": "<path d=\"M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4\"></path><path d=\"M9 18c-4.51 2-5-2-7-2\"></path>", "slack": "<rect height=\"8\" rx=\"1.5\" width=\"3\" x=\"13\" y=\"2\"></rect><path d=\"M19 8.5V10h1.5A1.5 1.5 0 1 0 19 8.5\"></path><rect height=\"8\" rx=\"1.5\" width=\"3\" x=\"8\" y=\"14\"></rect><path d=\"M5 15.5V14H3.5A1.5 1.5 0 1 0 5 15.5\"></path><rect height=\"3\" rx=\"1.5\" width=\"8\" x=\"14\" y=\"13\"></rect><path d=\"M15.5 19H14v1.5a1.5 1.5 0 1 0 1.5-1.5\"></path><rect height=\"3\" rx=\"1.5\" width=\"8\" x=\"2\" y=\"8\"></rect><path d=\"M8.5 5H10V3.5A1.5 1.5 0 1 0 8.5 5\"></path>", "settings": "<path d=\"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\"></path><circle cx=\"12\" cy=\"12\" r=\"3\"></circle>", "arrow-right": "<path d=\"M5 12h14\"></path><path d=\"m12 5 7 7-7 7\"></path>", "search": "<circle cx=\"11\" cy=\"11\" r=\"8\"></circle><path d=\"m21 21-4.3-4.3\"></path>", "file-search": "<path d=\"M14 2v4a2 2 0 0 0 2 2h4\"></path><path d=\"M4.268 21a2 2 0 0 0 1.727 1H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v3\"></path><path d=\"m9 18-1.5-1.5\"></path><circle cx=\"5\" cy=\"14\" r=\"3\"></circle>", "message-circle-question": "<path d=\"M7.9 20A9 9 0 1 0 4 16.1L2 22Z\"></path><path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\"></path><path d=\"M12 17h.01\"></path>", "text-search": "<path d=\"M21 6H3\"></path><path d=\"M10 12H3\"></path><path d=\"M10 18H3\"></path><circle cx=\"17\" cy=\"15\" r=\"3\"></circle><path d=\"m21 19-1.9-1.9\"></path>", "maximize": "<path d=\"M8 3H5a2 2 0 0 0-2 2v3\"></path><path d=\"M21 8V5a2 2 0 0 0-2-2h-3\"></path><path d=\"M3 16v3a2 2 0 0 0 2 2h3\"></path><path d=\"M16 21h3a2 2 0 0 0 2-2v-3\"></path>", "expand": "<path d=\"m21 21-6-6m6 6v-4.8m0 4.8h-4.8\"></path><path d=\"M3 16.2V21m0 0h4.8M3 21l6-6\"></path><path d=\"M21 7.8V3m0 0h-4.8M21 3l-6 6\"></path><path d=\"M3 7.8V3m0 0h4.8M3 3l6 6\"></path>", "fullscreen": "<path d=\"M3 7V5a2 2 0 0 1 2-2h2\"></path><path d=\"M17 3h2a2 2 0 0 1 2 2v2\"></path><path d=\"M21 17v2a2 2 0 0 1-2 2h-2\"></path><path d=\"M7 21H5a2 2 0 0 1-2-2v-2\"></path><rect height=\"8\" rx=\"1\" width=\"10\" x=\"7\" y=\"8\"></rect>", "images": "<path d=\"M18 22H4a2 2 0 0 1-2-2V6\"></path><path d=\"m22 13-1.296-1.296a2.41 2.41 0 0 0-3.408 0L11 18\"></path><circle cx=\"12\" cy=\"8\" r=\"2\"></circle><rect height=\"16\" rx=\"2\" width=\"16\" x=\"6\" y=\"2\"></rect>", "circle": "<circle cx=\"12\" cy=\"12\" r=\"10\"></circle>", "loader-circle": "<path d=\"M21 12a9 9 0 1 1-6.219-8.56\"></path>", "file-text": "<path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"></path><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"></path><path d=\"M10 9H8\"></path><path d=\"M16 13H8\"></path><path d=\"M16 17H8\"></path>", "file-question": "<path d=\"M12 17h.01\"></path><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z\"></path><path d=\"M9.1 9a3 3 0 0 1 5.82 1c0 2-3 3-3 3\"></path>"}
|
|
|
1 |
+
ICONS = {"chevrons-right": "<path d=\"m6 17 5-5-5-5\"></path><path d=\"m13 17 5-5-5-5\"></path>", "moon": "<path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\"></path>", "sun": "<circle cx=\"12\" cy=\"12\" r=\"4\"></circle><path d=\"M12 2v2\"></path><path d=\"M12 20v2\"></path><path d=\"m4.93 4.93 1.41 1.41\"></path><path d=\"m17.66 17.66 1.41 1.41\"></path><path d=\"M2 12h2\"></path><path d=\"M20 12h2\"></path><path d=\"m6.34 17.66-1.41 1.41\"></path><path d=\"m19.07 4.93-1.41 1.41\"></path>", "github": "<path d=\"M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4\"></path><path d=\"M9 18c-4.51 2-5-2-7-2\"></path>", "slack": "<rect height=\"8\" rx=\"1.5\" width=\"3\" x=\"13\" y=\"2\"></rect><path d=\"M19 8.5V10h1.5A1.5 1.5 0 1 0 19 8.5\"></path><rect height=\"8\" rx=\"1.5\" width=\"3\" x=\"8\" y=\"14\"></rect><path d=\"M5 15.5V14H3.5A1.5 1.5 0 1 0 5 15.5\"></path><rect height=\"3\" rx=\"1.5\" width=\"8\" x=\"14\" y=\"13\"></rect><path d=\"M15.5 19H14v1.5a1.5 1.5 0 1 0 1.5-1.5\"></path><rect height=\"3\" rx=\"1.5\" width=\"8\" x=\"2\" y=\"8\"></rect><path d=\"M8.5 5H10V3.5A1.5 1.5 0 1 0 8.5 5\"></path>", "settings": "<path d=\"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\"></path><circle cx=\"12\" cy=\"12\" r=\"3\"></circle>", "arrow-right": "<path d=\"M5 12h14\"></path><path d=\"m12 5 7 7-7 7\"></path>", "search": "<circle cx=\"11\" cy=\"11\" r=\"8\"></circle><path d=\"m21 21-4.3-4.3\"></path>", "file-search": "<path d=\"M14 2v4a2 2 0 0 0 2 2h4\"></path><path d=\"M4.268 21a2 2 0 0 0 1.727 1H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v3\"></path><path d=\"m9 18-1.5-1.5\"></path><circle cx=\"5\" cy=\"14\" r=\"3\"></circle>", "message-circle-question": "<path d=\"M7.9 20A9 9 0 1 0 4 16.1L2 22Z\"></path><path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\"></path><path d=\"M12 17h.01\"></path>", "text-search": "<path d=\"M21 6H3\"></path><path d=\"M10 12H3\"></path><path d=\"M10 18H3\"></path><circle cx=\"17\" cy=\"15\" r=\"3\"></circle><path d=\"m21 19-1.9-1.9\"></path>", "maximize": "<path d=\"M8 3H5a2 2 0 0 0-2 2v3\"></path><path d=\"M21 8V5a2 2 0 0 0-2-2h-3\"></path><path d=\"M3 16v3a2 2 0 0 0 2 2h3\"></path><path d=\"M16 21h3a2 2 0 0 0 2-2v-3\"></path>", "expand": "<path d=\"m21 21-6-6m6 6v-4.8m0 4.8h-4.8\"></path><path d=\"M3 16.2V21m0 0h4.8M3 21l6-6\"></path><path d=\"M21 7.8V3m0 0h-4.8M21 3l-6 6\"></path><path d=\"M3 7.8V3m0 0h4.8M3 3l6 6\"></path>", "fullscreen": "<path d=\"M3 7V5a2 2 0 0 1 2-2h2\"></path><path d=\"M17 3h2a2 2 0 0 1 2 2v2\"></path><path d=\"M21 17v2a2 2 0 0 1-2 2h-2\"></path><path d=\"M7 21H5a2 2 0 0 1-2-2v-2\"></path><rect height=\"8\" rx=\"1\" width=\"10\" x=\"7\" y=\"8\"></rect>", "images": "<path d=\"M18 22H4a2 2 0 0 1-2-2V6\"></path><path d=\"m22 13-1.296-1.296a2.41 2.41 0 0 0-3.408 0L11 18\"></path><circle cx=\"12\" cy=\"8\" r=\"2\"></circle><rect height=\"16\" rx=\"2\" width=\"16\" x=\"6\" y=\"2\"></rect>", "circle": "<circle cx=\"12\" cy=\"12\" r=\"10\"></circle>", "loader-circle": "<path d=\"M21 12a9 9 0 1 1-6.219-8.56\"></path>", "file-text": "<path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"></path><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"></path><path d=\"M10 9H8\"></path><path d=\"M16 13H8\"></path><path d=\"M16 17H8\"></path>", "file-question": "<path d=\"M12 17h.01\"></path><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z\"></path><path d=\"M9.1 9a3 3 0 0 1 5.82 1c0 2-3 3-3 3\"></path>", "external-link": "<path d=\"M15 3h6v6\"></path><path d=\"M10 14 21 3\"></path><path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"></path>"}
|
main.py
CHANGED
@@ -1,36 +1,37 @@
|
|
1 |
import asyncio
|
|
|
2 |
import os
|
3 |
import time
|
4 |
-
from pathlib import Path
|
5 |
-
from concurrent.futures import ThreadPoolExecutor
|
6 |
import uuid
|
|
|
|
|
|
|
7 |
import google.generativeai as genai
|
|
|
8 |
from fasthtml.common import (
|
|
|
9 |
Div,
|
|
|
|
|
10 |
Img,
|
|
|
|
|
11 |
Main,
|
12 |
P,
|
13 |
-
Script,
|
14 |
-
Link,
|
15 |
-
fast_app,
|
16 |
-
HighlightJS,
|
17 |
-
FileResponse,
|
18 |
RedirectResponse,
|
19 |
-
|
20 |
StreamingResponse,
|
21 |
-
|
22 |
serve,
|
23 |
)
|
|
|
24 |
from shad4fast import ShadHead
|
25 |
from vespa.application import Vespa
|
26 |
-
import base64
|
27 |
-
from fastcore.parallel import threaded
|
28 |
-
from PIL import Image
|
29 |
|
30 |
-
from backend.colpali import
|
31 |
-
from backend.modelmanager import ModelManager
|
32 |
from backend.vespa_app import VespaQueryClient
|
33 |
from frontend.app import (
|
|
|
34 |
ChatResult,
|
35 |
Home,
|
36 |
Search,
|
@@ -38,7 +39,6 @@ from frontend.app import (
|
|
38 |
SearchResult,
|
39 |
SimMapButtonPoll,
|
40 |
SimMapButtonReady,
|
41 |
-
AboutThisDemo,
|
42 |
)
|
43 |
from frontend.layout import Layout
|
44 |
|
@@ -90,10 +90,10 @@ thread_pool = ThreadPoolExecutor()
|
|
90 |
genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
|
91 |
GEMINI_SYSTEM_PROMPT = """If the user query is a question, try your best to answer it based on the provided images.
|
92 |
If the user query can not be interpreted as a question, or if the answer to the query can not be inferred from the images,
|
93 |
-
answer with the exact phrase "I am sorry, I
|
94 |
Your response should be HTML formatted, but only simple tags, such as <b>. <p>, <i>, <br> <ul> and <li> are allowed. No HTML tables.
|
95 |
This means that newlines will be replaced with <br> tags, bold text will be enclosed in <b> tags, and so on.
|
96 |
-
|
97 |
"""
|
98 |
gemini_model = genai.GenerativeModel(
|
99 |
"gemini-1.5-flash-8b", system_instruction=GEMINI_SYSTEM_PROMPT
|
@@ -107,7 +107,7 @@ os.makedirs(SIM_MAP_DIR, exist_ok=True)
|
|
107 |
|
108 |
@app.on_event("startup")
|
109 |
def load_model_on_startup():
|
110 |
-
app.
|
111 |
return
|
112 |
|
113 |
|
@@ -131,7 +131,7 @@ def serve_static(filepath: str):
|
|
131 |
def get(session):
|
132 |
if "session_id" not in session:
|
133 |
session["session_id"] = str(uuid.uuid4())
|
134 |
-
return Layout(Main(Home()))
|
135 |
|
136 |
|
137 |
@rt("/about-this-demo")
|
@@ -140,19 +140,16 @@ def get():
|
|
140 |
|
141 |
|
142 |
@rt("/search")
|
143 |
-
def get(request):
|
144 |
-
|
145 |
-
query_value = request.query_params.get("query", "").strip()
|
146 |
-
ranking_value = request.query_params.get("ranking", "nn+colpali")
|
147 |
-
print("/search: Fetching results for ranking_value:", ranking_value)
|
148 |
|
149 |
# Always render the SearchBox first
|
150 |
-
if not
|
151 |
# Show SearchBox and a message for missing query
|
152 |
return Layout(
|
153 |
Main(
|
154 |
Div(
|
155 |
-
SearchBox(query_value=
|
156 |
Div(
|
157 |
P(
|
158 |
"No query provided. Please enter a query.",
|
@@ -165,35 +162,17 @@ def get(request):
|
|
165 |
)
|
166 |
)
|
167 |
# Generate a unique query_id based on the query and ranking value
|
168 |
-
query_id = generate_query_id(
|
169 |
# Show the loading message if a query is provided
|
170 |
return Layout(
|
171 |
Main(Search(request), data_overlayscrollbars_initialize=True, cls="border-t"),
|
172 |
Aside(
|
173 |
-
ChatResult(query_id=query_id, query=
|
174 |
cls="border-t border-l hidden md:block",
|
175 |
),
|
176 |
) # Show SearchBox and Loading message initially
|
177 |
|
178 |
|
179 |
-
@rt("/fetch_results2")
|
180 |
-
def get(query: str, ranking: str):
|
181 |
-
# 1. Get the results from Vespa (without sim_maps and full_images)
|
182 |
-
# Call search-endpoint in Vespa sync.
|
183 |
-
|
184 |
-
# 2. Kick off tasks to fetch sim_maps and full_images
|
185 |
-
# Sim maps - call search endpoint async.
|
186 |
-
# (A) New rank_profile that does not calculate sim_maps.
|
187 |
-
# (A) Make vespa endpoints take select_fields as a parameter.
|
188 |
-
# One sim map per image per token.
|
189 |
-
# the filename query_id_result_idx_token_idx.png
|
190 |
-
# Full image. based on the doc_id.
|
191 |
-
# Each of these tasks saves to disk.
|
192 |
-
# Need a cleanup task to delete old files.
|
193 |
-
# Polling endpoints for sim_maps and full_images checks if file exists and returns it.
|
194 |
-
pass
|
195 |
-
|
196 |
-
|
197 |
@rt("/fetch_results")
|
198 |
async def get(session, request, query: str, ranking: str):
|
199 |
if "hx-request" not in request.headers:
|
@@ -203,9 +182,10 @@ async def get(session, request, query: str, ranking: str):
|
|
203 |
query_id = generate_query_id(query, ranking)
|
204 |
print(f"Query id in /fetch_results: {query_id}")
|
205 |
# Run the embedding and query against Vespa app
|
206 |
-
|
207 |
-
|
208 |
-
|
|
|
209 |
|
210 |
start = time.perf_counter()
|
211 |
# Fetch real search results from Vespa
|
@@ -219,15 +199,20 @@ async def get(session, request, query: str, ranking: str):
|
|
219 |
print(
|
220 |
f"Search results fetched in {end - start:.2f} seconds, Vespa says searchtime was {result['timing']['searchtime']} seconds"
|
221 |
)
|
|
|
|
|
|
|
222 |
search_results = vespa_app.results_to_search_results(result, idx_to_token)
|
|
|
223 |
get_and_store_sim_maps(
|
224 |
query_id=query_id,
|
225 |
query=query,
|
226 |
q_embs=q_embs,
|
227 |
ranking=ranking,
|
228 |
idx_to_token=idx_to_token,
|
|
|
229 |
)
|
230 |
-
return SearchResult(search_results, query_id)
|
231 |
|
232 |
|
233 |
def get_results_children(result):
|
@@ -247,7 +232,9 @@ async def poll_vespa_keepalive():
|
|
247 |
|
248 |
|
249 |
@threaded
|
250 |
-
def get_and_store_sim_maps(
|
|
|
|
|
251 |
ranking_sim = ranking + "_sim"
|
252 |
vespa_sim_maps = vespa_app.get_sim_maps_from_query(
|
253 |
query=query,
|
@@ -255,9 +242,7 @@ def get_and_store_sim_maps(query_id, query: str, q_embs, ranking, idx_to_token):
|
|
255 |
ranking=ranking_sim,
|
256 |
idx_to_token=idx_to_token,
|
257 |
)
|
258 |
-
img_paths = [
|
259 |
-
IMG_DIR / f"{query_id}_{idx}.jpg" for idx in range(len(vespa_sim_maps))
|
260 |
-
]
|
261 |
# All images should be downloaded, but best to wait 5 secs
|
262 |
max_wait = 5
|
263 |
start_time = time.time()
|
@@ -269,10 +254,7 @@ def get_and_store_sim_maps(query_id, query: str, q_embs, ranking, idx_to_token):
|
|
269 |
if not all([os.path.exists(img_path) for img_path in img_paths]):
|
270 |
print(f"Images not ready in 5 seconds for query_id: {query_id}")
|
271 |
return False
|
272 |
-
sim_map_generator = gen_similarity_maps(
|
273 |
-
model=app.manager.model,
|
274 |
-
processor=app.manager.processor,
|
275 |
-
device=app.manager.device,
|
276 |
query=query,
|
277 |
query_embs=q_embs,
|
278 |
token_idx_map=idx_to_token,
|
@@ -312,17 +294,17 @@ async def get_sim_map(query_id: str, idx: int, token: str, token_idx: int):
|
|
312 |
|
313 |
|
314 |
@app.get("/full_image")
|
315 |
-
async def full_image(
|
316 |
"""
|
317 |
Endpoint to get the full quality image for a given result id.
|
318 |
"""
|
319 |
-
img_path = IMG_DIR / f"{
|
320 |
if not os.path.exists(img_path):
|
321 |
-
image_data = await vespa_app.get_full_image_from_vespa(
|
322 |
# image data is base 64 encoded string. Save it to disk as jpg.
|
323 |
with open(img_path, "wb") as f:
|
324 |
f.write(base64.b64decode(image_data))
|
325 |
-
print(f"Full image saved to disk for
|
326 |
else:
|
327 |
with open(img_path, "rb") as f:
|
328 |
image_data = base64.b64encode(f.read()).decode("utf-8")
|
@@ -334,8 +316,9 @@ async def full_image(docid: str, query_id: str, idx: int):
|
|
334 |
|
335 |
|
336 |
@rt("/suggestions")
|
337 |
-
async def get_suggestions(
|
338 |
-
|
|
|
339 |
|
340 |
if query:
|
341 |
suggestions = await vespa_app.get_suggestions(query)
|
@@ -345,15 +328,20 @@ async def get_suggestions(request):
|
|
345 |
return JSONResponse({"suggestions": []})
|
346 |
|
347 |
|
348 |
-
async def message_generator(query_id: str, query: str):
|
349 |
-
|
|
|
350 |
num_images = 3 # Number of images before firing chat request
|
351 |
max_wait = 10 # seconds
|
352 |
start_time = time.time()
|
353 |
# Check if full images are ready on disk
|
354 |
-
while
|
|
|
|
|
|
|
355 |
for idx in range(num_images):
|
356 |
-
|
|
|
357 |
print(
|
358 |
f"Message generator: Full image not ready for query_id: {query_id}, idx: {idx}"
|
359 |
)
|
@@ -362,12 +350,14 @@ async def message_generator(query_id: str, query: str):
|
|
362 |
print(
|
363 |
f"Message generator: image ready for query_id: {query_id}, idx: {idx}"
|
364 |
)
|
365 |
-
images
|
366 |
await asyncio.sleep(0.2)
|
|
|
|
|
367 |
# yield message with number of images ready
|
368 |
-
yield f"event: message\ndata: Generating response based on {len(images)} images
|
369 |
if not images:
|
370 |
-
yield "event: message\ndata:
|
371 |
yield "event: close\ndata: \n\n"
|
372 |
return
|
373 |
|
@@ -388,9 +378,9 @@ async def message_generator(query_id: str, query: str):
|
|
388 |
|
389 |
|
390 |
@app.get("/get-message")
|
391 |
-
async def get_message(query_id: str, query: str):
|
392 |
return StreamingResponse(
|
393 |
-
message_generator(query_id=query_id, query=query),
|
394 |
media_type="text/event-stream",
|
395 |
)
|
396 |
|
|
|
1 |
import asyncio
|
2 |
+
import base64
|
3 |
import os
|
4 |
import time
|
|
|
|
|
5 |
import uuid
|
6 |
+
from concurrent.futures import ThreadPoolExecutor
|
7 |
+
from pathlib import Path
|
8 |
+
|
9 |
import google.generativeai as genai
|
10 |
+
from fastcore.parallel import threaded
|
11 |
from fasthtml.common import (
|
12 |
+
Aside,
|
13 |
Div,
|
14 |
+
FileResponse,
|
15 |
+
HighlightJS,
|
16 |
Img,
|
17 |
+
JSONResponse,
|
18 |
+
Link,
|
19 |
Main,
|
20 |
P,
|
|
|
|
|
|
|
|
|
|
|
21 |
RedirectResponse,
|
22 |
+
Script,
|
23 |
StreamingResponse,
|
24 |
+
fast_app,
|
25 |
serve,
|
26 |
)
|
27 |
+
from PIL import Image
|
28 |
from shad4fast import ShadHead
|
29 |
from vespa.application import Vespa
|
|
|
|
|
|
|
30 |
|
31 |
+
from backend.colpali import SimMapGenerator
|
|
|
32 |
from backend.vespa_app import VespaQueryClient
|
33 |
from frontend.app import (
|
34 |
+
AboutThisDemo,
|
35 |
ChatResult,
|
36 |
Home,
|
37 |
Search,
|
|
|
39 |
SearchResult,
|
40 |
SimMapButtonPoll,
|
41 |
SimMapButtonReady,
|
|
|
42 |
)
|
43 |
from frontend.layout import Layout
|
44 |
|
|
|
90 |
genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
|
91 |
GEMINI_SYSTEM_PROMPT = """If the user query is a question, try your best to answer it based on the provided images.
|
92 |
If the user query can not be interpreted as a question, or if the answer to the query can not be inferred from the images,
|
93 |
+
answer with the exact phrase "I am sorry, I can't find enough relevant information on these pages to answer your question.".
|
94 |
Your response should be HTML formatted, but only simple tags, such as <b>. <p>, <i>, <br> <ul> and <li> are allowed. No HTML tables.
|
95 |
This means that newlines will be replaced with <br> tags, bold text will be enclosed in <b> tags, and so on.
|
96 |
+
Do NOT include backticks (`) in your response. Only simple HTML tags and text.
|
97 |
"""
|
98 |
gemini_model = genai.GenerativeModel(
|
99 |
"gemini-1.5-flash-8b", system_instruction=GEMINI_SYSTEM_PROMPT
|
|
|
107 |
|
108 |
@app.on_event("startup")
|
109 |
def load_model_on_startup():
|
110 |
+
app.sim_map_generator = SimMapGenerator()
|
111 |
return
|
112 |
|
113 |
|
|
|
131 |
def get(session):
|
132 |
if "session_id" not in session:
|
133 |
session["session_id"] = str(uuid.uuid4())
|
134 |
+
return Layout(Main(Home()), is_home=True)
|
135 |
|
136 |
|
137 |
@rt("/about-this-demo")
|
|
|
140 |
|
141 |
|
142 |
@rt("/search")
|
143 |
+
def get(request, query: str = "", ranking: str = "nn+colpali"):
|
144 |
+
print("/search: Fetching results for ranking_value:", ranking)
|
|
|
|
|
|
|
145 |
|
146 |
# Always render the SearchBox first
|
147 |
+
if not query:
|
148 |
# Show SearchBox and a message for missing query
|
149 |
return Layout(
|
150 |
Main(
|
151 |
Div(
|
152 |
+
SearchBox(query_value=query, ranking_value=ranking),
|
153 |
Div(
|
154 |
P(
|
155 |
"No query provided. Please enter a query.",
|
|
|
162 |
)
|
163 |
)
|
164 |
# Generate a unique query_id based on the query and ranking value
|
165 |
+
query_id = generate_query_id(query, ranking)
|
166 |
# Show the loading message if a query is provided
|
167 |
return Layout(
|
168 |
Main(Search(request), data_overlayscrollbars_initialize=True, cls="border-t"),
|
169 |
Aside(
|
170 |
+
ChatResult(query_id=query_id, query=query),
|
171 |
cls="border-t border-l hidden md:block",
|
172 |
),
|
173 |
) # Show SearchBox and Loading message initially
|
174 |
|
175 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
176 |
@rt("/fetch_results")
|
177 |
async def get(session, request, query: str, ranking: str):
|
178 |
if "hx-request" not in request.headers:
|
|
|
182 |
query_id = generate_query_id(query, ranking)
|
183 |
print(f"Query id in /fetch_results: {query_id}")
|
184 |
# Run the embedding and query against Vespa app
|
185 |
+
|
186 |
+
q_embs, idx_to_token = app.sim_map_generator.get_query_embeddings_and_token_map(
|
187 |
+
query
|
188 |
+
)
|
189 |
|
190 |
start = time.perf_counter()
|
191 |
# Fetch real search results from Vespa
|
|
|
199 |
print(
|
200 |
f"Search results fetched in {end - start:.2f} seconds, Vespa says searchtime was {result['timing']['searchtime']} seconds"
|
201 |
)
|
202 |
+
search_time = result["timing"]["searchtime"]
|
203 |
+
total_count = result["root"]["fields"]["totalCount"]
|
204 |
+
|
205 |
search_results = vespa_app.results_to_search_results(result, idx_to_token)
|
206 |
+
|
207 |
get_and_store_sim_maps(
|
208 |
query_id=query_id,
|
209 |
query=query,
|
210 |
q_embs=q_embs,
|
211 |
ranking=ranking,
|
212 |
idx_to_token=idx_to_token,
|
213 |
+
doc_ids=[result["fields"]["id"] for result in search_results],
|
214 |
)
|
215 |
+
return SearchResult(search_results, query, query_id, search_time, total_count)
|
216 |
|
217 |
|
218 |
def get_results_children(result):
|
|
|
232 |
|
233 |
|
234 |
@threaded
|
235 |
+
def get_and_store_sim_maps(
|
236 |
+
query_id, query: str, q_embs, ranking, idx_to_token, doc_ids
|
237 |
+
):
|
238 |
ranking_sim = ranking + "_sim"
|
239 |
vespa_sim_maps = vespa_app.get_sim_maps_from_query(
|
240 |
query=query,
|
|
|
242 |
ranking=ranking_sim,
|
243 |
idx_to_token=idx_to_token,
|
244 |
)
|
245 |
+
img_paths = [IMG_DIR / f"{doc_id}.jpg" for doc_id in doc_ids]
|
|
|
|
|
246 |
# All images should be downloaded, but best to wait 5 secs
|
247 |
max_wait = 5
|
248 |
start_time = time.time()
|
|
|
254 |
if not all([os.path.exists(img_path) for img_path in img_paths]):
|
255 |
print(f"Images not ready in 5 seconds for query_id: {query_id}")
|
256 |
return False
|
257 |
+
sim_map_generator = app.sim_map_generator.gen_similarity_maps(
|
|
|
|
|
|
|
258 |
query=query,
|
259 |
query_embs=q_embs,
|
260 |
token_idx_map=idx_to_token,
|
|
|
294 |
|
295 |
|
296 |
@app.get("/full_image")
|
297 |
+
async def full_image(doc_id: str):
|
298 |
"""
|
299 |
Endpoint to get the full quality image for a given result id.
|
300 |
"""
|
301 |
+
img_path = IMG_DIR / f"{doc_id}.jpg"
|
302 |
if not os.path.exists(img_path):
|
303 |
+
image_data = await vespa_app.get_full_image_from_vespa(doc_id)
|
304 |
# image data is base 64 encoded string. Save it to disk as jpg.
|
305 |
with open(img_path, "wb") as f:
|
306 |
f.write(base64.b64decode(image_data))
|
307 |
+
print(f"Full image saved to disk for doc_id: {doc_id}")
|
308 |
else:
|
309 |
with open(img_path, "rb") as f:
|
310 |
image_data = base64.b64encode(f.read()).decode("utf-8")
|
|
|
316 |
|
317 |
|
318 |
@rt("/suggestions")
|
319 |
+
async def get_suggestions(query: str = ""):
|
320 |
+
"""Endpoint to get suggestions as user types in the search box"""
|
321 |
+
query = query.lower().strip()
|
322 |
|
323 |
if query:
|
324 |
suggestions = await vespa_app.get_suggestions(query)
|
|
|
328 |
return JSONResponse({"suggestions": []})
|
329 |
|
330 |
|
331 |
+
async def message_generator(query_id: str, query: str, doc_ids: list):
|
332 |
+
"""Generator function to yield SSE messages for chat response"""
|
333 |
+
images = {}
|
334 |
num_images = 3 # Number of images before firing chat request
|
335 |
max_wait = 10 # seconds
|
336 |
start_time = time.time()
|
337 |
# Check if full images are ready on disk
|
338 |
+
while (
|
339 |
+
len(images) < min(num_images, len(doc_ids))
|
340 |
+
and time.time() - start_time < max_wait
|
341 |
+
):
|
342 |
for idx in range(num_images):
|
343 |
+
image_filename = IMG_DIR / f"{doc_ids[idx]}.jpg"
|
344 |
+
if not os.path.exists(image_filename):
|
345 |
print(
|
346 |
f"Message generator: Full image not ready for query_id: {query_id}, idx: {idx}"
|
347 |
)
|
|
|
350 |
print(
|
351 |
f"Message generator: image ready for query_id: {query_id}, idx: {idx}"
|
352 |
)
|
353 |
+
images[image_filename] = Image.open(image_filename)
|
354 |
await asyncio.sleep(0.2)
|
355 |
+
|
356 |
+
images = list(images.values())
|
357 |
# yield message with number of images ready
|
358 |
+
yield f"event: message\ndata: Generating response based on {len(images)} images...\n\n"
|
359 |
if not images:
|
360 |
+
yield "event: message\ndata: Failed to send images to Gemini-8B!\n\n"
|
361 |
yield "event: close\ndata: \n\n"
|
362 |
return
|
363 |
|
|
|
378 |
|
379 |
|
380 |
@app.get("/get-message")
|
381 |
+
async def get_message(query_id: str, query: str, doc_ids: str):
|
382 |
return StreamingResponse(
|
383 |
+
message_generator(query_id=query_id, query=query, doc_ids=doc_ids.split(",")),
|
384 |
media_type="text/event-stream",
|
385 |
)
|
386 |
|
output.css
CHANGED
@@ -555,58 +555,105 @@ video {
|
|
555 |
}
|
556 |
|
557 |
:root {
|
558 |
-
--background:
|
559 |
-
|
560 |
-
--
|
561 |
-
|
562 |
-
--
|
563 |
-
|
564 |
-
--
|
565 |
-
|
566 |
-
--
|
567 |
-
|
568 |
-
--
|
569 |
-
|
570 |
-
--
|
571 |
-
|
572 |
-
--
|
573 |
-
|
574 |
-
--
|
575 |
-
|
576 |
-
--
|
577 |
-
|
578 |
-
--
|
579 |
-
|
580 |
-
--
|
581 |
-
|
582 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
583 |
}
|
584 |
|
585 |
.dark {
|
586 |
-
--background:
|
587 |
-
|
588 |
-
--
|
589 |
-
|
590 |
-
--
|
591 |
-
|
592 |
-
--
|
593 |
-
|
594 |
-
--
|
595 |
-
|
596 |
-
--
|
597 |
-
|
598 |
-
--
|
599 |
-
|
600 |
-
--
|
601 |
-
|
602 |
-
--
|
603 |
-
|
604 |
-
--
|
605 |
-
|
606 |
-
--
|
607 |
-
|
608 |
-
--
|
609 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
610 |
}
|
611 |
|
612 |
:root:has(.no-bg-scroll) {
|
@@ -1134,6 +1181,10 @@ body {
|
|
1134 |
grid-template-rows: minmax(0,55px) minmax(0,1fr);
|
1135 |
}
|
1136 |
|
|
|
|
|
|
|
|
|
1137 |
.flex-col {
|
1138 |
flex-direction: column;
|
1139 |
}
|
@@ -1248,6 +1299,12 @@ body {
|
|
1248 |
margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
|
1249 |
}
|
1250 |
|
|
|
|
|
|
|
|
|
|
|
|
|
1251 |
.self-stretch {
|
1252 |
align-self: stretch;
|
1253 |
}
|
@@ -1407,6 +1464,11 @@ body {
|
|
1407 |
background-color: hsl(var(--secondary));
|
1408 |
}
|
1409 |
|
|
|
|
|
|
|
|
|
|
|
1410 |
.bg-gradient-to-r {
|
1411 |
background-image: linear-gradient(to right, var(--tw-gradient-stops));
|
1412 |
}
|
@@ -1415,15 +1477,15 @@ body {
|
|
1415 |
background-image: linear-gradient(to top, var(--tw-gradient-stops));
|
1416 |
}
|
1417 |
|
1418 |
-
.from
|
1419 |
-
--tw-gradient-from: #
|
1420 |
-
--tw-gradient-to: rgb(
|
1421 |
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
|
1422 |
}
|
1423 |
|
1424 |
-
.from-
|
1425 |
-
--tw-gradient-from: #
|
1426 |
-
--tw-gradient-to: rgb(
|
1427 |
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
|
1428 |
}
|
1429 |
|
@@ -2084,6 +2146,15 @@ header {
|
|
2084 |
grid-column: 1/-1;
|
2085 |
}
|
2086 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2087 |
main {
|
2088 |
overflow: auto;
|
2089 |
}
|
@@ -2139,8 +2210,8 @@ aside {
|
|
2139 |
|
2140 |
.awesomplete > ul > :not([hidden]) ~ :not([hidden]) {
|
2141 |
--tw-space-y-reverse: 0;
|
2142 |
-
margin-top: calc(0.
|
2143 |
-
margin-bottom: calc(0.
|
2144 |
}
|
2145 |
|
2146 |
.awesomplete > ul {
|
@@ -2152,7 +2223,10 @@ aside {
|
|
2152 |
border-right: 1px solid hsl(var(--input));
|
2153 |
border-bottom: 1px solid hsl(var(--input));
|
2154 |
border-radius: 0 0 calc(var(--radius) - 2px) calc(var(--radius) - 2px);
|
2155 |
-
background:
|
|
|
|
|
|
|
2156 |
box-shadow: none;
|
2157 |
text-shadow: none;
|
2158 |
}
|
@@ -2700,6 +2774,12 @@ aside {
|
|
2700 |
}
|
2701 |
}
|
2702 |
|
|
|
|
|
|
|
|
|
|
|
|
|
2703 |
.dark\:block:where(.dark, .dark *) {
|
2704 |
display: block;
|
2705 |
}
|
@@ -2716,9 +2796,13 @@ aside {
|
|
2716 |
border-color: hsl(var(--destructive));
|
2717 |
}
|
2718 |
|
2719 |
-
.dark\:
|
2720 |
-
|
2721 |
-
|
|
|
|
|
|
|
|
|
2722 |
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
|
2723 |
}
|
2724 |
|
|
|
555 |
}
|
556 |
|
557 |
:root {
|
558 |
+
--background: 240 20% 99%;
|
559 |
+
/* 1 */
|
560 |
+
--foreground: 210 13% 13%;
|
561 |
+
/* 12 */
|
562 |
+
--card: 240 20% 99%;
|
563 |
+
/* 1 */
|
564 |
+
--card-foreground: 210 13% 13%;
|
565 |
+
/* 12 */
|
566 |
+
--popover: 240 20% 99%;
|
567 |
+
/* 1 */
|
568 |
+
--popover-foreground: 210 13% 13%;
|
569 |
+
/* 12 */
|
570 |
+
--primary: 210 13% 13%;
|
571 |
+
/* 12 */
|
572 |
+
--primary-foreground: 240 20% 98%;
|
573 |
+
/* 2 */
|
574 |
+
--secondary: 240 11% 95%;
|
575 |
+
/* 3 */
|
576 |
+
--secondary-foreground: 210 13% 13%;
|
577 |
+
/* 12 */
|
578 |
+
--muted: 240 11% 95%;
|
579 |
+
/* 3 */
|
580 |
+
--muted-foreground: 220 6% 40%;
|
581 |
+
/* 11 */
|
582 |
+
--accent: 240 11% 95%;
|
583 |
+
/* 3 */
|
584 |
+
--accent-foreground: 210 13% 13%;
|
585 |
+
/* 12 */
|
586 |
+
--destructive: 358 75% 59%;
|
587 |
+
/* 9 - red */
|
588 |
+
--destructive-foreground: 240 20% 98%;
|
589 |
+
/* 2 */
|
590 |
+
--border: 240 10% 86%;
|
591 |
+
/* 6 */
|
592 |
+
--input: 240 10% 86%;
|
593 |
+
/* 6 */
|
594 |
+
--ring: 210 13% 13%;
|
595 |
+
/* 12 */
|
596 |
+
--chart-1: 10 78% 54%;
|
597 |
+
/* 9 - tomato */
|
598 |
+
--chart-2: 173 80% 36%;
|
599 |
+
/* 9 - teal */
|
600 |
+
--chart-3: 206 100% 50%;
|
601 |
+
/* 9 - blue */
|
602 |
+
--chart-4: 42 100% 62%;
|
603 |
+
/* 9 - amber */
|
604 |
+
--chart-5: 23 93% 53%;
|
605 |
+
/* 9 - orange */
|
606 |
}
|
607 |
|
608 |
.dark {
|
609 |
+
--background: 240 6% 7%;
|
610 |
+
/* 1 */
|
611 |
+
--foreground: 220 9% 94%;
|
612 |
+
/* 12 */
|
613 |
+
--card: 240 6% 7%;
|
614 |
+
/* 1 */
|
615 |
+
--card-foreground: 220 9% 94%;
|
616 |
+
/* 12 */
|
617 |
+
--popover: 240 6% 7%;
|
618 |
+
/* 1 */
|
619 |
+
--popover-foreground: 220 9% 94%;
|
620 |
+
/* 12 */
|
621 |
+
--primary: 220 9% 94%;
|
622 |
+
/* 12 */
|
623 |
+
--primary-foreground: 220 6% 10%;
|
624 |
+
/* 2 */
|
625 |
+
--secondary: 225 6% 14%;
|
626 |
+
/* 3 */
|
627 |
+
--secondary-foreground: 220 9% 94%;
|
628 |
+
/* 12 */
|
629 |
+
--muted: 225 6% 14%;
|
630 |
+
/* 3 */
|
631 |
+
--muted-foreground: 216 7% 71%;
|
632 |
+
/* 11 */
|
633 |
+
--accent: 225 6% 14%;
|
634 |
+
/* 3 */
|
635 |
+
--accent-foreground: 220 9% 94%;
|
636 |
+
/* 12 */
|
637 |
+
--destructive: 358 75% 59%;
|
638 |
+
/* 9 - red */
|
639 |
+
--destructive-foreground: 220 9% 94%;
|
640 |
+
/* 12 */
|
641 |
+
--border: 213 8% 23%;
|
642 |
+
/* 6 */
|
643 |
+
--input: 213 8% 23%;
|
644 |
+
/* 6 */
|
645 |
+
--ring: 220 9% 94%;
|
646 |
+
/* 12 */
|
647 |
+
--chart-1: 10 78% 54%;
|
648 |
+
/* 9 - tomato */
|
649 |
+
--chart-2: 173 80% 36%;
|
650 |
+
/* 9 - teal */
|
651 |
+
--chart-3: 206 100% 50%;
|
652 |
+
/* 9 - blue */
|
653 |
+
--chart-4: 42 100% 62%;
|
654 |
+
/* 9 - amber */
|
655 |
+
--chart-5: 23 93% 53%;
|
656 |
+
/* 9 - orange */
|
657 |
}
|
658 |
|
659 |
:root:has(.no-bg-scroll) {
|
|
|
1181 |
grid-template-rows: minmax(0,55px) minmax(0,1fr);
|
1182 |
}
|
1183 |
|
1184 |
+
.grid-rows-\[auto_auto_1fr\] {
|
1185 |
+
grid-template-rows: auto auto 1fr;
|
1186 |
+
}
|
1187 |
+
|
1188 |
.flex-col {
|
1189 |
flex-direction: column;
|
1190 |
}
|
|
|
1299 |
margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
|
1300 |
}
|
1301 |
|
1302 |
+
.space-x-1 > :not([hidden]) ~ :not([hidden]) {
|
1303 |
+
--tw-space-x-reverse: 0;
|
1304 |
+
margin-right: calc(0.25rem * var(--tw-space-x-reverse));
|
1305 |
+
margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse)));
|
1306 |
+
}
|
1307 |
+
|
1308 |
.self-stretch {
|
1309 |
align-self: stretch;
|
1310 |
}
|
|
|
1464 |
background-color: hsl(var(--secondary));
|
1465 |
}
|
1466 |
|
1467 |
+
.bg-white {
|
1468 |
+
--tw-bg-opacity: 1;
|
1469 |
+
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
1470 |
+
}
|
1471 |
+
|
1472 |
.bg-gradient-to-r {
|
1473 |
background-image: linear-gradient(to right, var(--tw-gradient-stops));
|
1474 |
}
|
|
|
1477 |
background-image: linear-gradient(to top, var(--tw-gradient-stops));
|
1478 |
}
|
1479 |
|
1480 |
+
.from-\[\#fcfcfd\] {
|
1481 |
+
--tw-gradient-from: #fcfcfd var(--tw-gradient-from-position);
|
1482 |
+
--tw-gradient-to: rgb(252 252 253 / 0) var(--tw-gradient-to-position);
|
1483 |
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
|
1484 |
}
|
1485 |
|
1486 |
+
.from-black {
|
1487 |
+
--tw-gradient-from: #000 var(--tw-gradient-from-position);
|
1488 |
+
--tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);
|
1489 |
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
|
1490 |
}
|
1491 |
|
|
|
2146 |
grid-column: 1/-1;
|
2147 |
}
|
2148 |
|
2149 |
+
body {
|
2150 |
+
&[data-is-home="true"] {
|
2151 |
+
background: radial-gradient(circle at 50% 100%, #fcfcfd, #fcfcfd, #fdfdfe, #fdfdfe, #fefefe, #fefefe, #ffffff, #ffffff);
|
2152 |
+
.dark & {
|
2153 |
+
background: radial-gradient(circle at 50% 50%, #272a2d, #242629, #212326, #1e1f22, #1b1c1e, #18181b, #151517, #111113);
|
2154 |
+
}
|
2155 |
+
}
|
2156 |
+
}
|
2157 |
+
|
2158 |
main {
|
2159 |
overflow: auto;
|
2160 |
}
|
|
|
2210 |
|
2211 |
.awesomplete > ul > :not([hidden]) ~ :not([hidden]) {
|
2212 |
--tw-space-y-reverse: 0;
|
2213 |
+
margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
|
2214 |
+
margin-bottom: calc(0.25rem * var(--tw-space-y-reverse));
|
2215 |
}
|
2216 |
|
2217 |
.awesomplete > ul {
|
|
|
2223 |
border-right: 1px solid hsl(var(--input));
|
2224 |
border-bottom: 1px solid hsl(var(--input));
|
2225 |
border-radius: 0 0 calc(var(--radius) - 2px) calc(var(--radius) - 2px);
|
2226 |
+
background: white;
|
2227 |
+
.dark & {
|
2228 |
+
background: hsl(var(--background));
|
2229 |
+
}
|
2230 |
box-shadow: none;
|
2231 |
text-shadow: none;
|
2232 |
}
|
|
|
2774 |
}
|
2775 |
}
|
2776 |
|
2777 |
+
@media (min-width: 1280px) {
|
2778 |
+
.xl\:grid-rows-\[1fr_2fr\] {
|
2779 |
+
grid-template-rows: 1fr 2fr;
|
2780 |
+
}
|
2781 |
+
}
|
2782 |
+
|
2783 |
.dark\:block:where(.dark, .dark *) {
|
2784 |
display: block;
|
2785 |
}
|
|
|
2796 |
border-color: hsl(var(--destructive));
|
2797 |
}
|
2798 |
|
2799 |
+
.dark\:bg-background:where(.dark, .dark *) {
|
2800 |
+
background-color: hsl(var(--background));
|
2801 |
+
}
|
2802 |
+
|
2803 |
+
.dark\:from-\[\#1c2024\]:where(.dark, .dark *) {
|
2804 |
+
--tw-gradient-from: #1c2024 var(--tw-gradient-from-position);
|
2805 |
+
--tw-gradient-to: rgb(28 32 36 / 0) var(--tw-gradient-to-position);
|
2806 |
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
|
2807 |
}
|
2808 |
|
requirements.txt
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
# This file was autogenerated by uv via the following command:
|
2 |
-
# uv pip compile pyproject.toml -o requirements.txt
|
3 |
accelerate==0.34.2
|
4 |
# via peft
|
5 |
aiohappyeyeballs==2.4.3
|
|
|
1 |
# This file was autogenerated by uv via the following command:
|
2 |
+
# uv pip compile pyproject.toml -o src/requirements.txt
|
3 |
accelerate==0.34.2
|
4 |
# via peft
|
5 |
aiohappyeyeballs==2.4.3
|
static/.DS_Store
CHANGED
Binary files a/static/.DS_Store and b/static/.DS_Store differ
|
|