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