cloudunity commited on
Commit
1f49d04
·
verified ·
1 Parent(s): 23d57e6

Update server.py

Browse files
Files changed (1) hide show
  1. server.py +43 -252
server.py CHANGED
@@ -1,93 +1,46 @@
1
- import base64
2
  import io
3
  import logging
4
  import os
5
  import threading
6
- import time
7
- import uuid
8
 
9
  import torch
10
  from flask import Flask, jsonify, request, send_file
11
  from flask_cors import CORS
12
 
13
- # ---------------------------------------------------------------------------
14
- # CPU Optimization - Limit threads to avoid thrashing on HF Spaces
15
- # ---------------------------------------------------------------------------
16
  os.environ["OMP_NUM_THREADS"] = "4"
17
  os.environ["MKL_NUM_THREADS"] = "4"
18
  torch.set_num_threads(4)
19
  torch.set_num_interop_threads(1)
20
 
21
- # ---------------------------------------------------------------------------
22
- # Logging
23
- # ---------------------------------------------------------------------------
24
-
25
- logging.basicConfig(
26
- level=logging.INFO,
27
- format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
28
- )
29
  logger = logging.getLogger("sd-turbo-server")
30
 
31
- # ---------------------------------------------------------------------------
32
- # Configuration
33
- # ---------------------------------------------------------------------------
34
-
35
- IMAGE_MODEL = os.environ.get("IMAGE_MODEL", "stabilityai/sd-turbo")
36
- MODEL_LOCAL_DIR = os.environ.get("MODEL_LOCAL_DIR", "/app/models/sd-turbo")
37
-
38
- DEFAULT_STEPS = int(os.environ.get("DEFAULT_STEPS", "4")) # Increased for better quality
39
- DEFAULT_GUIDANCE = float(os.environ.get("DEFAULT_GUIDANCE", "1.0")) # Slightly increased
40
-
41
- MODEL_SHORT_ID = "sd-turbo"
42
- MODEL_OWNER = "stabilityai"
43
-
44
- SUPPORTED_SIZES = {
45
- "256x256": (256, 256),
46
- "512x512": (512, 512),
47
- "768x768": (768, 768),
48
- }
49
-
50
- # ---------------------------------------------------------------------------
51
- # Flask app
52
- # ---------------------------------------------------------------------------
53
 
54
  app = Flask(__name__)
55
  CORS(app)
56
 
57
- # ---------------------------------------------------------------------------
58
- # Global pipeline state
59
- # ---------------------------------------------------------------------------
60
-
61
  _pipeline = None
62
  _pipeline_lock = threading.Lock()
63
  _pipeline_load_error = None
64
 
65
 
66
- def _resolve_model_source():
67
- if os.path.isdir(MODEL_LOCAL_DIR) and os.listdir(MODEL_LOCAL_DIR):
68
- logger.info("Using pre-downloaded model directory: %s", MODEL_LOCAL_DIR)
69
- return MODEL_LOCAL_DIR
70
- logger.info("Local model directory not found. Falling back to hub.")
71
- return IMAGE_MODEL
72
-
73
-
74
  def load_pipeline():
75
  global _pipeline, _pipeline_load_error
76
-
77
  from diffusers import AutoPipelineForText2Image
78
 
79
- model_source = _resolve_model_source()
80
-
81
- logger.info("Loading text-to-image pipeline from '%s' ...", model_source)
82
  try:
83
  pipeline = AutoPipelineForText2Image.from_pretrained(
84
- model_source,
85
- torch_dtype=torch.float32,
86
- safety_checker=None,
87
  )
88
  pipeline.to("cpu")
89
-
90
- # Dynamic quantization for CPU speed (good quality/speed tradeoff)
91
  pipeline.unet = torch.quantization.quantize_dynamic(
92
  pipeline.unet, {torch.nn.Linear}, dtype=torch.qint8
93
  )
@@ -95,224 +48,62 @@ def load_pipeline():
95
  pipeline.text_encoder = torch.quantization.quantize_dynamic(
96
  pipeline.text_encoder, {torch.nn.Linear}, dtype=torch.qint8
97
  )
98
-
99
  pipeline.set_progress_bar_config(disable=True)
100
  _pipeline = pipeline
101
  logger.info("Pipeline loaded and quantized successfully.")
102
- except Exception as exc:
103
- _pipeline_load_error = str(exc)
104
- logger.exception("Failed to load pipeline: %s", exc)
105
- raise
106
-
107
-
108
- # ---------------------------------------------------------------------------
109
- # Helpers
110
- # ---------------------------------------------------------------------------
111
-
112
- def openai_error_response(message, err_type="invalid_request_error", param=None, code=None, status=400):
113
- body = {"error": {"message": message, "type": err_type, "param": param, "code": code}}
114
- response = jsonify(body)
115
- response.status_code = status
116
- return response
117
-
118
 
119
- def parse_size(size_str):
120
- if size_str is None:
121
- return SUPPORTED_SIZES["512x512"]
122
- return SUPPORTED_SIZES.get(size_str)
123
 
124
-
125
- def image_to_b64(pil_image, image_format="PNG"):
126
- buffer = io.BytesIO()
127
- pil_image.save(buffer, format=image_format)
128
- return base64.b64encode(buffer.getvalue()).decode("utf-8")
129
-
130
-
131
- def run_generation(prompt, width, height, steps, guidance_scale, n_images):
132
- if _pipeline is None:
133
- raise RuntimeError(_pipeline_load_error or "Pipeline not initialized.")
134
-
135
- images = []
136
  with _pipeline_lock:
137
- for _ in range(n_images):
138
- result = _pipeline(
139
- prompt=prompt,
140
- num_inference_steps=steps,
141
- guidance_scale=guidance_scale,
142
- width=width,
143
- height=height,
144
- )
145
- images.append(result.images[0])
146
- return images
147
-
148
 
149
- # ---------------------------------------------------------------------------
150
- # Routes
151
- # ---------------------------------------------------------------------------
152
 
153
- @app.route("/health", methods=["GET"])
154
  def health():
155
- status = "ok" if _pipeline is not None else "loading"
156
- status_code = 200 if _pipeline is not None else 503
157
- return jsonify({
158
- "status": status,
159
- "model": MODEL_SHORT_ID,
160
- "error": _pipeline_load_error,
161
- }), status_code
162
-
163
-
164
- @app.route("/v1/models", methods=["GET"])
165
- def list_models():
166
- return jsonify({
167
- "object": "list",
168
- "data": [{
169
- "id": MODEL_SHORT_ID,
170
- "object": "model",
171
- "owned_by": MODEL_OWNER,
172
- }]
173
- })
174
-
175
-
176
- @app.route("/v1/images/generations", methods=["POST"])
177
- def images_generations():
178
- # (OpenAI-compatible endpoint - kept unchanged from original)
179
- if not request.is_json:
180
- return openai_error_response("Request body must be valid JSON.", code="invalid_json")
181
-
182
- try:
183
- payload = request.get_json(silent=False)
184
- except Exception:
185
- return openai_error_response("Invalid JSON.", code="invalid_json")
186
-
187
- if not isinstance(payload, dict):
188
- return openai_error_response("Request body must be a JSON object.", code="invalid_json")
189
-
190
- prompt = payload.get("prompt")
191
- if not prompt or not isinstance(prompt, str) or not prompt.strip():
192
- return openai_error_response("Missing 'prompt'.", param="prompt", code="missing_prompt")
193
-
194
- requested_model = payload.get("model", MODEL_SHORT_ID)
195
- if requested_model not in (MODEL_SHORT_ID, IMAGE_MODEL):
196
- return openai_error_response(f"Model '{requested_model}' not supported.", param="model", code="model_not_found", status=404)
197
-
198
- size_str = payload.get("size", "512x512")
199
- dimensions = parse_size(size_str)
200
- if dimensions is None:
201
- supported = ", ".join(sorted(SUPPORTED_SIZES.keys()))
202
- return openai_error_response(f"Unsupported size. Supported: {supported}.", param="size", code="unsupported_size")
203
-
204
- width, height = dimensions
205
-
206
- n_images = payload.get("n", 1)
207
- if not isinstance(n_images, int) or n_images < 1 or n_images > 4:
208
- return openai_error_response("'n' must be between 1 and 4.", param="n", code="invalid_n")
209
-
210
- response_format = payload.get("response_format", "b64_json")
211
- if response_format != "b64_json":
212
- return openai_error_response("Only b64_json supported.", param="response_format", code="unsupported_response_format")
213
-
214
- steps = payload.get("num_inference_steps", DEFAULT_STEPS)
215
- guidance_scale = payload.get("guidance_scale", DEFAULT_GUIDANCE)
216
-
217
- try:
218
- steps = int(steps)
219
- guidance_scale = float(guidance_scale)
220
- except (TypeError, ValueError):
221
- return openai_error_response("Invalid steps or guidance_scale.", code="invalid_generation_params")
222
-
223
- if steps < 1 or steps > 6:
224
- steps = 4
225
- if guidance_scale < 0 or guidance_scale > 5:
226
- guidance_scale = 1.0
227
-
228
- if _pipeline is None:
229
- return openai_error_response("Model not ready.", err_type="server_error", code="model_not_ready", status=503)
230
-
231
- request_id = uuid.uuid4().hex[:12]
232
- logger.info("[%s] Generating %d image(s) | %dx%d steps=%d guidance=%.2f", request_id, n_images, width, height, steps, guidance_scale)
233
-
234
- try:
235
- images = run_generation(prompt, width, height, steps, guidance_scale, n_images)
236
- except Exception as exc:
237
- logger.exception("[%s] Generation failed: %s", request_id, exc)
238
- return openai_error_response(f"Generation failed: {exc}", err_type="server_error", code="generation_failed", status=500)
239
-
240
- data = [{"b64_json": image_to_b64(img)} for img in images]
241
- return jsonify({"created": int(time.time()), "data": data})
242
 
243
 
244
  @app.route("/image", methods=["GET"])
245
  def generate_image():
246
- """
247
- New direct image endpoint.
248
- Example: /image?prompt=a%20cute%20cat&width=512&height=512&steps=4&guidance=1.0
249
- Returns PNG directly (works in <img src=""> tags).
250
- """
251
- prompt = request.args.get("prompt")
252
- if not prompt or not prompt.strip():
253
- return "Missing 'prompt' query parameter", 400
254
 
255
  try:
256
- width = int(request.args.get("width", 512))
257
- height = int(request.args.get("height", 512))
258
  steps = int(request.args.get("steps", DEFAULT_STEPS))
259
  guidance = float(request.args.get("guidance", DEFAULT_GUIDANCE))
260
  except ValueError:
261
- return "Invalid width/height/steps/guidance parameters", 400
262
 
263
- # Safety caps for CPU feasibility
264
- if width > 768 or height > 768:
265
- width, height = 512, 512
266
- if steps > 6:
267
- steps = 6
268
- if guidance < 0 or guidance > 5:
269
- guidance = 1.0
270
 
271
  try:
272
- images = run_generation(
273
- prompt=prompt,
274
- width=width,
275
- height=height,
276
- steps=steps,
277
- guidance_scale=guidance,
278
- n_images=1,
279
- )
280
- pil_image = images[0]
281
-
282
- img_io = io.BytesIO()
283
- pil_image.save(img_io, format="PNG")
284
- img_io.seek(0)
285
-
286
- return send_file(img_io, mimetype="image/png", as_attachment=False)
287
- except Exception as exc:
288
- logger.exception("GET /image failed")
289
- return f"Image generation failed: {exc}", 500
290
-
291
 
292
- @app.errorhandler(404)
293
- def not_found(_error):
294
- return openai_error_response("Endpoint not found.", code="not_found", status=404)
295
-
296
-
297
- @app.errorhandler(405)
298
- def method_not_allowed(_error):
299
- return openai_error_response("Method not allowed.", code="method_not_allowed", status=405)
300
-
301
-
302
- @app.errorhandler(500)
303
- def internal_error(_error):
304
- return openai_error_response("Internal server error.", err_type="server_error", code="internal_error", status=500)
305
-
306
-
307
- # ---------------------------------------------------------------------------
308
- # Entrypoint
309
- # ---------------------------------------------------------------------------
310
 
311
  if __name__ == "__main__":
312
- try:
313
- load_pipeline()
314
- except Exception:
315
- logger.error("Starting in degraded mode - pipeline failed to load.")
316
-
317
  port = int(os.environ.get("PORT", "7860"))
318
  app.run(host="0.0.0.0", port=port, threaded=True)
 
 
1
  import io
2
  import logging
3
  import os
4
  import threading
 
 
5
 
6
  import torch
7
  from flask import Flask, jsonify, request, send_file
8
  from flask_cors import CORS
9
 
10
+ # CPU limits
 
 
11
  os.environ["OMP_NUM_THREADS"] = "4"
12
  os.environ["MKL_NUM_THREADS"] = "4"
13
  torch.set_num_threads(4)
14
  torch.set_num_interop_threads(1)
15
 
16
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
 
 
 
 
 
 
 
17
  logger = logging.getLogger("sd-turbo-server")
18
 
19
+ # Config
20
+ DEFAULT_STEPS = int(os.environ.get("DEFAULT_STEPS", "4"))
21
+ DEFAULT_GUIDANCE = float(os.environ.get("DEFAULT_GUIDANCE", "1.0"))
22
+ DEFAULT_WIDTH = 768
23
+ DEFAULT_HEIGHT = 768
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
  app = Flask(__name__)
26
  CORS(app)
27
 
 
 
 
 
28
  _pipeline = None
29
  _pipeline_lock = threading.Lock()
30
  _pipeline_load_error = None
31
 
32
 
 
 
 
 
 
 
 
 
33
  def load_pipeline():
34
  global _pipeline, _pipeline_load_error
 
35
  from diffusers import AutoPipelineForText2Image
36
 
37
+ model_dir = os.environ.get("MODEL_LOCAL_DIR", "/app/models/sd-turbo")
38
+ logger.info("Loading pipeline from %s", model_dir)
 
39
  try:
40
  pipeline = AutoPipelineForText2Image.from_pretrained(
41
+ model_dir, torch_dtype=torch.float32, safety_checker=None
 
 
42
  )
43
  pipeline.to("cpu")
 
 
44
  pipeline.unet = torch.quantization.quantize_dynamic(
45
  pipeline.unet, {torch.nn.Linear}, dtype=torch.qint8
46
  )
 
48
  pipeline.text_encoder = torch.quantization.quantize_dynamic(
49
  pipeline.text_encoder, {torch.nn.Linear}, dtype=torch.qint8
50
  )
 
51
  pipeline.set_progress_bar_config(disable=True)
52
  _pipeline = pipeline
53
  logger.info("Pipeline loaded and quantized successfully.")
54
+ except Exception as e:
55
+ _pipeline_load_error = str(e)
56
+ logger.error("Pipeline load failed: %s", e)
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
 
 
 
 
58
 
59
+ def run_generation(prompt, width, height, steps, guidance):
 
 
 
 
 
 
 
 
 
 
 
60
  with _pipeline_lock:
61
+ result = _pipeline(
62
+ prompt=prompt,
63
+ num_inference_steps=steps,
64
+ guidance_scale=guidance,
65
+ width=width,
66
+ height=height,
67
+ )
68
+ return result.images[0]
 
 
 
69
 
 
 
 
70
 
71
+ @app.route("/health")
72
  def health():
73
+ return jsonify({"status": "ok" if _pipeline else "loading", "model": "sd-turbo"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
 
76
  @app.route("/image", methods=["GET"])
77
  def generate_image():
78
+ prompt = request.args.get("prompt", "").replace("+", " ")
79
+ if not prompt:
80
+ return "Missing prompt", 400
 
 
 
 
 
81
 
82
  try:
83
+ width = int(request.args.get("width", DEFAULT_WIDTH))
84
+ height = int(request.args.get("height", DEFAULT_HEIGHT))
85
  steps = int(request.args.get("steps", DEFAULT_STEPS))
86
  guidance = float(request.args.get("guidance", DEFAULT_GUIDANCE))
87
  except ValueError:
88
+ return "Invalid parameters", 400
89
 
90
+ if width > 768: width = 768
91
+ if height > 768: height = 768
92
+ if steps > 6: steps = 6
93
+ if guidance > 2.0: guidance = 1.0
 
 
 
94
 
95
  try:
96
+ img = run_generation(prompt, width, height, steps, guidance)
97
+ buf = io.BytesIO()
98
+ img.save(buf, format="PNG")
99
+ buf.seek(0)
100
+ return send_file(buf, mimetype="image/png")
101
+ except Exception as e:
102
+ logger.exception("Generation failed")
103
+ return str(e), 500
 
 
 
 
 
 
 
 
 
 
 
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
  if __name__ == "__main__":
107
+ load_pipeline()
 
 
 
 
108
  port = int(os.environ.get("PORT", "7860"))
109
  app.run(host="0.0.0.0", port=port, threaded=True)