ChoruYt commited on
Commit
596966d
Β·
verified Β·
1 Parent(s): 35516cf

Upload server.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. server.py +356 -0
server.py ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import base64
4
+ import ctypes
5
+ import threading
6
+ from flask import Flask, request, jsonify, Response
7
+ from flask_cors import CORS
8
+
9
+ HF_REPO = "litert-community/gemma-4-E2B-it-litert-lm"
10
+ HF_FILE = "gemma-4-E2B-it.litertlm"
11
+
12
+ _SERVER_DIR = os.path.dirname(os.path.abspath(__file__))
13
+ _DEFAULT_PATH = os.path.join(_SERVER_DIR, "models", "gemma", HF_FILE)
14
+
15
+ # litert_lm links against libvulkan.so.1 even on CPU-only runs.
16
+ # Pre-load a stub so the dynamic linker is satisfied without a real GPU.
17
+ _vk_stub = os.path.join(_SERVER_DIR, "libvulkan.so.1")
18
+ if os.path.exists(_vk_stub):
19
+ try:
20
+ ctypes.CDLL(_vk_stub, mode=ctypes.RTLD_GLOBAL)
21
+ except OSError:
22
+ pass
23
+
24
+ # Suppress verbose C++ logs from litert_lm
25
+ os.environ.setdefault("GLOG_minloglevel", "3")
26
+
27
+ MODEL_PATH = os.environ.get("GEMMA_MODEL_PATH", _DEFAULT_PATH).strip()
28
+
29
+ model_status = "loading"
30
+ engine = None
31
+ _engine_ctx = None
32
+ engine_lock = threading.Lock()
33
+
34
+ app = Flask(__name__)
35
+ CORS(app)
36
+
37
+
38
+ # ─── Model loading ─────────────────────────────────────────────────────────────
39
+
40
+ def load_model():
41
+ global engine, model_status, _engine_ctx
42
+ if not MODEL_PATH:
43
+ print("[INFO] GEMMA_MODEL_PATH not set β€” no model loaded", flush=True)
44
+ model_status = "no_model_path"
45
+ return
46
+ try:
47
+ import litert_lm as _lm
48
+ _lm.set_min_log_severity(_lm.LogSeverity.SILENT)
49
+ except ImportError:
50
+ print("[INFO] litert_lm not installed β€” no model loaded", flush=True)
51
+ model_status = "no_litert_lm"
52
+ return
53
+ if not os.path.exists(MODEL_PATH):
54
+ print(f"[WARN] Model file not found: {MODEL_PATH}", flush=True)
55
+ model_status = "model_file_missing"
56
+ return
57
+ try:
58
+ _engine_ctx = _lm.Engine(
59
+ MODEL_PATH,
60
+ backend=_lm.interfaces.CPU(),
61
+ vision_backend=_lm.interfaces.CPU(),
62
+ )
63
+ engine = _engine_ctx.__enter__()
64
+ model_status = "ready"
65
+ print(f"[INFO] Model ready β†’ {MODEL_PATH}", flush=True)
66
+ except Exception as e:
67
+ print(f"[ERROR] Failed to load model: {e}", flush=True)
68
+ model_status = "error"
69
+
70
+
71
+ # ─── Image analysis via Pillow (no model) ──────────────────────────────────────
72
+
73
+ def _analyze_image(image_bytes: bytes) -> dict:
74
+ from PIL import Image
75
+ img = Image.open(io.BytesIO(image_bytes))
76
+ w, h = img.size
77
+ fmt = (img.format or "image").lower()
78
+
79
+ thumb = img.convert("RGB").resize((64, 64))
80
+ pixels = list(thumb.getdata())
81
+ n = len(pixels)
82
+ r = sum(p[0] for p in pixels) // n
83
+ g = sum(p[1] for p in pixels) // n
84
+ b = sum(p[2] for p in pixels) // n
85
+
86
+ lum = (r * 299 + g * 587 + b * 114) // 1000
87
+ tone = "bright" if lum > 200 else "medium" if lum > 130 else "dark" if lum > 60 else "very dark"
88
+
89
+ diff = max(r, g, b) - min(r, g, b)
90
+ if diff < 25:
91
+ hue = "neutral / grayscale"
92
+ elif max(r, g, b) == r and r - g > 30:
93
+ hue = "red / warm"
94
+ elif max(r, g, b) == g and g - b > 20:
95
+ hue = "green"
96
+ elif max(r, g, b) == b:
97
+ hue = "blue / cool"
98
+ elif r > 180 and g > 150 and b < 100:
99
+ hue = "yellow / orange"
100
+ elif r > 150 and b > 150 and g < 100:
101
+ hue = "purple / violet"
102
+ else:
103
+ hue = "mixed"
104
+
105
+ ratio = w / h if h else 1
106
+ orientation = "landscape" if ratio > 1.4 else "portrait" if ratio < 0.72 else "square"
107
+
108
+ return dict(w=w, h=h, fmt=fmt, r=r, g=g, b=b, tone=tone, hue=hue, orientation=orientation)
109
+
110
+
111
+ def _describe_image(ask: str, image_bytes: bytes) -> str:
112
+ try:
113
+ i = _analyze_image(image_bytes)
114
+ q = ask.lower()
115
+ out = [
116
+ f"This is a {i['orientation']} {i['fmt']} image ({i['w']}Γ—{i['h']} px).",
117
+ f"Overall tone: {i['tone']}. Dominant color: {i['hue']}. Average RGB: ({i['r']}, {i['g']}, {i['b']}).",
118
+ ]
119
+ if any(w in q for w in ["color", "colour", "kulay"]):
120
+ out.append(f"Main color: {i['hue']} β€” RGB({i['r']}, {i['g']}, {i['b']}).")
121
+ elif any(w in q for w in ["size", "dimension", "laki", "sukat"]):
122
+ out.append(f"Dimensions: {i['w']}Γ—{i['h']} pixels.")
123
+ elif any(w in q for w in ["bright", "dark", "liwanag"]):
124
+ out.append(f"Brightness: {i['tone']}.")
125
+
126
+ out.append(
127
+ f"For full scene/object understanding, load the real model: "
128
+ f"GET /gemma/download or set GEMMA_MODEL_PATH."
129
+ )
130
+ return "\n".join(out)
131
+ except Exception as e:
132
+ return f"Could not analyze image: {e}"
133
+
134
+
135
+ # ─── Fallback text replies (no model) ─────────────────────────────────────────
136
+
137
+ def _hint() -> str:
138
+ if model_status == "no_litert_lm":
139
+ return "litert_lm is not installed. Run: pip install litert-lm"
140
+ if model_status in ("no_model_path", "model_file_missing"):
141
+ return (f"Model not found at '{MODEL_PATH}'. "
142
+ f"Run: GET /gemma/download to fetch it automatically.")
143
+ return "Connect a Gemma 4 model to enable full responses."
144
+
145
+ _REPLIES = [
146
+ lambda h: f"Hello! I'm the Gemma 4 API. {h}",
147
+ lambda h: f"Gemma 4 is Google's open multimodal model β€” text + image input, runs on-device via LiteRT. {h}",
148
+ lambda h: f"Send images as base64 JSON (field 'image') or multipart/form-data file upload. {h}",
149
+ lambda h: f"API: GET /gemma?ask=... or POST /gemma {{\"ask\":\"...\",\"image\":\"<base64>\"}}. {h}",
150
+ ]
151
+ _idx = 0
152
+
153
+ def _text_reply(ask: str) -> str:
154
+ global _idx
155
+ h, q = _hint(), ask.lower()
156
+ if any(w in q for w in ["hello", "hi", "hey", "kumusta"]):
157
+ return _REPLIES[0](h)
158
+ if any(w in q for w in ["gemma", "model", "what are you"]):
159
+ return _REPLIES[1](h)
160
+ if any(w in q for w in ["image", "photo", "picture", "larawan"]):
161
+ return _REPLIES[2](h)
162
+ if any(w in q for w in ["how", "api", "endpoint", "use", "query"]):
163
+ return _REPLIES[3](h)
164
+ r = _REPLIES[_idx % len(_REPLIES)](h)
165
+ _idx += 1
166
+ return r
167
+
168
+
169
+ # ─── Real model inference (litert_lm) ─────────────────────────────────────────
170
+
171
+ def _run_real_model(ask: str, image_bytes: bytes | None) -> str:
172
+ import litert_lm
173
+ with engine_lock:
174
+ try:
175
+ with engine.create_conversation() as conv:
176
+ if image_bytes:
177
+ msg = litert_lm.Contents.of(
178
+ litert_lm.Content.ImageBytes(image_bytes),
179
+ litert_lm.Content.Text(ask),
180
+ )
181
+ else:
182
+ msg = ask
183
+
184
+ out = []
185
+ for chunk in conv.send_message_async(msg):
186
+ for part in chunk.get("content", []):
187
+ if part.get("type") == "text":
188
+ out.append(part.get("text", ""))
189
+ return "".join(out) or "(empty response)"
190
+ except Exception as e:
191
+ return f"Model error: {e}"
192
+
193
+
194
+ def run_model(ask: str, image_bytes: bytes | None) -> str:
195
+ if engine is not None and model_status == "ready":
196
+ return _run_real_model(ask, image_bytes)
197
+ if image_bytes:
198
+ return _describe_image(ask, image_bytes)
199
+ return _text_reply(ask)
200
+
201
+
202
+ # ─── Request extraction ────────────────────────────────────────────────────────
203
+
204
+ def extract_request() -> tuple[str, bytes | None]:
205
+ if request.method == "GET":
206
+ return request.args.get("ask", "").strip(), None
207
+
208
+ ct = request.content_type or ""
209
+ if "multipart/form-data" in ct:
210
+ ask = request.form.get("ask", "").strip()
211
+ f = request.files.get("image")
212
+ return ask, (f.read() if f else None)
213
+
214
+ data = request.get_json(silent=True) or {}
215
+ ask = data.get("ask", "").strip()
216
+ image_bytes = None
217
+ raw = data.get("image", "")
218
+ if raw:
219
+ try:
220
+ if "," in raw:
221
+ raw = raw.split(",", 1)[1]
222
+ image_bytes = base64.b64decode(raw)
223
+ except Exception:
224
+ pass
225
+ return ask, image_bytes
226
+
227
+
228
+ # ─── Routes ────────────────────────────────────────────────────────────────────
229
+
230
+ @app.route("/favicon.ico")
231
+ def favicon():
232
+ return "", 204
233
+
234
+
235
+ @app.route("/")
236
+ def index():
237
+ return jsonify({
238
+ "service": "Gemma 4 API",
239
+ "model_status": model_status,
240
+ "model_path": MODEL_PATH or None,
241
+ "endpoints": {
242
+ "GET /gemma?ask=hello": "Text query",
243
+ "POST /gemma {ask, image?}": "JSON body β€” text + optional base64 image",
244
+ "POST /gemma multipart/form-data": "File upload β€” text + optional image file",
245
+ "GET /gemma/download": "Download Gemma 4 model from HuggingFace",
246
+ "GET /health": "Health check",
247
+ },
248
+ })
249
+
250
+
251
+ @app.route("/health")
252
+ @app.route("/api/health")
253
+ def health():
254
+ info = {"status": "ok", "model_status": model_status}
255
+ if MODEL_PATH:
256
+ info["model_path"] = MODEL_PATH
257
+ return jsonify(info)
258
+
259
+
260
+ @app.route("/gemma", methods=["GET", "POST"])
261
+ def gemma():
262
+ ask, image_bytes = extract_request()
263
+ if not ask:
264
+ return jsonify({"error": "Missing 'ask' parameter"}), 400
265
+ response = run_model(ask, image_bytes)
266
+ return jsonify({
267
+ "ask": ask,
268
+ "response": response,
269
+ "has_image": image_bytes is not None,
270
+ "model_status": model_status,
271
+ })
272
+
273
+
274
+ # ─── Download state ────────────────────────────────────────────────────────────
275
+
276
+ _dl: dict = {"status": "idle", "path": None, "error": None, "bytes_done": 0}
277
+ _dl_lock = threading.Lock()
278
+
279
+
280
+ def _do_download(save_to: str):
281
+ global _dl
282
+ with _dl_lock:
283
+ _dl.update(status="downloading", path=save_to, error=None, bytes_done=0)
284
+ try:
285
+ from huggingface_hub import hf_hub_download
286
+ save_dir = os.path.dirname(os.path.abspath(save_to))
287
+ os.makedirs(save_dir, exist_ok=True)
288
+ result_path = hf_hub_download(
289
+ repo_id = HF_REPO,
290
+ filename = HF_FILE,
291
+ local_dir = save_dir,
292
+ )
293
+ # hf_hub_download saves to local_dir/<filename>
294
+ canonical = os.path.join(save_dir, HF_FILE)
295
+ if result_path != save_to and os.path.exists(result_path):
296
+ os.replace(result_path, save_to)
297
+ with _dl_lock:
298
+ _dl.update(status="done", path=save_to, bytes_done=os.path.getsize(save_to))
299
+ print(f"[INFO] Model downloaded β†’ {save_to}", flush=True)
300
+ except Exception as e:
301
+ with _dl_lock:
302
+ _dl.update(status="error", error=str(e))
303
+ print(f"[ERROR] Download failed: {e}", flush=True)
304
+
305
+
306
+ @app.route("/gemma/download", methods=["GET", "POST"])
307
+ def download_model():
308
+ """Start (or check) background download of the Gemma 4 model from HuggingFace."""
309
+ save_to = request.args.get("path", "").strip() or _DEFAULT_PATH
310
+
311
+ # Status check only (no action)
312
+ if request.args.get("status"):
313
+ with _dl_lock:
314
+ info = dict(_dl)
315
+ info["model_path"] = save_to
316
+ return jsonify(info)
317
+
318
+ # Already on disk
319
+ if os.path.exists(save_to):
320
+ size_mb = os.path.getsize(save_to) // (1024 * 1024)
321
+ return jsonify({
322
+ "status": "already_exists",
323
+ "model_path": save_to,
324
+ "size_mb": size_mb,
325
+ "next_step": f"Set env var GEMMA_MODEL_PATH={save_to} and restart the server.",
326
+ })
327
+
328
+ with _dl_lock:
329
+ current = _dl["status"]
330
+
331
+ if current == "downloading":
332
+ return jsonify({
333
+ "status": "downloading",
334
+ "message": "Download already in progress. Poll GET /gemma/download?status=1",
335
+ })
336
+
337
+ # Kick off background download
338
+ threading.Thread(target=_do_download, args=(save_to,), daemon=True).start()
339
+ return jsonify({
340
+ "status": "started",
341
+ "model": f"{HF_REPO}/{HF_FILE}",
342
+ "saving_to": save_to,
343
+ "size": "~2.5 GB β€” will take a few minutes",
344
+ "poll": "GET /gemma/download?status=1",
345
+ "next_step": f"When done, set GEMMA_MODEL_PATH={save_to} and restart the server.",
346
+ })
347
+
348
+
349
+ # ─── Entry ─────────────────────────────────────────────────────────────────────
350
+
351
+ if __name__ == "__main__":
352
+ port = int(os.environ.get("PORT", 5173))
353
+ threading.Thread(target=load_model, daemon=True).start()
354
+ print(f"[INFO] Gemma API on :{port}", flush=True)
355
+ print(f"[INFO] Model: {MODEL_PATH or '(not set β€” GET /gemma/download to fetch)'}", flush=True)
356
+ app.run(host="0.0.0.0", port=port, debug=False)