Merge github code to hugging face

#1
Files changed (9) hide show
  1. .gitattributes +36 -36
  2. README.md +118 -43
  3. app.py +2 -5
  4. caption_store.py +1 -0
  5. captions.json +0 -0
  6. compress_image.py +91 -0
  7. logic.py +7 -5
  8. requirements.txt +86 -5
  9. search.py +6 -2
.gitattributes CHANGED
@@ -1,36 +1,36 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
36
- public/heroImage.png filter=lfs diff=lfs merge=lfs -text
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ public/heroImage.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,57 +1,132 @@
1
- ---
2
- title: Photographer's Archive
3
- emoji: 📷
4
- colorFrom: indigo
5
- colorTo: purple
6
- sdk: gradio
7
- sdk_version: 6.18.0
8
- python_version: '3.12'
9
- app_file: app.py
10
- pinned: false
11
- license: apache-2.0
12
- tags:
13
- - backyard-ai
14
- - build-small
15
- - vision-language-model
16
- - image-search
17
- - local-first
18
- short_description: Local-first photo search powered by MiniCPM-V-4.6
19
- ---
 
 
 
 
20
 
21
- # Photographer's Archive
22
 
23
- A local-first, privacy-preserving photo search app for photographers. Built for the Hugging Face [Build Small](https://huggingface.co/build-small) hackathon — **Backyard AI** track.
24
 
25
- ## What it does
26
 
27
- 1. **Ingest** Point the app at a local folder of photos. A small Vision-Language Model (MiniCPM-V-4.6, ≤7B params) runs entirely on your machine and generates rich captions describing objects, lighting, mood, and composition.
28
- 2. **Search** — Type a plain-English query (e.g. "golden hour portrait with soft bokeh") and instantly retrieve the most relevant photos ranked by semantic similarity.
29
 
30
- No images are uploaded to any external service. Everything stays on your hardware.
 
 
 
31
 
32
- ## Demo
33
 
34
- > 📹 Demo video: _link TBD_
35
- > 🐦 Social post: _link TBD_
36
 
37
- ## Running locally
 
 
38
 
39
- ```bash
40
- pip install -r requirements.txt
41
- python app.py
42
- ```
43
 
44
- The first run will download MiniCPM-V-4.6 (~8 GB). Subsequent runs use cached weights and work fully offline.
45
 
46
- ## Tech stack
47
 
48
- | Component | Library |
49
- |-----------|---------|
50
- | VLM | `openbmb/MiniCPM-V-4.6` via 🤗 Transformers |
51
- | Semantic search | `sentence-transformers/all-MiniLM-L6-v2` |
52
- | UI | Gradio 5 |
53
- | Caption store | Local `captions.json` |
54
 
55
- ## Privacy
56
 
57
- All model inference happens locally. No image pixels, captions, or metadata leave your machine.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: ShutterSearch
3
+ emoji: 📷
4
+ colorFrom: indigo
5
+ colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: 6.18.0
8
+ python_version: '3.12'
9
+ app_file: app.py
10
+ pinned: false
11
+ license: apache-2.0
12
+ tags:
13
+ - backyard-ai
14
+ - openbmb
15
+ - modal-labs
16
+ - minicpm
17
+ - build-small
18
+ - vision-language-model
19
+ - image-search
20
+ short_description: Local-first photo search powered by MiniCPM-V-4.6
21
+ fullWidth: true
22
+ ---
23
+ # 📷 ShutterSearch — Intelligent Photo Archive
24
 
25
+ ShutterSearch is a local-first, privacy-preserving semantic search and photography archive manager. It turns unstructured folders of raw photographs into organized, natural-language searchable catalogs without uploading your private master files to any third-party cloud.
26
 
27
+ ---
28
 
29
+ ## 🏆 Hackathon Submission Details
30
 
31
+ This project was built for the **Hugging Face [Build Small](https://huggingface.co/build-small) Hackathon** under the following tracks:
 
32
 
33
+ * **Primary Track:** Backyard AI (Local-first / Offline track)
34
+ * **Sponsor Award Compatibility:**
35
+ * **OpenBMB Awards:** Powered by the flagship lightweight visual-understanding model **MiniCPM-V-4.6** (≤7B params) for high-performance visual scene parsing.
36
+ * **Modal Labs Awards:** Seamlessly integrates with **Modal Labs** serverless GPU compute infrastructure to scale visual ingestion pipelines off-site, returning detailed annotations back to your local cache.
37
 
38
+ ---
39
 
40
+ ## 👥 The Team
 
41
 
42
+ * **Subash-Lamichhane** ([@Subash-Lamichhane](https://huggingface.co/Subash-Lamichhane))
43
+ * **najus** ([@najus](https://huggingface.co/najus))
44
+ * **Swikar Gautam** ([@SwikarG](https://huggingface.co/SwikarG))
45
 
46
+ ---
 
 
 
47
 
48
+ ## Pre-Flight Validation Checklist
49
 
50
+ This section verifies compliance with the submission requirements of the *Build Small* Hackathon:
51
 
52
+ - [x] **Stay under 32B:** The core Vision Language Model used is **MiniCPM-V-4.6** (8B/7B class model), and the text embedding model is **all-MiniLM-L6-v2** (22M parameters). Combined, the total parameter footprint is less than 9B parameters, safely under the 32B limit.
53
+ - [x] **Ship a Gradio App:** Fully deployed as a native Gradio Application Space within the official Build Small organization on Hugging Face.
54
+ - [x] **Record a Demo:** A visual walkthrough demonstrating local indexing, search, selection, and download is linked below.
55
+ - [x] **Post It:** A public showcase of ShutterSearch has been published on social media.
56
+ - [x] **Mind the GPU Limit:** Fully self-contained. Local execution relies on local memory and GPU resources, while our offloaded inference operates within standard boundaries.
 
57
 
58
+ ---
59
 
60
+ ## 📹 Presentation & Links
61
+
62
+ * **Demo Video:** *https://www.youtube.com/watch?v=aTLrOBhSRwU&feature=youtu.be*
63
+ * **Social Media Post:** *https://x.com/SUJANKOIRA96725/status/2066583761597436253*
64
+
65
+ ---
66
+
67
+ ## 💡 The Problem & The Backyard Solution
68
+
69
+ Photographers manage massive directories of RAW/JPEG images across external drives. Managing them typically requires sacrificing ownership by uploading them to third-party image hosts, manually tagging files, or enduring slow loading speeds when viewing high-resolution imagery.
70
+
71
+ **ShutterSearch solves this on your terms:**
72
+ * **Local Caching & Privacy:** Your original master image files never leave your machine.
73
+ * **Dual-Inference Pipeline (Local & Modal Labs):** Run inference fully offline on your own local GPU, or scale up your pipeline using **Modal Labs** serverless containers to process large batches on cloud-based H100s, caching the resulting semantic captions back locally.
74
+ * **On-the-Fly WebP Thumbnails:** Avoid high-resolution display lag. ShutterSearch caches images as lightweight WebP thumbnails (300px max, 70% quality) for smooth, lag-free visual scrolling.
75
+ * **Multi-Select Bulk Export:** Select multiple images (indicated visually by inline `✅` overlay badges) across Search results or Collections to package and download high-resolution originals in a structured ZIP.
76
+
77
+ ---
78
+
79
+ ## 🛠️ Tech Stack & Model Selection
80
+
81
+ | Component | Technology | Role |
82
+ |-----------|------------|------|
83
+ | **Core VLM** | `openbmb/MiniCPM-V-4.6` | Scene parsing, composition classification, and tagging |
84
+ | **Semantic Search** | `all-MiniLM-L6-v2` | High-dimensional text-to-image semantic index maps |
85
+ | **Inference Scaling** | `Modal Labs` (Optional) | Serverless GPU execution for scalable batch parsing |
86
+ | **Frontend UI** | Gradio | Dark-workspace layout and interface state engine |
87
+ | **Thumbnail Optimizer** | `Pillow` (PIL) | Compresses files to WebP (300px, 70% quality) |
88
+ | **Local Database** | Flat JSON Storage | No bulky setups; simple, human-readable data mapping |
89
+
90
+ ### Why MiniCPM-V-4.6?
91
+ We chose OpenBMB’s MiniCPM-V-4.6 because it matches or outperforms larger models (like Claude 3 Opus and GPT-4V) in optical character recognition (OCR), layout understanding, and fine-grained visual reasoning while remaining compact enough to run on standard consumer-grade workstations.
92
+
93
+ ---
94
+
95
+ ## 📦 Setting Up Locally
96
+
97
+ ### Prerequisites
98
+ Make sure your environment has Python 3.10+ and a GPU with at least 8GB VRAM (or a configured CPU environment).
99
+
100
+ 1. **Clone the repository:**
101
+ ```bash
102
+ git clone https://huggingface.co/spaces/build-small-hackathon/ShutterSearch
103
+ cd ShutterSearch
104
+ ```
105
+
106
+ 2. **Set up a virtual environment and activate it:**
107
+ ```bash
108
+ python -m venv venv
109
+ # On Windows:
110
+ .\venv\Scripts\activate
111
+ # On macOS/Linux:
112
+ source venv/bin/activate
113
+ ```
114
+
115
+ 3. **Install dependencies:**
116
+ ```bash
117
+ pip install -r requirements.txt
118
+ ```
119
+ 4. ***Setup modal***
120
+ ```bash
121
+ modal setup
122
+ modal deploy modal_caption.py
123
+ ```
124
+ 5. **Launch the application:**
125
+ ```bash
126
+ python app.py
127
+ ```
128
+
129
+ ---
130
+
131
+ ## 🏆 Hackathon Details
132
+ Developed for the Hugging Face "Build Small" Hackathon (Backyard AI / OpenBMB Tracks). Focused on model-efficiency, local UI caching pipelines, high-fidelity source protection, and a professional workspace interface.
app.py CHANGED
@@ -144,9 +144,9 @@ from ui.about import render_about
144
  with open("style.css", "r") as f:
145
  custom_css = f.read()
146
 
147
- with gr.Blocks(css=custom_css) as demo:
148
  # 1. Render Navigation Sidebar
149
- nav_btns, stats_label = render_sidebar()
150
 
151
  # 2. Render Main Content Container Pages
152
  with gr.Column(elem_classes="main-content"):
@@ -215,9 +215,6 @@ with gr.Blocks(css=custom_css) as demo:
215
  fn=run_ingest,
216
  inputs=[upload, coll_dropdown, use_new_coll, new_coll_name],
217
  outputs=[ingest_status, caption_table, coll_dropdown]
218
- ).then(
219
- fn=lambda: f"Total Photos: {entry_count()}",
220
- outputs=stats_label
221
  )
222
 
223
  # --- Shared Selection Utility Functions ---
 
144
  with open("style.css", "r") as f:
145
  custom_css = f.read()
146
 
147
+ with gr.Blocks(css=custom_css, title="ShutterSearch — Photo Archive", fill_height=False) as demo:
148
  # 1. Render Navigation Sidebar
149
+ nav_btns = render_sidebar()
150
 
151
  # 2. Render Main Content Container Pages
152
  with gr.Column(elem_classes="main-content"):
 
215
  fn=run_ingest,
216
  inputs=[upload, coll_dropdown, use_new_coll, new_coll_name],
217
  outputs=[ingest_status, caption_table, coll_dropdown]
 
 
 
218
  )
219
 
220
  # --- Shared Selection Utility Functions ---
caption_store.py CHANGED
@@ -48,6 +48,7 @@ def _try_parse_json(raw: str) -> dict | None:
48
 
49
 
50
  def flatten_metadata(meta: dict) -> str:
 
51
  parts = []
52
 
53
  if meta.get("summary"):
 
48
 
49
 
50
  def flatten_metadata(meta: dict) -> str:
51
+ """Convert structured JSON metadata into a rich text string for embedding."""
52
  parts = []
53
 
54
  if meta.get("summary"):
captions.json CHANGED
The diff for this file is too large to render. See raw diff
 
compress_image.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pathlib import Path
3
+ from PIL import Image, ImageOps
4
+
5
+ # --- CONFIGURATION ---
6
+ INPUT_FOLDER = r"C:\Users\Dell\Desktop\gradio\weddingimages-hackathon\WeddingImagesHAck" # Replace with your input folder path
7
+ OUTPUT_FOLDER = r"C:\Users\Dell\Desktop\gradio\weddingimages-hackathon-compressed" # Replace with where you want to save them
8
+ MAX_DIMENSION = 2048 # Maximum width or height in pixels
9
+ QUALITY = 85 # Image quality (1-95). 85 is highly optimized
10
+ # ---------------------
11
+
12
+ SUPPORTED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.webp', '.tiff'}
13
+
14
+ def compress_image(file_path: Path, output_path: Path, max_dim: int, quality: int):
15
+ try:
16
+ with Image.open(file_path) as img:
17
+ # 1. Correct image rotation from camera EXIF data automatically
18
+ img = ImageOps.exif_transpose(img)
19
+
20
+ # 2. Calculate new dimensions preserving the aspect ratio
21
+ width, height = img.size
22
+ if max(width, height) > max_dim:
23
+ if width > height:
24
+ new_width = max_dim
25
+ new_height = int((max_dim / width) * height)
26
+ else:
27
+ new_height = max_dim
28
+ new_width = int((max_dim / height) * width)
29
+
30
+ img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
31
+
32
+ # 3. Handle transparency channels if saving formats change
33
+ ext = file_path.suffix.lower()
34
+ if ext in {'.jpg', '.jpeg'}:
35
+ # Force RGB conversion if image has an alpha channel (like transparent PNG to JPG conversion)
36
+ if img.mode in ('RGBA', 'LA'):
37
+ img = img.convert('RGB')
38
+ img.save(output_path, "JPEG", quality=quality, optimize=True)
39
+ elif ext == '.webp':
40
+ img.save(output_path, "WEBP", quality=quality, optimize=True)
41
+ elif ext == '.png':
42
+ # PNG is lossless, so we compress the file storage space directly
43
+ img.save(output_path, "PNG", optimize=True)
44
+ else:
45
+ img.save(output_path, img.format, optimize=True)
46
+
47
+ return True
48
+ except Exception as e:
49
+ print(f"Failed to process {file_path.name}: {e}")
50
+ return False
51
+
52
+ def main():
53
+ input_path = Path(INPUT_FOLDER)
54
+ output_path = Path(OUTPUT_FOLDER)
55
+
56
+ if not input_path.exists():
57
+ print(f"Error: The input folder '{INPUT_FOLDER}' does not exist.")
58
+ return
59
+
60
+ output_path.mkdir(parents=True, exist_ok=True)
61
+
62
+ files = [f for f in input_path.iterdir() if f.suffix.lower() in SUPPORTED_EXTENSIONS]
63
+ total_files = len(files)
64
+
65
+ if total_files == 0:
66
+ print("No supported images found in the input folder.")
67
+ return
68
+
69
+ print(f"Starting compression of {total_files} images...")
70
+ successful = 0
71
+
72
+ for idx, file in enumerate(files, 1):
73
+ out_file = output_path / file.name
74
+
75
+ # Check original file size
76
+ orig_size_mb = file.stat().st_size / (1024 * 1024)
77
+
78
+ success = compress_image(file, out_file, MAX_DIMENSION, QUALITY)
79
+
80
+ if success:
81
+ successful += 1
82
+ new_size_mb = out_file.stat().st_size / (1024 * 1024)
83
+ reduction = ((orig_size_mb - new_size_mb) / orig_size_mb) * 100
84
+ print(f"[{idx}/{total_files}] Compressed: {file.name} "
85
+ f"({orig_size_mb:.2f}MB -> {new_size_mb:.2f}MB | -{reduction:.1f}%)")
86
+
87
+ print(f"\nCompression complete! Successfully processed {successful}/{total_files} images.")
88
+ print(f"Your optimized files are located in: {OUTPUT_FOLDER}")
89
+
90
+ if __name__ == "__main__":
91
+ main()
logic.py CHANGED
@@ -318,17 +318,19 @@ def run_search(query: str, collection: str = "All"):
318
 
319
  col_filter = None if collection == "All" else collection
320
  results = search(query.strip(), collection=col_filter)
321
-
322
- if not results:
 
 
323
  return [], [], f"Zero matches in database for target {collection} (Threshold constraint: {MIN_RELEVANCE})."
324
 
325
- original_paths = [r["path"] for r in results]
326
  gallery_items = []
327
- for r in results:
328
  thumb = get_thumbnail_path(r["path"])
329
  gallery_items.append((thumb, os.path.basename(r["path"])))
330
 
331
- return gallery_items, original_paths, f"Found {len(results)} search matches."
332
 
333
 
334
  def load_collections_view(collection_name):
 
318
 
319
  col_filter = None if collection == "All" else collection
320
  results = search(query.strip(), collection=col_filter)
321
+
322
+ CUSTOM_THRESHOLD = 0.60
323
+ filtered_results = [r for r in results if r.get("score", 0) >= CUSTOM_THRESHOLD]
324
+ if not filtered_results:
325
  return [], [], f"Zero matches in database for target {collection} (Threshold constraint: {MIN_RELEVANCE})."
326
 
327
+ original_paths = [r["path"] for r in filtered_results]
328
  gallery_items = []
329
+ for r in filtered_results:
330
  thumb = get_thumbnail_path(r["path"])
331
  gallery_items.append((thumb, os.path.basename(r["path"])))
332
 
333
+ return gallery_items, original_paths, f"Found {len(filtered_results)} search matches."
334
 
335
 
336
  def load_collections_view(collection_name):
requirements.txt CHANGED
@@ -1,5 +1,86 @@
1
- gradio>=5.40.0
2
- sentence-transformers>=3.0.0
3
- modal>=0.73.0
4
- numpy
5
- Pillow
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiohappyeyeballs==2.6.2
2
+ aiohttp==3.14.1
3
+ aiosignal==1.4.0
4
+ annotated-doc==0.0.4
5
+ annotated-types==0.7.0
6
+ anyio==4.13.0
7
+ attrs==26.1.0
8
+ audioop-lts==0.2.2
9
+ brotli==1.2.0
10
+ cbor2==6.1.2
11
+ certifi==2026.5.20
12
+ click==8.4.1
13
+ colorama==0.4.6
14
+ fastapi==0.137.1
15
+ filelock==3.29.4
16
+ frozenlist==1.8.0
17
+ fsspec==2026.4.0
18
+ gradio==6.18.0
19
+ gradio_client==2.5.0
20
+ groovy==0.1.2
21
+ grpclib==0.4.9
22
+ h11==0.16.0
23
+ h2==4.3.0
24
+ hf-gradio==0.4.1
25
+ hf-xet==1.5.1
26
+ hpack==4.1.0
27
+ httpcore==1.0.9
28
+ httpx==0.28.1
29
+ huggingface_hub==1.19.0
30
+ hyperframe==6.1.0
31
+ idna==3.18
32
+ Jinja2==3.1.6
33
+ joblib==1.5.3
34
+ markdown-it-py==4.2.0
35
+ MarkupSafe==3.0.3
36
+ mdurl==0.1.2
37
+ modal==1.5.0
38
+ mpmath==1.3.0
39
+ multidict==6.7.1
40
+ narwhals==2.22.1
41
+ networkx==3.6.1
42
+ numpy==2.4.6
43
+ orjson==3.11.9
44
+ packaging==26.2
45
+ pandas==3.0.3
46
+ pillow==12.2.0
47
+ propcache==0.5.2
48
+ protobuf==6.33.6
49
+ pydantic==2.13.4
50
+ pydantic_core==2.46.4
51
+ pydub==0.25.1
52
+ Pygments==2.20.0
53
+ python-dateutil==2.9.0.post0
54
+ python-multipart==0.0.32
55
+ pytz==2026.2
56
+ PyYAML==6.0.3
57
+ regex==2026.5.9
58
+ rich==15.0.0
59
+ safehttpx==0.1.7
60
+ safetensors==0.8.0
61
+ scikit-learn==1.9.0
62
+ scipy==1.17.1
63
+ semantic-version==2.10.0
64
+ sentence-transformers==5.5.1
65
+ setuptools==81.0.0
66
+ shellingham==1.5.4
67
+ six==1.17.0
68
+ starlette==1.3.1
69
+ sympy==1.14.0
70
+ synchronicity==0.12.3
71
+ threadpoolctl==3.6.0
72
+ tokenizers==0.22.2
73
+ toml==0.10.2
74
+ tomlkit==0.14.0
75
+ torch==2.12.0
76
+ tqdm==4.68.2
77
+ transformers==5.12.0
78
+ typer==0.25.1
79
+ types-certifi==2021.10.8.3
80
+ types-toml==0.10.8.20260518
81
+ typing-inspection==0.4.2
82
+ typing_extensions==4.15.0
83
+ tzdata==2026.2
84
+ uvicorn==0.49.0
85
+ watchfiles==1.2.0
86
+ yarl==1.24.2
search.py CHANGED
@@ -13,11 +13,11 @@ from caption_store import all_entries
13
  _embed_model: SentenceTransformer | None = None
14
  _MODEL_NAME = "BAAI/bge-base-en-v1.5" # stronger than MiniLM
15
 
16
- MIN_RELEVANCE = 0.55
17
  TOP_K = 20
18
 
19
  # Weight given to keyword boost relative to semantic score (0–1 additive)
20
- KEYWORD_BOOST = 0.3
21
 
22
 
23
  def _get_embed_model() -> SentenceTransformer:
@@ -33,6 +33,10 @@ def _query_tokens(query: str) -> list[str]:
33
 
34
 
35
  def _keyword_score(query_tokens: list[str], search_text: str, raw_caption: str) -> float:
 
 
 
 
36
  if not query_tokens:
37
  return 0.0
38
 
 
13
  _embed_model: SentenceTransformer | None = None
14
  _MODEL_NAME = "BAAI/bge-base-en-v1.5" # stronger than MiniLM
15
 
16
+ MIN_RELEVANCE = 0.6
17
  TOP_K = 20
18
 
19
  # Weight given to keyword boost relative to semantic score (0–1 additive)
20
+ KEYWORD_BOOST = 0.25
21
 
22
 
23
  def _get_embed_model() -> SentenceTransformer:
 
33
 
34
 
35
  def _keyword_score(query_tokens: list[str], search_text: str, raw_caption: str) -> float:
36
+ """
37
+ Boost score if query tokens appear in high-signal fields (attire, tags, summary).
38
+ Returns a value in [0, 1].
39
+ """
40
  if not query_tokens:
41
  return 0.0
42