Files
ttstd-infinite-canvas/web/src/lib/canvas/canvas-face-detection.ts
ddcat666 d2fbc413b3 feat: 完成短剧项目空间与结构化资产创作闭环 (#38)
* feat(canvas): 短剧项目空间 - 建立项目章节与画布关联 MVP

* feat(canvas): 短剧项目空间 - 完善项目资产、镜头与流程闭环

Refs #30

* fix(deploy): 本地部署 - 修复前端构建并忽略本地 PostgreSQL 配置

* feat(*): 短剧项目空间 - 完成项目、章节、资产、角色与画布创作闭环

Refs #30

---------

Co-authored-by: ddCat <ddcat666@88.com>
2026-07-27 10:45:11 +08:00

80 lines
2.7 KiB
TypeScript

import type { CanvasFaceBox } from "./canvas-emotion";
type DetectFaceResponse = {
id: number;
faces?: CanvasFaceBox[];
imageWidth?: number;
imageHeight?: number;
error?: string;
};
type PendingRequest = {
resolve: (result: CanvasFaceDetectionResult) => void;
reject: (error: Error) => void;
cleanup: () => void;
};
export type CanvasFaceDetectionResult = {
faces: CanvasFaceBox[];
imageWidth: number;
imageHeight: number;
};
let detectorWorker: Worker | null = null;
let requestSequence = 0;
const pendingRequests = new Map<number, PendingRequest>();
export async function detectCanvasFaces(dataUrl: string, signal?: AbortSignal): Promise<CanvasFaceDetectionResult> {
if (signal?.aborted) throw new DOMException("人脸识别已取消", "AbortError");
const response = await fetch(dataUrl, { signal });
if (!response.ok) throw new Error("无法读取源图片,请重新上传后再试");
const image = await createImageBitmap(await response.blob());
const worker = getDetectorWorker();
const id = ++requestSequence;
return new Promise((resolve, reject) => {
const abort = () => {
const request = pendingRequests.get(id);
if (!request) return;
pendingRequests.delete(id);
request.cleanup();
reject(new DOMException("人脸识别已取消", "AbortError"));
};
const cleanup = () => signal?.removeEventListener("abort", abort);
pendingRequests.set(id, { resolve, reject, cleanup });
signal?.addEventListener("abort", abort, { once: true });
worker.postMessage({ id, image }, [image]);
});
}
function getDetectorWorker() {
if (detectorWorker) return detectorWorker;
const worker = new Worker(new URL("./canvas-face-detector.worker.ts", import.meta.url), { type: "module" });
worker.onmessage = (event: MessageEvent<DetectFaceResponse>) => {
const request = pendingRequests.get(event.data.id);
if (!request) return;
pendingRequests.delete(event.data.id);
request.cleanup();
if (event.data.error) {
request.reject(new Error(event.data.error));
return;
}
request.resolve({
faces: event.data.faces || [],
imageWidth: event.data.imageWidth || 0,
imageHeight: event.data.imageHeight || 0,
});
};
worker.onerror = (event) => {
const error = new Error(event.message || "人脸识别服务初始化失败");
pendingRequests.forEach((request) => {
request.cleanup();
request.reject(error);
});
pendingRequests.clear();
worker.terminate();
detectorWorker = null;
};
detectorWorker = worker;
return worker;
}