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