Maksymilian Jankowski commited on
Commit
6cbab81
·
1 Parent(s): a69a40e

background tasks for quicker return

Browse files
Files changed (1) hide show
  1. routers/user_models.py +241 -280
routers/user_models.py CHANGED
@@ -149,80 +149,39 @@ async def _check_and_decrement_credits(user_id: str):
149
  new_credits = credit_data["num_of_available_gens"] - 1
150
  supabase.from_("User_Credit_Account").update({"num_of_available_gens": new_credits}).eq("user_id", user_id).execute()
151
 
152
- @router.post("/text-to-3d")
153
- async def text_to_3d(prompt: TextPrompt, current_user: User = Depends(get_current_active_user)):
154
- """
155
- Create a Meshy Text-to-3D generation job.
156
-
157
- The endpoint performs the minimum work required to *start* the generation:
158
- 1. Persists an initial *Generated_Models* row (status = IN_PROGRESS).
159
- 2. Optionally reframes the user prompt via OpenAI.
160
- 3. Submits the request to Meshy and stores the returned task id.
161
-
162
- No long-running background polling is started here – the front-end must
163
- later call the */refresh/{generated_model_id}* endpoint to sync the final
164
- status back into Supabase.
165
- """
166
-
167
- # Credit check and decrement
168
- await _check_and_decrement_credits(current_user.id)
169
-
170
- generated_model_id: Optional[int] = None
171
-
172
- # 1. Insert initial DB record
173
  try:
174
- insert_res = supabase.from_("Generated_Models").insert({
175
- "status": "IN_PROGRESS",
176
- "user_id": current_user.id,
177
- "meshy_api_job_id": None,
178
- "model_name": f"{prompt.text[:50]}{'...' if len(prompt.text) > 50 else ''}",
179
- "prompts_and_models_config": {
180
- "generation_type": "text_to_3d",
181
- "original_prompt": prompt.text,
182
- "status": "initializing",
183
- "stage": "reframing_prompt",
184
- },
185
- }).execute()
186
- generated_model_id = insert_res.data[0]["generated_model_id"] if insert_res.data else None
187
- except Exception as ex:
188
- logging.error(f"Failed to create initial model DB record: {ex}")
189
-
190
- # 2. Reframe prompt via OpenAI
191
- if True:
192
  openai_key = os.getenv("OPENAI_API_KEY")
193
- if not openai_key:
194
- raise HTTPException(status_code=500, detail="OPENAI_API_KEY not configured")
195
-
196
- try:
197
- async with httpx.AsyncClient(timeout=30.0) as client:
198
- oai_resp = await client.post(
199
- "https://api.openai.com/v1/chat/completions",
200
- headers={"Authorization": f"Bearer {openai_key}"},
201
- json={
202
- "model": "gpt-4o-mini",
203
- "messages": [
204
- {"role": "system", "content": "REPLY ONLY WITH NEW PROMPT, no other text. IF USER PROMPT CONTAINS HARMFUL CONTENT, CHANGE IT TO SOMETHING SAFE and somewhat related. Rephrase the user description to simple and short."},
205
- {"role": "user", "content": prompt.text},
206
- ],
207
- },
208
- )
209
- except Exception as ex:
210
- raise HTTPException(status_code=502, detail=f"Failed to contact OpenAI: {ex}")
211
-
212
- if oai_resp.status_code != 200:
213
- raise HTTPException(status_code=oai_resp.status_code, detail=oai_resp.text)
214
-
215
- reframed_prompt = oai_resp.json()["choices"][0]["message"]["content"]
216
-
217
- reframed_prompt = prompt.text
218
-
219
- # 3. Update DB with reframed prompt (if record exists)
220
- if generated_model_id:
221
  try:
222
  supabase.from_("Generated_Models").update({
223
  "prompts_and_models_config": {
224
  "generation_type": "text_to_3d",
225
- "original_prompt": prompt.text,
226
  "reframed_prompt": reframed_prompt,
227
  "status": "processing",
228
  "stage": "creating_3d_model",
@@ -231,62 +190,161 @@ async def text_to_3d(prompt: TextPrompt, current_user: User = Depends(get_curren
231
  except Exception as ex:
232
  logging.warning(f"Failed to update DB with reframed prompt: {ex}")
233
 
234
- # 4. Send request to Meshy
235
- meshy_key = os.getenv("MESHY_API_KEY")
236
- if not meshy_key:
237
- raise HTTPException(status_code=500, detail="MESHY_API_KEY not configured")
 
238
 
239
- meshy_payload = {
240
- "mode": "preview",
241
- "prompt": reframed_prompt,
242
- "ai_model": "meshy-5",
243
- }
244
 
245
- async with httpx.AsyncClient(timeout=30.0) as client:
246
- meshy_resp = await client.post(
247
- "https://api.meshy.ai/openapi/v2/text-to-3d",
248
- headers={"Authorization": f"Bearer {meshy_key}"},
249
- json=meshy_payload,
250
- )
251
 
252
- if meshy_resp.status_code not in (200, 201, 202):
253
- raise HTTPException(status_code=meshy_resp.status_code, detail=meshy_resp.text)
 
254
 
255
- meshy_data = meshy_resp.json()
256
- meshy_task_id = meshy_data.get("result") or meshy_data.get("id") or meshy_data.get("task_id")
257
 
258
- if not meshy_task_id:
259
- raise HTTPException(status_code=500, detail="No task ID received from Meshy API")
 
260
 
261
- # 5. Finalize DB record
262
- if generated_model_id:
263
  try:
264
  supabase.from_("Generated_Models").update({
265
  "meshy_api_job_id": meshy_task_id,
266
  "prompts_and_models_config": {
267
  "generation_type": "text_to_3d",
268
- "original_prompt": prompt.text,
269
  "reframed_prompt": reframed_prompt,
270
  "status": "processing",
271
  "stage": "generating",
272
  "meshy_response": meshy_data,
273
  },
274
  }).eq("generated_model_id", generated_model_id).execute()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  except Exception as ex:
276
- logging.warning(f"Failed to update DB with Meshy taskId: {ex}")
 
 
 
 
 
 
 
 
277
 
278
- # 6. (Simplified) No background polling. The newly-created record will
279
- # be refreshed only when the front-end explicitly calls the *refresh* API
280
- # endpoint. This keeps the generation step lightweight and avoids long-
281
- # running background tasks.
 
 
282
 
283
- return {
284
- "generated_model_id": generated_model_id,
285
- "meshy_task_id": meshy_task_id,
286
- "status": "processing",
287
- "original_prompt": prompt.text,
288
- "reframed_prompt": reframed_prompt,
289
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
 
291
  # NEW: Image to 3D generation endpoint
292
  @router.post("/image-to-3d")
@@ -294,19 +352,13 @@ async def image_to_3d(request: ImageTo3DRequest, background_tasks: BackgroundTas
294
  """
295
  Create a Meshy Image-to-3D generation job.
296
 
297
- This mirrors the *text_to_3d* workflow but skips prompt reframing. The steps are:
298
- 1. Deduct one credit.
299
- 2. Persist an initial *Generated_Models* row (status = IN_PROGRESS).
300
- 3. Submit the image to Meshy and store the returned task id.
301
- 4. Return the identifiers so the front-end can poll the *progress_update* endpoint.
302
  """
303
 
304
  # 1. Credit check and decrement
305
  await _check_and_decrement_credits(current_user.id)
306
 
307
- generated_model_id: Optional[int] = None
308
-
309
- # 2. Insert initial DB record
310
  try:
311
  insert_res = supabase.from_("Generated_Models").insert({
312
  "status": "IN_PROGRESS",
@@ -321,73 +373,44 @@ async def image_to_3d(request: ImageTo3DRequest, background_tasks: BackgroundTas
321
  },
322
  }).execute()
323
  generated_model_id = insert_res.data[0]["generated_model_id"] if insert_res.data else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  except Exception as ex:
325
  logging.error(f"Failed to create initial model DB record: {ex}")
326
-
327
- # 3. Send request to Meshy
328
- meshy_key = os.getenv("MESHY_API_KEY")
329
- if not meshy_key:
330
- raise HTTPException(status_code=500, detail="MESHY_API_KEY not configured")
331
-
332
- # Build payload by including all non-None fields from the request model
333
- payload: Dict[str, Any] = {k: v for k, v in request.dict().items() if v is not None}
334
-
335
- async with httpx.AsyncClient(timeout=30.0) as client:
336
- meshy_resp = await client.post(
337
- "https://api.meshy.ai/openapi/v1/image-to-3d",
338
- headers={"Authorization": f"Bearer {meshy_key}"},
339
- json=payload,
340
- )
341
-
342
- if meshy_resp.status_code not in (200, 201, 202):
343
- raise HTTPException(status_code=meshy_resp.status_code, detail=meshy_resp.text)
344
-
345
- meshy_data = meshy_resp.json()
346
- meshy_task_id = meshy_data.get("result") or meshy_data.get("id") or meshy_data.get("task_id")
347
-
348
- if not meshy_task_id:
349
- raise HTTPException(status_code=500, detail="No task ID received from Meshy API")
350
-
351
- # 4. Finalize DB record
352
- if generated_model_id:
353
- try:
354
- supabase.from_("Generated_Models").update({
355
- "meshy_api_job_id": meshy_task_id,
356
- "prompts_and_models_config": {
357
- "generation_type": "image_to_3d",
358
- "input_image_url": request.image_url,
359
- "status": "processing",
360
- "stage": "generating",
361
- "meshy_response": meshy_data,
362
- },
363
- }).eq("generated_model_id", generated_model_id).execute()
364
- except Exception as ex:
365
- logging.warning(f"Failed to update DB with Meshy taskId: {ex}")
366
-
367
- # 5. Return identifiers
368
- return {
369
- "generated_model_id": generated_model_id,
370
- "meshy_task_id": meshy_task_id,
371
- "status": "processing",
372
- "input_image_url": request.image_url,
373
- }
374
 
375
  @router.post("/image-to-3d/upload")
376
  async def image_to_3d_upload(
 
377
  file: UploadFile = File(...),
378
  current_user: User = Depends(get_current_active_user),
379
  ):
380
  """Upload an image file and create a Meshy *Image-to-3D* task.
381
 
382
- The raw image file is base64-encoded on the server and sent to Meshy as a
383
- `data:image/<ext>;base64,<data>` Data-URI, so the front-end doesn't need to
384
- perform that conversion.
385
  """
386
 
387
  # 1. Credit check and decrement
388
  await _check_and_decrement_credits(current_user.id)
389
 
390
- # 2. Read & encode file
391
  try:
392
  file_bytes = await file.read()
393
  mime_type = file.content_type or "image/png"
@@ -396,9 +419,7 @@ async def image_to_3d_upload(
396
  except Exception as ex:
397
  raise HTTPException(status_code=400, detail=f"Failed to read uploaded file: {ex}")
398
 
399
- generated_model_id: Optional[int] = None
400
-
401
- # 3. Insert initial DB record
402
  try:
403
  insert_res = supabase.from_("Generated_Models").insert({
404
  "status": "IN_PROGRESS",
@@ -413,72 +434,43 @@ async def image_to_3d_upload(
413
  },
414
  }).execute()
415
  generated_model_id = insert_res.data[0]["generated_model_id"] if insert_res.data else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
  except Exception as ex:
417
  logging.error(f"Failed to create initial model DB record: {ex}")
418
-
419
- # 4. Send request to Meshy
420
- meshy_key = os.getenv("MESHY_API_KEY")
421
- if not meshy_key:
422
- raise HTTPException(status_code=500, detail="MESHY_API_KEY not configured")
423
-
424
- # Build payload
425
- payload: Dict[str, Any] = {
426
- "image_url": data_uri,
427
- "mode": "preview",
428
- "ai_model": "meshy-5",
429
- #"should_texture": False,
430
- }
431
-
432
- async with httpx.AsyncClient(timeout=30.0) as client:
433
- meshy_resp = await client.post(
434
- "https://api.meshy.ai/openapi/v1/image-to-3d",
435
- headers={"Authorization": f"Bearer {meshy_key}"},
436
- json=payload,
437
- )
438
-
439
- if meshy_resp.status_code not in (200, 201, 202):
440
- raise HTTPException(status_code=meshy_resp.status_code, detail=meshy_resp.text)
441
-
442
- meshy_data = meshy_resp.json()
443
- meshy_task_id = meshy_data.get("result") or meshy_data.get("id") or meshy_data.get("task_id")
444
-
445
- if not meshy_task_id:
446
- raise HTTPException(status_code=500, detail="No task ID received from Meshy API")
447
-
448
- # 5. Finalize DB record
449
- if generated_model_id:
450
- try:
451
- supabase.from_("Generated_Models").update({
452
- "meshy_api_job_id": meshy_task_id,
453
- "prompts_and_models_config": {
454
- "generation_type": "image_to_3d",
455
- "input_image_filename": file.filename,
456
- "status": "processing",
457
- "stage": "generating",
458
- "meshy_response": meshy_data,
459
- },
460
- }).eq("generated_model_id", generated_model_id).execute()
461
- except Exception as ex:
462
- logging.warning(f"Failed to update DB with Meshy taskId: {ex}")
463
-
464
- # 6. Return identifiers
465
- return {
466
- "generated_model_id": generated_model_id,
467
- "meshy_task_id": meshy_task_id,
468
- "status": "processing",
469
- "input_image_filename": file.filename,
470
- }
471
 
472
  @router.post("/multi-image-to-3d")
473
  async def multi_image_to_3d_upload(
 
474
  files: list[UploadFile] = File(...),
475
  current_user: User = Depends(get_current_active_user),
476
  ):
477
  """Upload multiple image files and create a Meshy *Multi-Image-to-3D* task.
478
 
479
- The raw image files are base64-encoded on the server and sent to Meshy as
480
- `data:image/<ext>;base64,<data>` Data-URIs, so the front-end doesn't need to
481
- perform that conversion.
482
  """
483
 
484
  # 1. Credit check and decrement
@@ -488,7 +480,7 @@ async def multi_image_to_3d_upload(
488
  if len(files) < 2:
489
  raise HTTPException(status_code=400, detail="At least 2 images are required for multi-image generation")
490
 
491
- # 3. Read & encode all files
492
  try:
493
  image_urls = []
494
  filenames = []
@@ -502,9 +494,7 @@ async def multi_image_to_3d_upload(
502
  except Exception as ex:
503
  raise HTTPException(status_code=400, detail=f"Failed to read uploaded files: {ex}")
504
 
505
- generated_model_id: Optional[int] = None
506
-
507
- # 4. Insert initial DB record
508
  try:
509
  insert_res = supabase.from_("Generated_Models").insert({
510
  "status": "IN_PROGRESS",
@@ -519,59 +509,30 @@ async def multi_image_to_3d_upload(
519
  },
520
  }).execute()
521
  generated_model_id = insert_res.data[0]["generated_model_id"] if insert_res.data else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
  except Exception as ex:
523
  logging.error(f"Failed to create initial model DB record: {ex}")
524
-
525
- # 5. Send request to Meshy
526
- meshy_key = os.getenv("MESHY_API_KEY")
527
- if not meshy_key:
528
- raise HTTPException(status_code=500, detail="MESHY_API_KEY not configured")
529
-
530
- # Build payload
531
- payload: Dict[str, Any] = {
532
- "image_urls": image_urls,
533
- "mode": "preview",
534
- "ai_model": "meshy-5",
535
- "should_texture": False,
536
- }
537
-
538
- async with httpx.AsyncClient(timeout=30.0) as client:
539
- meshy_resp = await client.post(
540
- "https://api.meshy.ai/openapi/v1/multi-image-to-3d",
541
- headers={"Authorization": f"Bearer {meshy_key}"},
542
- json=payload,
543
- )
544
-
545
- if meshy_resp.status_code not in (200, 201, 202):
546
- raise HTTPException(status_code=meshy_resp.status_code, detail=meshy_resp.text)
547
-
548
- meshy_data = meshy_resp.json()
549
- meshy_task_id = meshy_data.get("result") or meshy_data.get("id") or meshy_data.get("task_id")
550
-
551
- if not meshy_task_id:
552
- raise HTTPException(status_code=500, detail="No task ID received from Meshy API")
553
-
554
- # 6. Finalize DB record
555
- if generated_model_id:
556
- try:
557
- supabase.from_("Generated_Models").update({
558
- "meshy_api_job_id": meshy_task_id,
559
- "prompts_and_models_config": {
560
- "generation_type": "multi_image_to_3d",
561
- "input_image_filenames": filenames,
562
- "status": "processing",
563
- "stage": "generating",
564
- "meshy_response": meshy_data,
565
- },
566
- }).eq("generated_model_id", generated_model_id).execute()
567
- except Exception as ex:
568
- logging.warning(f"Failed to update DB with Meshy taskId: {ex}")
569
-
570
- # 7. Return identifiers
571
- return {
572
- "generated_model_id": generated_model_id,
573
- "meshy_task_id": meshy_task_id,
574
- "status": "processing",
575
- "input_image_filenames": filenames,
576
- }
577
 
 
149
  new_credits = credit_data["num_of_available_gens"] - 1
150
  supabase.from_("User_Credit_Account").update({"num_of_available_gens": new_credits}).eq("user_id", user_id).execute()
151
 
152
+ # Background task for text-to-3d processing
153
+ async def _process_text_to_3d_background(generated_model_id: int, user_id: str, original_prompt: str):
154
+ """Background task to handle OpenAI reframing and Meshy API call for text-to-3d"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  try:
156
+ # 1. Reframe prompt via OpenAI
157
+ reframed_prompt = original_prompt # Default fallback
158
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  openai_key = os.getenv("OPENAI_API_KEY")
160
+ if openai_key:
161
+ try:
162
+ async with httpx.AsyncClient(timeout=30.0) as client:
163
+ oai_resp = await client.post(
164
+ "https://api.openai.com/v1/chat/completions",
165
+ headers={"Authorization": f"Bearer {openai_key}"},
166
+ json={
167
+ "model": "gpt-4o-mini",
168
+ "messages": [
169
+ {"role": "system", "content": "REPLY ONLY WITH NEW PROMPT, no other text. IF USER PROMPT CONTAINS HARMFUL CONTENT, CHANGE IT TO SOMETHING SAFE and somewhat related. Rephrase the user description to simple and short."},
170
+ {"role": "user", "content": original_prompt},
171
+ ],
172
+ },
173
+ )
174
+ if oai_resp.status_code == 200:
175
+ reframed_prompt = oai_resp.json()["choices"][0]["message"]["content"]
176
+ except Exception as ex:
177
+ logging.warning(f"OpenAI reframing failed, using original prompt: {ex}")
178
+
179
+ # 2. Update DB with reframed prompt
 
 
 
 
 
 
 
 
180
  try:
181
  supabase.from_("Generated_Models").update({
182
  "prompts_and_models_config": {
183
  "generation_type": "text_to_3d",
184
+ "original_prompt": original_prompt,
185
  "reframed_prompt": reframed_prompt,
186
  "status": "processing",
187
  "stage": "creating_3d_model",
 
190
  except Exception as ex:
191
  logging.warning(f"Failed to update DB with reframed prompt: {ex}")
192
 
193
+ # 3. Send request to Meshy
194
+ meshy_key = os.getenv("MESHY_API_KEY")
195
+ if not meshy_key:
196
+ logging.error("MESHY_API_KEY not configured")
197
+ return
198
 
199
+ meshy_payload = {
200
+ "mode": "preview",
201
+ "prompt": reframed_prompt,
202
+ "ai_model": "meshy-5",
203
+ }
204
 
205
+ async with httpx.AsyncClient(timeout=30.0) as client:
206
+ meshy_resp = await client.post(
207
+ "https://api.meshy.ai/openapi/v2/text-to-3d",
208
+ headers={"Authorization": f"Bearer {meshy_key}"},
209
+ json=meshy_payload,
210
+ )
211
 
212
+ if meshy_resp.status_code not in (200, 201, 202):
213
+ logging.error(f"Meshy API failed: {meshy_resp.status_code} - {meshy_resp.text}")
214
+ return
215
 
216
+ meshy_data = meshy_resp.json()
217
+ meshy_task_id = meshy_data.get("result") or meshy_data.get("id") or meshy_data.get("task_id")
218
 
219
+ if not meshy_task_id:
220
+ logging.error("No task ID received from Meshy API")
221
+ return
222
 
223
+ # 4. Finalize DB record
 
224
  try:
225
  supabase.from_("Generated_Models").update({
226
  "meshy_api_job_id": meshy_task_id,
227
  "prompts_and_models_config": {
228
  "generation_type": "text_to_3d",
229
+ "original_prompt": original_prompt,
230
  "reframed_prompt": reframed_prompt,
231
  "status": "processing",
232
  "stage": "generating",
233
  "meshy_response": meshy_data,
234
  },
235
  }).eq("generated_model_id", generated_model_id).execute()
236
+ logging.info(f"Successfully started text-to-3d generation for model {generated_model_id}")
237
+ except Exception as ex:
238
+ logging.error(f"Failed to update DB with Meshy taskId: {ex}")
239
+
240
+ except Exception as ex:
241
+ logging.error(f"Background processing failed for text-to-3d model {generated_model_id}: {ex}")
242
+
243
+ # Background task for image-to-3d processing
244
+ async def _process_image_to_3d_background(generated_model_id: int, payload: Dict[str, Any], generation_type: str):
245
+ """Background task to handle Meshy API call for image-to-3d"""
246
+ try:
247
+ # Send request to Meshy
248
+ meshy_key = os.getenv("MESHY_API_KEY")
249
+ if not meshy_key:
250
+ logging.error("MESHY_API_KEY not configured")
251
+ return
252
+
253
+ # Determine the correct Meshy endpoint
254
+ if generation_type == "multi_image_to_3d":
255
+ meshy_endpoint = "https://api.meshy.ai/openapi/v1/multi-image-to-3d"
256
+ else:
257
+ meshy_endpoint = "https://api.meshy.ai/openapi/v1/image-to-3d"
258
+
259
+ async with httpx.AsyncClient(timeout=30.0) as client:
260
+ meshy_resp = await client.post(
261
+ meshy_endpoint,
262
+ headers={"Authorization": f"Bearer {meshy_key}"},
263
+ json=payload,
264
+ )
265
+
266
+ if meshy_resp.status_code not in (200, 201, 202):
267
+ logging.error(f"Meshy API failed: {meshy_resp.status_code} - {meshy_resp.text}")
268
+ return
269
+
270
+ meshy_data = meshy_resp.json()
271
+ meshy_task_id = meshy_data.get("result") or meshy_data.get("id") or meshy_data.get("task_id")
272
+
273
+ if not meshy_task_id:
274
+ logging.error("No task ID received from Meshy API")
275
+ return
276
+
277
+ # Update DB record with Meshy task ID
278
+ try:
279
+ # Get current config to preserve it
280
+ current_record = supabase.from_("Generated_Models").select("prompts_and_models_config").eq("generated_model_id", generated_model_id).single().execute()
281
+ current_config = current_record.data.get("prompts_and_models_config", {}) if current_record.data else {}
282
+
283
+ # Update config with Meshy response
284
+ updated_config = {**current_config}
285
+ updated_config.update({
286
+ "status": "processing",
287
+ "stage": "generating",
288
+ "meshy_response": meshy_data,
289
+ })
290
+
291
+ supabase.from_("Generated_Models").update({
292
+ "meshy_api_job_id": meshy_task_id,
293
+ "prompts_and_models_config": updated_config,
294
+ }).eq("generated_model_id", generated_model_id).execute()
295
+
296
+ logging.info(f"Successfully started {generation_type} generation for model {generated_model_id}")
297
  except Exception as ex:
298
+ logging.error(f"Failed to update DB with Meshy taskId: {ex}")
299
+
300
+ except Exception as ex:
301
+ logging.error(f"Background processing failed for {generation_type} model {generated_model_id}: {ex}")
302
+
303
+ @router.post("/text-to-3d")
304
+ async def text_to_3d(prompt: TextPrompt, background_tasks: BackgroundTasks, current_user: User = Depends(get_current_active_user)):
305
+ """
306
+ Create a Meshy Text-to-3D generation job.
307
 
308
+ Returns immediately after creating the database record. All processing
309
+ (OpenAI reframing, Meshy API call) happens in the background.
310
+ """
311
+
312
+ # Credit check and decrement
313
+ await _check_and_decrement_credits(current_user.id)
314
 
315
+ # Insert initial DB record and return immediately
316
+ try:
317
+ insert_res = supabase.from_("Generated_Models").insert({
318
+ "status": "IN_PROGRESS",
319
+ "user_id": current_user.id,
320
+ "meshy_api_job_id": None,
321
+ "model_name": f"{prompt.text[:50]}{'...' if len(prompt.text) > 50 else ''}",
322
+ "prompts_and_models_config": {
323
+ "generation_type": "text_to_3d",
324
+ "original_prompt": prompt.text,
325
+ "status": "initializing",
326
+ "stage": "reframing_prompt",
327
+ },
328
+ }).execute()
329
+ generated_model_id = insert_res.data[0]["generated_model_id"] if insert_res.data else None
330
+
331
+ if not generated_model_id:
332
+ raise HTTPException(status_code=500, detail="Failed to create model record")
333
+
334
+ # Add background task for processing
335
+ background_tasks.add_task(_process_text_to_3d_background, generated_model_id, current_user.id, prompt.text)
336
+
337
+ # Return immediately
338
+ return {
339
+ "generated_model_id": generated_model_id,
340
+ "status": "initializing",
341
+ "original_prompt": prompt.text,
342
+ "message": "Generation started. Use the progress_update endpoint to check status."
343
+ }
344
+
345
+ except Exception as ex:
346
+ logging.error(f"Failed to create initial model DB record: {ex}")
347
+ raise HTTPException(status_code=500, detail=f"Failed to start generation: {ex}")
348
 
349
  # NEW: Image to 3D generation endpoint
350
  @router.post("/image-to-3d")
 
352
  """
353
  Create a Meshy Image-to-3D generation job.
354
 
355
+ Returns immediately after creating the database record. Meshy API call happens in the background.
 
 
 
 
356
  """
357
 
358
  # 1. Credit check and decrement
359
  await _check_and_decrement_credits(current_user.id)
360
 
361
+ # 2. Insert initial DB record and return immediately
 
 
362
  try:
363
  insert_res = supabase.from_("Generated_Models").insert({
364
  "status": "IN_PROGRESS",
 
373
  },
374
  }).execute()
375
  generated_model_id = insert_res.data[0]["generated_model_id"] if insert_res.data else None
376
+
377
+ if not generated_model_id:
378
+ raise HTTPException(status_code=500, detail="Failed to create model record")
379
+
380
+ # Build payload by including all non-None fields from the request model
381
+ payload: Dict[str, Any] = {k: v for k, v in request.dict().items() if v is not None}
382
+
383
+ # Add background task for Meshy API processing
384
+ background_tasks.add_task(_process_image_to_3d_background, generated_model_id, payload, "image_to_3d")
385
+
386
+ # Return immediately
387
+ return {
388
+ "generated_model_id": generated_model_id,
389
+ "status": "initializing",
390
+ "input_image_url": request.image_url,
391
+ "message": "Generation started. Use the progress_update endpoint to check status."
392
+ }
393
+
394
  except Exception as ex:
395
  logging.error(f"Failed to create initial model DB record: {ex}")
396
+ raise HTTPException(status_code=500, detail=f"Failed to start generation: {ex}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
 
398
  @router.post("/image-to-3d/upload")
399
  async def image_to_3d_upload(
400
+ background_tasks: BackgroundTasks,
401
  file: UploadFile = File(...),
402
  current_user: User = Depends(get_current_active_user),
403
  ):
404
  """Upload an image file and create a Meshy *Image-to-3D* task.
405
 
406
+ Returns immediately after creating the database record and encoding the image.
407
+ Meshy API call happens in the background.
 
408
  """
409
 
410
  # 1. Credit check and decrement
411
  await _check_and_decrement_credits(current_user.id)
412
 
413
+ # 2. Read & encode file (this is fast, so we do it synchronously)
414
  try:
415
  file_bytes = await file.read()
416
  mime_type = file.content_type or "image/png"
 
419
  except Exception as ex:
420
  raise HTTPException(status_code=400, detail=f"Failed to read uploaded file: {ex}")
421
 
422
+ # 3. Insert initial DB record and return immediately
 
 
423
  try:
424
  insert_res = supabase.from_("Generated_Models").insert({
425
  "status": "IN_PROGRESS",
 
434
  },
435
  }).execute()
436
  generated_model_id = insert_res.data[0]["generated_model_id"] if insert_res.data else None
437
+
438
+ if not generated_model_id:
439
+ raise HTTPException(status_code=500, detail="Failed to create model record")
440
+
441
+ # Build payload
442
+ payload: Dict[str, Any] = {
443
+ "image_url": data_uri,
444
+ "mode": "preview",
445
+ "ai_model": "meshy-5",
446
+ #"should_texture": False,
447
+ }
448
+
449
+ # Add background task for Meshy API processing
450
+ background_tasks.add_task(_process_image_to_3d_background, generated_model_id, payload, "image_to_3d")
451
+
452
+ # Return immediately
453
+ return {
454
+ "generated_model_id": generated_model_id,
455
+ "status": "initializing",
456
+ "input_image_filename": file.filename,
457
+ "message": "Generation started. Use the progress_update endpoint to check status."
458
+ }
459
+
460
  except Exception as ex:
461
  logging.error(f"Failed to create initial model DB record: {ex}")
462
+ raise HTTPException(status_code=500, detail=f"Failed to start generation: {ex}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
 
464
  @router.post("/multi-image-to-3d")
465
  async def multi_image_to_3d_upload(
466
+ background_tasks: BackgroundTasks,
467
  files: list[UploadFile] = File(...),
468
  current_user: User = Depends(get_current_active_user),
469
  ):
470
  """Upload multiple image files and create a Meshy *Multi-Image-to-3D* task.
471
 
472
+ Returns immediately after creating the database record and encoding the images.
473
+ Meshy API call happens in the background.
 
474
  """
475
 
476
  # 1. Credit check and decrement
 
480
  if len(files) < 2:
481
  raise HTTPException(status_code=400, detail="At least 2 images are required for multi-image generation")
482
 
483
+ # 3. Read & encode all files (this is reasonably fast, so we do it synchronously)
484
  try:
485
  image_urls = []
486
  filenames = []
 
494
  except Exception as ex:
495
  raise HTTPException(status_code=400, detail=f"Failed to read uploaded files: {ex}")
496
 
497
+ # 4. Insert initial DB record and return immediately
 
 
498
  try:
499
  insert_res = supabase.from_("Generated_Models").insert({
500
  "status": "IN_PROGRESS",
 
509
  },
510
  }).execute()
511
  generated_model_id = insert_res.data[0]["generated_model_id"] if insert_res.data else None
512
+
513
+ if not generated_model_id:
514
+ raise HTTPException(status_code=500, detail="Failed to create model record")
515
+
516
+ # Build payload
517
+ payload: Dict[str, Any] = {
518
+ "image_urls": image_urls,
519
+ "mode": "preview",
520
+ "ai_model": "meshy-5",
521
+ "should_texture": False,
522
+ }
523
+
524
+ # Add background task for Meshy API processing
525
+ background_tasks.add_task(_process_image_to_3d_background, generated_model_id, payload, "multi_image_to_3d")
526
+
527
+ # Return immediately
528
+ return {
529
+ "generated_model_id": generated_model_id,
530
+ "status": "initializing",
531
+ "input_image_filenames": filenames,
532
+ "message": "Generation started. Use the progress_update endpoint to check status."
533
+ }
534
+
535
  except Exception as ex:
536
  logging.error(f"Failed to create initial model DB record: {ex}")
537
+ raise HTTPException(status_code=500, detail=f"Failed to start generation: {ex}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538