Spaces:
Running
Running
Update api_server.py
Browse files- 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 |
-
#
|
237 |
-
from app import predict_simple, predict_middle, predict_full
|
238 |
-
|
239 |
from app import (
|
240 |
-
|
241 |
ReferenceBoxNotDetectedError,
|
242 |
-
FingerCutOverlapError
|
|
|
|
|
|
|
243 |
)
|
244 |
|
245 |
-
|
246 |
app = FastAPI()
|
247 |
|
248 |
-
#
|
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 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
|
|
|
|
|
|
|
|
278 |
):
|
279 |
-
"""Helper to save all
|
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 |
-
#
|
296 |
-
if
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
|
|
|
|
|
|
|
|
301 |
dxf_fn = f"DXF_{current_date}.dxf"
|
302 |
|
303 |
-
#
|
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 |
-
#
|
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 |
-
#
|
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 |
-
|
326 |
-
|
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("/
|
349 |
-
async def
|
350 |
-
file: UploadFile = File(...)
|
|
|
351 |
):
|
352 |
"""
|
353 |
-
Simple predict:
|
354 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
366 |
elapsed = timeit.default_timer() - start
|
367 |
-
print(f"[{session_id}]
|
368 |
|
369 |
-
|
370 |
session_id=session_id,
|
371 |
-
output_image=out_img,
|
372 |
-
outlines=outlines,
|
373 |
dxf_path=dxf_path,
|
374 |
-
|
375 |
-
|
|
|
|
|
|
|
376 |
)
|
377 |
-
|
378 |
-
|
379 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
386 |
|
387 |
-
|
388 |
-
|
|
|
389 |
file: UploadFile = File(...),
|
390 |
-
|
391 |
-
|
|
|
|
|
392 |
):
|
393 |
"""
|
394 |
-
|
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 |
-
|
407 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
408 |
)
|
|
|
409 |
elapsed = timeit.default_timer() - start
|
410 |
-
print(f"[{session_id}]
|
411 |
|
412 |
-
|
413 |
session_id=session_id,
|
414 |
-
output_image=out_img,
|
415 |
-
outlines=outlines,
|
416 |
dxf_path=dxf_path,
|
417 |
-
|
418 |
-
|
419 |
-
|
|
|
|
|
|
|
|
|
|
|
420 |
)
|
421 |
-
|
422 |
-
|
423 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
430 |
|
431 |
|
432 |
-
@app.post("/
|
433 |
-
async def
|
434 |
file: UploadFile = File(...),
|
435 |
-
|
436 |
-
|
437 |
-
|
|
|
|
|
438 |
):
|
439 |
"""
|
440 |
-
Full predict: image +
|
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 |
-
|
453 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
454 |
)
|
|
|
455 |
elapsed = timeit.default_timer() - start
|
456 |
-
print(f"[{session_id}]
|
457 |
|
458 |
-
|
459 |
session_id=session_id,
|
460 |
-
output_image=out_img,
|
461 |
-
outlines=outlines,
|
462 |
dxf_path=dxf_path,
|
463 |
-
|
464 |
-
|
465 |
-
|
|
|
|
|
|
|
|
|
466 |
finger_cut=enable_finger_cut
|
467 |
)
|
468 |
-
|
469 |
-
|
470 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 + offset → DXF + 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 cuts → DXF + 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)
|
|
|
|
|
|