Spaces:
Sleeping
Sleeping
| // API Client Module - Backend communication | |
| APP.api.client = {}; | |
| APP.api.client.hfDetectAsync = async function (formData) { | |
| const { state } = APP.core; | |
| if (!state.hf.baseUrl) return; | |
| const resp = await fetch(`${state.hf.baseUrl}/detect/async`, { | |
| method: "POST", | |
| body: formData | |
| }); | |
| if (!resp.ok) { | |
| const err = await resp.json().catch(() => ({ detail: resp.statusText })); | |
| throw new Error(err.detail || "Async detection submission failed"); | |
| } | |
| const data = await resp.json(); | |
| // Store URLs from response | |
| if (data.status_url) { | |
| state.hf.statusUrl = data.status_url.startsWith("http") | |
| ? data.status_url | |
| : `${state.hf.baseUrl}${data.status_url}`; | |
| } | |
| if (data.video_url) { | |
| state.hf.videoUrl = data.video_url.startsWith("http") | |
| ? data.video_url | |
| : `${state.hf.baseUrl}${data.video_url}`; | |
| } | |
| if (data.depth_video_url) { | |
| state.hf.depthVideoUrl = data.depth_video_url.startsWith("http") | |
| ? data.depth_video_url | |
| : `${state.hf.baseUrl}${data.depth_video_url}`; | |
| } | |
| if (data.depth_first_frame_url) { | |
| state.hf.depthFirstFrameUrl = data.depth_first_frame_url.startsWith("http") | |
| ? data.depth_first_frame_url | |
| : `${state.hf.baseUrl}${data.depth_first_frame_url}`; | |
| } | |
| return data; | |
| }; | |
| APP.api.client.checkJobStatus = async function (jobId) { | |
| const { state } = APP.core; | |
| if (!state.hf.baseUrl) return { status: "error" }; | |
| const url = state.hf.statusUrl || `${state.hf.baseUrl}/detect/job/${jobId}`; | |
| const resp = await fetch(url, { cache: "no-store" }); | |
| if (!resp.ok) { | |
| if (resp.status === 404) return { status: "not_found" }; | |
| throw new Error(`Status check failed: ${resp.status}`); | |
| } | |
| return await resp.json(); | |
| }; | |
| APP.api.client.cancelBackendJob = async function (jobId, reason) { | |
| const { state } = APP.core; | |
| const { log } = APP.ui.logging; | |
| if (!state.hf.baseUrl || !jobId) return; | |
| // Don't attempt cancel on HF Space (it doesn't support it) | |
| if (state.hf.baseUrl.includes("hf.space")) { | |
| log(`Job cancel skipped for HF Space (${reason || "user request"})`, "w"); | |
| return { status: "skipped", message: "Cancel disabled for HF Space" }; | |
| } | |
| try { | |
| const resp = await fetch(`${state.hf.baseUrl}/detect/job/${jobId}`, { | |
| method: "DELETE" | |
| }); | |
| if (resp.ok) { | |
| const result = await resp.json(); | |
| log(`Job ${jobId.substring(0, 8)} cancelled`, "w"); | |
| return result; | |
| } | |
| if (resp.status === 404) return { status: "not_found" }; | |
| throw new Error("Cancel failed"); | |
| } catch (err) { | |
| log(`Cancel error: ${err.message}`, "e"); | |
| return { status: "error", message: err.message }; | |
| } | |
| }; | |
| APP.api.client.pollAsyncJob = async function () { | |
| const { state } = APP.core; | |
| const { log, setHfStatus } = APP.ui.logging; | |
| const { fetchProcessedVideo, fetchDepthVideo, fetchDepthFirstFrame } = APP.core.video; | |
| const pollInterval = 3000; // 3 seconds | |
| const maxAttempts = 200; // 10 minutes max | |
| let attempts = 0; | |
| let fetchingVideo = false; | |
| return new Promise((resolve, reject) => { | |
| state.hf.asyncPollInterval = setInterval(async () => { | |
| attempts++; | |
| try { | |
| const resp = await fetch(state.hf.statusUrl, { cache: "no-store" }); | |
| if (!resp.ok) { | |
| if (resp.status === 404) { | |
| clearInterval(state.hf.asyncPollInterval); | |
| reject(new Error("Job expired or not found")); | |
| return; | |
| } | |
| throw new Error(`Status check failed: ${resp.statusText}`); | |
| } | |
| const status = await resp.json(); | |
| state.hf.asyncStatus = status.status; | |
| state.hf.asyncProgress = status; | |
| if (status.status === "completed") { | |
| if (fetchingVideo) return; | |
| fetchingVideo = true; | |
| const completedJobId = state.hf.asyncJobId; | |
| log(`✓ Backend job ${completedJobId.substring(0, 8)}: completed successfully`, "g"); | |
| setHfStatus("job completed, fetching video..."); | |
| try { | |
| await fetchProcessedVideo(); | |
| await fetchDepthVideo(); | |
| await fetchDepthFirstFrame(); | |
| clearInterval(state.hf.asyncPollInterval); | |
| state.hf.asyncJobId = null; | |
| setHfStatus("ready"); | |
| resolve(); | |
| } catch (err) { | |
| if (err && err.code === "VIDEO_PENDING") { | |
| setHfStatus("job completed, finalizing video..."); | |
| fetchingVideo = false; | |
| return; | |
| } | |
| clearInterval(state.hf.asyncPollInterval); | |
| state.hf.asyncJobId = null; | |
| reject(err); | |
| } | |
| } else if (status.status === "failed") { | |
| clearInterval(state.hf.asyncPollInterval); | |
| const errMsg = status.error || "Processing failed"; | |
| log(`✗ Backend job ${state.hf.asyncJobId.substring(0, 8)}: failed - ${errMsg}`, "e"); | |
| state.hf.asyncJobId = null; | |
| setHfStatus(`error: ${errMsg}`); | |
| reject(new Error(errMsg)); | |
| } else { | |
| // Still processing | |
| const progressInfo = status.progress ? ` (${Math.round(status.progress * 100)}%)` : ""; | |
| setHfStatus(`job ${state.hf.asyncJobId.substring(0, 8)}: ${status.status}${progressInfo} (${attempts})`); | |
| } | |
| if (attempts >= maxAttempts) { | |
| clearInterval(state.hf.asyncPollInterval); | |
| reject(new Error("Polling timeout (10 minutes)")); | |
| } | |
| } catch (err) { | |
| clearInterval(state.hf.asyncPollInterval); | |
| reject(err); | |
| } | |
| }, pollInterval); | |
| }); | |
| }; | |
| // External detection hook (can be replaced by user) | |
| APP.api.client.externalDetect = async function (input) { | |
| console.log("externalDetect called", input); | |
| return []; | |
| }; | |
| // External features hook (can be replaced by user) | |
| APP.api.client.externalFeatures = async function (detections, frameInfo) { | |
| console.log("externalFeatures called for", detections.length, "objects"); | |
| return {}; | |
| }; | |
| // External tracker hook (can be replaced by user) | |
| APP.api.client.externalTrack = async function (videoEl) { | |
| console.log("externalTrack called"); | |
| return []; | |
| }; | |
| // Call HF object detection directly (for first frame) | |
| APP.api.client.callHfObjectDetection = async function (canvas) { | |
| const { state } = APP.core; | |
| const { canvasToBlob } = APP.core.utils; | |
| const { CONFIG } = APP.core; | |
| const proxyBase = (CONFIG.PROXY_URL || "").trim(); | |
| if (proxyBase) { | |
| const blob = await canvasToBlob(canvas); | |
| const form = new FormData(); | |
| form.append("image", blob, "frame.jpg"); | |
| const resp = await fetch(`${proxyBase.replace(/\/$/, "")}/detect`, { | |
| method: "POST", | |
| body: form | |
| }); | |
| if (!resp.ok) { | |
| let detail = `Proxy inference failed (${resp.status})`; | |
| try { | |
| const err = await resp.json(); | |
| detail = err.detail || err.error || detail; | |
| } catch (_) { } | |
| throw new Error(detail); | |
| } | |
| return await resp.json(); | |
| } | |
| // Default: use the backend base URL | |
| const blob = await canvasToBlob(canvas); | |
| const form = new FormData(); | |
| form.append("image", blob, "frame.jpg"); | |
| const resp = await fetch(`${state.hf.baseUrl}/detect/frame`, { | |
| method: "POST", | |
| body: form | |
| }); | |
| if (!resp.ok) { | |
| throw new Error(`Frame detection failed: ${resp.statusText}`); | |
| } | |
| return await resp.json(); | |
| }; | |