/**
* 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);
}