/** * facezoomClient.ts — API client for the Face Zoom feature. * * Completely independent of client.ts, upscaleClient.ts, bgremoveClient.ts, * facerestoreClient.ts, and objectRemovalClient.ts. * Do not import from or share state with any other feature's client. * * Endpoints * --------- * POST /facezoom/upload — submit image, get job_id back immediately * GET /facezoom/result/{job_id} — poll status; completed response carries * per-face results (face_index, image URL, * codeformer_applied, similarity) * POST /facezoom/download — JSON body {job_id, face_indices} → ZIP blob * * ZIP download pattern * -------------------- * Unlike all other features (single image via GET), Face Zoom downloads a ZIP * via POST with a JSON body (because the caller selects which face indices to * include). The response is a binary blob (application/zip) — we convert it * to an object URL and trigger a temporary click. A plain * is NOT used because cross-origin binary POSTs require a fetch first. */ export const FACEZOOM_API_BASE = import.meta.env.VITE_API_BASE ?? "http://localhost:8000"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface FaceZoomFaceResult { face_index: number; result_image_url: string; // full http://localhost:8000/storage/… URL codeformer_applied: boolean; // true = CodeFormer accepted; false = ESRGAN-only similarity: number; // ArcFace cosine sim (0.0–1.0) } export interface FaceZoomResult { job_id: string; status: "pending" | "processing" | "completed" | "failed"; error_message: string | null; faces: FaceZoomFaceResult[]; // empty until completed } // --------------------------------------------------------------------------- // Upload // --------------------------------------------------------------------------- /** * POST /facezoom/upload — submit an image for face detection + zoom restoration. * * Backend detects all faces, crops each with 40% margin, upscales 4× via * RealESRGAN, then runs CodeFormer with ArcFace identity check per face. * Returns job_id immediately; poll getFaceZoomResult() for per-face results. */ export async function uploadForFaceZoom(file: File): Promise<{ job_id: string }> { const formData = new FormData(); formData.append("file", file); // Send "preserve" (w=0.95) — Face Zoom targets small crops (60-80 px) where // higher fidelity avoids uncanny eye sharpening that "balanced" (w=0.85) can // introduce. No per-preset picker is exposed in the UI. formData.append("fidelity_preset", "preserve"); const response = await fetch(`${FACEZOOM_API_BASE}/facezoom/upload`, { method: "POST", body: formData, }); if (!response.ok) { let detail = `HTTP ${response.status}`; try { const json = await response.json(); if (json?.detail) detail = json.detail; } catch { /* ignore */ } throw new Error(detail); } return response.json() as Promise<{ job_id: string }>; } // --------------------------------------------------------------------------- // Result polling // --------------------------------------------------------------------------- /** * GET /facezoom/result/{job_id} — poll the current status of a face zoom job. * * When status === "completed", the faces array contains one entry per detected * face with its result image URL, whether CodeFormer was applied, and the * ArcFace cosine similarity score. The array is empty while the job is still * pending or processing. */ export async function getFaceZoomResult(jobId: string): Promise { const response = await fetch(`${FACEZOOM_API_BASE}/facezoom/result/${jobId}`); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json() as Promise; } // --------------------------------------------------------------------------- // ZIP download // --------------------------------------------------------------------------- /** * POST /facezoom/download — download a ZIP of selected face crops. * * Sends {job_id, face_indices} as JSON, expects a binary ZIP response. * Converts the blob to an object URL and triggers a synthetic click so * the browser saves it as "face_zoom_results.zip". * * @param jobId The completed job's ID. * @param faceIndices Which face indices to include (subset or all). */ export async function downloadFaceZoomZip( jobId: string, faceIndices: number[], ): Promise { const response = await fetch(`${FACEZOOM_API_BASE}/facezoom/download`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ job_id: jobId, face_indices: faceIndices }), }); if (!response.ok) { let detail = `HTTP ${response.status}`; try { const json = await response.json(); if (json?.detail) detail = json.detail; } catch { /* ignore */ } throw new Error(detail); } const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "face_zoom_results.zip"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }