LogicGoInfotechSpaces commited on
Commit
f7573cd
·
1 Parent(s): ae10bc2

API: implement inpainting endpoints (upload image/mask, inpaint, download, logs); optional Bearer via API_TOKEN

Browse files
Files changed (2) hide show
  1. README.md +6 -5
  2. api/main.py +91 -32
README.md CHANGED
@@ -13,14 +13,15 @@ hardware: cpu-basic
13
 
14
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
15
 
16
- ## API (FastAPI)
17
- - Base URL: `https://logicgoinfotechspaces-hair-stable-new.hf.space`
18
- - Auth: Bearer token `logicgo@123`
19
 
20
  Endpoints:
21
  - `GET /health` → {"status":"healthy"}
22
- - `POST /upload` (form-data: image=file) → {"id":"<id>","filename":"name.png"}
23
- - `POST /get-hairswap` (JSON body with source_id, reference_id, …) → {"result":"output_xxx.png"}
 
 
24
  - `GET /download/{filename}` → image file
25
  - `GET /logs` → recent uploads/results
26
 
 
13
 
14
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
15
 
16
+ ## Inpainting API (FastAPI)
17
+ - Auth: optional Bearer via env `API_TOKEN`. If set, send `Authorization: Bearer <token>`.
 
18
 
19
  Endpoints:
20
  - `GET /health` → {"status":"healthy"}
21
+ - `POST /upload-image` (form-data: image=file) → {"id":"<image_id>","filename":"name.png"}
22
+ - `POST /upload-mask` (form-data: mask=file) → {"id":"<mask_id>","filename":"mask.png"}
23
+ - `POST /inpaint` (JSON: {image_id, mask_id}) → {"result":"output_xxx.png"}
24
+ - `POST /inpaint-multipart` (form-data: image=file, mask=file) → {"result":"output_xxx.png"}
25
  - `GET /download/{filename}` → image file
26
  - `GET /logs` → recent uploads/results
27
 
api/main.py CHANGED
@@ -4,19 +4,26 @@ import shutil
4
  from datetime import datetime
5
  from typing import Dict, List, Optional
6
 
 
7
  from fastapi import FastAPI, UploadFile, File, HTTPException, Depends, Header
8
  from fastapi.responses import FileResponse, JSONResponse
9
  from pydantic import BaseModel
 
10
 
11
- # Simple settings
12
- UPLOAD_DIR = os.path.join(os.getcwd(), "uploads")
13
- OUTPUT_DIR = os.path.join(os.getcwd(), "outputs")
14
- AUTH_TOKEN = "logicgo@123" # Bearer token
 
 
15
 
16
  os.makedirs(UPLOAD_DIR, exist_ok=True)
17
  os.makedirs(OUTPUT_DIR, exist_ok=True)
18
 
19
- app = FastAPI(title="Hair Swap API", version="1.0.0")
 
 
 
20
 
21
  # In-memory stores
22
  file_store: Dict[str, Dict[str, str]] = {}
@@ -24,20 +31,18 @@ logs: List[Dict[str, str]] = []
24
 
25
 
26
  def bearer_auth(authorization: Optional[str] = Header(default=None)) -> None:
 
 
27
  if authorization is None or not authorization.lower().startswith("bearer "):
28
  raise HTTPException(status_code=401, detail="Unauthorized")
29
  token = authorization.split(" ", 1)[1]
30
- if token != AUTH_TOKEN:
31
  raise HTTPException(status_code=403, detail="Forbidden")
32
 
33
 
34
- class HairSwapRequest(BaseModel):
35
- source_id: str
36
- reference_id: str
37
- converter_scale: Optional[float] = 1.0
38
- scale: Optional[float] = 1.0
39
- guidance_scale: Optional[float] = 1.5
40
- controlnet_conditioning_scale: Optional[float] = 1.0
41
 
42
 
43
  @app.get("/health")
@@ -45,38 +50,97 @@ def health() -> Dict[str, str]:
45
  return {"status": "healthy"}
46
 
47
 
48
- @app.post("/upload")
49
  def upload_image(image: UploadFile = File(...), _: None = Depends(bearer_auth)) -> Dict[str, str]:
50
  ext = os.path.splitext(image.filename)[1] or ".png"
51
  file_id = str(uuid.uuid4())
52
  stored_name = f"{file_id}{ext}"
53
  stored_path = os.path.join(UPLOAD_DIR, stored_name)
54
-
55
  with open(stored_path, "wb") as f:
56
  shutil.copyfileobj(image.file, f)
57
-
58
  file_store[file_id] = {
 
59
  "filename": image.filename,
60
  "stored_name": stored_name,
61
  "path": stored_path,
62
  "timestamp": datetime.utcnow().isoformat(),
63
  }
64
- logs.append({"id": file_id, "filename": image.filename, "timestamp": datetime.utcnow().isoformat()})
65
  return {"id": file_id, "filename": image.filename}
66
 
67
 
68
- @app.post("/get-hairswap")
69
- def get_hairswap(req: HairSwapRequest, _: None = Depends(bearer_auth)) -> Dict[str, str]:
70
- if req.source_id not in file_store:
71
- raise HTTPException(status_code=404, detail="source_id not found")
72
- if req.reference_id not in file_store:
73
- raise HTTPException(status_code=404, detail="reference_id not found")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
- # Placeholder: In lieu of a real hair-swap model, return the source image as the result
76
- src_path = file_store[req.source_id]["path"]
77
  result_name = f"output_{uuid.uuid4().hex}.png"
78
  result_path = os.path.join(OUTPUT_DIR, result_name)
79
- shutil.copyfile(src_path, result_path)
80
 
81
  logs.append({"result": result_name, "timestamp": datetime.utcnow().isoformat()})
82
  return {"result": result_name}
@@ -84,14 +148,9 @@ def get_hairswap(req: HairSwapRequest, _: None = Depends(bearer_auth)) -> Dict[s
84
 
85
  @app.get("/download/{filename}")
86
  def download_file(filename: str, _: None = Depends(bearer_auth)):
87
- # Serve from outputs first, then uploads as a fallback
88
  path = os.path.join(OUTPUT_DIR, filename)
89
  if not os.path.isfile(path):
90
- alt = os.path.join(UPLOAD_DIR, filename)
91
- if os.path.isfile(alt):
92
- path = alt
93
- else:
94
- raise HTTPException(status_code=404, detail="file not found")
95
  return FileResponse(path)
96
 
97
 
 
4
  from datetime import datetime
5
  from typing import Dict, List, Optional
6
 
7
+ import numpy as np
8
  from fastapi import FastAPI, UploadFile, File, HTTPException, Depends, Header
9
  from fastapi.responses import FileResponse, JSONResponse
10
  from pydantic import BaseModel
11
+ from PIL import Image
12
 
13
+ from src.core import process_inpaint
14
+
15
+ # Directories
16
+ ROOT = os.getcwd()
17
+ UPLOAD_DIR = os.path.join(ROOT, "uploads")
18
+ OUTPUT_DIR = os.path.join(ROOT, "outputs")
19
 
20
  os.makedirs(UPLOAD_DIR, exist_ok=True)
21
  os.makedirs(OUTPUT_DIR, exist_ok=True)
22
 
23
+ # Optional Bearer token: set env API_TOKEN to require auth; if not set, endpoints are open
24
+ ENV_TOKEN = os.environ.get("API_TOKEN")
25
+
26
+ app = FastAPI(title="Photo Object Removal API", version="1.0.0")
27
 
28
  # In-memory stores
29
  file_store: Dict[str, Dict[str, str]] = {}
 
31
 
32
 
33
  def bearer_auth(authorization: Optional[str] = Header(default=None)) -> None:
34
+ if not ENV_TOKEN:
35
+ return
36
  if authorization is None or not authorization.lower().startswith("bearer "):
37
  raise HTTPException(status_code=401, detail="Unauthorized")
38
  token = authorization.split(" ", 1)[1]
39
+ if token != ENV_TOKEN:
40
  raise HTTPException(status_code=403, detail="Forbidden")
41
 
42
 
43
+ class InpaintRequest(BaseModel):
44
+ image_id: str
45
+ mask_id: str
 
 
 
 
46
 
47
 
48
  @app.get("/health")
 
50
  return {"status": "healthy"}
51
 
52
 
53
+ @app.post("/upload-image")
54
  def upload_image(image: UploadFile = File(...), _: None = Depends(bearer_auth)) -> Dict[str, str]:
55
  ext = os.path.splitext(image.filename)[1] or ".png"
56
  file_id = str(uuid.uuid4())
57
  stored_name = f"{file_id}{ext}"
58
  stored_path = os.path.join(UPLOAD_DIR, stored_name)
 
59
  with open(stored_path, "wb") as f:
60
  shutil.copyfileobj(image.file, f)
 
61
  file_store[file_id] = {
62
+ "type": "image",
63
  "filename": image.filename,
64
  "stored_name": stored_name,
65
  "path": stored_path,
66
  "timestamp": datetime.utcnow().isoformat(),
67
  }
68
+ logs.append({"id": file_id, "filename": image.filename, "type": "image", "timestamp": datetime.utcnow().isoformat()})
69
  return {"id": file_id, "filename": image.filename}
70
 
71
 
72
+ @app.post("/upload-mask")
73
+ def upload_mask(mask: UploadFile = File(...), _: None = Depends(bearer_auth)) -> Dict[str, str]:
74
+ ext = os.path.splitext(mask.filename)[1] or ".png"
75
+ file_id = str(uuid.uuid4())
76
+ stored_name = f"{file_id}{ext}"
77
+ stored_path = os.path.join(UPLOAD_DIR, stored_name)
78
+ with open(stored_path, "wb") as f:
79
+ shutil.copyfileobj(mask.file, f)
80
+ file_store[file_id] = {
81
+ "type": "mask",
82
+ "filename": mask.filename,
83
+ "stored_name": stored_name,
84
+ "path": stored_path,
85
+ "timestamp": datetime.utcnow().isoformat(),
86
+ }
87
+ logs.append({"id": file_id, "filename": mask.filename, "type": "mask", "timestamp": datetime.utcnow().isoformat()})
88
+ return {"id": file_id, "filename": mask.filename}
89
+
90
+
91
+ def _load_rgba_image(path: str) -> Image.Image:
92
+ img = Image.open(path)
93
+ return img.convert("RGBA")
94
+
95
+
96
+ def _load_rgba_mask_from_image(img: Image.Image) -> np.ndarray:
97
+ # Expected by process_inpaint: RGBA where alpha=0 for drawn (to remove), 255 elsewhere
98
+ if img.mode != "RGBA":
99
+ # If no alpha, treat non-black/white>0 as masked areas
100
+ gray = img.convert("L")
101
+ arr = np.array(gray)
102
+ alpha = np.where(arr > 0, 0, 255).astype(np.uint8)
103
+ rgba = np.zeros((img.height, img.width, 4), dtype=np.uint8)
104
+ rgba[:, :, 3] = alpha
105
+ return rgba
106
+ return np.array(img)
107
+
108
+
109
+ @app.post("/inpaint")
110
+ def inpaint(req: InpaintRequest, _: None = Depends(bearer_auth)) -> Dict[str, str]:
111
+ if req.image_id not in file_store or file_store[req.image_id]["type"] != "image":
112
+ raise HTTPException(status_code=404, detail="image_id not found")
113
+ if req.mask_id not in file_store or file_store[req.mask_id]["type"] != "mask":
114
+ raise HTTPException(status_code=404, detail="mask_id not found")
115
+
116
+ img_rgba = _load_rgba_image(file_store[req.image_id]["path"])
117
+ mask_img = Image.open(file_store[req.mask_id]["path"]) # may be RGB/gray/RGBA
118
+ mask_rgba = _load_rgba_mask_from_image(mask_img)
119
+
120
+ result = process_inpaint(np.array(img_rgba), mask_rgba)
121
+ result_name = f"output_{uuid.uuid4().hex}.png"
122
+ result_path = os.path.join(OUTPUT_DIR, result_name)
123
+ Image.fromarray(result).save(result_path)
124
+
125
+ logs.append({"result": result_name, "timestamp": datetime.utcnow().isoformat()})
126
+ return {"result": result_name}
127
+
128
+
129
+ @app.post("/inpaint-multipart")
130
+ def inpaint_multipart(
131
+ image: UploadFile = File(...),
132
+ mask: UploadFile = File(...),
133
+ _: None = Depends(bearer_auth),
134
+ ) -> Dict[str, str]:
135
+ # Load in-memory
136
+ img = Image.open(image.file).convert("RGBA")
137
+ m = Image.open(mask.file)
138
+ mask_rgba = _load_rgba_mask_from_image(m)
139
 
140
+ result = process_inpaint(np.array(img), mask_rgba)
 
141
  result_name = f"output_{uuid.uuid4().hex}.png"
142
  result_path = os.path.join(OUTPUT_DIR, result_name)
143
+ Image.fromarray(result).save(result_path)
144
 
145
  logs.append({"result": result_name, "timestamp": datetime.utcnow().isoformat()})
146
  return {"result": result_name}
 
148
 
149
  @app.get("/download/{filename}")
150
  def download_file(filename: str, _: None = Depends(bearer_auth)):
 
151
  path = os.path.join(OUTPUT_DIR, filename)
152
  if not os.path.isfile(path):
153
+ raise HTTPException(status_code=404, detail="file not found")
 
 
 
 
154
  return FileResponse(path)
155
 
156