image-tool / frontend /src /hooks /useFaceRestorePolling.ts
JunyoungK7's picture
Claude Sonnet 4.6
Deploy to HF Spaces: Docker setup, CPU inference, env-gated config
a3953cb
Raw History Blame Contribute Delete
5.14 kB
/**
* useFaceRestorePolling.ts — polling hook for Face Restoration jobs.
*
* INDEPENDENT of useBgRemovePolling.ts, useUpscalePolling.ts, and
* useJobPolling.ts. Structurally mirrors useBgRemovePolling.ts (same
* elapsed-timer + recursive-setTimeout pattern) but imports from
* facerestoreClient and carries the three extra face-count fields returned
* by the /facerestore/result endpoint.
*
* Why not reuse useBgRemovePolling.ts?
* The face restoration result shape differs — it carries faces_detected,
* faces_restored, faces_rejected — so parameterizing a shared hook would
* push type complexity into it for no benefit. Independent hooks per feature
* is the established pattern in this codebase.
*
* Elapsed timer
* -------------
* startPolling() records a start timestamp. A setInterval fires every second
* to update elapsedSeconds = floor((now - start) / 1000) for display.
* The interval is cleared on completion, failure, or reset.
*
* Polling
* -------
* Recursive-setTimeout: schedules the next poll only after the previous
* response arrives. 2 s interval.
*/
import { useCallback, useEffect, useRef, useState } from "react";
import { getFaceRestoreResult } from "../api/facerestoreClient";
const POLL_INTERVAL_MS = 2000;
export type FaceRestorePhase = "idle" | "polling" | "completed" | "failed";
interface FaceRestoreState {
phase: FaceRestorePhase;
resultImageUrl: string | null;
errorMessage: string | null;
elapsedSeconds: number;
facesDetected: number;
facesRestored: number;
facesRejected: number;
}
const INITIAL: FaceRestoreState = {
phase: "idle",
resultImageUrl: null,
errorMessage: null,
elapsedSeconds: 0,
facesDetected: 0,
facesRestored: 0,
facesRejected: 0,
};
export function useFaceRestorePolling() {
const [state, setState] = useState<FaceRestoreState>(INITIAL);
const pollTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const elapsedInterval = useRef<ReturnType<typeof setInterval> | null>(null);
const startTimeRef = useRef<number>(0);
const isMountedRef = useRef(true);
useEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
clearTimers();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
function clearTimers() {
if (pollTimeoutRef.current !== null) {
clearTimeout(pollTimeoutRef.current);
pollTimeoutRef.current = null;
}
if (elapsedInterval.current !== null) {
clearInterval(elapsedInterval.current);
elapsedInterval.current = null;
}
}
const startPolling = useCallback((jobId: string) => {
clearTimers();
if (!isMountedRef.current) return;
startTimeRef.current = Date.now();
setState({
phase: "polling",
resultImageUrl: null,
errorMessage: null,
elapsedSeconds: 0,
facesDetected: 0,
facesRestored: 0,
facesRejected: 0,
});
elapsedInterval.current = setInterval(() => {
if (isMountedRef.current) {
setState(prev => ({
...prev,
elapsedSeconds: Math.floor((Date.now() - startTimeRef.current) / 1000),
}));
}
}, 1000);
const poll = async () => {
try {
const result = await getFaceRestoreResult(jobId);
if (!isMountedRef.current) return;
if (result.status === "completed") {
clearTimers();
setState({
phase: "completed",
resultImageUrl: result.result_image_url,
errorMessage: null,
elapsedSeconds: Math.floor((Date.now() - startTimeRef.current) / 1000),
facesDetected: result.faces_detected,
facesRestored: result.faces_restored,
facesRejected: result.faces_rejected,
});
} else if (result.status === "failed") {
clearTimers();
setState(prev => ({
...prev,
phase: "failed",
errorMessage: result.error_message ?? "Face restoration failed with no details.",
}));
} else {
// "pending" or "processing" — keep polling
pollTimeoutRef.current = setTimeout(poll, POLL_INTERVAL_MS);
}
} catch (err) {
if (!isMountedRef.current) return;
clearTimers();
setState(prev => ({
...prev,
phase: "failed",
errorMessage: `Network error while polling: ${(err as Error).message}`,
}));
}
};
poll();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const reset = useCallback(() => {
clearTimers();
if (isMountedRef.current) setState(INITIAL);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return {
phase: state.phase,
resultImageUrl: state.resultImageUrl,
errorMessage: state.errorMessage,
elapsedSeconds: state.elapsedSeconds,
facesDetected: state.facesDetected,
facesRestored: state.facesRestored,
facesRejected: state.facesRejected,
startPolling,
reset,
};
}