Spaces:
Sleeping
Sleeping
GitHub Actions commited on
Commit Β·
23e79c5
0
Parent(s):
deploy to Hugging Face Spaces
Browse files- .gitignore +55 -0
- README.md +41 -0
- app.py +13 -0
- packages.txt +4 -0
- requirements-local.txt +25 -0
- requirements.txt +25 -0
- scripts/test_cpu_patch.py +39 -0
- src/__init__.py +1 -0
- src/app.py +198 -0
- src/pipeline/__init__.py +1 -0
- src/pipeline/preprocess.py +26 -0
- src/pipeline/translate.py +38 -0
- src/pipeline/tts.py +68 -0
- src/pipeline/vision_model.py +23 -0
- src/registry.py +171 -0
- src/utils/__init__.py +1 -0
- src/utils/monitor.py +22 -0
.gitignore
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*.pyo
|
| 5 |
+
*.pyd
|
| 6 |
+
*.pdb
|
| 7 |
+
*.egg
|
| 8 |
+
*.egg-info/
|
| 9 |
+
dist/
|
| 10 |
+
build/
|
| 11 |
+
*.whl
|
| 12 |
+
*.so
|
| 13 |
+
*.dylib
|
| 14 |
+
|
| 15 |
+
# Virtual environments
|
| 16 |
+
.venv/
|
| 17 |
+
venv/
|
| 18 |
+
env/
|
| 19 |
+
ENV/
|
| 20 |
+
|
| 21 |
+
# Testing / Coverage
|
| 22 |
+
.pytest_cache/
|
| 23 |
+
.coverage
|
| 24 |
+
htmlcov/
|
| 25 |
+
*.cover
|
| 26 |
+
coverage.xml
|
| 27 |
+
nosetests.xml
|
| 28 |
+
junit*.xml
|
| 29 |
+
|
| 30 |
+
# IDEs & editors
|
| 31 |
+
.vscode/
|
| 32 |
+
.idea/
|
| 33 |
+
*.swp
|
| 34 |
+
*.swo
|
| 35 |
+
*~
|
| 36 |
+
|
| 37 |
+
# OS
|
| 38 |
+
.DS_Store
|
| 39 |
+
Thumbs.db
|
| 40 |
+
desktop.ini
|
| 41 |
+
|
| 42 |
+
# Model cache / Hugging Face cache
|
| 43 |
+
.cache/
|
| 44 |
+
models/
|
| 45 |
+
*.bin
|
| 46 |
+
*.safetensors
|
| 47 |
+
*.gguf
|
| 48 |
+
|
| 49 |
+
# Logs
|
| 50 |
+
*.log
|
| 51 |
+
logs/
|
| 52 |
+
|
| 53 |
+
# Misc
|
| 54 |
+
*.tmp
|
| 55 |
+
*.bak
|
README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: SmartSight AI
|
| 3 |
+
emoji: π
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 4.44.1
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: apache-2.0
|
| 11 |
+
python_version: "3.10"
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# π SmartSight AI β Hugging Face Space Deployment
|
| 15 |
+
|
| 16 |
+
BαΊ£n cαΊp nhαΊt cαΊ₯u hΓ¬nh cho Hugging Face Spaces chαΊ‘y mΓ΄ phα»ng trợ lΓ½ mΓ΄ tαΊ£ hΓ¬nh αΊ£nh hα» trợ tiαΊΏp cαΊn (Accessibility System) bαΊ±ng giα»ng nΓ³i TiαΊΏng Viα»t dΓ nh cho ngΖ°α»i khiαΊΏm thα».///
|
| 17 |
+
|
| 18 |
+
## π CαΊ₯u hΓ¬nh Hugging Face Spaces
|
| 19 |
+
|
| 20 |
+
Ứng dα»₯ng nΓ y Δược thiαΊΏt kαΊΏ Δα» triα»n khai trα»±c tiαΊΏp trΓͺn Hugging Face Spaces bαΊ±ng cΓ‘c cΓ i ΔαΊ·t sau:
|
| 21 |
+
|
| 22 |
+
- **SDK:** Gradio
|
| 23 |
+
- **PhiΓͺn bαΊ£n SDK:** 4.44.1
|
| 24 |
+
- **PhiΓͺn bαΊ£n Python:** 3.10
|
| 25 |
+
- **Tα»p khα»i chαΊ‘y chΓnh:** `app.py` (tα»p wrapper α» thΖ° mα»₯c gα»c)
|
| 26 |
+
|
| 27 |
+
---
|
| 28 |
+
|
| 29 |
+
## π οΈ HΖ°α»ng dαΊ«n Triα»n khai trΓͺn Hugging Face Space
|
| 30 |
+
|
| 31 |
+
1. TαΊ‘o mα»t Space mα»i trΓͺn **Hugging Face**.
|
| 32 |
+
2. Chα»n SDK lΓ **Gradio**.
|
| 33 |
+
3. KαΊΏt nα»i Space cα»§a bαΊ‘n vα»i kho chα»©a mΓ£ nguα»n GitHub nΓ y.
|
| 34 |
+
4. ThiαΊΏt lαΊp nhΓ‘nh triα»n khai lΓ `huggingface-deployment`.
|
| 35 |
+
5. Hugging Face sαΊ½ tα»± Δα»ng cΓ i ΔαΊ·t cΓ‘c thΖ° viα»n trong `requirements.txt`, khα»i chαΊ‘y `app.py` vΓ chαΊ‘y α»©ng dα»₯ng.
|
| 36 |
+
|
| 37 |
+
---
|
| 38 |
+
|
| 39 |
+
## π KiαΊΏn trΓΊc nhΓ‘nh Hugging Face Deployment
|
| 40 |
+
|
| 41 |
+
Sα»± khΓ‘c biα»t chΓnh cα»§a nhΓ‘nh nΓ y lΓ viα»c ΔΖ°a tα»p khα»i chαΊ‘y chΓnh `app.py` ra thΖ° mα»₯c gα»c dΖ°α»i dαΊ‘ng wrapper Δα» tΖ°Ζ‘ng thΓch hoΓ n toΓ n vα»i cΖ‘ chαΊΏ phΓ‘t hiα»n tα»± Δα»ng cα»§a Hugging Face Spaces, Δα»ng thα»i tΓch hợp Metadata YAML α» phαΊ§n ΔαΊ§u cα»§a tα»p `README.md`.
|
app.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from src.app import demo
|
| 2 |
+
|
| 3 |
+
if __name__ == "__main__":
|
| 4 |
+
# Model loads lazily on first "Run" click via ModelRegistry singleton.
|
| 5 |
+
# This lets the UI become available in ~5-10s instead of waiting ~40s
|
| 6 |
+
# to download the 3.85GB Moondream2 model before Gradio even starts.
|
| 7 |
+
demo.queue().launch(
|
| 8 |
+
# Required for Hugging Face Spaces containers:
|
| 9 |
+
server_name="0.0.0.0", # bind all interfaces, not just localhost
|
| 10 |
+
server_port=7860, # standard HF Spaces port
|
| 11 |
+
show_api=False, # disable API schema generation (prevents TypeError
|
| 12 |
+
# in gradio_client/utils.py get_type() on gradio 4.40.x)
|
| 13 |
+
)
|
packages.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
libvips-dev
|
| 2 |
+
espeak-ng
|
| 3 |
+
libespeak1
|
| 4 |
+
ffmpeg
|
requirements-local.txt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==4.44.1
|
| 2 |
+
transformers==4.40.0
|
| 3 |
+
torch==2.2.2
|
| 4 |
+
numpy==1.26.4
|
| 5 |
+
pillow==10.3.0
|
| 6 |
+
deep-translator==1.11.4
|
| 7 |
+
gTTS==2.5.1
|
| 8 |
+
pyttsx3==2.90
|
| 9 |
+
psutil==5.9.8
|
| 10 |
+
sentencepiece==0.2.0
|
| 11 |
+
sacremoses==0.1.1
|
| 12 |
+
pytest==8.2.0
|
| 13 |
+
huggingface_hub==0.23.0
|
| 14 |
+
pyvips==2.2.3
|
| 15 |
+
pyvips-binary==8.18.2
|
| 16 |
+
einops==0.8.0
|
| 17 |
+
timm==0.9.16
|
| 18 |
+
accelerate==0.30.0
|
| 19 |
+
# Pin pydantic to avoid gradio_client JSON schema TypeError (bool-not-iterable bug)
|
| 20 |
+
pydantic==2.10.6
|
| 21 |
+
# Pin jinja2 to avoid unhashable type: 'dict' crash in Gradio 4.x template cache
|
| 22 |
+
jinja2==3.1.4
|
| 23 |
+
# Pin fastapi and starlette to avoid unhashable type: 'dict' crash in TemplateResponse
|
| 24 |
+
fastapi==0.112.4
|
| 25 |
+
starlette==0.38.2
|
requirements.txt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==4.44.1
|
| 2 |
+
transformers==4.40.0
|
| 3 |
+
torch==2.2.2
|
| 4 |
+
numpy==1.26.4
|
| 5 |
+
pillow==10.3.0
|
| 6 |
+
deep-translator==1.11.4
|
| 7 |
+
gTTS==2.5.1
|
| 8 |
+
pyttsx3==2.90
|
| 9 |
+
psutil==5.9.8
|
| 10 |
+
sentencepiece==0.2.0
|
| 11 |
+
sacremoses==0.1.1
|
| 12 |
+
pytest==8.2.0
|
| 13 |
+
huggingface_hub==0.23.0
|
| 14 |
+
# Required by Moondream2 VLM model
|
| 15 |
+
pyvips==2.2.3
|
| 16 |
+
einops==0.8.0
|
| 17 |
+
timm==0.9.16
|
| 18 |
+
accelerate==0.30.0
|
| 19 |
+
# Pin pydantic to avoid gradio_client JSON schema TypeError (bool-not-iterable bug)
|
| 20 |
+
pydantic==2.10.6
|
| 21 |
+
# Pin jinja2 to avoid unhashable type: 'dict' crash in Gradio 4.x template cache
|
| 22 |
+
jinja2==3.1.4
|
| 23 |
+
# Pin fastapi and starlette to avoid unhashable type: 'dict' crash in TemplateResponse
|
| 24 |
+
fastapi==0.112.4
|
| 25 |
+
starlette==0.38.2
|
scripts/test_cpu_patch.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Quick smoke-test for the CPU float32 monkey-patch.
|
| 3 |
+
Run with: .venv/Scripts/python scripts/test_cpu_patch.py
|
| 4 |
+
NOTE: Make sure app.py is NOT running at the same time (pagefile limit on Windows).
|
| 5 |
+
"""
|
| 6 |
+
import sys, os
|
| 7 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
| 8 |
+
|
| 9 |
+
from PIL import Image
|
| 10 |
+
import urllib.request, io
|
| 11 |
+
|
| 12 |
+
print("=== SmartSight CPU Patch Smoke Test ===\n")
|
| 13 |
+
|
| 14 |
+
# 1. Download a tiny test image (100x100 public domain)
|
| 15 |
+
print("1. Loading test image...")
|
| 16 |
+
url = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/320px-Cat03.jpg"
|
| 17 |
+
try:
|
| 18 |
+
with urllib.request.urlopen(url, timeout=10) as r:
|
| 19 |
+
img_bytes = r.read()
|
| 20 |
+
image = Image.open(io.BytesIO(img_bytes)).convert("RGB")
|
| 21 |
+
print(f" Image loaded: {image.size}, mode={image.mode}")
|
| 22 |
+
except Exception as e:
|
| 23 |
+
# Fallback: create a solid-colour test image
|
| 24 |
+
print(f" Download failed ({e}), using synthetic image.")
|
| 25 |
+
image = Image.new("RGB", (320, 240), color=(128, 64, 32))
|
| 26 |
+
|
| 27 |
+
# 2. Load model via registry (triggers the patch)
|
| 28 |
+
print("\n2. Loading Moondream2 (2B) via ModelRegistry...")
|
| 29 |
+
from src.registry import ModelRegistry
|
| 30 |
+
registry = ModelRegistry()
|
| 31 |
+
model, processor = registry.get_vlm("Moondream2 (2B)")
|
| 32 |
+
print(" Model loaded β")
|
| 33 |
+
|
| 34 |
+
# 3. Run inference
|
| 35 |
+
print("\n3. Running VLM inference on CPU...")
|
| 36 |
+
from src.pipeline.vision_model import run_vlm_inference
|
| 37 |
+
result = run_vlm_inference(image, "Moondream2 (2B)", model, processor)
|
| 38 |
+
print(f"\n=== RESULT ===\n{result}\n")
|
| 39 |
+
print("β
Patch working correctly β no LayerNormKernelImpl error!")
|
src/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
src/app.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import tempfile
|
| 4 |
+
import time
|
| 5 |
+
import uuid
|
| 6 |
+
import logging
|
| 7 |
+
import gradio as gr
|
| 8 |
+
from PIL import Image
|
| 9 |
+
from src.registry import ModelRegistry
|
| 10 |
+
from src.pipeline.preprocess import preprocess_image
|
| 11 |
+
from src.pipeline.vision_model import run_vlm_inference
|
| 12 |
+
from src.pipeline.tts import TTSModule
|
| 13 |
+
from src.utils.monitor import ExecutionMonitor
|
| 14 |
+
|
| 15 |
+
# βββ Logging setup ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 16 |
+
logging.basicConfig(
|
| 17 |
+
level=logging.INFO,
|
| 18 |
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
| 19 |
+
handlers=[logging.StreamHandler(sys.stdout)],
|
| 20 |
+
force=True,
|
| 21 |
+
)
|
| 22 |
+
logger = logging.getLogger("smartsight")
|
| 23 |
+
|
| 24 |
+
logger.info("=" * 60)
|
| 25 |
+
logger.info("SmartSight AI β startup")
|
| 26 |
+
logger.info(f" Python : {sys.version.split()[0]}")
|
| 27 |
+
logger.info(f" Gradio : {gr.__version__}")
|
| 28 |
+
logger.info(f" Platform: {sys.platform}")
|
| 29 |
+
logger.info("=" * 60)
|
| 30 |
+
|
| 31 |
+
# βββ Global singletons ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 32 |
+
logger.info("Initialising ModelRegistry and TTSModule singletons...")
|
| 33 |
+
try:
|
| 34 |
+
registry = ModelRegistry()
|
| 35 |
+
tts_module = TTSModule()
|
| 36 |
+
logger.info("Singletons ready (models will lazy-load on first Run).")
|
| 37 |
+
except Exception:
|
| 38 |
+
logger.critical("Failed to initialise singletons!", exc_info=True)
|
| 39 |
+
raise
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# βββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 43 |
+
def get_performance_html(durations: dict) -> str:
|
| 44 |
+
total = sum(durations.values())
|
| 45 |
+
if total == 0:
|
| 46 |
+
return "<p>ChΖ°a cΓ³ dα»― liα»u hiα»u nΔng.</p>"
|
| 47 |
+
|
| 48 |
+
html = "<div style='font-family: monospace; background: #1e1e1e; padding: 10px; border-radius: 5px; color: #fff;'>"
|
| 49 |
+
html += "<h4 style='margin-top:0; color:#58a6ff;'>Timing Breakdown:</h4>"
|
| 50 |
+
for stage, duration in durations.items():
|
| 51 |
+
pct = (duration / total) * 100 if total > 0 else 0
|
| 52 |
+
bar_count = int(pct / 5)
|
| 53 |
+
bar = "β" * bar_count + "β" * (20 - bar_count)
|
| 54 |
+
html += f"<div style='margin-bottom: 5px;'><b>{stage.capitalize()}:</b> {duration:.3f}s <span style='color: #8b949e;'>[{bar}]</span> {pct:.1f}%</div>"
|
| 55 |
+
html += "</div>"
|
| 56 |
+
return html
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# βββ Main pipeline ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 60 |
+
def run_pipeline(image, vlm_version, translate_mode, tts_mode, custom_prompt):
|
| 61 |
+
logger.info(
|
| 62 |
+
"run_pipeline called | vlm=%s | translate=%s | tts=%s",
|
| 63 |
+
vlm_version, translate_mode, tts_mode,
|
| 64 |
+
)
|
| 65 |
+
monitor = ExecutionMonitor()
|
| 66 |
+
|
| 67 |
+
if image is None:
|
| 68 |
+
logger.warning("run_pipeline: no image provided")
|
| 69 |
+
raise gr.Error("Vui lΓ²ng chα»₯p αΊ£nh hoαΊ·c tαΊ£i αΊ£nh lΓͺn trΖ°α»c!")
|
| 70 |
+
|
| 71 |
+
# Preprocessing
|
| 72 |
+
with monitor.track("preprocess"):
|
| 73 |
+
try:
|
| 74 |
+
img = preprocess_image(image)
|
| 75 |
+
logger.info("Preprocess OK: %s β %s", image.size if hasattr(image, 'size') else '?', img.size)
|
| 76 |
+
except Exception as e:
|
| 77 |
+
logger.error("Preprocess failed: %s", e, exc_info=True)
|
| 78 |
+
raise gr.Error(f"Lα»i xα» lΓ½ αΊ£nh: {str(e)}")
|
| 79 |
+
|
| 80 |
+
# Load VLM and Inference
|
| 81 |
+
with monitor.track("vlm_inference"):
|
| 82 |
+
try:
|
| 83 |
+
logger.info("Loading VLM: %s", vlm_version)
|
| 84 |
+
vlm_model, vlm_processor = registry.get_vlm(vlm_version)
|
| 85 |
+
eng_desc = run_vlm_inference(img, vlm_version, vlm_model, vlm_processor, custom_prompt)
|
| 86 |
+
logger.info("VLM inference OK (%d chars)", len(eng_desc))
|
| 87 |
+
except Exception as e:
|
| 88 |
+
logger.error("VLM inference failed: %s", e, exc_info=True)
|
| 89 |
+
raise gr.Error(f"Lα»i VLM Inference: {str(e)}")
|
| 90 |
+
|
| 91 |
+
# Translate
|
| 92 |
+
with monitor.track("translation"):
|
| 93 |
+
try:
|
| 94 |
+
translator = registry.get_translator_module(translate_mode)
|
| 95 |
+
vi_desc, is_offline_trans = translator.translate(eng_desc, translate_mode)
|
| 96 |
+
logger.info("Translation OK (offline=%s)", is_offline_trans)
|
| 97 |
+
except Exception as e:
|
| 98 |
+
logger.warning("Translation failed: %s", e, exc_info=True)
|
| 99 |
+
vi_desc = f"[Lα»i dα»ch] {eng_desc}"
|
| 100 |
+
is_offline_trans = False
|
| 101 |
+
gr.Warning(f"Dα»ch thuαΊt thαΊ₯t bαΊ‘i: {str(e)}")
|
| 102 |
+
|
| 103 |
+
# TTS
|
| 104 |
+
with monitor.track("tts"):
|
| 105 |
+
try:
|
| 106 |
+
temp_path = os.path.join(tempfile.gettempdir(), f"output_{uuid.uuid4().hex}.mp3")
|
| 107 |
+
audio_path = tts_module.generate_speech(vi_desc, tts_mode, filename=temp_path)
|
| 108 |
+
logger.info("TTS OK: %s", audio_path)
|
| 109 |
+
except Exception as e:
|
| 110 |
+
logger.warning("TTS failed: %s", e, exc_info=True)
|
| 111 |
+
audio_path = None
|
| 112 |
+
gr.Warning(f"KhΓ΄ng thα» tαΊ‘o giα»ng Δα»c: {str(e)}")
|
| 113 |
+
|
| 114 |
+
total_time = sum(monitor.get_durations().values())
|
| 115 |
+
ram_usage = monitor.get_ram_usage()
|
| 116 |
+
timing_html = get_performance_html(monitor.get_durations())
|
| 117 |
+
|
| 118 |
+
logger.info("Pipeline complete: %.3fs | RAM %.1fMB", total_time, ram_usage)
|
| 119 |
+
|
| 120 |
+
if "Auto-Detect" in translate_mode and is_offline_trans:
|
| 121 |
+
gr.Warning("MαΊ₯t kαΊΏt nα»i Internet - Tα»± Δα»ng chuyα»n sang dα»ch Offline (Helsinki-NLP)")
|
| 122 |
+
|
| 123 |
+
return (
|
| 124 |
+
eng_desc,
|
| 125 |
+
vi_desc,
|
| 126 |
+
audio_path,
|
| 127 |
+
f"{total_time:.3f} s",
|
| 128 |
+
f"{ram_usage:.1f} MB",
|
| 129 |
+
timing_html
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
# βββ Gradio UI ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 134 |
+
logger.info("Building Gradio Blocks UI...")
|
| 135 |
+
try:
|
| 136 |
+
with gr.Blocks(theme=gr.themes.Default(primary_hue="blue", secondary_hue="indigo")) as demo:
|
| 137 |
+
gr.HTML("<h1 style='text-align: center; color: #1f6feb;'>π SmartSight AI β Hα» Trợ NgΖ°α»i KhiαΊΏm Thα»</h1>")
|
| 138 |
+
gr.HTML("<p style='text-align: center;'>Hα» thα»ng mΓ΄ tαΊ£ hΓ¬nh αΊ£nh tα»± Δα»ng bαΊ±ng giα»ng nΓ³i TiαΊΏng Viα»t</p>")
|
| 139 |
+
|
| 140 |
+
with gr.Row():
|
| 141 |
+
with gr.Column(scale=1):
|
| 142 |
+
input_image = gr.Image(sources=["webcam", "upload"], type="pil", label="ΔαΊ§u vΓ o hΓ¬nh αΊ£nh")
|
| 143 |
+
vlm_version = gr.Radio(
|
| 144 |
+
choices=["Moondream2 (2B)", "Moondream2 (0.5B)"],
|
| 145 |
+
value="Moondream2 (2B)",
|
| 146 |
+
label="Mô hình VLM"
|
| 147 |
+
)
|
| 148 |
+
with gr.Row():
|
| 149 |
+
run_btn = gr.Button("Run Pipeline", variant="primary")
|
| 150 |
+
cancel_btn = gr.Button("Cancel", variant="stop")
|
| 151 |
+
|
| 152 |
+
with gr.Accordion("Parameters & Thresholds (CαΊ₯u hΓ¬nh nΓ’ng cao)", open=False):
|
| 153 |
+
translate_mode = gr.Dropdown(
|
| 154 |
+
choices=["Auto-Detect (Online)", "Offline (Helsinki-NLP)"],
|
| 155 |
+
value="Auto-Detect (Online)",
|
| 156 |
+
label="ChαΊΏ Δα» dα»ch"
|
| 157 |
+
)
|
| 158 |
+
tts_mode = gr.Dropdown(
|
| 159 |
+
choices=["Auto-Detect (Online)", "Offline (pyttsx3)"],
|
| 160 |
+
value="Auto-Detect (Online)",
|
| 161 |
+
label="ChαΊΏ Δα» TTS"
|
| 162 |
+
)
|
| 163 |
+
custom_prompt = gr.Textbox(
|
| 164 |
+
lines=2,
|
| 165 |
+
label="VLM Prompt Template",
|
| 166 |
+
placeholder="MαΊ·c Δα»nh: Describe what you see..."
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
with gr.Column(scale=1):
|
| 170 |
+
eng_out = gr.Textbox(label="MΓ΄ tαΊ£ TiαΊΏng Anh (VLM Output)", interactive=False)
|
| 171 |
+
vi_out = gr.Textbox(label="MΓ΄ tαΊ£ TiαΊΏng Viα»t (Dα»ch)", interactive=False)
|
| 172 |
+
audio_out = gr.Audio(label="Giα»ng Δα»c TiαΊΏng Viα»t", autoplay=True, interactive=False)
|
| 173 |
+
|
| 174 |
+
with gr.Group():
|
| 175 |
+
gr.Markdown("### π Performance Dashboard")
|
| 176 |
+
with gr.Row():
|
| 177 |
+
total_time_lbl = gr.Textbox(label="TOTAL TIME", value="0.000 s", interactive=False)
|
| 178 |
+
ram_usage_lbl = gr.Textbox(label="RAM USAGE", value="0.0 MB", interactive=False)
|
| 179 |
+
timing_chart = gr.HTML(value="<p>ChΖ°a chαΊ‘y xα» lΓ½.</p>")
|
| 180 |
+
|
| 181 |
+
run_event = run_btn.click(
|
| 182 |
+
fn=run_pipeline,
|
| 183 |
+
inputs=[input_image, vlm_version, translate_mode, tts_mode, custom_prompt],
|
| 184 |
+
outputs=[eng_out, vi_out, audio_out, total_time_lbl, ram_usage_lbl, timing_chart]
|
| 185 |
+
)
|
| 186 |
+
cancel_btn.click(fn=None, cancels=[run_event])
|
| 187 |
+
|
| 188 |
+
logger.info("Gradio Blocks UI built successfully.")
|
| 189 |
+
|
| 190 |
+
except Exception:
|
| 191 |
+
logger.critical("Failed to build Gradio UI!", exc_info=True)
|
| 192 |
+
raise
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
if __name__ == "__main__":
|
| 196 |
+
# For local development only β HF Spaces uses the root app.py instead.
|
| 197 |
+
# Model loads lazily on first Run click (no warm-start blocking the UI).
|
| 198 |
+
demo.queue().launch()
|
src/pipeline/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
src/pipeline/preprocess.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from PIL import Image
|
| 2 |
+
|
| 3 |
+
def preprocess_image(image: Image.Image) -> Image.Image:
|
| 4 |
+
if image is None:
|
| 5 |
+
raise ValueError("No image provided")
|
| 6 |
+
|
| 7 |
+
w, h = image.size
|
| 8 |
+
if w < 224 or h < 224:
|
| 9 |
+
raise ValueError(f"Image is too small ({w}x{h}). Minimum size is 224x224 px.")
|
| 10 |
+
|
| 11 |
+
# Convert RGBA or L to RGB
|
| 12 |
+
if image.mode != "RGB":
|
| 13 |
+
image = image.convert("RGB")
|
| 14 |
+
|
| 15 |
+
# Center crop / Resize if too large (> 1280px on any dimension)
|
| 16 |
+
max_size = 1280
|
| 17 |
+
if w > max_size or h > max_size:
|
| 18 |
+
if w > h:
|
| 19 |
+
new_w = max_size
|
| 20 |
+
new_h = int(h * (max_size / w))
|
| 21 |
+
else:
|
| 22 |
+
new_h = max_size
|
| 23 |
+
new_w = int(w * (max_size / h))
|
| 24 |
+
image = image.resize((new_w, new_h), Image.Resampling.LANCZOS)
|
| 25 |
+
|
| 26 |
+
return image
|
src/pipeline/translate.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from deep_translator import GoogleTranslator
|
| 3 |
+
|
| 4 |
+
class TranslatorModule:
|
| 5 |
+
def __init__(self, offline_model=None, offline_tokenizer=None):
|
| 6 |
+
self.offline_model = offline_model
|
| 7 |
+
self.offline_tokenizer = offline_tokenizer
|
| 8 |
+
self.google_translator = GoogleTranslator(source="auto", target="vi")
|
| 9 |
+
|
| 10 |
+
def translate(self, text: str, mode: str = "Auto-Detect (Online)") -> tuple[str, bool]:
|
| 11 |
+
if not text.strip():
|
| 12 |
+
return "", False
|
| 13 |
+
|
| 14 |
+
if mode == "Offline (Helsinki-NLP)":
|
| 15 |
+
return self._translate_offline(text), True
|
| 16 |
+
|
| 17 |
+
# Try Online translation
|
| 18 |
+
try:
|
| 19 |
+
translated_text = self.google_translator.translate(text)
|
| 20 |
+
return translated_text, False
|
| 21 |
+
except Exception as e:
|
| 22 |
+
logging.warning(f"Online translation failed: {e}. Falling back to offline translation.")
|
| 23 |
+
if self.offline_model and self.offline_tokenizer:
|
| 24 |
+
return self._translate_offline(text), True
|
| 25 |
+
else:
|
| 26 |
+
return f"[Lα»i mαΊ‘ng - KhΓ΄ng dα»ch Δược] {text}", False
|
| 27 |
+
|
| 28 |
+
def _translate_offline(self, text: str) -> str:
|
| 29 |
+
if not self.offline_model or not self.offline_tokenizer:
|
| 30 |
+
return f"[ChΖ°a load model offline] {text}"
|
| 31 |
+
|
| 32 |
+
inputs = self.offline_tokenizer(text, return_tensors="pt")
|
| 33 |
+
# Move tensors to same device as the model (handles CPU/GPU HF Spaces)
|
| 34 |
+
if hasattr(self.offline_model, "device") and hasattr(inputs, "to"):
|
| 35 |
+
inputs = inputs.to(self.offline_model.device)
|
| 36 |
+
generated_ids = self.offline_model.generate(**inputs)
|
| 37 |
+
translated_text = self.offline_tokenizer.decode(generated_ids[0], skip_special_tokens=True)
|
| 38 |
+
return translated_text
|
src/pipeline/tts.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import tempfile
|
| 3 |
+
import logging
|
| 4 |
+
from gtts import gTTS
|
| 5 |
+
import pyttsx3
|
| 6 |
+
|
| 7 |
+
class TTSModule:
|
| 8 |
+
def __init__(self):
|
| 9 |
+
self.offline_engine = None
|
| 10 |
+
|
| 11 |
+
def _init_offline(self):
|
| 12 |
+
if self.offline_engine is None:
|
| 13 |
+
try:
|
| 14 |
+
self.offline_engine = pyttsx3.init()
|
| 15 |
+
voices = self.offline_engine.getProperty("voices")
|
| 16 |
+
for voice in voices:
|
| 17 |
+
if voice.languages and any("vi" in lang for lang in voice.languages):
|
| 18 |
+
self.offline_engine.setProperty("voice", voice.id)
|
| 19 |
+
break
|
| 20 |
+
elif "vietnam" in voice.name.lower():
|
| 21 |
+
self.offline_engine.setProperty("voice", voice.id)
|
| 22 |
+
break
|
| 23 |
+
except Exception as e:
|
| 24 |
+
logging.error(f"Failed to initialize pyttsx3 offline TTS: {e}")
|
| 25 |
+
|
| 26 |
+
def generate_speech(self, text: str, mode: str = "Auto-Detect (Online)", filename: str = None) -> str:
|
| 27 |
+
if not text or not text.strip():
|
| 28 |
+
return None
|
| 29 |
+
|
| 30 |
+
if filename is None:
|
| 31 |
+
filename = os.path.join(tempfile.gettempdir(), "output.mp3")
|
| 32 |
+
|
| 33 |
+
# Clean existing file to avoid permission or handle lock issues
|
| 34 |
+
if os.path.exists(filename):
|
| 35 |
+
try:
|
| 36 |
+
os.remove(filename)
|
| 37 |
+
except Exception:
|
| 38 |
+
pass
|
| 39 |
+
|
| 40 |
+
if mode == "Offline (pyttsx3)":
|
| 41 |
+
try:
|
| 42 |
+
self._generate_offline(text, filename)
|
| 43 |
+
return filename
|
| 44 |
+
except Exception as e:
|
| 45 |
+
logging.error(f"Offline TTS failed: {e}")
|
| 46 |
+
return None
|
| 47 |
+
|
| 48 |
+
# Try Online gTTS
|
| 49 |
+
try:
|
| 50 |
+
tts = gTTS(text=text, lang="vi", slow=False)
|
| 51 |
+
tts.save(filename)
|
| 52 |
+
return filename
|
| 53 |
+
except Exception as e:
|
| 54 |
+
logging.warning(f"Online gTTS failed: {e}. Falling back to pyttsx3.")
|
| 55 |
+
try:
|
| 56 |
+
self._generate_offline(text, filename)
|
| 57 |
+
return filename
|
| 58 |
+
except Exception as e_off:
|
| 59 |
+
logging.error(f"Offline TTS failed: {e_off}")
|
| 60 |
+
return None
|
| 61 |
+
|
| 62 |
+
def _generate_offline(self, text: str, filename: str):
|
| 63 |
+
self._init_offline()
|
| 64 |
+
if self.offline_engine:
|
| 65 |
+
self.offline_engine.save_to_file(text, filename)
|
| 66 |
+
self.offline_engine.runAndWait()
|
| 67 |
+
else:
|
| 68 |
+
raise RuntimeError("Offline TTS engine pyttsx3 is not available.")
|
src/pipeline/vision_model.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from PIL import Image
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def run_vlm_inference(image: Image.Image, version: str, model, processor, prompt: str = "") -> str:
|
| 6 |
+
if version not in ["Moondream2 (2B)", "Moondream2 (0.5B)"]:
|
| 7 |
+
raise ValueError(f"Unsupported model version: {version}")
|
| 8 |
+
|
| 9 |
+
if not prompt or not prompt.strip():
|
| 10 |
+
prompt = (
|
| 11 |
+
"Describe what you see in this image briefly and clearly. "
|
| 12 |
+
"Focus on the main subject, people, objects, and any important context. "
|
| 13 |
+
"Keep it under 3 sentences."
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
# Check for the newer API (model.query) or fallback to older API
|
| 17 |
+
if hasattr(model, "query"):
|
| 18 |
+
response = model.query(image, prompt)
|
| 19 |
+
return response["answer"]
|
| 20 |
+
else:
|
| 21 |
+
# Fallback to older Moondream version API
|
| 22 |
+
enc_image = model.encode_image(image)
|
| 23 |
+
return model.answer_question(enc_image, prompt, processor)
|
src/registry.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import types
|
| 3 |
+
import logging
|
| 4 |
+
import threading
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer, MarianMTModel, MarianTokenizer
|
| 8 |
+
from src.pipeline.translate import TranslatorModule
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger("smartsight")
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _patch_moondream_for_cpu(model: nn.Module) -> nn.Module:
|
| 14 |
+
"""
|
| 15 |
+
Patch Moondream2 (revision 2025-01-09) for correct float32 CPU inference.
|
| 16 |
+
|
| 17 |
+
The upstream cached files have two hardcoded float16 issues that break CPU:
|
| 18 |
+
|
| 19 |
+
[Bug 1] vision.py line 45:
|
| 20 |
+
all_crops = torch.from_numpy(...).to(device=device, dtype=torch.float16)
|
| 21 |
+
β image crop tensor is Half, but model weights are Float32 after .float()
|
| 22 |
+
β RuntimeError: mat1 and mat2 must have the same dtype, but got Half and Float
|
| 23 |
+
|
| 24 |
+
[Bug 2] moondream.py encode_image():
|
| 25 |
+
kv_cache = torch.zeros(..., dtype=torch.float16)
|
| 26 |
+
β kv_cache is Half, but text model activations are Float32
|
| 27 |
+
β Potential dtype mismatch in prefill/decode_one_token ops
|
| 28 |
+
|
| 29 |
+
These files are cached by HuggingFace Hub and cannot be edited at deploy time.
|
| 30 |
+
We fix both bugs by monkey-patching the relevant functions after model load.
|
| 31 |
+
|
| 32 |
+
On GPU (CUDA / MPS) the model is left completely untouched β float16 is fine there.
|
| 33 |
+
"""
|
| 34 |
+
# HfMoondream wraps MoondreamModel as .model
|
| 35 |
+
inner: nn.Module = getattr(model, "model", model)
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
device = next(inner.parameters()).device
|
| 39 |
+
except StopIteration:
|
| 40 |
+
device = next(model.parameters()).device
|
| 41 |
+
|
| 42 |
+
if device.type != "cpu":
|
| 43 |
+
logger.info("GPU detected: skipping CPU float32 patch for Moondream2.")
|
| 44 |
+
return model
|
| 45 |
+
|
| 46 |
+
logger.info("CPU detected: applying Moondream2 float32 patches for HF Space.")
|
| 47 |
+
|
| 48 |
+
# ββ Step 1: Convert all nn.Parameters and registered buffers to float32 ββββ
|
| 49 |
+
inner.float()
|
| 50 |
+
logger.info("Step 1 done: model weights/buffers converted to float32.")
|
| 51 |
+
|
| 52 |
+
# ββ Step 2: Patch ops["vision_encoder"] βββββββββββββββββββββββββββββββββββββ
|
| 53 |
+
# Bug 1 fix: vision.py hardcodes .to(dtype=torch.float16) for image crops.
|
| 54 |
+
# We wrap the vision_encoder op to convert the crop tensor to float32 first.
|
| 55 |
+
_orig_vision_encoder = inner.ops["vision_encoder"]
|
| 56 |
+
|
| 57 |
+
def _float32_vision_encoder(crops: torch.Tensor, w, cfg):
|
| 58 |
+
return _orig_vision_encoder(crops.to(torch.float32), w, cfg)
|
| 59 |
+
|
| 60 |
+
inner.ops["vision_encoder"] = _float32_vision_encoder
|
| 61 |
+
logger.info("Step 2 done: vision_encoder patched β image crops cast float16βfloat32.")
|
| 62 |
+
|
| 63 |
+
# ββ Step 3: Patch encode_image to use float32 kv_cache ββββββββββββββββββββββ
|
| 64 |
+
# Bug 2 fix: encode_image hardcodes dtype=torch.float16 for kv_cache.
|
| 65 |
+
# We rewrite encode_image to use float32 for the kv_cache tensor.
|
| 66 |
+
moondream_mod = sys.modules.get(type(inner).__module__)
|
| 67 |
+
if moondream_mod is None:
|
| 68 |
+
logger.warning("Step 3 skipped: could not locate moondream module in sys.modules.")
|
| 69 |
+
else:
|
| 70 |
+
_EncodedImage = getattr(moondream_mod, "EncodedImage", None)
|
| 71 |
+
_text_encoder = getattr(moondream_mod, "text_encoder", None)
|
| 72 |
+
|
| 73 |
+
if _EncodedImage is None or _text_encoder is None:
|
| 74 |
+
logger.warning(
|
| 75 |
+
"Step 3 skipped: EncodedImage or text_encoder not found in moondream module."
|
| 76 |
+
)
|
| 77 |
+
else:
|
| 78 |
+
def _patched_encode_image(self, image):
|
| 79 |
+
# Pass-through if image is already encoded
|
| 80 |
+
if isinstance(image, _EncodedImage):
|
| 81 |
+
return image
|
| 82 |
+
if not hasattr(image, "size"):
|
| 83 |
+
raise ValueError("image must be a PIL Image or EncodedImage")
|
| 84 |
+
|
| 85 |
+
# Use float32 instead of the hardcoded float16
|
| 86 |
+
kv_cache = torch.zeros(
|
| 87 |
+
self.config.text.n_layers,
|
| 88 |
+
2, # k, v
|
| 89 |
+
1, # batch size
|
| 90 |
+
self.config.text.n_heads,
|
| 91 |
+
self.config.text.max_context,
|
| 92 |
+
self.config.text.dim // self.config.text.n_heads,
|
| 93 |
+
device=self.device,
|
| 94 |
+
dtype=torch.float32, # β patched from float16
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
with torch.no_grad():
|
| 98 |
+
img_emb = self._run_vision_encoder(image)
|
| 99 |
+
bos_emb = _text_encoder(
|
| 100 |
+
torch.tensor(
|
| 101 |
+
[[self.config.tokenizer.bos_id]], device=self.device
|
| 102 |
+
),
|
| 103 |
+
self.text,
|
| 104 |
+
)
|
| 105 |
+
inputs_embeds = torch.cat([bos_emb, img_emb[None]], dim=1)
|
| 106 |
+
self.ops["prefill"](
|
| 107 |
+
inputs_embeds, kv_cache, 0, self.text, self.config.text
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
return _EncodedImage(pos=inputs_embeds.size(1), kv_cache=kv_cache)
|
| 111 |
+
|
| 112 |
+
inner.encode_image = types.MethodType(_patched_encode_image, inner)
|
| 113 |
+
logger.info(
|
| 114 |
+
"Step 3 done: encode_image patched β kv_cache dtype float16βfloat32."
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
logger.info("Moondream2 CPU patches applied. Ready for float32 inference.")
|
| 118 |
+
return model
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
class ModelRegistry:
|
| 122 |
+
_instance = None
|
| 123 |
+
_lock = threading.Lock()
|
| 124 |
+
|
| 125 |
+
def __new__(cls):
|
| 126 |
+
with cls._lock:
|
| 127 |
+
if cls._instance is None:
|
| 128 |
+
cls._instance = super(ModelRegistry, cls).__new__(cls)
|
| 129 |
+
cls._instance.vlm_models = {"Moondream2 (2B)": None, "Moondream2 (0.5B)": None}
|
| 130 |
+
cls._instance.vlm_processors = {"Moondream2 (2B)": None, "Moondream2 (0.5B)": None}
|
| 131 |
+
cls._instance.translation_model = None
|
| 132 |
+
cls._instance.translation_tokenizer = None
|
| 133 |
+
cls._instance.translator_instance = None
|
| 134 |
+
return cls._instance
|
| 135 |
+
|
| 136 |
+
def get_vlm(self, version: str) -> tuple:
|
| 137 |
+
if version not in self.vlm_models:
|
| 138 |
+
raise ValueError(f"Unknown VLM model version: {version}")
|
| 139 |
+
|
| 140 |
+
if self.vlm_models[version] is None:
|
| 141 |
+
if version == "Moondream2 (0.5B)":
|
| 142 |
+
model_id = "andito/moondream05" # Community transformers 0.5B model
|
| 143 |
+
processor = AutoTokenizer.from_pretrained(model_id)
|
| 144 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 145 |
+
model_id,
|
| 146 |
+
trust_remote_code=True,
|
| 147 |
+
torch_dtype=torch.float32,
|
| 148 |
+
)
|
| 149 |
+
else: # Moondream2 (2B)
|
| 150 |
+
model_id = "vikhyatk/moondream2"
|
| 151 |
+
revision = "2025-01-09" # API 2025 compatible
|
| 152 |
+
processor = AutoTokenizer.from_pretrained(model_id, revision=revision)
|
| 153 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 154 |
+
model_id,
|
| 155 |
+
revision=revision,
|
| 156 |
+
trust_remote_code=True,
|
| 157 |
+
torch_dtype=torch.float32, # load weights as float32
|
| 158 |
+
)
|
| 159 |
+
model = _patch_moondream_for_cpu(model)
|
| 160 |
+
self.vlm_models[version] = model
|
| 161 |
+
self.vlm_processors[version] = processor
|
| 162 |
+
|
| 163 |
+
return self.vlm_models[version], self.vlm_processors[version]
|
| 164 |
+
|
| 165 |
+
def get_translator_module(self, mode: str) -> TranslatorModule:
|
| 166 |
+
if self.translator_instance is None:
|
| 167 |
+
model_id = "Helsinki-NLP/opus-mt-en-vi"
|
| 168 |
+
self.translation_tokenizer = MarianTokenizer.from_pretrained(model_id)
|
| 169 |
+
self.translation_model = MarianMTModel.from_pretrained(model_id)
|
| 170 |
+
self.translator_instance = TranslatorModule(self.translation_model, self.translation_tokenizer)
|
| 171 |
+
return self.translator_instance
|
src/utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
src/utils/monitor.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import psutil
|
| 2 |
+
import time
|
| 3 |
+
from contextlib import contextmanager
|
| 4 |
+
|
| 5 |
+
class ExecutionMonitor:
|
| 6 |
+
def __init__(self):
|
| 7 |
+
self.durations = {}
|
| 8 |
+
|
| 9 |
+
def get_ram_usage(self) -> float:
|
| 10 |
+
process = psutil.Process()
|
| 11 |
+
return process.memory_info().rss / (1024 * 1024) # MB
|
| 12 |
+
|
| 13 |
+
@contextmanager
|
| 14 |
+
def track(self, stage_name: str):
|
| 15 |
+
start = time.perf_counter()
|
| 16 |
+
try:
|
| 17 |
+
yield
|
| 18 |
+
finally:
|
| 19 |
+
self.durations[stage_name] = time.perf_counter() - start
|
| 20 |
+
|
| 21 |
+
def get_durations(self) -> dict:
|
| 22 |
+
return self.durations
|