Maksymilian Jankowski commited on
Commit
fd314e2
·
1 Parent(s): 17d201b

proxy for model viewing

Browse files
Files changed (1) hide show
  1. main.py +419 -16
main.py CHANGED
@@ -8,6 +8,10 @@ from fastapi.middleware.cors import CORSMiddleware
8
  from pydantic import BaseModel
9
  from auth import get_current_active_user, User, supabase
10
  import logging
 
 
 
 
11
 
12
  load_dotenv()
13
 
@@ -85,13 +89,35 @@ class PlaceOrderRequest(BaseModel):
85
  @app.post("/auth/signup", tags=["Authentication"])
86
  async def signup(request: SignUpRequest):
87
  """
88
- Sign up a new user
89
  """
90
  try:
91
  response = supabase.auth.sign_up({
92
  "email": request.email,
93
  "password": request.password
94
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  return {"message": "User created successfully", "user": response.user}
96
  except Exception as e:
97
  raise HTTPException(status_code=400, detail=str(e))
@@ -101,7 +127,7 @@ async def signin(request: SignInRequest):
101
  """
102
  Sign in an existing user
103
  """
104
- logging.basicConfig(level=logging.DEBUG)
105
  logging.debug(f"SignIn request: email={request.email}")
106
  try:
107
  response = supabase.auth.sign_in_with_password({
@@ -118,6 +144,7 @@ async def signin(request: SignInRequest):
118
  logging.error(f"SignIn error: {str(e)}")
119
  raise HTTPException(status_code=401, detail=f"Invalid credentials: {str(e)}")
120
 
 
121
  @app.post("/auth/complete-profile", tags=["Authentication"])
122
  async def complete_profile(request: CompleteProfileRequest, current_user: User = Depends(get_current_active_user)):
123
  """
@@ -147,13 +174,16 @@ async def complete_profile(request: CompleteProfileRequest, current_user: User =
147
  # Protected endpoints
148
  async def check_and_decrement_credits(user_id: str):
149
  # Get current credits
150
- credit = supabase.from_("User_Credit_Account").select("num_of_available_gens").eq("user_id", user_id).single().execute()
151
- if not credit.data or credit.data["num_of_available_gens"] is None:
152
  raise HTTPException(status_code=403, detail="No credit account found. Please complete your profile.")
153
- if credit.data["num_of_available_gens"] <= 0:
 
 
154
  raise HTTPException(status_code=402, detail="No credits left. Please purchase more to generate models.")
 
155
  # Decrement credits atomically
156
- new_credits = credit.data["num_of_available_gens"] - 1
157
  supabase.from_("User_Credit_Account").update({"num_of_available_gens": new_credits}).eq("user_id", user_id).execute()
158
 
159
  @app.post("/req-img-to-3d", tags=["Image-to-3D"])
@@ -179,9 +209,34 @@ async def req_img_to_3d(
179
  json=payload,
180
  headers=headers,
181
  )
182
- if resp.status_code not in (200, 201):
183
  raise HTTPException(status_code=resp.status_code, detail=resp.text)
184
- return resp.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
  @app.get("/req-result-img-to-3d/{task_id}", tags=["Image-to-3D"])
187
  async def req_result_img_to_3d(
@@ -197,7 +252,33 @@ async def req_result_img_to_3d(
197
  resp = await app.state.client.get(url, headers=headers)
198
  if resp.status_code != 200:
199
  raise HTTPException(status_code=resp.status_code, detail=resp.text)
200
- return resp.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
  @app.get("/list-img-to-3d-req", tags=["Image-to-3D"])
203
  async def list_img_to_3d_req(
@@ -227,12 +308,33 @@ async def req_text_to_3d(
227
  """
228
  # Credit check and decrement
229
  await check_and_decrement_credits(current_user.id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  # Reframe prompt
231
  openai_resp = await app.state.client.post(
232
  "https://api.openai.com/v1/chat/completions",
233
  headers={"Authorization": f"Bearer {settings.openai_api_key}"},
234
  json={
235
- "model": "gpt-3.5-turbo",
236
  "messages": [
237
  {"role": "system", "content": "Rephrase the user description to be an ideal prompt for 3D model generation."},
238
  {"role": "user", "content": prompt.text}
@@ -240,17 +342,117 @@ async def req_text_to_3d(
240
  }
241
  )
242
  if openai_resp.status_code != 200:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  raise HTTPException(status_code=openai_resp.status_code, detail=openai_resp.text)
 
244
  reframed = openai_resp.json()["choices"][0]["message"]["content"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  # Create Meshy Text-to-3D task
 
 
 
246
  meshy_resp = await app.state.client.post(
247
  "https://api.meshy.ai/openapi/v2/text-to-3d",
248
  headers={"Authorization": f"Bearer {settings.mesh_api_key}"},
249
- json={"mode": "preview", "prompt": reframed}
250
  )
251
- if meshy_resp.status_code not in (200, 201):
252
- raise HTTPException(status_code=meshy_resp.status_code, detail=meshy_resp.text)
253
- return meshy_resp.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
  @app.get("/req-result-text-to-3d/{task_id}", tags=["Text-to-3D"])
256
  async def req_result_text_to_3d(
@@ -267,7 +469,33 @@ async def req_result_text_to_3d(
267
  )
268
  if resp.status_code != 200:
269
  raise HTTPException(status_code=resp.status_code, detail=resp.text)
270
- return resp.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
  @app.get("/list-text-to-3d-req", tags=["Text-to-3D"])
273
  async def list_text_to_3d_req(
@@ -326,9 +554,161 @@ async def get_orders(current_user: User = Depends(get_current_active_user)):
326
 
327
  @app.get("/user/models", tags=["User"])
328
  async def get_models(current_user: User = Depends(get_current_active_user)):
329
- models = supabase.from_("Generated_Models").select("*").eq("user_id", current_user.id).execute()
330
  return models.data
331
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  @app.post("/orders/place", tags=["Orders"])
333
  async def place_order(request: PlaceOrderRequest, current_user: User = Depends(get_current_active_user)):
334
  """
@@ -355,6 +735,29 @@ async def health_check():
355
  """Basic health-check endpoint returning service status."""
356
  return {"status": "ok"}
357
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  if __name__ == "__main__":
359
  import uvicorn
360
  uvicorn.run("main:app", host="0.0.0.0", port=8000, log_level="info")
 
8
  from pydantic import BaseModel
9
  from auth import get_current_active_user, User, supabase
10
  import logging
11
+ import random
12
+ from io import BytesIO
13
+ from urllib.parse import unquote
14
+ from fastapi.responses import StreamingResponse
15
 
16
  load_dotenv()
17
 
 
89
  @app.post("/auth/signup", tags=["Authentication"])
90
  async def signup(request: SignUpRequest):
91
  """
92
+ Sign up a new user and create initial profile
93
  """
94
  try:
95
  response = supabase.auth.sign_up({
96
  "email": request.email,
97
  "password": request.password
98
  })
99
+
100
+ # Create user profile entry if user creation was successful
101
+ if response.user and response.user.id:
102
+ try:
103
+ supabase.from_("User").insert({
104
+ "user_id": response.user.id,
105
+ "email": response.user.email,
106
+ "address": None,
107
+ "fullname": None,
108
+ "phone_number": None
109
+ }).execute()
110
+
111
+ # Initialize credits
112
+ supabase.from_("User_Credit_Account").insert({
113
+ "user_id": response.user.id,
114
+ "num_of_available_gens": 3
115
+ }).execute()
116
+
117
+ print(f"User created successfully: {response.user}")
118
+ except Exception as profile_error:
119
+ logging.error(f"Failed to create user profile: {str(profile_error)}")
120
+
121
  return {"message": "User created successfully", "user": response.user}
122
  except Exception as e:
123
  raise HTTPException(status_code=400, detail=str(e))
 
127
  """
128
  Sign in an existing user
129
  """
130
+ #logging.basicConfig(level=logging.DEBUG)
131
  logging.debug(f"SignIn request: email={request.email}")
132
  try:
133
  response = supabase.auth.sign_in_with_password({
 
144
  logging.error(f"SignIn error: {str(e)}")
145
  raise HTTPException(status_code=401, detail=f"Invalid credentials: {str(e)}")
146
 
147
+ # TODO: The signup also creates a profile, so this endpoint should either be removed or /signup updated.
148
  @app.post("/auth/complete-profile", tags=["Authentication"])
149
  async def complete_profile(request: CompleteProfileRequest, current_user: User = Depends(get_current_active_user)):
150
  """
 
174
  # Protected endpoints
175
  async def check_and_decrement_credits(user_id: str):
176
  # Get current credits
177
+ credit = supabase.from_("User_Credit_Account").select("num_of_available_gens").eq("user_id", user_id).execute()
178
+ if not credit.data:
179
  raise HTTPException(status_code=403, detail="No credit account found. Please complete your profile.")
180
+
181
+ credit_data = credit.data[0]
182
+ if credit_data["num_of_available_gens"] is None or credit_data["num_of_available_gens"] <= 0:
183
  raise HTTPException(status_code=402, detail="No credits left. Please purchase more to generate models.")
184
+
185
  # Decrement credits atomically
186
+ new_credits = credit_data["num_of_available_gens"] - 1
187
  supabase.from_("User_Credit_Account").update({"num_of_available_gens": new_credits}).eq("user_id", user_id).execute()
188
 
189
  @app.post("/req-img-to-3d", tags=["Image-to-3D"])
 
209
  json=payload,
210
  headers=headers,
211
  )
212
+ if resp.status_code not in (200, 201, 202):
213
  raise HTTPException(status_code=resp.status_code, detail=resp.text)
214
+
215
+ meshy_response = resp.json()
216
+ meshy_job_id = meshy_response.get("id") or meshy_response.get("task_id")
217
+
218
+ if not meshy_job_id:
219
+ raise HTTPException(status_code=500, detail="No task ID received from Meshy API")
220
+
221
+ # Save to database
222
+ try:
223
+ supabase.from_("Generated_Models").insert({
224
+ "user_id": current_user.id,
225
+ "meshy_api_job_id": meshy_job_id,
226
+ "model_name": f"Image to 3D - {image.filename}",
227
+ "prompts_and_models_config": {
228
+ "generation_type": "image_to_3d",
229
+ "source_filename": image.filename,
230
+ "content_type": image.content_type,
231
+ "status": "processing",
232
+ "meshy_response": meshy_response
233
+ }
234
+ }).execute()
235
+ except Exception as e:
236
+ logging.error(f"Failed to save model to database: {str(e)}")
237
+ # Don't fail the request if database save fails
238
+
239
+ return {"id": meshy_job_id, "status": "processing", "meshy_response": meshy_response}
240
 
241
  @app.get("/req-result-img-to-3d/{task_id}", tags=["Image-to-3D"])
242
  async def req_result_img_to_3d(
 
252
  resp = await app.state.client.get(url, headers=headers)
253
  if resp.status_code != 200:
254
  raise HTTPException(status_code=resp.status_code, detail=resp.text)
255
+
256
+ result = resp.json()
257
+
258
+ # Update database with current status
259
+ try:
260
+ # Get existing config to merge with new data
261
+ existing_model = supabase.from_("Generated_Models").select("prompts_and_models_config").eq("meshy_api_job_id", task_id).eq("user_id", current_user.id).single().execute()
262
+
263
+ if existing_model.data:
264
+ config = existing_model.data.get("prompts_and_models_config", {})
265
+ config.update({
266
+ "status": result.get("status", "unknown"),
267
+ "meshy_response": result
268
+ })
269
+
270
+ # If completed, add the model URLs
271
+ if result.get("status") == "SUCCEEDED" and "model_urls" in result:
272
+ config["model_urls"] = result["model_urls"]
273
+ config["thumbnail_url"] = result.get("thumbnail_url")
274
+
275
+ supabase.from_("Generated_Models").update({
276
+ "prompts_and_models_config": config
277
+ }).eq("meshy_api_job_id", task_id).eq("user_id", current_user.id).execute()
278
+ except Exception as e:
279
+ logging.error(f"Failed to update model status in database: {str(e)}")
280
+
281
+ return result
282
 
283
  @app.get("/list-img-to-3d-req", tags=["Image-to-3D"])
284
  async def list_img_to_3d_req(
 
308
  """
309
  # Credit check and decrement
310
  await check_and_decrement_credits(current_user.id)
311
+
312
+ # Save initial record to database immediately after credit check
313
+ initial_record_id = None
314
+ try:
315
+ initial_result = supabase.from_("Generated_Models").insert({
316
+ "user_id": current_user.id,
317
+ "meshy_api_job_id": None,
318
+ "model_name": f"Text to 3D - {prompt.text[:50]}{'...' if len(prompt.text) > 50 else ''}",
319
+ "prompts_and_models_config": {
320
+ "generation_type": "text_to_3d",
321
+ "original_prompt": prompt.text,
322
+ "status": "initializing",
323
+ "stage": "reframing_prompt"
324
+ }
325
+ }).execute()
326
+ initial_record_id = initial_result.data[0]["generated_model_id"] if initial_result.data else None
327
+ print(f"Created initial database record: {initial_record_id}")
328
+ except Exception as e:
329
+ print(f"Failed to create initial database record: {str(e)}")
330
+ # Continue with the request even if database save fails
331
+
332
  # Reframe prompt
333
  openai_resp = await app.state.client.post(
334
  "https://api.openai.com/v1/chat/completions",
335
  headers={"Authorization": f"Bearer {settings.openai_api_key}"},
336
  json={
337
+ "model": "gpt-4o-mini",
338
  "messages": [
339
  {"role": "system", "content": "Rephrase the user description to be an ideal prompt for 3D model generation."},
340
  {"role": "user", "content": prompt.text}
 
342
  }
343
  )
344
  if openai_resp.status_code != 200:
345
+ # Update database with error status if initial record was created
346
+ if initial_record_id:
347
+ try:
348
+ supabase.from_("Generated_Models").update({
349
+ "prompts_and_models_config": {
350
+ "generation_type": "text_to_3d",
351
+ "original_prompt": prompt.text,
352
+ "status": "failed",
353
+ "stage": "reframing_prompt",
354
+ "error": f"OpenAI API error: {openai_resp.text}"
355
+ }
356
+ }).eq("generated_model_id", initial_record_id).execute()
357
+ except Exception as e:
358
+ print(f"Failed to update database with error: {str(e)}")
359
  raise HTTPException(status_code=openai_resp.status_code, detail=openai_resp.text)
360
+
361
  reframed = openai_resp.json()["choices"][0]["message"]["content"]
362
+
363
+ # Update database with reframed prompt
364
+ if initial_record_id:
365
+ try:
366
+ supabase.from_("Generated_Models").update({
367
+ "prompts_and_models_config": {
368
+ "generation_type": "text_to_3d",
369
+ "original_prompt": prompt.text,
370
+ "reframed_prompt": reframed,
371
+ "status": "processing",
372
+ "stage": "creating_3d_model"
373
+ }
374
+ }).eq("generated_model_id", initial_record_id).execute()
375
+ except Exception as e:
376
+ print(f"Failed to update database with reframed prompt: {str(e)}")
377
+
378
  # Create Meshy Text-to-3D task
379
+ meshy_payload = {"mode": "preview", "prompt": reframed}
380
+ print(f"Sending to Meshy API: {meshy_payload}")
381
+
382
  meshy_resp = await app.state.client.post(
383
  "https://api.meshy.ai/openapi/v2/text-to-3d",
384
  headers={"Authorization": f"Bearer {settings.mesh_api_key}"},
385
+ json=meshy_payload
386
  )
387
+ if meshy_resp.status_code not in (200, 201, 202):
388
+ # Log the full response for debugging
389
+ error_text = meshy_resp.text
390
+ print(f"Meshy API error - Status: {meshy_resp.status_code}, Response: {error_text}")
391
+
392
+ # Try to parse the response as JSON to provide better error details
393
+ try:
394
+ error_json = meshy_resp.json()
395
+ error_detail = f"Meshy API error (status {meshy_resp.status_code}): {error_json}"
396
+ except:
397
+ error_detail = f"Meshy API error (status {meshy_resp.status_code}): {error_text}"
398
+
399
+ # Update database with error status if initial record was created
400
+ if initial_record_id:
401
+ try:
402
+ supabase.from_("Generated_Models").update({
403
+ "prompts_and_models_config": {
404
+ "generation_type": "text_to_3d",
405
+ "original_prompt": prompt.text,
406
+ "reframed_prompt": reframed,
407
+ "status": "failed",
408
+ "stage": "creating_3d_model",
409
+ "error": error_detail
410
+ }
411
+ }).eq("generated_model_id", initial_record_id).execute()
412
+ except Exception as e:
413
+ print(f"Failed to update database with error: {str(e)}")
414
+ raise HTTPException(status_code=meshy_resp.status_code, detail=error_detail)
415
+
416
+ meshy_response = meshy_resp.json()
417
+ task_id = meshy_response.get("result") or meshy_response.get("id") or meshy_response.get("task_id")
418
+
419
+ if not task_id:
420
+ # Update database with error status if initial record was created
421
+ if initial_record_id:
422
+ try:
423
+ supabase.from_("Generated_Models").update({
424
+ "prompts_and_models_config": {
425
+ "generation_type": "text_to_3d",
426
+ "original_prompt": prompt.text,
427
+ "reframed_prompt": reframed,
428
+ "status": "failed",
429
+ "stage": "creating_3d_model",
430
+ "error": "No task ID received from Meshy API"
431
+ }
432
+ }).eq("generated_model_id", initial_record_id).execute()
433
+ except Exception as e:
434
+ print(f"Failed to update database with error: {str(e)}")
435
+ raise HTTPException(status_code=500, detail="No task ID received from Meshy API")
436
+
437
+ # Update database with final successful response
438
+ print(f"Updating database record with task ID: {task_id}")
439
+ try:
440
+ supabase.from_("Generated_Models").update({
441
+ "meshy_api_job_id": task_id,
442
+ "prompts_and_models_config": {
443
+ "generation_type": "text_to_3d",
444
+ "original_prompt": prompt.text,
445
+ "reframed_prompt": reframed,
446
+ "status": "processing",
447
+ "stage": "generating",
448
+ "meshy_response": meshy_response
449
+ }
450
+ }).eq("generated_model_id", initial_record_id).execute()
451
+ except Exception as e:
452
+ print(f"Failed to update database with final response: {str(e)}")
453
+ # Don't fail the request if database save fails
454
+
455
+ return {"id": initial_record_id, "meshy_task_id": task_id, "status": "processing", "original_prompt": prompt.text, "reframed_prompt": reframed, "meshy_response": meshy_response}
456
 
457
  @app.get("/req-result-text-to-3d/{task_id}", tags=["Text-to-3D"])
458
  async def req_result_text_to_3d(
 
469
  )
470
  if resp.status_code != 200:
471
  raise HTTPException(status_code=resp.status_code, detail=resp.text)
472
+
473
+ result = resp.json()
474
+
475
+ # Update database with current status
476
+ try:
477
+ # Get existing config to merge with new data
478
+ existing_model = supabase.from_("Generated_Models").select("prompts_and_models_config").eq("meshy_api_job_id", task_id).eq("user_id", current_user.id).single().execute()
479
+
480
+ if existing_model.data:
481
+ config = existing_model.data.get("prompts_and_models_config", {})
482
+ config.update({
483
+ "status": result.get("status", "unknown"),
484
+ "meshy_response": result
485
+ })
486
+
487
+ # If completed, add the model URLs
488
+ if result.get("status") == "SUCCEEDED" and "model_urls" in result:
489
+ config["model_urls"] = result["model_urls"]
490
+ config["thumbnail_url"] = result.get("thumbnail_url")
491
+
492
+ supabase.from_("Generated_Models").update({
493
+ "prompts_and_models_config": config
494
+ }).eq("meshy_api_job_id", task_id).eq("user_id", current_user.id).execute()
495
+ except Exception as e:
496
+ logging.error(f"Failed to update model status in database: {str(e)}")
497
+
498
+ return result
499
 
500
  @app.get("/list-text-to-3d-req", tags=["Text-to-3D"])
501
  async def list_text_to_3d_req(
 
554
 
555
  @app.get("/user/models", tags=["User"])
556
  async def get_models(current_user: User = Depends(get_current_active_user)):
557
+ models = supabase.from_("Generated_Models").select("*").eq("user_id", current_user.id).order("created_at", desc=True).execute()
558
  return models.data
559
 
560
+ @app.get("/user/models/{task_id}", tags=["User"])
561
+ async def get_model_by_task_id(task_id: str, current_user: User = Depends(get_current_active_user)):
562
+ """
563
+ Get a specific model by task ID for the current user.
564
+ """
565
+ model = supabase.from_("Generated_Models").select("*").eq("meshy_api_job_id", task_id).eq("user_id", current_user.id).single().execute()
566
+ if not model.data:
567
+ raise HTTPException(status_code=404, detail="Model not found")
568
+ return model.data
569
+
570
+ @app.post("/user/models/{task_id}/refresh", tags=["User"])
571
+ async def refresh_model_status(
572
+ task_id: str,
573
+ current_user: User = Depends(get_current_active_user),
574
+ settings: Settings = Depends(get_settings)
575
+ ):
576
+ """
577
+ Manually refresh the status of a model by checking with Meshy API.
578
+ """
579
+ # First check if the model exists and belongs to the user
580
+ try:
581
+ model = supabase.from_("Generated_Models").select("*").eq("meshy_api_job_id", task_id).eq("user_id", current_user.id).single().execute()
582
+ if not model.data:
583
+ raise HTTPException(status_code=404, detail="Model not found")
584
+ except Exception:
585
+ raise HTTPException(status_code=404, detail="Model not found")
586
+
587
+ config = model.data.get("prompts_and_models_config", {})
588
+ generation_type = config.get("generation_type")
589
+
590
+ try:
591
+ # Call the appropriate API based on generation type
592
+ if generation_type == "text_to_3d":
593
+ resp = await app.state.client.get(
594
+ f"https://api.meshy.ai/openapi/v2/text-to-3d/{task_id}",
595
+ headers={"Authorization": f"Bearer {settings.mesh_api_key}"}
596
+ )
597
+ elif generation_type == "image_to_3d":
598
+ resp = await app.state.client.get(
599
+ f"https://api.meshy.ai/openapi/v1/image-to-3d/{task_id}",
600
+ headers={"Authorization": f"Bearer {settings.mesh_api_key}"}
601
+ )
602
+ else:
603
+ raise HTTPException(status_code=400, detail="Unknown generation type")
604
+
605
+ if resp.status_code != 200:
606
+ raise HTTPException(status_code=resp.status_code, detail=resp.text)
607
+
608
+ result = resp.json()
609
+
610
+ # Update database with current status
611
+ config.update({
612
+ "status": result.get("status", "unknown"),
613
+ "meshy_response": result
614
+ })
615
+
616
+ # If completed, add the model URLs
617
+ if result.get("status") == "SUCCEEDED" and "model_urls" in result:
618
+ config["model_urls"] = result["model_urls"]
619
+ config["thumbnail_url"] = result.get("thumbnail_url")
620
+
621
+ supabase.from_("Generated_Models").update({
622
+ "prompts_and_models_config": config
623
+ }).eq("meshy_api_job_id", task_id).eq("user_id", current_user.id).execute()
624
+
625
+ # Return updated model data
626
+ updated_model = supabase.from_("Generated_Models").select("*").eq("meshy_api_job_id", task_id).eq("user_id", current_user.id).single().execute()
627
+ return updated_model.data
628
+
629
+ except Exception as e:
630
+ logging.error(f"Failed to refresh model status: {str(e)}")
631
+ raise HTTPException(status_code=500, detail=f"Failed to refresh model status: {str(e)}")
632
+
633
+ @app.post("/user/models/{generated_model_id}/refresh", tags=["User"])
634
+ async def refresh_model_status_by_id(
635
+ generated_model_id: str,
636
+ current_user: User = Depends(get_current_active_user),
637
+ settings: Settings = Depends(get_settings)
638
+ ):
639
+ """
640
+ Manually refresh the status of a model by checking with Meshy API using the generated model ID.
641
+ """
642
+ # First check if the model exists and belongs to the user
643
+ try:
644
+ model = supabase.from_("Generated_Models").select("*").eq("generated_model_id", generated_model_id).eq("user_id", current_user.id).single().execute()
645
+ if not model.data:
646
+ raise HTTPException(status_code=404, detail="Model not found")
647
+ except Exception:
648
+ raise HTTPException(status_code=404, detail="Model not found")
649
+
650
+ task_id = model.data.get("meshy_api_job_id")
651
+ if not task_id:
652
+ raise HTTPException(status_code=400, detail="No Meshy API task ID found for this model")
653
+
654
+ config = model.data.get("prompts_and_models_config", {})
655
+ generation_type = config.get("generation_type")
656
+
657
+ try:
658
+ # Call the appropriate API based on generation type
659
+ if generation_type == "text_to_3d":
660
+ resp = await app.state.client.get(
661
+ f"https://api.meshy.ai/openapi/v2/text-to-3d/{task_id}",
662
+ headers={"Authorization": f"Bearer {settings.mesh_api_key}"}
663
+ )
664
+ elif generation_type == "image_to_3d":
665
+ resp = await app.state.client.get(
666
+ f"https://api.meshy.ai/openapi/v1/image-to-3d/{task_id}",
667
+ headers={"Authorization": f"Bearer {settings.mesh_api_key}"}
668
+ )
669
+ else:
670
+ raise HTTPException(status_code=400, detail="Unknown generation type")
671
+
672
+ if resp.status_code != 200:
673
+ raise HTTPException(status_code=resp.status_code, detail=resp.text)
674
+
675
+ result = resp.json()
676
+
677
+ # Update database with current status
678
+ config.update({
679
+ "status": result.get("status", "unknown"),
680
+ "meshy_response": result
681
+ })
682
+
683
+ # If completed, add the model URLs
684
+ if result.get("status") == "SUCCEEDED" and "model_urls" in result:
685
+ config["model_urls"] = result["model_urls"]
686
+ config["thumbnail_url"] = result.get("thumbnail_url")
687
+
688
+ supabase.from_("Generated_Models").update({
689
+ "prompts_and_models_config": config
690
+ }).eq("generated_model_id", generated_model_id).eq("user_id", current_user.id).execute()
691
+
692
+ # Return updated model data
693
+ updated_model = supabase.from_("Generated_Models").select("*").eq("generated_model_id", generated_model_id).eq("user_id", current_user.id).single().execute()
694
+ return updated_model.data
695
+
696
+ except Exception as e:
697
+ logging.error(f"Failed to refresh model status: {str(e)}")
698
+ raise HTTPException(status_code=500, detail=f"Failed to refresh model status: {str(e)}")
699
+
700
+ # Keeping the old endpoint for backward compatibility
701
+ @app.post("/user/models/refresh/{generated_model_id}", tags=["User"])
702
+ async def refresh_model_status_by_id_legacy(
703
+ generated_model_id: str,
704
+ current_user: User = Depends(get_current_active_user),
705
+ settings: Settings = Depends(get_settings)
706
+ ):
707
+ """
708
+ Legacy endpoint - use /user/models/{generated_model_id}/refresh instead.
709
+ """
710
+ return await refresh_model_status_by_id(generated_model_id, current_user, settings)
711
+
712
  @app.post("/orders/place", tags=["Orders"])
713
  async def place_order(request: PlaceOrderRequest, current_user: User = Depends(get_current_active_user)):
714
  """
 
735
  """Basic health-check endpoint returning service status."""
736
  return {"status": "ok"}
737
 
738
+ @app.get("/proxy/model", tags=["Proxy"])
739
+ async def proxy_model(url: str):
740
+ """
741
+ Simple CORS-friendly proxy for GLB (or any binary) files.
742
+ The front-end should call this endpoint with the target URL percent-encoded,
743
+ e.g. /proxy/model?url=https%3A%2F%2Fassets.meshy.ai%2F....glb
744
+ """
745
+ try:
746
+ # Decode once in case the URL is already percent-encoded
747
+ target_url = unquote(url)
748
+ upstream_resp = await app.state.client.get(target_url)
749
+ if upstream_resp.status_code != 200:
750
+ raise HTTPException(status_code=upstream_resp.status_code,
751
+ detail=f"Failed to fetch remote model: {upstream_resp.text}")
752
+
753
+ content_type = upstream_resp.headers.get("Content-Type", "model/gltf-binary")
754
+ headers = {"Access-Control-Allow-Origin": "*"}
755
+
756
+ # Stream the content back to the client
757
+ return StreamingResponse(BytesIO(upstream_resp.content), media_type=content_type, headers=headers)
758
+ except Exception as e:
759
+ raise HTTPException(status_code=500, detail=str(e))
760
+
761
  if __name__ == "__main__":
762
  import uvicorn
763
  uvicorn.run("main:app", host="0.0.0.0", port=8000, log_level="info")