Samfy001 commited on
Commit
538d586
·
verified ·
1 Parent(s): 6562119

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +239 -16
main.py CHANGED
@@ -22,6 +22,38 @@ app = FastAPI(
22
  CAPTIONS_BASE_URL = "https://core.captions-web-api.xyz/proxy/v1/gen-ai/image"
23
  BEARER_TOKEN = os.getenv("CAPTIONS_BEARER_TOKEN", "eyJhbGciOiJSUzI1NiIsImtpZCI6IjU3YmZiMmExMWRkZmZjMGFkMmU2ODE0YzY4NzYzYjhjNjg3NTgxZDgiLCJ0eXAiOiJKV1QifQ.eyJnb29nbGUiOnRydWUsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9jYXB0aW9ucy1mNmRlOSIsImF1ZCI6ImNhcHRpb25zLWY2ZGU5IiwiYXV0aF90aW1lIjoxNzU1MzYyODEzLCJ1c2VyX2lkIjoic3hWek5XaUYyempXYmUxTjNjd3UiLCJzdWIiOiJzeFZ6TldpRjJ6aldiZTFOM2N3dSIsImlhdCI6MTc1NTM2MjgxMywiZXhwIjoxNzU1MzY2NDEzLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImN1c3RvbSJ9fQ.jGuhWp-w8jlGy8xmMjqOyig_LVcr53udFgMjrQTJtKtE_J_iVkvMLncO2TnJ2BquoEp9pwVlZIG-imlFe6Uhtz95-t1oHENf5yzUWu3HocFsNVeAZh9avi_iObSYM_pFOT9lwRNzk1oMa6LbwViuVgTXvHDse9T4_nDfmCBbWngWksh1_JGtnrK2qPb5YD8Hr26itDRMx8mzUr2cQqtU9mU0R910CROqsNaQ9ovemeGe-2RT-hZku4VVYAMDOdvcFsgcf_BJTLRikmc3T7Ekx8T0KM6ZpTgr34wtnl7rpDBNOX0cOSYu3NEUDBnhNJKmPl5qL08gcYEur1ijP2mcTA")
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  # OpenAI-compatible request models
26
  class ImageGenerationRequest(BaseModel):
27
  prompt: str = Field(..., description="A text description of the desired image(s)")
@@ -57,6 +89,10 @@ class CaptionsStatusRequest(BaseModel):
57
  # In-memory storage for operation tracking (use Redis in production)
58
  operations_store = {}
59
 
 
 
 
 
60
  def get_aspect_ratio_from_size(size: str) -> int:
61
  """Convert OpenAI size format to Captions aspect ratio"""
62
  size_map = {
@@ -68,7 +104,7 @@ def get_aspect_ratio_from_size(size: str) -> int:
68
  }
69
  return size_map.get(size, 1)
70
 
71
- async def submit_image_generation(prompt: str, size: str = "1024x1024") -> str:
72
  """Submit image generation request to Captions API"""
73
  headers = {
74
  "accept": "application/json, text/plain, */*",
@@ -82,7 +118,7 @@ async def submit_image_generation(prompt: str, size: str = "1024x1024") -> str:
82
  }
83
 
84
  payload = {
85
- "modelId": "openai-gpt-4o-image",
86
  "prompt": prompt,
87
  "aspectRatio": get_aspect_ratio_from_size(size),
88
  "magicPrompt": False,
@@ -155,21 +191,77 @@ async def check_generation_status(operation_id: str) -> dict:
155
  async def wait_for_completion(operation_id: str, max_wait_time: int = 300) -> dict:
156
  """Wait for image generation to complete with polling"""
157
  start_time = datetime.now()
 
 
158
 
159
  while True:
160
- status_data = await check_generation_status(operation_id)
161
-
162
- # State 2 means completed
163
- if status_data.get("state") == 2:
164
- return status_data["complete"]
165
-
166
- # Check if we've exceeded max wait time
167
- elapsed = (datetime.now() - start_time).total_seconds()
168
- if elapsed > max_wait_time:
169
- raise HTTPException(status_code=408, detail="Image generation timeout")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
- # Wait before next poll
172
- await asyncio.sleep(2)
 
 
 
 
 
 
 
 
 
 
 
173
 
174
  @app.post("/v1/images/generations", response_model=ImageGenerationResponse)
175
  async def create_image(request: ImageGenerationRequest):
@@ -178,14 +270,31 @@ async def create_image(request: ImageGenerationRequest):
178
  Compatible with OpenAI's image generation API.
179
  """
180
  try:
181
- logger.info(f"Received image generation request: {request.prompt}")
 
 
 
 
 
 
 
 
 
 
 
 
182
 
183
  # Submit the image generation request
184
- operation_id = await submit_image_generation(request.prompt, request.size)
 
185
 
186
  # Wait for completion
187
  completion_data = await wait_for_completion(operation_id)
188
 
 
 
 
 
189
  # Format response in OpenAI format
190
  image_data = ImageData(
191
  url=completion_data.get("assetResolvedUrl"),
@@ -206,6 +315,111 @@ async def create_image(request: ImageGenerationRequest):
206
  logger.error(f"Unexpected error in image generation: {e}")
207
  raise HTTPException(status_code=500, detail="Internal server error")
208
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  @app.get("/health")
210
  async def health_check():
211
  """Health check endpoint"""
@@ -217,10 +431,19 @@ async def root():
217
  return {
218
  "message": "OpenAI Compatible Image Generation API",
219
  "version": "1.0.0",
 
 
220
  "endpoints": {
 
221
  "image_generation": "/v1/images/generations",
 
 
222
  "health": "/health",
223
  "docs": "/docs"
 
 
 
 
224
  }
225
  }
226
 
 
22
  CAPTIONS_BASE_URL = "https://core.captions-web-api.xyz/proxy/v1/gen-ai/image"
23
  BEARER_TOKEN = os.getenv("CAPTIONS_BEARER_TOKEN", "eyJhbGciOiJSUzI1NiIsImtpZCI6IjU3YmZiMmExMWRkZmZjMGFkMmU2ODE0YzY4NzYzYjhjNjg3NTgxZDgiLCJ0eXAiOiJKV1QifQ.eyJnb29nbGUiOnRydWUsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9jYXB0aW9ucy1mNmRlOSIsImF1ZCI6ImNhcHRpb25zLWY2ZGU5IiwiYXV0aF90aW1lIjoxNzU1MzYyODEzLCJ1c2VyX2lkIjoic3hWek5XaUYyempXYmUxTjNjd3UiLCJzdWIiOiJzeFZ6TldpRjJ6aldiZTFOM2N3dSIsImlhdCI6MTc1NTM2MjgxMywiZXhwIjoxNzU1MzY2NDEzLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImN1c3RvbSJ9fQ.jGuhWp-w8jlGy8xmMjqOyig_LVcr53udFgMjrQTJtKtE_J_iVkvMLncO2TnJ2BquoEp9pwVlZIG-imlFe6Uhtz95-t1oHENf5yzUWu3HocFsNVeAZh9avi_iObSYM_pFOT9lwRNzk1oMa6LbwViuVgTXvHDse9T4_nDfmCBbWngWksh1_JGtnrK2qPb5YD8Hr26itDRMx8mzUr2cQqtU9mU0R910CROqsNaQ9ovemeGe-2RT-hZku4VVYAMDOdvcFsgcf_BJTLRikmc3T7Ekx8T0KM6ZpTgr34wtnl7rpDBNOX0cOSYu3NEUDBnhNJKmPl5qL08gcYEur1ijP2mcTA")
24
 
25
+ # Model mappings from OpenAI model names to Captions model IDs
26
+ MODEL_MAPPINGS = {
27
+ "dall-e-3": "openai-dalle-3",
28
+ "dall-e-2": "openai-dalle-3", # Fallback to dalle-3
29
+ "gpt-4o": "openai-gpt-4o-image",
30
+ "google-imagen-3": "google-imagen-3",
31
+ "imagen-3": "google-imagen-3",
32
+ "luma-photon": "luma-photon",
33
+ "photon": "luma-photon",
34
+ "flux-1-1-pro": "bfl-flux-1-1-pro",
35
+ "flux": "bfl-flux-1-1-pro",
36
+ "ideogram-v1": "ideogram-v1",
37
+ "ideogram": "ideogram-v1",
38
+ "recraft-v3": "recraft-v3",
39
+ "recraft": "recraft-v3",
40
+ "stable-diffusion-3-5": "stable-diffusion-3-5-large",
41
+ "sd-3-5": "stable-diffusion-3-5-large",
42
+ "stable-diffusion": "stable-diffusion-3-5-large"
43
+ }
44
+
45
+ # Available models information
46
+ AVAILABLE_MODELS = {
47
+ "google-imagen-3": {"name": "Imagen 3", "provider": "Google"},
48
+ "openai-gpt-4o-image": {"name": "GPT-4o", "provider": "OpenAI"},
49
+ "luma-photon": {"name": "Photon", "provider": "Luma AI"},
50
+ "bfl-flux-1-1-pro": {"name": "Flux 1.1 Pro", "provider": "Black Forest Labs"},
51
+ "ideogram-v1": {"name": "Ideogram V1", "provider": "Ideogram"},
52
+ "openai-dalle-3": {"name": "DALL-E 3 HD", "provider": "OpenAI"},
53
+ "recraft-v3": {"name": "Recraft V3", "provider": "Recraft"},
54
+ "stable-diffusion-3-5-large": {"name": "SD 3.5", "provider": "Stability AI"}
55
+ }
56
+
57
  # OpenAI-compatible request models
58
  class ImageGenerationRequest(BaseModel):
59
  prompt: str = Field(..., description="A text description of the desired image(s)")
 
89
  # In-memory storage for operation tracking (use Redis in production)
90
  operations_store = {}
91
 
92
+ def get_captions_model_id(openai_model: str) -> str:
93
+ """Convert OpenAI model name to Captions model ID"""
94
+ return MODEL_MAPPINGS.get(openai_model, "openai-dalle-3") # Default to DALL-E 3
95
+
96
  def get_aspect_ratio_from_size(size: str) -> int:
97
  """Convert OpenAI size format to Captions aspect ratio"""
98
  size_map = {
 
104
  }
105
  return size_map.get(size, 1)
106
 
107
+ async def submit_image_generation(prompt: str, model: str = "dall-e-3", size: str = "1024x1024") -> str:
108
  """Submit image generation request to Captions API"""
109
  headers = {
110
  "accept": "application/json, text/plain, */*",
 
118
  }
119
 
120
  payload = {
121
+ "modelId": get_captions_model_id(model),
122
  "prompt": prompt,
123
  "aspectRatio": get_aspect_ratio_from_size(size),
124
  "magicPrompt": False,
 
191
  async def wait_for_completion(operation_id: str, max_wait_time: int = 300) -> dict:
192
  """Wait for image generation to complete with polling"""
193
  start_time = datetime.now()
194
+ retry_count = 0
195
+ max_retries = 3
196
 
197
  while True:
198
+ try:
199
+ status_data = await check_generation_status(operation_id)
200
+ retry_count = 0 # Reset retry count on successful request
201
+
202
+ # State 2 means completed
203
+ if status_data.get("state") == 2:
204
+ if "complete" in status_data:
205
+ return status_data["complete"]
206
+ else:
207
+ raise HTTPException(status_code=500, detail="Generation completed but no result data")
208
+
209
+ # State 3 means failed
210
+ if status_data.get("state") == 3:
211
+ raise HTTPException(status_code=500, detail="Image generation failed")
212
+
213
+ # Check if we've exceeded max wait time
214
+ elapsed = (datetime.now() - start_time).total_seconds()
215
+ if elapsed > max_wait_time:
216
+ raise HTTPException(status_code=408, detail="Image generation timeout")
217
+
218
+ # Log progress
219
+ if status_data.get("state") == 1:
220
+ logger.info(f"Operation {operation_id} still processing...")
221
+
222
+ # Wait before next poll (progressive backoff)
223
+ wait_time = min(5, 2 + (elapsed / 60)) # Start at 2s, increase to max 5s
224
+ await asyncio.sleep(wait_time)
225
+
226
+ except HTTPException:
227
+ raise
228
+ except Exception as e:
229
+ retry_count += 1
230
+ if retry_count >= max_retries:
231
+ logger.error(f"Max retries exceeded for operation {operation_id}: {e}")
232
+ raise HTTPException(status_code=500, detail="Failed to check generation status after multiple retries")
233
+
234
+ logger.warning(f"Retry {retry_count}/{max_retries} for operation {operation_id}: {e}")
235
+ await asyncio.sleep(2 ** retry_count) # Exponential backoff
236
+
237
+ @app.get("/v1/models")
238
+ async def list_models():
239
+ """List available models compatible with OpenAI format"""
240
+ models = []
241
+ for model_id, info in AVAILABLE_MODELS.items():
242
+ # Add both the Captions ID and common aliases
243
+ models.append({
244
+ "id": model_id,
245
+ "object": "model",
246
+ "created": 1234567890, # Static timestamp
247
+ "owned_by": info["provider"].lower().replace(" ", "-"),
248
+ "name": info["name"],
249
+ "provider": info["provider"]
250
+ })
251
 
252
+ # Add OpenAI-style aliases
253
+ for alias, captions_id in MODEL_MAPPINGS.items():
254
+ if captions_id == model_id and alias not in [m["id"] for m in models]:
255
+ models.append({
256
+ "id": alias,
257
+ "object": "model",
258
+ "created": 1234567890,
259
+ "owned_by": info["provider"].lower().replace(" ", "-"),
260
+ "name": info["name"],
261
+ "provider": info["provider"]
262
+ })
263
+
264
+ return {"object": "list", "data": models}
265
 
266
  @app.post("/v1/images/generations", response_model=ImageGenerationResponse)
267
  async def create_image(request: ImageGenerationRequest):
 
270
  Compatible with OpenAI's image generation API.
271
  """
272
  try:
273
+ logger.info(f"Received image generation request: prompt='{request.prompt[:100]}...', model='{request.model}', size='{request.size}'")
274
+
275
+ # Validate model
276
+ captions_model_id = get_captions_model_id(request.model)
277
+ if captions_model_id not in AVAILABLE_MODELS:
278
+ raise HTTPException(status_code=400, detail=f"Model '{request.model}' is not supported")
279
+
280
+ # Validate request parameters
281
+ if not request.prompt or len(request.prompt.strip()) == 0:
282
+ raise HTTPException(status_code=400, detail="Prompt cannot be empty")
283
+
284
+ if len(request.prompt) > 1000:
285
+ raise HTTPException(status_code=400, detail="Prompt exceeds maximum length of 1000 characters")
286
 
287
  # Submit the image generation request
288
+ operation_id = await submit_image_generation(request.prompt, request.model, request.size)
289
+ logger.info(f"Image generation submitted with operation ID: {operation_id}")
290
 
291
  # Wait for completion
292
  completion_data = await wait_for_completion(operation_id)
293
 
294
+ # Validate completion data
295
+ if not completion_data.get("assetResolvedUrl"):
296
+ raise HTTPException(status_code=500, detail="Generation completed but no image URL received")
297
+
298
  # Format response in OpenAI format
299
  image_data = ImageData(
300
  url=completion_data.get("assetResolvedUrl"),
 
315
  logger.error(f"Unexpected error in image generation: {e}")
316
  raise HTTPException(status_code=500, detail="Internal server error")
317
 
318
+ @app.post("/v1/images/generations/async")
319
+ async def create_image_async(request: ImageGenerationRequest):
320
+ """
321
+ Starts an image generation request and returns operation ID for status checking.
322
+ Non-blocking version of the generation API.
323
+ """
324
+ try:
325
+ logger.info(f"Received async image generation request: prompt='{request.prompt[:100]}...', model='{request.model}', size='{request.size}'")
326
+
327
+ # Validate model
328
+ captions_model_id = get_captions_model_id(request.model)
329
+ if captions_model_id not in AVAILABLE_MODELS:
330
+ raise HTTPException(status_code=400, detail=f"Model '{request.model}' is not supported")
331
+
332
+ # Validate request parameters
333
+ if not request.prompt or len(request.prompt.strip()) == 0:
334
+ raise HTTPException(status_code=400, detail="Prompt cannot be empty")
335
+
336
+ if len(request.prompt) > 1000:
337
+ raise HTTPException(status_code=400, detail="Prompt exceeds maximum length of 1000 characters")
338
+
339
+ # Submit the image generation request
340
+ operation_id = await submit_image_generation(request.prompt, request.model, request.size)
341
+
342
+ # Store request details for later retrieval
343
+ operations_store[operation_id] = {
344
+ "created": int(datetime.now().timestamp()),
345
+ "prompt": request.prompt,
346
+ "model": request.model,
347
+ "size": request.size,
348
+ "status": "processing"
349
+ }
350
+
351
+ return {
352
+ "operation_id": operation_id,
353
+ "status": "submitted",
354
+ "created": int(datetime.now().timestamp()),
355
+ "status_url": f"/v1/images/generations/status/{operation_id}"
356
+ }
357
+
358
+ except HTTPException:
359
+ raise
360
+ except Exception as e:
361
+ logger.error(f"Unexpected error in async image generation: {e}")
362
+ raise HTTPException(status_code=500, detail="Internal server error")
363
+
364
+ @app.get("/v1/images/generations/status/{operation_id}")
365
+ async def get_generation_status(operation_id: str):
366
+ """
367
+ Check the status of an image generation operation.
368
+ """
369
+ try:
370
+ if operation_id not in operations_store:
371
+ raise HTTPException(status_code=404, detail="Operation ID not found")
372
+
373
+ # Get current status from Captions API
374
+ status_data = await check_generation_status(operation_id)
375
+ operation_info = operations_store[operation_id]
376
+
377
+ # State 1 = processing, State 2 = completed, State 3 = failed
378
+ if status_data.get("state") == 1:
379
+ return {
380
+ "operation_id": operation_id,
381
+ "status": "processing",
382
+ "created": operation_info["created"],
383
+ "estimated_completion": None
384
+ }
385
+ elif status_data.get("state") == 2:
386
+ # Update stored info
387
+ operations_store[operation_id]["status"] = "completed"
388
+
389
+ # Format response in OpenAI format
390
+ image_data = ImageData(
391
+ url=status_data["complete"].get("assetResolvedUrl"),
392
+ revised_prompt=operation_info["prompt"]
393
+ )
394
+
395
+ return {
396
+ "operation_id": operation_id,
397
+ "status": "completed",
398
+ "created": operation_info["created"],
399
+ "data": [image_data.dict()]
400
+ }
401
+ elif status_data.get("state") == 3:
402
+ operations_store[operation_id]["status"] = "failed"
403
+ return {
404
+ "operation_id": operation_id,
405
+ "status": "failed",
406
+ "created": operation_info["created"],
407
+ "error": "Image generation failed"
408
+ }
409
+ else:
410
+ return {
411
+ "operation_id": operation_id,
412
+ "status": "unknown",
413
+ "created": operation_info["created"],
414
+ "error": "Unknown status"
415
+ }
416
+
417
+ except HTTPException:
418
+ raise
419
+ except Exception as e:
420
+ logger.error(f"Error checking generation status: {e}")
421
+ raise HTTPException(status_code=500, detail="Failed to check generation status")
422
+
423
  @app.get("/health")
424
  async def health_check():
425
  """Health check endpoint"""
 
431
  return {
432
  "message": "OpenAI Compatible Image Generation API",
433
  "version": "1.0.0",
434
+ "supported_models": list(AVAILABLE_MODELS.keys()),
435
+ "openai_aliases": list(MODEL_MAPPINGS.keys()),
436
  "endpoints": {
437
+ "models": "/v1/models",
438
  "image_generation": "/v1/images/generations",
439
+ "async_generation": "/v1/images/generations/async",
440
+ "status_check": "/v1/images/generations/status/{operation_id}",
441
  "health": "/health",
442
  "docs": "/docs"
443
+ },
444
+ "example_curl": {
445
+ "generate_image": "curl -X POST 'http://localhost:8000/v1/images/generations' -H 'Content-Type: application/json' -d '{\"prompt\": \"a cat\", \"model\": \"dall-e-3\", \"size\": \"1024x1024\"}'",
446
+ "list_models": "curl -X GET 'http://localhost:8000/v1/models'"
447
  }
448
  }
449