GitHub Actions commited on
Commit
23e79c5
Β·
0 Parent(s):

deploy to Hugging Face Spaces

Browse files
.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