Spaces:
Runtime error
Runtime error
Merge github code to hugging face
#1
by Subash-Lamichhane - opened
- .gitattributes +36 -36
- README.md +118 -43
- app.py +2 -5
- caption_store.py +1 -0
- captions.json +0 -0
- compress_image.py +91 -0
- logic.py +7 -5
- requirements.txt +86 -5
- 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:
|
| 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 |
-
-
|
| 15 |
-
-
|
| 16 |
-
-
|
| 17 |
-
-
|
| 18 |
-
|
| 19 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
|
| 22 |
|
| 23 |
-
|
| 24 |
|
| 25 |
-
##
|
| 26 |
|
| 27 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
-
|
| 33 |
|
| 34 |
-
|
| 35 |
-
> 🐦 Social post: _link TBD_
|
| 36 |
|
| 37 |
-
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
|
| 40 |
-
pip install -r requirements.txt
|
| 41 |
-
python app.py
|
| 42 |
-
```
|
| 43 |
|
| 44 |
-
|
| 45 |
|
| 46 |
-
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
| Caption store | Local `captions.json` |
|
| 54 |
|
| 55 |
-
|
| 56 |
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
| 323 |
return [], [], f"Zero matches in database for target {collection} (Threshold constraint: {MIN_RELEVANCE})."
|
| 324 |
|
| 325 |
-
original_paths = [r["path"] for r in
|
| 326 |
gallery_items = []
|
| 327 |
-
for r in
|
| 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(
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 17 |
TOP_K = 20
|
| 18 |
|
| 19 |
# Weight given to keyword boost relative to semantic score (0–1 additive)
|
| 20 |
-
KEYWORD_BOOST = 0.
|
| 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 |
|