fix(hf): revert to standard demo.launch for ZeroGPU compatibility and add base64 API endpoint
Browse files- app.py +89 -68
- frontend/src/app/api/remove-bg/route.js +87 -96
- requirements.txt +1 -4
app.py
CHANGED
|
@@ -7,39 +7,8 @@ import numpy as np
|
|
| 7 |
# pyrefly: ignore [missing-import]
|
| 8 |
from PIL import Image
|
| 9 |
from rembg import remove, new_session
|
| 10 |
-
import
|
| 11 |
-
import os
|
| 12 |
import io
|
| 13 |
-
from fastapi import FastAPI, File, UploadFile
|
| 14 |
-
from fastapi.responses import JSONResponse
|
| 15 |
-
|
| 16 |
-
# ββ Custom FastAPI app (di-mount ke Gradio) βββββββββββββββββββββββββββββββββββ
|
| 17 |
-
# Digunakan sebagai endpoint upload untuk Vercel frontend.
|
| 18 |
-
# Gradio menerima file input hanya via URL publik β bukan base64.
|
| 19 |
-
app_fastapi = FastAPI()
|
| 20 |
-
|
| 21 |
-
# Folder sementara untuk simpan file upload
|
| 22 |
-
UPLOAD_DIR = "/tmp/rembg_uploads"
|
| 23 |
-
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
| 24 |
-
|
| 25 |
-
@app_fastapi.post("/custom/upload")
|
| 26 |
-
async def upload_file(file: UploadFile = File(...)):
|
| 27 |
-
"""
|
| 28 |
-
Upload gambar dan kembalikan URL publik yang bisa dipakai oleh Gradio API.
|
| 29 |
-
URL format: https://ilhamdev-rembg.hf.space/file=/tmp/rembg_uploads/{filename}
|
| 30 |
-
"""
|
| 31 |
-
ext = os.path.splitext(file.filename or "image.png")[1] or ".png"
|
| 32 |
-
unique_name = f"{uuid.uuid4().hex}{ext}"
|
| 33 |
-
save_path = os.path.join(UPLOAD_DIR, unique_name)
|
| 34 |
-
|
| 35 |
-
content = await file.read()
|
| 36 |
-
with open(save_path, "wb") as f:
|
| 37 |
-
f.write(content)
|
| 38 |
-
|
| 39 |
-
# URL yang bisa diakses publik via Gradio file serving
|
| 40 |
-
file_url = f"https://ilhamdev-rembg.hf.space/file={save_path}"
|
| 41 |
-
return JSONResponse({"url": file_url, "path": save_path})
|
| 42 |
-
|
| 43 |
|
| 44 |
# ββ Cache model βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 45 |
_SESSIONS = {}
|
|
@@ -50,17 +19,9 @@ def get_session(model_name: str):
|
|
| 50 |
_SESSIONS[model_name] = new_session(model_name)
|
| 51 |
return _SESSIONS[model_name]
|
| 52 |
|
| 53 |
-
|
| 54 |
-
def remove_bg(img, model_name, alpha_matting):
|
| 55 |
-
if img is None:
|
| 56 |
-
return None
|
| 57 |
-
|
| 58 |
-
if not isinstance(img, Image.Image):
|
| 59 |
-
img = Image.fromarray(np.uint8(img))
|
| 60 |
img = img.convert("RGB")
|
| 61 |
-
|
| 62 |
session = get_session(model_name)
|
| 63 |
-
|
| 64 |
kwargs = {}
|
| 65 |
if alpha_matting:
|
| 66 |
kwargs.update(
|
|
@@ -69,35 +30,95 @@ def remove_bg(img, model_name, alpha_matting):
|
|
| 69 |
alpha_matting_background_threshold=10,
|
| 70 |
alpha_matting_erode_size=10,
|
| 71 |
)
|
| 72 |
-
|
| 73 |
-
# Hasil: PIL mode RGBA (background transparan, RGB subjek UTUH)
|
| 74 |
return remove(img, session=session, **kwargs)
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
api_name="remove_bg",
|
| 96 |
-
)
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
if __name__ == "__main__":
|
| 102 |
-
|
| 103 |
-
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
| 7 |
# pyrefly: ignore [missing-import]
|
| 8 |
from PIL import Image
|
| 9 |
from rembg import remove, new_session
|
| 10 |
+
import base64
|
|
|
|
| 11 |
import io
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
# ββ Cache model βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 14 |
_SESSIONS = {}
|
|
|
|
| 19 |
_SESSIONS[model_name] = new_session(model_name)
|
| 20 |
return _SESSIONS[model_name]
|
| 21 |
|
| 22 |
+
def process_image(img: Image.Image, model_name: str, alpha_matting: bool) -> Image.Image:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
img = img.convert("RGB")
|
|
|
|
| 24 |
session = get_session(model_name)
|
|
|
|
| 25 |
kwargs = {}
|
| 26 |
if alpha_matting:
|
| 27 |
kwargs.update(
|
|
|
|
| 30 |
alpha_matting_background_threshold=10,
|
| 31 |
alpha_matting_erode_size=10,
|
| 32 |
)
|
|
|
|
|
|
|
| 33 |
return remove(img, session=session, **kwargs)
|
| 34 |
|
| 35 |
+
# ββ Handler untuk HuggingFace Web UI ββββββββββββββββββββββββββββββββββββββββββ
|
| 36 |
+
@spaces.GPU(duration=60)
|
| 37 |
+
def remove_bg_ui(img, model_name, alpha_matting):
|
| 38 |
+
if img is None:
|
| 39 |
+
return None
|
| 40 |
+
if not isinstance(img, Image.Image):
|
| 41 |
+
img = Image.fromarray(np.uint8(img))
|
| 42 |
+
return process_image(img, model_name, alpha_matting)
|
| 43 |
+
|
| 44 |
+
# ββ Handler untuk API Eksternal (Vercel / Next.js) ββββββββββββββββββββββββββββ
|
| 45 |
+
@spaces.GPU(duration=60)
|
| 46 |
+
def remove_bg_api(img_data, model_name, alpha_matting):
|
| 47 |
+
"""
|
| 48 |
+
Endpoint API untuk Vercel / frontend.
|
| 49 |
+
Menerima base64 data URL atau objek {path: ...}.
|
| 50 |
+
Mengembalikan data:image/png;base64,... secara langsung tanpa perlu file upload terpisah.
|
| 51 |
+
"""
|
| 52 |
+
if not img_data:
|
| 53 |
+
return None
|
|
|
|
|
|
|
| 54 |
|
| 55 |
+
try:
|
| 56 |
+
if isinstance(img_data, str):
|
| 57 |
+
# Jika berupa data URL atau base64 murni
|
| 58 |
+
if "," in img_data:
|
| 59 |
+
img_data = img_data.split(",", 1)[1]
|
| 60 |
+
img_bytes = base64.b64decode(img_data)
|
| 61 |
+
pil_img = Image.open(io.BytesIO(img_bytes))
|
| 62 |
+
elif isinstance(img_data, dict) and "path" in img_data:
|
| 63 |
+
pil_img = Image.open(img_data["path"])
|
| 64 |
+
else:
|
| 65 |
+
return None
|
| 66 |
+
|
| 67 |
+
result_img = process_image(pil_img, model_name, alpha_matting)
|
| 68 |
+
|
| 69 |
+
# Encode hasil ke base64 PNG
|
| 70 |
+
buffered = io.BytesIO()
|
| 71 |
+
result_img.save(buffered, format="PNG")
|
| 72 |
+
b64_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
| 73 |
+
return f"data:image/png;base64,{b64_str}"
|
| 74 |
+
except Exception as e:
|
| 75 |
+
print(f"[ERROR] API processing failed: {e}")
|
| 76 |
+
raise e
|
| 77 |
+
|
| 78 |
+
# ββ Gradio Blocks Interface βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 79 |
+
with gr.Blocks(title="Background Remover AI") as demo:
|
| 80 |
+
gr.Markdown("# π AI Background Remover")
|
| 81 |
+
gr.Markdown("Hapus background gambar dengan presisi tinggi menggunakan ZeroGPU.")
|
| 82 |
+
|
| 83 |
+
# Tampilan UI untuk pengunjung HuggingFace Space
|
| 84 |
+
with gr.Row():
|
| 85 |
+
with gr.Column():
|
| 86 |
+
ui_input = gr.Image(type="pil", label="Upload Gambar")
|
| 87 |
+
ui_model = gr.Dropdown(
|
| 88 |
+
choices=[
|
| 89 |
+
"birefnet-portrait",
|
| 90 |
+
"birefnet-general",
|
| 91 |
+
"isnet-general-use",
|
| 92 |
+
"u2net_human_seg",
|
| 93 |
+
],
|
| 94 |
+
value="birefnet-portrait",
|
| 95 |
+
label="Model AI",
|
| 96 |
+
)
|
| 97 |
+
ui_alpha = gr.Checkbox(value=False, label="Alpha Matting (haluskan edge rambut)")
|
| 98 |
+
ui_btn = gr.Button("Hapus Background", variant="primary")
|
| 99 |
+
with gr.Column():
|
| 100 |
+
ui_output = gr.Image(type="pil", label="Hasil (PNG Transparan)")
|
| 101 |
+
|
| 102 |
+
ui_btn.click(
|
| 103 |
+
fn=remove_bg_ui,
|
| 104 |
+
inputs=[ui_input, ui_model, ui_alpha],
|
| 105 |
+
outputs=ui_output,
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
# Komponen API untuk Vercel / endpoint /gradio_api/call/remove_bg
|
| 109 |
+
with gr.Row(visible=False):
|
| 110 |
+
api_input = gr.Textbox(label="Image Base64")
|
| 111 |
+
api_model = gr.Textbox(value="birefnet-portrait", label="Model")
|
| 112 |
+
api_alpha = gr.Checkbox(value=False, label="Alpha Matting")
|
| 113 |
+
api_output = gr.Textbox(label="Result Base64")
|
| 114 |
+
api_btn = gr.Button("API Trigger")
|
| 115 |
+
|
| 116 |
+
api_btn.click(
|
| 117 |
+
fn=remove_bg_api,
|
| 118 |
+
inputs=[api_input, api_model, api_alpha],
|
| 119 |
+
outputs=api_output,
|
| 120 |
+
api_name="remove_bg",
|
| 121 |
+
)
|
| 122 |
|
| 123 |
if __name__ == "__main__":
|
| 124 |
+
demo.launch()
|
|
|
frontend/src/app/api/remove-bg/route.js
CHANGED
|
@@ -1,18 +1,12 @@
|
|
| 1 |
/**
|
| 2 |
* API Route: /api/remove-bg
|
| 3 |
*
|
| 4 |
-
*
|
| 5 |
-
*
|
| 6 |
-
* β dapat URL publik file di HF server
|
| 7 |
-
* 2. POST ke /call/remove_bg dengan { "data": [{"path": url}, model, alpha] }
|
| 8 |
-
* β dapat event_id
|
| 9 |
-
* 3. GET SSE /call/remove_bg/{event_id}
|
| 10 |
-
* β parse event:complete β dapat URL gambar hasil
|
| 11 |
-
* 4. Fetch gambar hasil β kembalikan base64 ke browser
|
| 12 |
*/
|
| 13 |
|
| 14 |
-
const HF_SPACE_URL = process.env.NEXT_PUBLIC_HF_SPACE_URL || 'https://ilhamdev-rembg.hf.space'
|
| 15 |
-
const TIMEOUT_MS = 90_000
|
| 16 |
|
| 17 |
export async function POST(request) {
|
| 18 |
try {
|
|
@@ -22,52 +16,60 @@ export async function POST(request) {
|
|
| 22 |
const alphaMatting = formData.get('alpha_matting') === 'true'
|
| 23 |
|
| 24 |
if (!imageFile) {
|
| 25 |
-
return Response.json({ error: '
|
| 26 |
}
|
| 27 |
|
| 28 |
-
// ββ STEP 1:
|
| 29 |
-
const
|
| 30 |
-
const
|
| 31 |
-
|
|
|
|
| 32 |
|
| 33 |
-
const
|
| 34 |
-
|
| 35 |
-
body: uploadForm,
|
| 36 |
-
})
|
| 37 |
-
|
| 38 |
-
if (!uploadRes.ok) {
|
| 39 |
-
const errText = await uploadRes.text()
|
| 40 |
-
console.error('[Upload Error]', uploadRes.status, errText.slice(0, 200))
|
| 41 |
-
return Response.json(
|
| 42 |
-
{ error: `Gagal upload gambar: ${uploadRes.status} β ${errText.slice(0, 100)}` },
|
| 43 |
-
{ status: 502 }
|
| 44 |
-
)
|
| 45 |
}
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
}
|
| 59 |
|
| 60 |
-
|
| 61 |
-
method: 'POST',
|
| 62 |
-
headers: { 'Content-Type': 'application/json' },
|
| 63 |
-
body: JSON.stringify(callPayload),
|
| 64 |
-
})
|
| 65 |
-
|
| 66 |
-
if (!callRes.ok) {
|
| 67 |
-
const errText = await callRes.text()
|
| 68 |
-
console.error('[Call Error]', callRes.status, errText.slice(0, 300))
|
| 69 |
return Response.json(
|
| 70 |
-
{
|
|
|
|
|
|
|
| 71 |
{ status: 502 }
|
| 72 |
)
|
| 73 |
}
|
|
@@ -78,13 +80,15 @@ export async function POST(request) {
|
|
| 78 |
}
|
| 79 |
console.log('[Call OK] event_id:', event_id)
|
| 80 |
|
| 81 |
-
// ββ STEP 3: Baca SSE
|
|
|
|
| 82 |
const sseController = new AbortController()
|
| 83 |
const sseTimer = setTimeout(() => sseController.abort(), TIMEOUT_MS)
|
| 84 |
|
| 85 |
let sseRes
|
| 86 |
try {
|
| 87 |
-
sseRes = await fetch(
|
|
|
|
| 88 |
signal: sseController.signal,
|
| 89 |
})
|
| 90 |
} finally {
|
|
@@ -93,16 +97,15 @@ export async function POST(request) {
|
|
| 93 |
|
| 94 |
if (!sseRes.ok) {
|
| 95 |
return Response.json(
|
| 96 |
-
{ error: `Gagal
|
| 97 |
{ status: 502 }
|
| 98 |
)
|
| 99 |
}
|
| 100 |
|
| 101 |
const sseText = await sseRes.text()
|
| 102 |
-
console.log('[SSE
|
| 103 |
|
| 104 |
-
|
| 105 |
-
let resultData = null
|
| 106 |
let errorMsg = null
|
| 107 |
const lines = sseText.split('\n')
|
| 108 |
let currentEvent = ''
|
|
@@ -121,77 +124,65 @@ export async function POST(request) {
|
|
| 121 |
try {
|
| 122 |
const parsed = JSON.parse(raw)
|
| 123 |
if (Array.isArray(parsed) && parsed.length > 0) {
|
| 124 |
-
|
| 125 |
}
|
| 126 |
} catch {
|
| 127 |
-
console.error('[SSE parse
|
| 128 |
}
|
| 129 |
}
|
| 130 |
}
|
| 131 |
}
|
| 132 |
|
| 133 |
if (errorMsg) {
|
| 134 |
-
return Response.json({ error: `Gradio error: ${errorMsg}` }, { status: 502 })
|
| 135 |
}
|
| 136 |
|
| 137 |
-
if (!
|
| 138 |
return Response.json(
|
| 139 |
-
{ error: '
|
| 140 |
{ status: 502 }
|
| 141 |
)
|
| 142 |
}
|
| 143 |
|
| 144 |
-
// ββ STEP 4:
|
| 145 |
-
//
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
if (!imgUrl) {
|
| 151 |
-
return Response.json({ error: 'Tidak ada URL gambar hasil dari Gradio' }, { status: 502 })
|
| 152 |
-
}
|
| 153 |
-
|
| 154 |
-
// Jika path relatif, buat URL lengkap
|
| 155 |
-
if (!imgUrl.startsWith('http') && !imgUrl.startsWith('data:')) {
|
| 156 |
-
// Format Gradio untuk serve file: /file=/tmp/gradio/xxx/image.png
|
| 157 |
-
imgUrl = `${HF_SPACE_URL}/file=${imgUrl}`
|
| 158 |
-
}
|
| 159 |
-
|
| 160 |
-
const imgRes = await fetch(imgUrl)
|
| 161 |
-
if (!imgRes.ok) {
|
| 162 |
-
// Coba format alternatif dengan /c/ prefix (Gradio CDN)
|
| 163 |
-
const altUrl = `${HF_SPACE_URL}/c/file=${resultData.path || ''}`
|
| 164 |
-
console.log('[Retry alt URL]', altUrl)
|
| 165 |
-
const altRes = await fetch(altUrl)
|
| 166 |
-
if (!altRes.ok) {
|
| 167 |
-
return Response.json(
|
| 168 |
-
{ error: `Gagal fetch gambar hasil: ${imgRes.status}` },
|
| 169 |
-
{ status: 502 }
|
| 170 |
-
)
|
| 171 |
}
|
| 172 |
-
const buf = await altRes.arrayBuffer()
|
| 173 |
return Response.json({
|
| 174 |
success: true,
|
| 175 |
-
image: `data:image/png;base64,${
|
| 176 |
})
|
| 177 |
}
|
| 178 |
|
| 179 |
-
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
|
| 182 |
-
return Response.json({
|
| 183 |
-
success: true,
|
| 184 |
-
image: `data:${mime};base64,${Buffer.from(imgBuf).toString('base64')}`,
|
| 185 |
-
})
|
| 186 |
|
| 187 |
} catch (err) {
|
| 188 |
if (err.name === 'AbortError') {
|
| 189 |
return Response.json(
|
| 190 |
-
{ error: 'Timeout 90 detik. ZeroGPU sedang cold start, coba lagi
|
| 191 |
{ status: 504 }
|
| 192 |
)
|
| 193 |
}
|
| 194 |
-
console.error('[API Error]', err)
|
| 195 |
return Response.json({ error: err.message }, { status: 500 })
|
| 196 |
}
|
| 197 |
}
|
|
|
|
| 1 |
/**
|
| 2 |
* API Route: /api/remove-bg
|
| 3 |
*
|
| 4 |
+
* Mengirim gambar langsung sebagai base64 ke HuggingFace Gradio Space (ZeroGPU).
|
| 5 |
+
* Tidak memerlukan endpoint /upload terpisah, menghindari masalah 404/405 di Hugging Face SSR proxy.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
*/
|
| 7 |
|
| 8 |
+
const HF_SPACE_URL = (process.env.NEXT_PUBLIC_HF_SPACE_URL || 'https://ilhamdev-rembg.hf.space').replace(/\/+$/, '')
|
| 9 |
+
const TIMEOUT_MS = 90_000 // 90 detik untuk toleransi ZeroGPU cold start
|
| 10 |
|
| 11 |
export async function POST(request) {
|
| 12 |
try {
|
|
|
|
| 16 |
const alphaMatting = formData.get('alpha_matting') === 'true'
|
| 17 |
|
| 18 |
if (!imageFile) {
|
| 19 |
+
return Response.json({ error: 'Tidak ada file gambar yang dikirim' }, { status: 400 })
|
| 20 |
}
|
| 21 |
|
| 22 |
+
// ββ STEP 1: Konversi file gambar ke Base64 Data URL ββββββββββββββββββββ
|
| 23 |
+
const arrayBuffer = await imageFile.arrayBuffer()
|
| 24 |
+
const base64 = Buffer.from(arrayBuffer).toString('base64')
|
| 25 |
+
const mimeType = imageFile.type || 'image/png'
|
| 26 |
+
const dataUrl = `data:${mimeType};base64,${base64}`
|
| 27 |
|
| 28 |
+
const payload = {
|
| 29 |
+
data: [dataUrl, modelName, alphaMatting],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
}
|
| 31 |
|
| 32 |
+
// ββ STEP 2: POST ke Gradio API Endpoint ββββββββββββββββββββββββββββββββ
|
| 33 |
+
// Coba path standar Hugging Face (/gradio_api/call/remove_bg), lalu fallback ke (/call/remove_bg)
|
| 34 |
+
const candidateEndpoints = [
|
| 35 |
+
`${HF_SPACE_URL}/gradio_api/call/remove_bg`,
|
| 36 |
+
`${HF_SPACE_URL}/call/remove_bg`,
|
| 37 |
+
]
|
| 38 |
+
|
| 39 |
+
let callRes = null
|
| 40 |
+
let usedEndpoint = ''
|
| 41 |
+
let lastErrorText = ''
|
| 42 |
+
|
| 43 |
+
for (const endpoint of candidateEndpoints) {
|
| 44 |
+
try {
|
| 45 |
+
console.log('[Connecting] Mengirim request ke:', endpoint)
|
| 46 |
+
const res = await fetch(endpoint, {
|
| 47 |
+
method: 'POST',
|
| 48 |
+
headers: {
|
| 49 |
+
'Content-Type': 'application/json',
|
| 50 |
+
'Accept': 'application/json',
|
| 51 |
+
},
|
| 52 |
+
body: JSON.stringify(payload),
|
| 53 |
+
})
|
| 54 |
+
|
| 55 |
+
if (res.ok) {
|
| 56 |
+
callRes = res
|
| 57 |
+
usedEndpoint = endpoint
|
| 58 |
+
break
|
| 59 |
+
} else {
|
| 60 |
+
lastErrorText = await res.text()
|
| 61 |
+
console.warn(`[Endpoint ${res.status}] ${endpoint}:`, lastErrorText.slice(0, 120))
|
| 62 |
+
}
|
| 63 |
+
} catch (err) {
|
| 64 |
+
console.warn(`[Fetch Fail] ${endpoint}:`, err.message)
|
| 65 |
+
}
|
| 66 |
}
|
| 67 |
|
| 68 |
+
if (!callRes) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
return Response.json(
|
| 70 |
+
{
|
| 71 |
+
error: `Gagal memanggil backend HuggingFace. Status server mungkin sedang starting/rebuilding: ${lastErrorText.slice(0, 100) || 'Connection failed'}`,
|
| 72 |
+
},
|
| 73 |
{ status: 502 }
|
| 74 |
)
|
| 75 |
}
|
|
|
|
| 80 |
}
|
| 81 |
console.log('[Call OK] event_id:', event_id)
|
| 82 |
|
| 83 |
+
// ββ STEP 3: Baca SSE Stream untuk mengambil hasil ββββββββββββββββββββββ
|
| 84 |
+
const sseUrl = `${usedEndpoint}/${event_id}`
|
| 85 |
const sseController = new AbortController()
|
| 86 |
const sseTimer = setTimeout(() => sseController.abort(), TIMEOUT_MS)
|
| 87 |
|
| 88 |
let sseRes
|
| 89 |
try {
|
| 90 |
+
sseRes = await fetch(sseUrl, {
|
| 91 |
+
headers: { Accept: 'text/event-stream' },
|
| 92 |
signal: sseController.signal,
|
| 93 |
})
|
| 94 |
} finally {
|
|
|
|
| 97 |
|
| 98 |
if (!sseRes.ok) {
|
| 99 |
return Response.json(
|
| 100 |
+
{ error: `Gagal membaca SSE status dari HuggingFace: ${sseRes.status}` },
|
| 101 |
{ status: 502 }
|
| 102 |
)
|
| 103 |
}
|
| 104 |
|
| 105 |
const sseText = await sseRes.text()
|
| 106 |
+
console.log('[SSE Status received, length:]', sseText.length)
|
| 107 |
|
| 108 |
+
let resultOutput = null
|
|
|
|
| 109 |
let errorMsg = null
|
| 110 |
const lines = sseText.split('\n')
|
| 111 |
let currentEvent = ''
|
|
|
|
| 124 |
try {
|
| 125 |
const parsed = JSON.parse(raw)
|
| 126 |
if (Array.isArray(parsed) && parsed.length > 0) {
|
| 127 |
+
resultOutput = parsed[0]
|
| 128 |
}
|
| 129 |
} catch {
|
| 130 |
+
console.error('[SSE parse error]', raw.slice(0, 100))
|
| 131 |
}
|
| 132 |
}
|
| 133 |
}
|
| 134 |
}
|
| 135 |
|
| 136 |
if (errorMsg) {
|
| 137 |
+
return Response.json({ error: `Gradio inference error: ${errorMsg}` }, { status: 502 })
|
| 138 |
}
|
| 139 |
|
| 140 |
+
if (!resultOutput) {
|
| 141 |
return Response.json(
|
| 142 |
+
{ error: 'ZeroGPU belum selesai atau sedang antre. Silakan coba kembali sesaat lagi.' },
|
| 143 |
{ status: 502 }
|
| 144 |
)
|
| 145 |
}
|
| 146 |
|
| 147 |
+
// ββ STEP 4: Format Kembalian Gambar βββββββββββββββββββββββββββββββββββ
|
| 148 |
+
// Jika resultOutput adalah base64 data URL langsung (sesuai fungsi remove_bg_api kita)
|
| 149 |
+
if (typeof resultOutput === 'string') {
|
| 150 |
+
if (resultOutput.startsWith('data:')) {
|
| 151 |
+
return Response.json({ success: true, image: resultOutput })
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
}
|
|
|
|
| 153 |
return Response.json({
|
| 154 |
success: true,
|
| 155 |
+
image: `data:image/png;base64,${resultOutput}`,
|
| 156 |
})
|
| 157 |
}
|
| 158 |
|
| 159 |
+
// Fallback jika berupa objek file Gradio { path, url }
|
| 160 |
+
let imgUrl = resultOutput.url || resultOutput.path
|
| 161 |
+
if (imgUrl) {
|
| 162 |
+
if (!imgUrl.startsWith('http') && !imgUrl.startsWith('data:')) {
|
| 163 |
+
imgUrl = `${HF_SPACE_URL}/gradio_api/file=${imgUrl}`
|
| 164 |
+
}
|
| 165 |
+
const fileRes = await fetch(imgUrl)
|
| 166 |
+
if (fileRes.ok) {
|
| 167 |
+
const buf = await fileRes.arrayBuffer()
|
| 168 |
+
const mime = fileRes.headers.get('content-type') || 'image/png'
|
| 169 |
+
return Response.json({
|
| 170 |
+
success: true,
|
| 171 |
+
image: `data:${mime};base64,${Buffer.from(buf).toString('base64')}`,
|
| 172 |
+
})
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
|
| 176 |
+
return Response.json({ error: 'Format output dari HuggingFace tidak dikenali' }, { status: 502 })
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
} catch (err) {
|
| 179 |
if (err.name === 'AbortError') {
|
| 180 |
return Response.json(
|
| 181 |
+
{ error: 'Timeout 90 detik. Model ZeroGPU sedang cold start, silakan coba lagi.' },
|
| 182 |
{ status: 504 }
|
| 183 |
)
|
| 184 |
}
|
| 185 |
+
console.error('[API Handler Error]', err)
|
| 186 |
return Response.json({ error: err.message }, { status: 500 })
|
| 187 |
}
|
| 188 |
}
|
requirements.txt
CHANGED
|
@@ -3,7 +3,4 @@ spaces
|
|
| 3 |
pillow
|
| 4 |
numpy
|
| 5 |
rembg[cpu]
|
| 6 |
-
pymatting
|
| 7 |
-
fastapi
|
| 8 |
-
uvicorn
|
| 9 |
-
python-multipart
|
|
|
|
| 3 |
pillow
|
| 4 |
numpy
|
| 5 |
rembg[cpu]
|
| 6 |
+
pymatting
|
|
|
|
|
|
|
|
|