Spaces:
Running
Running
File size: 4,620 Bytes
4633f70 9171a06 4633f70 9171a06 4633f70 9171a06 4633f70 | 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 | // ============================================================================
// API client — RELATIVE urls (/api/...). Same-origin in prod (backend serves
// this build), so no CORS. A dev-flag mock fallback lets the UI render when the
// backend is down: set VITE_USE_MOCK=1, OR it auto-falls-back on /api/methods
// network failure during `npm run dev` (never in prod builds).
// ============================================================================
import type {
ApiError,
EnhanceResponse,
HealthResponse,
MethodId,
MethodsResponse,
RestoreResponse,
Severity,
SimulateResponse,
} from './types';
import { MOCK_METHODS, mockEnhance, mockRestore, mockSimulate } from './mock';
const FORCE_MOCK = import.meta.env.VITE_USE_MOCK === '1';
const IS_DEV = import.meta.env.DEV;
// once we discover /api is unreachable in dev, latch mock mode for the session
let mockLatched = false;
export function isMockActive(): boolean {
return FORCE_MOCK || mockLatched;
}
function apiError(kind: ApiError['kind'], message: string, retriable: boolean): ApiError {
return { kind, message, retriable };
}
async function parseError(res: Response): Promise<ApiError> {
let detail = '';
try {
const body = await res.json();
detail = (body && (body.detail || body.message || body.error)) ?? '';
} catch {
/* non-JSON error body */
}
if (res.status === 413) return apiError('too_large', detail || 'That image is too large.', true);
if (res.status === 415) return apiError('wrong_type', detail || 'That file type is not supported.', true);
if (res.status >= 500) return apiError('inference_failed', detail || 'The server hit an error. Try again.', true);
return apiError('unknown', detail || `Request failed (${res.status}).`, true);
}
async function getJSON<T>(path: string, signal?: AbortSignal): Promise<T> {
const res = await fetch(path, { signal });
if (!res.ok) throw await parseError(res);
return (await res.json()) as T;
}
async function postForm<T>(path: string, form: FormData, signal?: AbortSignal): Promise<T> {
const res = await fetch(path, { method: 'POST', body: form, signal });
if (!res.ok) throw await parseError(res);
return (await res.json()) as T;
}
// ---- endpoints -------------------------------------------------------------
export async function getHealth(): Promise<HealthResponse> {
if (isMockActive()) return { status: 'ok', models_loaded: true, device: 'mock' };
try {
return await getJSON<HealthResponse>('/api/health');
} catch {
if (IS_DEV) {
mockLatched = true;
return { status: 'ok', models_loaded: true, device: 'mock' };
}
throw apiError('network', 'Could not reach the server.', true);
}
}
export async function getMethods(): Promise<MethodsResponse> {
if (isMockActive()) return MOCK_METHODS;
try {
return await getJSON<MethodsResponse>('/api/methods');
} catch (e) {
if (IS_DEV) {
mockLatched = true;
return MOCK_METHODS;
}
throw e;
}
}
export async function postRestore(
file: File | Blob,
method: MethodId,
reference?: File | Blob,
signal?: AbortSignal,
): Promise<RestoreResponse> {
if (isMockActive()) {
await delay(900);
return mockRestore(method, 'moderate');
}
const form = new FormData();
form.append('file', file);
form.append('method', method);
if (reference) form.append('reference', reference);
return postForm<RestoreResponse>('/api/restore', form, signal);
}
export async function postSimulate(
file: File | Blob,
severity: Severity,
opts?: { seed?: number; thenRestore?: boolean; signal?: AbortSignal },
): Promise<SimulateResponse> {
if (isMockActive()) {
await delay(800);
return mockSimulate(severity, opts?.thenRestore ?? false);
}
const form = new FormData();
form.append('file', file);
form.append('severity', severity);
if (opts?.seed != null) form.append('seed', String(opts.seed));
if (opts?.thenRestore) form.append('then_restore', 'true');
return postForm<SimulateResponse>('/api/simulate', form, opts?.signal);
}
export async function postEnhance(
file: File | Blob,
signal?: AbortSignal,
): Promise<EnhanceResponse> {
if (isMockActive()) {
await delay(1400);
return mockEnhance();
}
const form = new FormData();
form.append('file', file);
return postForm<EnhanceResponse>('/api/enhance', form, signal);
}
function delay(ms: number): Promise<void> {
return new Promise((r) => setTimeout(r, ms));
}
export function isApiError(e: unknown): e is ApiError {
return typeof e === 'object' && e !== null && 'kind' in e && 'message' in e;
}
|