cloudunity commited on
Commit
e146485
·
verified ·
1 Parent(s): 508b792

Update server.py

Browse files
Files changed (1) hide show
  1. server.py +113 -252
server.py CHANGED
@@ -1,26 +1,3 @@
1
- """
2
- OpenAI-compatible Images API server for stabilityai/sd-turbo (or a
3
- compatible drop-in model) running on CPU inside a Hugging Face Docker Space.
4
-
5
- Endpoints:
6
- GET /health
7
- GET /v1/models
8
- POST /v1/images/generations
9
-
10
- The diffusion pipeline is loaded exactly once at process startup and
11
- reused for every request. Because the underlying model is not
12
- thread-safe for concurrent inference, a global lock serializes all
13
- generation calls.
14
-
15
- Note on ONNX: SD Turbo does not have a maintained, broadly compatible
16
- ONNX Runtime export that works with AutoPipelineForText2Image out of
17
- the box (optimum's ONNX SD pipelines require a separately exported
18
- ONNX model directory with its own graph format, and no such export is
19
- published/guaranteed for this model). Since none is available here,
20
- this server falls back to the standard Diffusers PyTorch pipeline,
21
- as instructed.
22
- """
23
-
24
  import base64
25
  import io
26
  import logging
@@ -29,9 +6,18 @@ import threading
29
  import time
30
  import uuid
31
 
32
- from flask import Flask, jsonify, request
 
33
  from flask_cors import CORS
34
 
 
 
 
 
 
 
 
 
35
  # ---------------------------------------------------------------------------
36
  # Logging
37
  # ---------------------------------------------------------------------------
@@ -43,19 +29,15 @@ logging.basicConfig(
43
  logger = logging.getLogger("sd-turbo-server")
44
 
45
  # ---------------------------------------------------------------------------
46
- # Configuration (overridable via environment variables)
47
  # ---------------------------------------------------------------------------
48
 
49
  IMAGE_MODEL = os.environ.get("IMAGE_MODEL", "stabilityai/sd-turbo")
50
-
51
- # If the model was pre-downloaded at build time to a local directory,
52
- # prefer loading from there so no network access is required at runtime.
53
  MODEL_LOCAL_DIR = os.environ.get("MODEL_LOCAL_DIR", "/app/models/sd-turbo")
54
 
55
- DEFAULT_STEPS = int(os.environ.get("DEFAULT_STEPS", "2"))
56
- DEFAULT_GUIDANCE = float(os.environ.get("DEFAULT_GUIDANCE", "0"))
57
 
58
- # Model-facing short id used in the OpenAI-compatible API responses.
59
  MODEL_SHORT_ID = "sd-turbo"
60
  MODEL_OWNER = "stabilityai"
61
 
@@ -77,36 +59,21 @@ CORS(app)
77
  # ---------------------------------------------------------------------------
78
 
79
  _pipeline = None
80
- _pipeline_lock = threading.Lock() # serializes generation calls
81
  _pipeline_load_error = None
82
 
83
 
84
  def _resolve_model_source():
85
- """
86
- Decide whether to load from the local pre-downloaded directory
87
- (populated at Docker build time) or fall back to the hub id.
88
- """
89
  if os.path.isdir(MODEL_LOCAL_DIR) and os.listdir(MODEL_LOCAL_DIR):
90
  logger.info("Using pre-downloaded model directory: %s", MODEL_LOCAL_DIR)
91
  return MODEL_LOCAL_DIR
92
- logger.info(
93
- "Local model directory not found or empty (%s). "
94
- "Falling back to downloading '%s' from the Hugging Face Hub.",
95
- MODEL_LOCAL_DIR,
96
- IMAGE_MODEL,
97
- )
98
  return IMAGE_MODEL
99
 
100
 
101
  def load_pipeline():
102
- """
103
- Load the diffusion pipeline exactly once at startup and cache it
104
- globally. Uses AutoPipelineForText2Image with float32 weights,
105
- since this deployment is CPU-only (no GPU / no fp16 support).
106
- """
107
  global _pipeline, _pipeline_load_error
108
 
109
- import torch
110
  from diffusers import AutoPipelineForText2Image
111
 
112
  model_source = _resolve_model_source()
@@ -120,12 +87,19 @@ def load_pipeline():
120
  )
121
  pipeline.to("cpu")
122
 
123
- # Disable the per-step progress bar (noisy in server logs / stdout).
124
- pipeline.set_progress_bar_config(disable=True)
 
 
 
 
 
 
125
 
 
126
  _pipeline = pipeline
127
- logger.info("Pipeline loaded successfully.")
128
- except Exception as exc: # noqa: BLE001 - we want to capture and report any load failure
129
  _pipeline_load_error = str(exc)
130
  logger.exception("Failed to load pipeline: %s", exc)
131
  raise
@@ -135,53 +109,28 @@ def load_pipeline():
135
  # Helpers
136
  # ---------------------------------------------------------------------------
137
 
138
-
139
  def openai_error_response(message, err_type="invalid_request_error", param=None, code=None, status=400):
140
- """
141
- Build a Flask response matching the OpenAI API error envelope:
142
- {"error": {"message": ..., "type": ..., "param": ..., "code": ...}}
143
- """
144
- body = {
145
- "error": {
146
- "message": message,
147
- "type": err_type,
148
- "param": param,
149
- "code": code,
150
- }
151
- }
152
  response = jsonify(body)
153
  response.status_code = status
154
  return response
155
 
156
 
157
  def parse_size(size_str):
158
- """
159
- Validate and convert an OpenAI-style size string (e.g. "512x512")
160
- into a (width, height) tuple. Returns None if unsupported.
161
- """
162
  if size_str is None:
163
  return SUPPORTED_SIZES["512x512"]
164
  return SUPPORTED_SIZES.get(size_str)
165
 
166
 
167
  def image_to_b64(pil_image, image_format="PNG"):
168
- """Encode a PIL image to a base64 string (no data URI prefix)."""
169
  buffer = io.BytesIO()
170
  pil_image.save(buffer, format=image_format)
171
- raw_bytes = buffer.getvalue()
172
- return base64.b64encode(raw_bytes).decode("utf-8")
173
 
174
 
175
  def run_generation(prompt, width, height, steps, guidance_scale, n_images):
176
- """
177
- Run the diffusion pipeline under the global lock so only one
178
- generation happens at a time (safe for a single CPU worker process).
179
- Returns a list of PIL.Image objects.
180
- """
181
  if _pipeline is None:
182
- raise RuntimeError(
183
- _pipeline_load_error or "Image generation pipeline is not initialized."
184
- )
185
 
186
  images = []
187
  with _pipeline_lock:
@@ -201,130 +150,67 @@ def run_generation(prompt, width, height, steps, guidance_scale, n_images):
201
  # Routes
202
  # ---------------------------------------------------------------------------
203
 
204
-
205
  @app.route("/health", methods=["GET"])
206
  def health():
207
- """Basic health/readiness check."""
208
  status = "ok" if _pipeline is not None else "loading"
209
  status_code = 200 if _pipeline is not None else 503
210
- return jsonify(
211
- {
212
- "status": status,
213
- "model": MODEL_SHORT_ID,
214
- "error": _pipeline_load_error,
215
- }
216
- ), status_code
217
 
218
 
219
  @app.route("/v1/models", methods=["GET"])
220
  def list_models():
221
- """OpenAI-compatible model listing endpoint."""
222
- return jsonify(
223
- {
224
- "object": "list",
225
- "data": [
226
- {
227
- "id": MODEL_SHORT_ID,
228
- "object": "model",
229
- "owned_by": MODEL_OWNER,
230
- }
231
- ],
232
- }
233
- )
234
 
235
 
236
  @app.route("/v1/images/generations", methods=["POST"])
237
  def images_generations():
238
- """OpenAI-compatible image generation endpoint."""
239
-
240
- # ---- Parse JSON body ----
241
  if not request.is_json:
242
- return openai_error_response(
243
- "Request body must be valid JSON with Content-Type: application/json.",
244
- param=None,
245
- code="invalid_json",
246
- status=400,
247
- )
248
 
249
  try:
250
  payload = request.get_json(silent=False)
251
- except Exception: # noqa: BLE001
252
- return openai_error_response(
253
- "Request body could not be parsed as JSON.",
254
- code="invalid_json",
255
- status=400,
256
- )
257
 
258
  if not isinstance(payload, dict):
259
- return openai_error_response(
260
- "Request body must be a JSON object.",
261
- code="invalid_json",
262
- status=400,
263
- )
264
 
265
- # ---- Validate prompt ----
266
  prompt = payload.get("prompt")
267
  if not prompt or not isinstance(prompt, str) or not prompt.strip():
268
- return openai_error_response(
269
- "You must provide a non-empty string in the 'prompt' field.",
270
- param="prompt",
271
- code="missing_prompt",
272
- status=400,
273
- )
274
 
275
- # ---- Validate model (optional field, informational only) ----
276
  requested_model = payload.get("model", MODEL_SHORT_ID)
277
  if requested_model not in (MODEL_SHORT_ID, IMAGE_MODEL):
278
- return openai_error_response(
279
- f"The model '{requested_model}' does not exist or is not supported "
280
- f"by this server. Available model: '{MODEL_SHORT_ID}'.",
281
- param="model",
282
- code="model_not_found",
283
- status=404,
284
- )
285
 
286
- # ---- Validate size ----
287
  size_str = payload.get("size", "512x512")
288
  dimensions = parse_size(size_str)
289
  if dimensions is None:
290
  supported = ", ".join(sorted(SUPPORTED_SIZES.keys()))
291
- return openai_error_response(
292
- f"Unsupported size '{size_str}'. Supported sizes are: {supported}.",
293
- param="size",
294
- code="unsupported_size",
295
- status=400,
296
- )
297
  width, height = dimensions
298
 
299
- # ---- Validate n ----
300
  n_images = payload.get("n", 1)
301
- if not isinstance(n_images, int) or isinstance(n_images, bool) or n_images < 1:
302
- return openai_error_response(
303
- "'n' must be a positive integer.",
304
- param="n",
305
- code="invalid_n",
306
- status=400,
307
- )
308
- if n_images > 4:
309
- return openai_error_response(
310
- "'n' must be less than or equal to 4 for this server.",
311
- param="n",
312
- code="invalid_n",
313
- status=400,
314
- )
315
 
316
- # ---- Validate response_format ----
317
  response_format = payload.get("response_format", "b64_json")
318
  if response_format != "b64_json":
319
- return openai_error_response(
320
- "This server only supports response_format='b64_json'. "
321
- "URL-based responses are not available on this CPU-only deployment.",
322
- param="response_format",
323
- code="unsupported_response_format",
324
- status=400,
325
- )
326
 
327
- # ---- Optional generation overrides ----
328
  steps = payload.get("num_inference_steps", DEFAULT_STEPS)
329
  guidance_scale = payload.get("guidance_scale", DEFAULT_GUIDANCE)
330
 
@@ -332,50 +218,55 @@ def images_generations():
332
  steps = int(steps)
333
  guidance_scale = float(guidance_scale)
334
  except (TypeError, ValueError):
335
- return openai_error_response(
336
- "'num_inference_steps' must be an integer and 'guidance_scale' must be a number.",
337
- code="invalid_generation_params",
338
- status=400,
339
- )
340
 
341
- if steps < 1 or steps > 50:
342
- return openai_error_response(
343
- "'num_inference_steps' must be between 1 and 50.",
344
- param="num_inference_steps",
345
- code="invalid_generation_params",
346
- status=400,
347
- )
348
-
349
- if guidance_scale < 0 or guidance_scale > 20:
350
- return openai_error_response(
351
- "'guidance_scale' must be between 0 and 20.",
352
- param="guidance_scale",
353
- code="invalid_generation_params",
354
- status=400,
355
- )
356
 
357
- # ---- Ensure pipeline is ready ----
358
  if _pipeline is None:
359
- return openai_error_response(
360
- _pipeline_load_error
361
- or "The image generation model is still loading. Please retry shortly.",
362
- err_type="server_error",
363
- code="model_not_ready",
364
- status=503,
365
- )
366
 
367
- # ---- Run generation ----
368
  request_id = uuid.uuid4().hex[:12]
369
- logger.info(
370
- "[%s] Generating %d image(s) | size=%dx%d steps=%d guidance=%.2f prompt=%r",
371
- request_id,
372
- n_images,
373
- width,
374
- height,
375
- steps,
376
- guidance_scale,
377
- prompt[:200],
378
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
 
380
  try:
381
  images = run_generation(
@@ -383,57 +274,34 @@ def images_generations():
383
  width=width,
384
  height=height,
385
  steps=steps,
386
- guidance_scale=guidance_scale,
387
- n_images=n_images,
388
- )
389
- except Exception as exc: # noqa: BLE001
390
- logger.exception("[%s] Generation failed: %s", request_id, exc)
391
- return openai_error_response(
392
- f"Image generation failed: {exc}",
393
- err_type="server_error",
394
- code="generation_failed",
395
- status=500,
396
  )
 
397
 
398
- # ---- Build OpenAI-compatible response ----
399
- data = [{"b64_json": image_to_b64(img)} for img in images]
 
400
 
401
- return jsonify(
402
- {
403
- "created": int(time.time()),
404
- "data": data,
405
- }
406
- )
407
 
408
 
409
  @app.errorhandler(404)
410
  def not_found(_error):
411
- return openai_error_response(
412
- "The requested endpoint does not exist.",
413
- err_type="invalid_request_error",
414
- code="not_found",
415
- status=404,
416
- )
417
 
418
 
419
  @app.errorhandler(405)
420
  def method_not_allowed(_error):
421
- return openai_error_response(
422
- "This HTTP method is not allowed for the requested endpoint.",
423
- err_type="invalid_request_error",
424
- code="method_not_allowed",
425
- status=405,
426
- )
427
 
428
 
429
  @app.errorhandler(500)
430
  def internal_error(_error):
431
- return openai_error_response(
432
- "An internal server error occurred.",
433
- err_type="server_error",
434
- code="internal_error",
435
- status=500,
436
- )
437
 
438
 
439
  # ---------------------------------------------------------------------------
@@ -441,17 +309,10 @@ def internal_error(_error):
441
  # ---------------------------------------------------------------------------
442
 
443
  if __name__ == "__main__":
444
- # Load the model once, synchronously, before accepting traffic.
445
  try:
446
  load_pipeline()
447
- except Exception: # noqa: BLE001
448
- # We still start the Flask app so /health reports the failure
449
- # instead of the container silently dying and HF Spaces retrying
450
- # forever without diagnostics.
451
- logger.error(
452
- "Starting server in degraded mode: pipeline failed to load. "
453
- "/health will report the error."
454
- )
455
 
456
  port = int(os.environ.get("PORT", "7860"))
457
  app.run(host="0.0.0.0", port=port, threaded=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import base64
2
  import io
3
  import logging
 
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
  # ---------------------------------------------------------------------------
 
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
 
 
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()
 
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
+ )
94
+ if hasattr(pipeline, "text_encoder"):
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
 
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:
 
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
 
 
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(
 
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
  # ---------------------------------------------------------------------------
 
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)