Zhen Ye
feat: Continuous object tracking, speed estimation, and overlay syncing
ff50694
// 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();
};