File size: 4,540 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
export const API_BASE = import.meta.env.VITE_API_BASE ?? "http://localhost:8000";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export interface StepSummary {
  step_number:        number;
  status:             "pending" | "processing" | "completed" | "failed";
  original_image_url: string | null;
  result_image_url:   string | null;
  error_message:      string | null;
}

// ---------------------------------------------------------------------------
// Upload
// ---------------------------------------------------------------------------

/**
 * Upload an image file to the backend.
 * POSTs as multipart/form-data; backend applies EXIF rotation, saves step 0,
 * and returns the job_id.
 */
export async function uploadImage(file: File): Promise<{ job_id: string }> {
  const formData = new FormData();
  formData.append("file", file);

  const response = await fetch(`${API_BASE}/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 }>;
}

// ---------------------------------------------------------------------------
// Generate
// ---------------------------------------------------------------------------

/**
 * POST /generate — send the mask JSON to the backend and start inpainting.
 *
 * The backend creates a new step (truncating any forward history if needed),
 * starts inference in a BackgroundTask, and returns immediately.
 */
export async function generateMask(
  jobId:      string,
  maskJson:   object,
  maskSource: "manual" | "ocr" | "mixed" = "manual",
): Promise<{ job_id: string; step_number: number; status: string }> {
  const response = await fetch(`${API_BASE}/generate`, {
    method:  "POST",
    headers: { "Content-Type": "application/json" },
    body:    JSON.stringify({
      job_id:      jobId,
      mask_source: maskSource,
      mask_json:   maskJson,
    }),
  });

  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; step_number: number; status: string }>;
}

// ---------------------------------------------------------------------------
// Result polling
// ---------------------------------------------------------------------------

/**
 * GET /result/{job_id} — poll current job state.
 *
 * Returns top-level `status` / `error_message` for the current step
 * (backward-compatible with the polling hook), plus the full `steps[]` array
 * and `current_step` for the multi-step history navigator.
 */
export async function getJobResult(jobId: string): Promise<{
  job_id:             string;
  current_step:       number;
  status:             "pending" | "processing" | "completed" | "failed";
  result_image_url:   string | null;
  original_image_url: string | null;
  error_message:      string | null;
  steps:              StepSummary[];
}> {
  const response = await fetch(`${API_BASE}/result/${jobId}`);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

// ---------------------------------------------------------------------------
// Step navigation
// ---------------------------------------------------------------------------

/**
 * POST /jobs/{job_id}/goto-step — set job.current_step to stepNumber.
 *
 * Only completed steps may be navigated to.  Truncation of forward history
 * happens lazily on the next POST /generate call.
 */
export async function goToStep(
  jobId:      string,
  stepNumber: number,
): Promise<{ job_id: string; current_step: number }> {
  const response = await fetch(`${API_BASE}/jobs/${jobId}/goto-step`, {
    method:  "POST",
    headers: { "Content-Type": "application/json" },
    body:    JSON.stringify({ step_number: stepNumber }),
  });

  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; current_step: number }>;
}