mlbench123 commited on
Commit
8492eab
·
verified ·
1 Parent(s): 50b2026

Update api_server.py

Browse files
Files changed (1) hide show
  1. api_server.py +256 -336
api_server.py CHANGED
@@ -1,228 +1,3 @@
1
- # from fastapi import FastAPI, HTTPException, UploadFile, File, Form
2
- # from pydantic import BaseModel
3
- # import numpy as np
4
- # from PIL import Image
5
- # import io, uuid, os, shutil, timeit
6
- # from datetime import datetime
7
- # from fastapi.staticfiles import StaticFiles
8
- # from fastapi.middleware.cors import CORSMiddleware
9
-
10
- # # import your three wrappers
11
- # from app import predict_simple, predict_middle, predict_full
12
-
13
- # app = FastAPI()
14
-
15
- # # allow CORS if needed
16
- # app.add_middleware(
17
- # CORSMiddleware,
18
- # allow_origins=["*"],
19
- # allow_methods=["*"],
20
- # allow_headers=["*"],
21
- # )
22
-
23
- # BASE_URL = "https://snapanddtraceapp-988917236820.us-central1.run.app"
24
- # OUTPUT_DIR = os.path.abspath("./outputs")
25
- # os.makedirs(OUTPUT_DIR, exist_ok=True)
26
- # app.mount("/outputs", StaticFiles(directory=OUTPUT_DIR), name="outputs")
27
-
28
- # UPDATES_DIR = os.path.abspath("./updates")
29
- # os.makedirs(UPDATES_DIR, exist_ok=True)
30
- # app.mount("/updates", StaticFiles(directory=UPDATES_DIR), name="updates")
31
-
32
-
33
- # def save_and_build_urls(
34
- # session_id: str,
35
- # output_image: np.ndarray,
36
- # outlines: np.ndarray,
37
- # dxf_path: str,
38
- # mask: np.ndarray
39
- # ):
40
- # """Helper to save all four artifacts and return public URLs."""
41
- # request_dir = os.path.join(OUTPUT_DIR, session_id)
42
- # os.makedirs(request_dir, exist_ok=True)
43
-
44
- # # filenames
45
- # out_fn = "overlay.jpg"
46
- # outlines_fn = "outlines.jpg"
47
- # mask_fn = "mask.jpg"
48
- # current_date = datetime.now().strftime("%d-%m-%Y")
49
- # dxf_fn = f"out_{current_date}_{session_id}.dxf"
50
-
51
- # # full paths
52
- # out_path = os.path.join(request_dir, out_fn)
53
- # outlines_path = os.path.join(request_dir, outlines_fn)
54
- # mask_path = os.path.join(request_dir, mask_fn)
55
- # new_dxf_path = os.path.join(request_dir, dxf_fn)
56
-
57
- # # save images
58
- # Image.fromarray(output_image).save(out_path)
59
- # Image.fromarray(outlines).save(outlines_path)
60
- # Image.fromarray(mask).save(mask_path)
61
-
62
- # # copy dx file
63
- # if os.path.exists(dxf_path):
64
- # shutil.copy(dxf_path, new_dxf_path)
65
- # else:
66
- # # fallback if your DXF generator returns bytes or string
67
- # with open(new_dxf_path, "wb") as f:
68
- # if isinstance(dxf_path, (bytes, bytearray)):
69
- # f.write(dxf_path)
70
- # else:
71
- # f.write(str(dxf_path).encode("utf-8"))
72
-
73
- # # build URLs
74
- # return {
75
- # "output_image_url": f"{BASE_URL}/outputs/{session_id}/{out_fn}",
76
- # "outlines_url": f"{BASE_URL}/outputs/{session_id}/{outlines_fn}",
77
- # "mask_url": f"{BASE_URL}/outputs/{session_id}/{mask_fn}",
78
- # "dxf_url": f"{BASE_URL}/outputs/{session_id}/{dxf_fn}",
79
- # }
80
-
81
-
82
- # @app.post("/predict1")
83
- # async def predict1_api(
84
- # file: UploadFile = File(...)
85
- # ):
86
- # """
87
- # Simple predict: only image → overlay, outlines, mask, DXF
88
- # """
89
- # session_id = str(uuid.uuid4())
90
- # try:
91
- # img_bytes = await file.read()
92
- # image = np.array(Image.open(io.BytesIO(img_bytes)).convert("RGB"))
93
- # except Exception:
94
- # raise HTTPException(400, "Invalid image upload")
95
-
96
- # try:
97
- # start = timeit.default_timer()
98
- # out_img, outlines, dxf_path, mask = predict_simple(image)
99
- # elapsed = timeit.default_timer() - start
100
- # print(f"[{session_id}] predict1 in {elapsed:.2f}s")
101
-
102
- # return save_and_build_urls(session_id, out_img, outlines, dxf_path, mask)
103
-
104
- # except Exception as e:
105
- # raise HTTPException(500, f"predict1 failed: {e}")
106
- # except ReferenceBoxNotDetectedError:
107
- # raise HTTPException(status_code=400, detail="Error detecting reference battery! Please try again with a clearer image.")
108
- # except FingerCutOverlapError:
109
- # raise HTTPException(status_code=400, detail="There was an overlap with fingercuts!s Please try again to generate dxf.")
110
-
111
-
112
- # @app.post("/predict2")
113
- # async def predict2_api(
114
- # file: UploadFile = File(...),
115
- # enable_fillet: str = Form(..., regex="^(On|Off)$"),
116
- # fillet_value_mm: float = Form(...)
117
- # ):
118
- # """
119
- # Middle predict: image + fillet toggle + fillet value → overlay, outlines, mask, DXF
120
- # """
121
- # session_id = str(uuid.uuid4())
122
- # try:
123
- # img_bytes = await file.read()
124
- # image = np.array(Image.open(io.BytesIO(img_bytes)).convert("RGB"))
125
- # except Exception:
126
- # raise HTTPException(400, "Invalid image upload")
127
-
128
- # try:
129
- # start = timeit.default_timer()
130
- # out_img, outlines, dxf_path, mask = predict_middle(
131
- # image, enable_fillet, fillet_value_mm
132
- # )
133
- # elapsed = timeit.default_timer() - start
134
- # print(f"[{session_id}] predict2 in {elapsed:.2f}s")
135
-
136
- # return save_and_build_urls(session_id, out_img, outlines, dxf_path, mask)
137
-
138
- # except Exception as e:
139
- # raise HTTPException(500, f"predict2 failed: {e}")
140
- # except ReferenceBoxNotDetectedError:
141
- # raise HTTPException(status_code=400, detail="Error detecting reference battery! Please try again with a clearer image.")
142
- # except FingerCutOverlapError:
143
- # raise HTTPException(status_code=400, detail="There was an overlap with fingercuts!s Please try again to generate dxf.")
144
-
145
- # @app.post("/predict3")
146
- # async def predict3_api(
147
- # file: UploadFile = File(...),
148
- # enable_fillet: str = Form(..., regex="^(On|Off)$"),
149
- # fillet_value_mm: float = Form(...),
150
- # enable_finger_cut: str = Form(..., regex="^(On|Off)$")
151
- # ):
152
- # """
153
- # Full predict: image + fillet toggle/value + finger-cut toggle → overlay, outlines, mask, DXF
154
- # """
155
- # session_id = str(uuid.uuid4())
156
- # try:
157
- # img_bytes = await file.read()
158
- # image = np.array(Image.open(io.BytesIO(img_bytes)).convert("RGB"))
159
- # except Exception:
160
- # raise HTTPException(400, "Invalid image upload")
161
-
162
- # try:
163
- # start = timeit.default_timer()
164
- # out_img, outlines, dxf_path, mask = predict_full(
165
- # image, enable_fillet, fillet_value_mm, enable_finger_cut
166
- # )
167
- # elapsed = timeit.default_timer() - start
168
- # print(f"[{session_id}] predict3 in {elapsed:.2f}s")
169
-
170
- # return save_and_build_urls(session_id, out_img, outlines, dxf_path, mask)
171
-
172
- # except Exception as e:
173
- # raise HTTPException(500, f"predict3 failed: {e}")
174
- # except ReferenceBoxNotDetectedError:
175
- # raise HTTPException(status_code=400, detail="Error detecting reference battery! Please try again with a clearer image.")
176
- # except FingerCutOverlapError:
177
- # raise HTTPException(status_code=400, detail="There was an overlap with fingercuts!s Please try again to generate dxf.")
178
-
179
- # @app.post("/update")
180
- # async def update_files(
181
- # output_image: UploadFile = File(...),
182
- # outlines_image: UploadFile = File(...),
183
- # mask_image: UploadFile = File(...),
184
- # dxf_file: UploadFile = File(...)
185
- # ):
186
- # session_id = str(uuid.uuid4())
187
- # update_dir = os.path.join(UPDATES_DIR, session_id)
188
- # os.makedirs(update_dir, exist_ok=True)
189
-
190
- # try:
191
- # upload_map = {
192
- # "output_image": output_image,
193
- # "outlines_image": outlines_image,
194
- # "mask_image": mask_image,
195
- # "dxf_file": dxf_file,
196
- # }
197
- # urls = {}
198
- # for key, up in upload_map.items():
199
- # fn = up.filename
200
- # path = os.path.join(update_dir, fn)
201
- # with open(path, "wb") as f:
202
- # shutil.copyfileobj(up.file, f)
203
- # urls[key] = f"{BASE_URL}/updates/{session_id}/{fn}"
204
-
205
- # return {"session_id": session_id, "uploaded": urls}
206
-
207
- # except Exception as e:
208
- # raise HTTPException(500, f"Update failed: {e}")
209
-
210
-
211
- # if __name__ == "__main__":
212
- # import uvicorn
213
- # port = int(os.environ.get("PORT", 8082))
214
- # print(f"Starting FastAPI server on 0.0.0.0:{port}...")
215
- # uvicorn.run(app, host="0.0.0.0", port=port)
216
-
217
-
218
-
219
-
220
-
221
-
222
-
223
-
224
-
225
-
226
  from fastapi import FastAPI, HTTPException, UploadFile, File, Form
227
  from pydantic import BaseModel
228
  import numpy as np
@@ -233,19 +8,19 @@ from fastapi.staticfiles import StaticFiles
233
  from fastapi.middleware.cors import CORSMiddleware
234
  from fastapi.responses import FileResponse
235
 
236
- # import your three wrappers
237
- from app import predict_simple, predict_middle, predict_full
238
-
239
  from app import (
240
- predict_simple, predict_middle, predict_full,
241
  ReferenceBoxNotDetectedError,
242
- FingerCutOverlapError
 
 
 
243
  )
244
 
245
-
246
  app = FastAPI()
247
 
248
- # allow CORS if needed
249
  app.add_middleware(
250
  CORSMiddleware,
251
  allow_origins=["*"],
@@ -268,68 +43,75 @@ app.mount("/updates", StaticFiles(directory=UPDATES_DIR), name="updates")
268
 
269
  def save_and_build_urls(
270
  session_id: str,
271
- output_image: np.ndarray,
272
- outlines: np.ndarray,
273
  dxf_path: str,
274
- mask: np.ndarray,
275
- endpoint_type: str,
276
- fillet_value: float = None,
277
- finger_cut: str = None
 
 
 
 
278
  ):
279
- """Helper to save all four artifacts and return public URLs."""
280
  request_dir = os.path.join(OUTPUT_DIR, session_id)
281
  os.makedirs(request_dir, exist_ok=True)
282
 
283
- # filenames
284
- out_fn = "overlay.jpg"
285
- outlines_fn = "outlines.jpg"
286
- mask_fn = "mask.jpg"
287
-
288
  # Get current date
289
  current_date = datetime.utcnow().strftime("%d-%m-%Y")
290
-
291
-
292
- # Format fillet value with underscore instead of dot
293
- fillet_str = f"{fillet_value:.2f}".replace(".", "_") if fillet_value is not None else None
294
 
295
- # Determine DXF filename based on endpoint type
296
- if endpoint_type == "predict1":
297
- dxf_fn = f"DXF_{current_date}.dxf"
298
- elif endpoint_type == "predict2":
299
- dxf_fn = f"DXF_{current_date}.dxf"
300
- elif endpoint_type == "predict3":
 
 
 
 
301
  dxf_fn = f"DXF_{current_date}.dxf"
302
 
303
- # full paths
304
- out_path = os.path.join(request_dir, out_fn)
305
- outlines_path = os.path.join(request_dir, outlines_fn)
306
- mask_path = os.path.join(request_dir, mask_fn)
307
  new_dxf_path = os.path.join(request_dir, dxf_fn)
308
 
309
- # save images
310
- Image.fromarray(output_image).save(out_path)
311
- Image.fromarray(outlines).save(outlines_path)
312
- Image.fromarray(mask).save(mask_path)
313
-
314
- # copy dxf file
315
  if os.path.exists(dxf_path):
316
  shutil.copy(dxf_path, new_dxf_path)
317
  else:
318
- # fallback if your DXF generator returns bytes or string
319
  with open(new_dxf_path, "wb") as f:
320
  if isinstance(dxf_path, (bytes, bytearray)):
321
  f.write(dxf_path)
322
  else:
323
  f.write(str(dxf_path).encode("utf-8"))
324
 
325
- # build URLs with /download prefix for DXF
326
- return {
327
- "output_image_url": f"{BASE_URL}/outputs/{session_id}/{out_fn}",
328
- "outlines_url": f"{BASE_URL}/outputs/{session_id}/{outlines_fn}",
329
- "mask_url": f"{BASE_URL}/outputs/{session_id}/{mask_fn}",
330
- "dxf_url": f"{BASE_URL}/download/{session_id}/{dxf_fn}", # Changed to use download endpoint
331
  }
332
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  # Add new endpoint for downloading DXF files
334
  @app.get("/download/{session_id}/{filename}")
335
  async def download_file(session_id: str, filename: str):
@@ -345,13 +127,14 @@ async def download_file(session_id: str, filename: str):
345
  )
346
 
347
 
348
- @app.post("/predict1")
349
- async def predict1_api(
350
- file: UploadFile = File(...)
 
351
  ):
352
  """
353
- Simple predict: only image overlay, outlines, mask, DXF
354
- DXF naming format: DXF_DD-MM-YYYY.dxf
355
  """
356
  session_id = str(uuid.uuid4())
357
  try:
@@ -362,37 +145,57 @@ async def predict1_api(
362
 
363
  try:
364
  start = timeit.default_timer()
365
- out_img, outlines, dxf_path, mask = predict_simple(image)
 
 
 
 
 
 
 
 
 
 
366
  elapsed = timeit.default_timer() - start
367
- print(f"[{session_id}] predict1 in {elapsed:.2f}s")
368
 
369
- return save_and_build_urls(
370
  session_id=session_id,
371
- output_image=out_img,
372
- outlines=outlines,
373
  dxf_path=dxf_path,
374
- mask=mask,
375
- endpoint_type="predict1"
 
 
 
376
  )
377
-
378
- except ReferenceBoxNotDetectedError:
379
- raise HTTPException(status_code=400, detail="Error detecting reference battery! Please try again with a clearer image.")
 
 
 
 
 
 
 
 
380
  except FingerCutOverlapError:
381
  raise HTTPException(status_code=400, detail="There was an overlap with fingercuts! Please try again to generate dxf.")
382
- except HTTPException as e:
383
- raise e
384
  except Exception as e:
385
- raise HTTPException(status_code=500, detail="Error detecting reference battery! Please try again with a clearer image.")
 
386
 
387
- @app.post("/predict2")
388
- async def predict2_api(
 
389
  file: UploadFile = File(...),
390
- enable_fillet: str = Form(..., regex="^(On|Off)$"),
391
- fillet_value_mm: float = Form(...)
 
 
392
  ):
393
  """
394
- Middle predict: image + fillet toggle + fillet value overlay, outlines, mask, DXF
395
- DXF naming format: DXF_DD-MM-YYYY_fillet-value_mm.dxf
396
  """
397
  session_id = str(uuid.uuid4())
398
  try:
@@ -401,44 +204,70 @@ async def predict2_api(
401
  except Exception:
402
  raise HTTPException(400, "Invalid image upload")
403
 
 
 
 
 
 
 
404
  try:
405
  start = timeit.default_timer()
406
- out_img, outlines, dxf_path, mask = predict_middle(
407
- image, enable_fillet, fillet_value_mm
 
 
 
 
 
 
 
 
 
408
  )
 
409
  elapsed = timeit.default_timer() - start
410
- print(f"[{session_id}] predict2 in {elapsed:.2f}s")
411
 
412
- return save_and_build_urls(
413
  session_id=session_id,
414
- output_image=out_img,
415
- outlines=outlines,
416
  dxf_path=dxf_path,
417
- mask=mask,
418
- endpoint_type="predict2",
419
- fillet_value=fillet_value_mm
 
 
 
 
 
420
  )
421
-
422
- except ReferenceBoxNotDetectedError:
423
- raise HTTPException(status_code=400, detail="Error detecting reference battery! Please try again with a clearer image.")
 
 
 
 
 
 
 
424
  except FingerCutOverlapError:
425
  raise HTTPException(status_code=400, detail="There was an overlap with fingercuts! Please try again to generate dxf.")
426
- except HTTPException as e:
427
- raise e
428
  except Exception as e:
429
- raise HTTPException(status_code=500, detail="Error detecting reference battery! Please try again with a clearer image.")
 
430
 
431
 
432
- @app.post("/predict3")
433
- async def predict3_api(
434
  file: UploadFile = File(...),
435
- enable_fillet: str = Form(..., regex="^(On|Off)$"),
436
- fillet_value_mm: float = Form(...),
437
- enable_finger_cut: str = Form(..., regex="^(On|Off)$")
 
 
438
  ):
439
  """
440
- Full predict: image + fillet toggle/value + finger-cut toggleoverlay, outlines, mask, DXF
441
- DXF naming format: DXF_DD-MM-YYYY_fillet-value_mm_fingercut-On|Off.dxf
442
  """
443
  session_id = str(uuid.uuid4())
444
  try:
@@ -447,33 +276,112 @@ async def predict3_api(
447
  except Exception:
448
  raise HTTPException(400, "Invalid image upload")
449
 
 
 
 
 
 
 
450
  try:
451
  start = timeit.default_timer()
452
- out_img, outlines, dxf_path, mask = predict_full(
453
- image, enable_fillet, fillet_value_mm, enable_finger_cut
 
 
 
 
 
 
 
 
 
454
  )
 
455
  elapsed = timeit.default_timer() - start
456
- print(f"[{session_id}] predict3 in {elapsed:.2f}s")
457
 
458
- return save_and_build_urls(
459
  session_id=session_id,
460
- output_image=out_img,
461
- outlines=outlines,
462
  dxf_path=dxf_path,
463
- mask=mask,
464
- endpoint_type="predict3",
465
- fillet_value=fillet_value_mm,
 
 
 
 
466
  finger_cut=enable_finger_cut
467
  )
468
-
469
- except ReferenceBoxNotDetectedError:
470
- raise HTTPException(status_code=400, detail="Error detecting reference battery! Please try again with a clearer image.")
 
 
 
 
 
 
 
471
  except FingerCutOverlapError:
472
  raise HTTPException(status_code=400, detail="There was an overlap with fingercuts! Please try again to generate dxf.")
473
- except HTTPException as e:
474
- raise e
475
  except Exception as e:
476
- raise HTTPException(status_code=500, detail="Error detecting reference battery! Please try again with a clearer image.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
 
478
 
479
  @app.post("/update")
@@ -515,11 +423,23 @@ def health():
515
  return Response(content="OK", status_code=200)
516
 
517
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
  if __name__ == "__main__":
519
  import uvicorn
520
  port = int(os.environ.get("PORT", 8080))
521
  print(f"Starting FastAPI server on 0.0.0.0:{port}...")
522
- uvicorn.run(app, host="0.0.0.0", port=port)
523
-
524
-
525
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from fastapi import FastAPI, HTTPException, UploadFile, File, Form
2
  from pydantic import BaseModel
3
  import numpy as np
 
8
  from fastapi.middleware.cors import CORSMiddleware
9
  from fastapi.responses import FileResponse
10
 
11
+ # Import your paper-based prediction function
 
 
12
  from app import (
13
+ predict_full_paper,
14
  ReferenceBoxNotDetectedError,
15
+ FingerCutOverlapError,
16
+ MultipleObjectsError,
17
+ NoObjectDetectedError,
18
+ PaperNotDetectedError
19
  )
20
 
 
21
  app = FastAPI()
22
 
23
+ # Allow CORS if needed
24
  app.add_middleware(
25
  CORSMiddleware,
26
  allow_origins=["*"],
 
43
 
44
  def save_and_build_urls(
45
  session_id: str,
 
 
46
  dxf_path: str,
47
+ output_image: np.ndarray = None,
48
+ outlines: np.ndarray = None,
49
+ mask: np.ndarray = None,
50
+ endpoint_type: str = "predict",
51
+ paper_size: str = None,
52
+ offset_value: float = None,
53
+ offset_unit: str = "mm",
54
+ finger_cut: str = "Off"
55
  ):
56
+ """Helper to save all artifacts and return public URLs."""
57
  request_dir = os.path.join(OUTPUT_DIR, session_id)
58
  os.makedirs(request_dir, exist_ok=True)
59
 
 
 
 
 
 
60
  # Get current date
61
  current_date = datetime.utcnow().strftime("%d-%m-%Y")
 
 
 
 
62
 
63
+ # Format offset value with underscore instead of dot
64
+ offset_str = f"{offset_value:.3f}".replace(".", "_") if offset_value is not None else "0_000"
65
+
66
+ # Create descriptive DXF filename
67
+ if paper_size and offset_value is not None:
68
+ dxf_fn = f"DXF_{current_date}_{paper_size}_{offset_str}{offset_unit}"
69
+ if finger_cut == "On":
70
+ dxf_fn += "_fingercut"
71
+ dxf_fn += ".dxf"
72
+ else:
73
  dxf_fn = f"DXF_{current_date}.dxf"
74
 
75
+ # Full path for DXF
 
 
 
76
  new_dxf_path = os.path.join(request_dir, dxf_fn)
77
 
78
+ # Copy DXF file
 
 
 
 
 
79
  if os.path.exists(dxf_path):
80
  shutil.copy(dxf_path, new_dxf_path)
81
  else:
82
+ # Fallback if your DXF generator returns bytes or string
83
  with open(new_dxf_path, "wb") as f:
84
  if isinstance(dxf_path, (bytes, bytearray)):
85
  f.write(dxf_path)
86
  else:
87
  f.write(str(dxf_path).encode("utf-8"))
88
 
89
+ urls = {
90
+ "dxf_url": f"{BASE_URL}/download/{session_id}/{dxf_fn}",
 
 
 
 
91
  }
92
 
93
+ # Save optional images if provided
94
+ if output_image is not None:
95
+ out_fn = "annotated_image.jpg"
96
+ out_path = os.path.join(request_dir, out_fn)
97
+ Image.fromarray(output_image).save(out_path)
98
+ urls["output_image_url"] = f"{BASE_URL}/outputs/{session_id}/{out_fn}"
99
+
100
+ if outlines is not None:
101
+ outlines_fn = "outlines.jpg"
102
+ outlines_path = os.path.join(request_dir, outlines_fn)
103
+ Image.fromarray(outlines).save(outlines_path)
104
+ urls["outlines_url"] = f"{BASE_URL}/outputs/{session_id}/{outlines_fn}"
105
+
106
+ if mask is not None:
107
+ mask_fn = "mask.jpg"
108
+ mask_path = os.path.join(request_dir, mask_fn)
109
+ Image.fromarray(mask).save(mask_path)
110
+ urls["mask_url"] = f"{BASE_URL}/outputs/{session_id}/{mask_fn}"
111
+
112
+ return urls
113
+
114
+
115
  # Add new endpoint for downloading DXF files
116
  @app.get("/download/{session_id}/{filename}")
117
  async def download_file(session_id: str, filename: str):
 
127
  )
128
 
129
 
130
+ @app.post("/predict_paper_simple")
131
+ async def predict_paper_simple_api(
132
+ file: UploadFile = File(...),
133
+ paper_size: str = Form(..., regex="^(A4|A3|US Letter)$"),
134
  ):
135
  """
136
+ Simple paper-based predict: image + paper size DXF only
137
+ Default: 0mm offset, no finger cuts
138
  """
139
  session_id = str(uuid.uuid4())
140
  try:
 
145
 
146
  try:
147
  start = timeit.default_timer()
148
+
149
+ # Call predict_full_paper with default values
150
+ dxf_path, ann_img, outlines_img, mask_img, scale_info = predict_full_paper(
151
+ image=image,
152
+ paper_size=paper_size,
153
+ offset_value_mm=0.0, # No offset
154
+ offset_unit="mm",
155
+ enable_finger_cut="Off", # No finger cuts
156
+ selected_outputs=[] # DXF only
157
+ )
158
+
159
  elapsed = timeit.default_timer() - start
160
+ print(f"[{session_id}] predict_paper_simple in {elapsed:.2f}s - {scale_info}")
161
 
162
+ urls = save_and_build_urls(
163
  session_id=session_id,
 
 
164
  dxf_path=dxf_path,
165
+ endpoint_type="predict_paper_simple",
166
+ paper_size=paper_size,
167
+ offset_value=0.0,
168
+ offset_unit="mm",
169
+ finger_cut="Off"
170
  )
171
+
172
+ # Add scaling info to response
173
+ urls["scale_info"] = scale_info
174
+ return urls
175
+
176
+ except (ReferenceBoxNotDetectedError, PaperNotDetectedError):
177
+ raise HTTPException(status_code=400, detail="Error detecting paper! Please ensure the paper is clearly visible and try again.")
178
+ except (MultipleObjectsError):
179
+ raise HTTPException(status_code=400, detail="Multiple objects detected! Please place only a single object on the paper.")
180
+ except (NoObjectDetectedError):
181
+ raise HTTPException(status_code=400, detail="No object detected! Please ensure an object is placed on the paper.")
182
  except FingerCutOverlapError:
183
  raise HTTPException(status_code=400, detail="There was an overlap with fingercuts! Please try again to generate dxf.")
 
 
184
  except Exception as e:
185
+ print(f"Error in predict_paper_simple: {str(e)}")
186
+ raise HTTPException(status_code=500, detail="Error processing image! Please try again with a clearer image.")
187
 
188
+
189
+ @app.post("/predict_paper_with_offset")
190
+ async def predict_paper_with_offset_api(
191
  file: UploadFile = File(...),
192
+ paper_size: str = Form(..., regex="^(A4|A3|US Letter)$"),
193
+ offset_value: float = Form(...),
194
+ offset_unit: str = Form(..., regex="^(mm|inches)$"),
195
+ include_images: bool = Form(False) # Optional: include preview images
196
  ):
197
  """
198
+ Paper-based predict with offset: image + paper size + offsetDXF + optional images
 
199
  """
200
  session_id = str(uuid.uuid4())
201
  try:
 
204
  except Exception:
205
  raise HTTPException(400, "Invalid image upload")
206
 
207
+ # Validate offset
208
+ if offset_value < 0:
209
+ raise HTTPException(400, "Offset value cannot be negative")
210
+ if offset_value > 50: # Reasonable upper limit
211
+ raise HTTPException(400, "Offset value too large (max 50)")
212
+
213
  try:
214
  start = timeit.default_timer()
215
+
216
+ # Determine which outputs to include
217
+ selected_outputs = ["Annotated Image", "Outlines", "Mask"] if include_images else []
218
+
219
+ dxf_path, ann_img, outlines_img, mask_img, scale_info = predict_full_paper(
220
+ image=image,
221
+ paper_size=paper_size,
222
+ offset_value_mm=offset_value,
223
+ offset_unit=offset_unit,
224
+ enable_finger_cut="Off", # No finger cuts
225
+ selected_outputs=selected_outputs
226
  )
227
+
228
  elapsed = timeit.default_timer() - start
229
+ print(f"[{session_id}] predict_paper_with_offset in {elapsed:.2f}s - {scale_info}")
230
 
231
+ urls = save_and_build_urls(
232
  session_id=session_id,
 
 
233
  dxf_path=dxf_path,
234
+ output_image=ann_img if include_images else None,
235
+ outlines=outlines_img if include_images else None,
236
+ mask=mask_img if include_images else None,
237
+ endpoint_type="predict_paper_with_offset",
238
+ paper_size=paper_size,
239
+ offset_value=offset_value,
240
+ offset_unit=offset_unit,
241
+ finger_cut="Off"
242
  )
243
+
244
+ urls["scale_info"] = scale_info
245
+ return urls
246
+
247
+ except (ReferenceBoxNotDetectedError, PaperNotDetectedError):
248
+ raise HTTPException(status_code=400, detail="Error detecting paper! Please ensure the paper is clearly visible and try again.")
249
+ except (MultipleObjectsError):
250
+ raise HTTPException(status_code=400, detail="Multiple objects detected! Please place only a single object on the paper.")
251
+ except (NoObjectDetectedError):
252
+ raise HTTPException(status_code=400, detail="No object detected! Please ensure an object is placed on the paper.")
253
  except FingerCutOverlapError:
254
  raise HTTPException(status_code=400, detail="There was an overlap with fingercuts! Please try again to generate dxf.")
 
 
255
  except Exception as e:
256
+ print(f"Error in predict_paper_with_offset: {str(e)}")
257
+ raise HTTPException(status_code=500, detail="Error processing image! Please try again with a clearer image.")
258
 
259
 
260
+ @app.post("/predict_paper_full")
261
+ async def predict_paper_full_api(
262
  file: UploadFile = File(...),
263
+ paper_size: str = Form(..., regex="^(A4|A3|US Letter)$"),
264
+ offset_value: float = Form(...),
265
+ offset_unit: str = Form(..., regex="^(mm|inches)$"),
266
+ enable_finger_cut: str = Form(..., regex="^(On|Off)$"),
267
+ include_images: bool = Form(False) # Optional: include preview images
268
  ):
269
  """
270
+ Full paper-based predict: image + paper size + offset + finger cutsDXF + optional images
 
271
  """
272
  session_id = str(uuid.uuid4())
273
  try:
 
276
  except Exception:
277
  raise HTTPException(400, "Invalid image upload")
278
 
279
+ # Validate offset
280
+ if offset_value < 0:
281
+ raise HTTPException(400, "Offset value cannot be negative")
282
+ if offset_value > 50:
283
+ raise HTTPException(400, "Offset value too large (max 50)")
284
+
285
  try:
286
  start = timeit.default_timer()
287
+
288
+ # Determine which outputs to include
289
+ selected_outputs = ["Annotated Image", "Outlines", "Mask"] if include_images else []
290
+
291
+ dxf_path, ann_img, outlines_img, mask_img, scale_info = predict_full_paper(
292
+ image=image,
293
+ paper_size=paper_size,
294
+ offset_value_mm=offset_value,
295
+ offset_unit=offset_unit,
296
+ enable_finger_cut=enable_finger_cut,
297
+ selected_outputs=selected_outputs
298
  )
299
+
300
  elapsed = timeit.default_timer() - start
301
+ print(f"[{session_id}] predict_paper_full in {elapsed:.2f}s - {scale_info}")
302
 
303
+ urls = save_and_build_urls(
304
  session_id=session_id,
 
 
305
  dxf_path=dxf_path,
306
+ output_image=ann_img if include_images else None,
307
+ outlines=outlines_img if include_images else None,
308
+ mask=mask_img if include_images else None,
309
+ endpoint_type="predict_paper_full",
310
+ paper_size=paper_size,
311
+ offset_value=offset_value,
312
+ offset_unit=offset_unit,
313
  finger_cut=enable_finger_cut
314
  )
315
+
316
+ urls["scale_info"] = scale_info
317
+ return urls
318
+
319
+ except (ReferenceBoxNotDetectedError, PaperNotDetectedError):
320
+ raise HTTPException(status_code=400, detail="Error detecting paper! Please ensure the paper is clearly visible and try again.")
321
+ except (MultipleObjectsError):
322
+ raise HTTPException(status_code=400, detail="Multiple objects detected! Please place only a single object on the paper.")
323
+ except (NoObjectDetectedError):
324
+ raise HTTPException(status_code=400, detail="No object detected! Please ensure an object is placed on the paper.")
325
  except FingerCutOverlapError:
326
  raise HTTPException(status_code=400, detail="There was an overlap with fingercuts! Please try again to generate dxf.")
 
 
327
  except Exception as e:
328
+ print(f"Error in predict_paper_full: {str(e)}")
329
+ raise HTTPException(status_code=500, detail="Error processing image! Please try again with a clearer image.")
330
+
331
+
332
+ # Keep the legacy endpoints for backward compatibility (optional)
333
+ @app.post("/predict1")
334
+ async def predict1_api(
335
+ file: UploadFile = File(...)
336
+ ):
337
+ """
338
+ Legacy endpoint - redirects to simple paper-based prediction with A4 default
339
+ """
340
+ return await predict_paper_simple_api(file=file, paper_size="A4")
341
+
342
+
343
+ @app.post("/predict2")
344
+ async def predict2_api(
345
+ file: UploadFile = File(...),
346
+ enable_fillet: str = Form(..., regex="^(On|Off)$"),
347
+ fillet_value_mm: float = Form(...)
348
+ ):
349
+ """
350
+ Legacy endpoint - redirects to paper-based prediction with offset
351
+ Note: Fillet functionality mapped to offset for compatibility
352
+ """
353
+ # Map fillet to offset (you might want to adjust this logic)
354
+ offset_value = fillet_value_mm if enable_fillet == "On" else 0.0
355
+
356
+ return await predict_paper_with_offset_api(
357
+ file=file,
358
+ paper_size="A4", # Default to A4
359
+ offset_value=offset_value,
360
+ offset_unit="mm",
361
+ include_images=True
362
+ )
363
+
364
+
365
+ @app.post("/predict3")
366
+ async def predict3_api(
367
+ file: UploadFile = File(...),
368
+ enable_fillet: str = Form(..., regex="^(On|Off)$"),
369
+ fillet_value_mm: float = Form(...),
370
+ enable_finger_cut: str = Form(..., regex="^(On|Off)$")
371
+ ):
372
+ """
373
+ Legacy endpoint - redirects to full paper-based prediction
374
+ """
375
+ offset_value = fillet_value_mm if enable_fillet == "On" else 0.0
376
+
377
+ return await predict_paper_full_api(
378
+ file=file,
379
+ paper_size="A4", # Default to A4
380
+ offset_value=offset_value,
381
+ offset_unit="mm",
382
+ enable_finger_cut=enable_finger_cut,
383
+ include_images=True
384
+ )
385
 
386
 
387
  @app.post("/update")
 
423
  return Response(content="OK", status_code=200)
424
 
425
 
426
+ @app.get("/")
427
+ def root():
428
+ return {
429
+ "message": "Paper-based DXF Generator API",
430
+ "endpoints": [
431
+ "/predict_paper_simple - Simple DXF generation with paper reference",
432
+ "/predict_paper_with_offset - DXF generation with contour offset",
433
+ "/predict_paper_full - Full DXF generation with all features",
434
+ "/predict1, /predict2, /predict3 - Legacy endpoints (backward compatibility)"
435
+ ],
436
+ "paper_sizes": ["A4", "A3", "US Letter"],
437
+ "units": ["mm", "inches"]
438
+ }
439
+
440
+
441
  if __name__ == "__main__":
442
  import uvicorn
443
  port = int(os.environ.get("PORT", 8080))
444
  print(f"Starting FastAPI server on 0.0.0.0:{port}...")
445
+ uvicorn.run(app, host="0.0.0.0", port=port)