cloudunity commited on
Commit
88ffff7
Β·
verified Β·
1 Parent(s): 701341b

Update server.py

Browse files
Files changed (1) hide show
  1. server.py +116 -118
server.py CHANGED
@@ -5,10 +5,11 @@ import ctypes
5
  import threading
6
  import json
7
  import time
 
8
  from flask import Flask, request, jsonify, Response
9
  from flask_cors import CORS
10
 
11
- # --- KEY CHANGE: Balik sa 2B Model ---
12
  HF_REPO = "litert-community/gemma-4-E2B-it-litert-lm"
13
  HF_FILE = "gemma-4-E2B-it.litertlm"
14
 
@@ -27,6 +28,7 @@ if os.path.exists(_vk_stub):
27
  os.environ.setdefault("GLOG_minloglevel", "3")
28
 
29
  MODEL_PATH = os.environ.get("GEMMA_MODEL_PATH", _DEFAULT_PATH).strip()
 
30
 
31
  model_status = "loading"
32
  engine = None
@@ -70,27 +72,38 @@ def load_model():
70
  model_status = "error"
71
 
72
 
73
- # ─── Fallback functions (No Model) ──────────────────────────────────────────────
74
 
75
- def _analyze_image(image_bytes: bytes) -> dict:
76
- from PIL import Image
77
- img = Image.open(io.BytesIO(image_bytes))
78
- w, h = img.size
79
- fmt = (img.format or "image").lower()
80
- return dict(w=w, h=h, fmt=fmt)
81
 
82
- def _describe_image(ask: str, image_bytes: bytes) -> str:
83
- try:
84
- i = _analyze_image(image_bytes)
85
- return f"[MOCK] This is a {i['fmt']} image ({i['w']}Γ—{i['h']} px). Connect the real model for full vision analysis."
86
- except Exception as e:
87
- return f"Could not analyze image: {e}"
88
 
89
- def _text_reply(ask: str) -> str:
90
- return f"[MOCK] Hello! I received: '{ask}'. Connect the Gemma model to see real answers."
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
 
 
92
 
93
- # ─── Real model inference (litert_lm Generator) ────────────────────────────────
 
94
 
95
  def _run_real_model_generator(ask: str, image_bytes: bytes | None):
96
  """Yields text chunks as they are generated by the model."""
@@ -114,122 +127,107 @@ def _run_real_model_generator(ask: str, image_bytes: bytes | None):
114
  yield text
115
 
116
 
117
- # ─── Request extraction ────────────────────────────────────────────────────────
118
-
119
- def extract_request() -> tuple[str, bytes | None, bool]:
120
- # Check query params first (e.g. ?stream=true)
121
- stream = str(request.args.get("stream", "")).lower() in ["true", "1", "yes"]
122
-
123
- if request.method == "GET":
124
- return request.args.get("ask", "").strip(), None, stream
125
-
126
- ct = request.content_type or ""
127
-
128
- # Handle Form Data (File Uploads)
129
- if "multipart/form-data" in ct:
130
- ask = request.form.get("ask", "").strip()
131
- f = request.files.get("image")
132
- if "stream" in request.form:
133
- stream = str(request.form.get("stream")).lower() in ["true", "1", "yes"]
134
- return ask, (f.read() if f else None), stream
135
-
136
- # Handle JSON
137
- data = request.get_json(silent=True) or {}
138
- ask = data.get("ask", "").strip()
139
- image_bytes = None
140
- raw = data.get("image", "")
141
-
142
- if "stream" in data:
143
- stream = str(data.get("stream")).lower() in ["true", "1", "yes"]
144
-
145
- if raw:
146
- try:
147
- if "," in raw:
148
- raw = raw.split(",", 1)[1]
149
- image_bytes = base64.b64decode(raw)
150
- except Exception:
151
- pass
152
-
153
- return ask, image_bytes, stream
154
 
155
 
156
  # ─── Routes ────────────────────────────────────────────────────────────────────
157
 
158
- @app.route("/")
159
- def index():
 
160
  return jsonify({
161
- "service": "Gemma 4 API (2B Model)",
162
- "model_status": model_status,
163
- "guide": {
164
- "1. Text Only Chat": {
165
- "GET_Example": "/gemma?ask=Hello&stream=false",
166
- "POST_JSON": {"ask": "What is AI?", "stream": True}
167
- },
168
- "2. Image with Text": {
169
- "POST_JSON": {
170
- "ask": "Describe this image",
171
- "image": "<base64_string_here>",
172
- "stream": False
173
- },
174
- "POST_FormData": {
175
- "ask": "What color is this?",
176
- "image": "@file.jpg (File Upload)",
177
- "stream": "true"
178
- }
179
- },
180
- "Streaming Info": "Set 'stream': true to receive Server-Sent Events (SSE) by token. Set false to wait for 1 full JSON response."
181
- }
182
  })
183
 
184
-
185
- @app.route("/health")
186
- def health():
187
- return jsonify({"status": "ok", "model_status": model_status})
188
-
189
-
190
- @app.route("/gemma", methods=["GET", "POST"])
191
- def gemma():
192
- ask, image_bytes, stream = extract_request()
193
 
194
- if not ask:
195
- return jsonify({"error": "Missing 'ask' parameter"}), 400
196
 
197
- # Fallback if model isn't ready
 
 
198
  if engine is None or model_status != "ready":
199
- fallback_msg = _describe_image(ask, image_bytes) if image_bytes else _text_reply(ask)
200
-
201
- if stream:
202
- def mock_stream():
203
- for word in fallback_msg.split():
204
- yield f"data: {json.dumps({'text': word + ' '})}\n\n"
205
- time.sleep(0.05)
206
- yield "data: [DONE]\n\n"
207
- return Response(mock_stream(), mimetype="text/event-stream")
208
- else:
209
- return jsonify({"ask": ask, "response": fallback_msg, "has_image": bool(image_bytes)})
210
-
211
- # Real Model Logic
212
  if stream:
213
- def generate_stream():
 
 
 
 
 
 
 
 
214
  try:
215
- for text_chunk in _run_real_model_generator(ask, image_bytes):
216
- yield f"data: {json.dumps({'text': text_chunk})}\n\n"
217
- yield "data: [DONE]\n\n"
 
 
 
218
  except Exception as e:
219
- yield f"data: {json.dumps({'error': str(e)})}\n\n"
220
-
221
- return Response(generate_stream(), mimetype="text/event-stream")
 
 
 
 
 
 
 
 
 
 
222
  else:
223
  try:
224
- full_text = "".join(list(_run_real_model_generator(ask, image_bytes)))
225
- return jsonify({
226
- "ask": ask,
227
- "response": full_text,
228
- "has_image": image_bytes is not None,
229
- "model_status": model_status
230
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  except Exception as e:
232
- return jsonify({"error": f"Model error: {e}"}), 500
233
 
234
 
235
  # ─── Entry ─────────────────────────────────────────────────────────────────────
@@ -237,5 +235,5 @@ def gemma():
237
  if __name__ == "__main__":
238
  port = int(os.environ.get("PORT", 5173))
239
  threading.Thread(target=load_model, daemon=True).start()
240
- print(f"[INFO] Gemma API on :{port}", flush=True)
241
  app.run(host="0.0.0.0", port=port, debug=False)
 
5
  import threading
6
  import json
7
  import time
8
+ import uuid
9
  from flask import Flask, request, jsonify, Response
10
  from flask_cors import CORS
11
 
12
+ # --- Model Configuration ---
13
  HF_REPO = "litert-community/gemma-4-E2B-it-litert-lm"
14
  HF_FILE = "gemma-4-E2B-it.litertlm"
15
 
 
28
  os.environ.setdefault("GLOG_minloglevel", "3")
29
 
30
  MODEL_PATH = os.environ.get("GEMMA_MODEL_PATH", _DEFAULT_PATH).strip()
31
+ MODEL_ID = "gemma-4-e2b"
32
 
33
  model_status = "loading"
34
  engine = None
 
72
  model_status = "error"
73
 
74
 
75
+ # ─── OpenAI Request Parsing ────────────────────────────────────────────────────
76
 
77
+ def parse_openai_messages(messages: list) -> tuple[str, bytes | None]:
78
+ """Parses OpenAI formatted messages into a flat text prompt and an optional image."""
79
+ prompt_text = ""
80
+ image_bytes = None
 
 
81
 
82
+ for msg in messages:
83
+ role = msg.get("role", "user")
84
+ content = msg.get("content", "")
 
 
 
85
 
86
+ if isinstance(content, str):
87
+ prompt_text += f"{role}: {content}\n"
88
+ elif isinstance(content, list):
89
+ prompt_text += f"{role}:\n"
90
+ for part in content:
91
+ if part.get("type") == "text":
92
+ prompt_text += part.get("text", "") + "\n"
93
+ elif part.get("type") == "image_url":
94
+ url = part.get("image_url", {}).get("url", "")
95
+ if url.startswith("data:image"):
96
+ try:
97
+ b64_data = url.split(",", 1)[1]
98
+ image_bytes = base64.b64decode(b64_data)
99
+ except Exception as e:
100
+ print(f"[WARN] Failed to decode base64 image: {e}")
101
 
102
+ prompt_text += "assistant: "
103
+ return prompt_text.strip(), image_bytes
104
 
105
+
106
+ # ─── Inference Engine ──────────────────────────────────────────────────────────
107
 
108
  def _run_real_model_generator(ask: str, image_bytes: bytes | None):
109
  """Yields text chunks as they are generated by the model."""
 
127
  yield text
128
 
129
 
130
+ def _run_mock_generator(ask: str, has_image: bool):
131
+ """Fallback generator when the model is missing/loading."""
132
+ msg = f"[MOCK] Received prompt. Vision included: {has_image}. Connect litert_lm for real output."
133
+ for word in msg.split():
134
+ yield word + " "
135
+ time.sleep(0.05)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
 
138
  # ─── Routes ────────────────────────────────────────────────────────────────────
139
 
140
+ @app.route("/v1/models", methods=["GET"])
141
+ def list_models():
142
+ """OpenAI models endpoint."""
143
  return jsonify({
144
+ "object": "list",
145
+ "data": [{
146
+ "id": MODEL_ID,
147
+ "object": "model",
148
+ "created": int(time.time()),
149
+ "owned_by": "litert-community"
150
+ }]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  })
152
 
153
+ @app.route("/v1/chat/completions", methods=["POST"])
154
+ def chat_completions():
155
+ """OpenAI compatible chat completions endpoint."""
156
+ data = request.get_json(silent=True) or {}
157
+ messages = data.get("messages", [])
158
+ stream = data.get("stream", False)
 
 
 
159
 
160
+ if not messages:
161
+ return jsonify({"error": {"message": "Missing 'messages' array", "type": "invalid_request_error"}}), 400
162
 
163
+ ask, image_bytes = parse_openai_messages(messages)
164
+
165
+ # Determine which generator to use
166
  if engine is None or model_status != "ready":
167
+ generator = _run_mock_generator(ask, bool(image_bytes))
168
+ else:
169
+ generator = _run_real_model_generator(ask, image_bytes)
170
+
171
+ req_model = data.get("model", MODEL_ID)
172
+ cmpl_id = f"chatcmpl-{uuid.uuid4().hex}"
173
+ created_time = int(time.time())
174
+
 
 
 
 
 
175
  if stream:
176
+ def stream_response():
177
+ # 1. Initial chunk indicating role
178
+ init_chunk = {
179
+ "id": cmpl_id, "object": "chat.completion.chunk", "created": created_time, "model": req_model,
180
+ "choices": [{"index": 0, "delta": {"role": "assistant"}, "finish_reason": None}]
181
+ }
182
+ yield f"data: {json.dumps(init_chunk)}\n\n"
183
+
184
+ # 2. Stream tokens
185
  try:
186
+ for text_chunk in generator:
187
+ chunk = {
188
+ "id": cmpl_id, "object": "chat.completion.chunk", "created": created_time, "model": req_model,
189
+ "choices": [{"index": 0, "delta": {"content": text_chunk}, "finish_reason": None}]
190
+ }
191
+ yield f"data: {json.dumps(chunk)}\n\n"
192
  except Exception as e:
193
+ err_chunk = {"error": str(e)}
194
+ yield f"data: {json.dumps(err_chunk)}\n\n"
195
+
196
+ # 3. Final chunk indicating stop
197
+ final_chunk = {
198
+ "id": cmpl_id, "object": "chat.completion.chunk", "created": created_time, "model": req_model,
199
+ "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]
200
+ }
201
+ yield f"data: {json.dumps(final_chunk)}\n\n"
202
+ yield "data: [DONE]\n\n"
203
+
204
+ return Response(stream_response(), mimetype="text/event-stream")
205
+
206
  else:
207
  try:
208
+ full_text = "".join(list(generator))
209
+ response = {
210
+ "id": cmpl_id,
211
+ "object": "chat.completion",
212
+ "created": created_time,
213
+ "model": req_model,
214
+ "choices": [{
215
+ "index": 0,
216
+ "message": {
217
+ "role": "assistant",
218
+ "content": full_text
219
+ },
220
+ "finish_reason": "stop"
221
+ }],
222
+ "usage": {
223
+ "prompt_tokens": 0, # litert_lm token counting not implemented
224
+ "completion_tokens": 0,
225
+ "total_tokens": 0
226
+ }
227
+ }
228
+ return jsonify(response)
229
  except Exception as e:
230
+ return jsonify({"error": {"message": f"Model error: {e}", "type": "server_error"}}), 500
231
 
232
 
233
  # ─── Entry ─────────────────────────────────────────────────────────────────────
 
235
  if __name__ == "__main__":
236
  port = int(os.environ.get("PORT", 5173))
237
  threading.Thread(target=load_model, daemon=True).start()
238
+ print(f"[INFO] Gemma OpenAI-Compatible API listening on :{port}", flush=True)
239
  app.run(host="0.0.0.0", port=port, debug=False)