Spaces:
Sleeping
Sleeping
File size: 5,481 Bytes
a3953cb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | /**
* 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 <a> click. A plain <a href download>
* 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<FaceZoomResult> {
const response = await fetch(`${FACEZOOM_API_BASE}/facezoom/result/${jobId}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json() as Promise<FaceZoomResult>;
}
// ---------------------------------------------------------------------------
// 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 <a> 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<void> {
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);
}
|