Add auto archived overlay recordings for live analysis

这个提交包含在:
cryptocommuniums-afk
2026-03-16 11:59:51 +08:00
父节点 e3fe9a8e7b
当前提交 4fb2d092d7
修改 7 个文件,包含 377 行新增60 行删除

查看文件

@@ -8,6 +8,26 @@ export type ChangeLogEntry = {
};
export const CHANGE_LOG_ENTRIES: ChangeLogEntry[] = [
{
version: "2026.03.16-live-analysis-overlay-archive",
releaseDate: "2026-03-16",
repoVersion: "e3fe9a8 + local changes",
summary: "实时分析新增 60 秒自动归档录像,录制内容会保留骨架、关键点和虚拟形象叠层,并同步进入视频库。",
features: [
"实时分析开始后会自动录制合成画布,每 60 秒自动切段归档",
"归档录像会保留原视频、骨架线、关键点和当前虚拟形象覆盖效果",
"归档片段会自动写入视频库,标签显示为“实时分析”",
"删除视频库中的实时分析录像时,不会删除已写入的实时分析数据和训练记录",
"线上 smoke 已确认 `https://te.hao.work/` 已切换到本次新构建,`/live-camera`、`/videos`、`/changelog` 页面均可正常访问",
],
tests: [
"pnpm check",
"pnpm test",
"pnpm build",
"pnpm test:e2e",
"Playwright smoke: 真实站点登录 H1,完成 /live-camera 引导、开始/结束分析,并确认 /videos 可见实时分析条目",
],
},
{
version: "2026.03.15-live-analysis-leave-hint",
releaseDate: "2026-03-15",

查看文件

@@ -662,18 +662,22 @@ function drawFullFigureAvatar(
drawLimbs(ctx, anchors, visual.limbStroke);
}
export function drawLiveCameraOverlay(
canvas: HTMLCanvasElement | null,
export function renderLiveCameraOverlayToContext(
ctx: CanvasRenderingContext2D | null,
width: number,
height: number,
landmarks: PosePoint[] | undefined,
avatarState?: AvatarRenderState,
options?: { clear?: boolean },
) {
const ctx = canvas?.getContext("2d");
if (!canvas || !ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (!ctx) return;
if (options?.clear !== false) {
ctx.clearRect(0, 0, width, height);
}
if (!landmarks) return;
if (avatarState?.enabled) {
const anchors = getAvatarAnchors(landmarks, canvas.width, canvas.height);
const anchors = getAvatarAnchors(landmarks, width, height);
if (anchors) {
const sprite = getAvatarImage(avatarState.avatarKey);
const visual = AVATAR_VISUALS[avatarState.avatarKey];
@@ -715,8 +719,8 @@ export function drawLiveCameraOverlay(
const end = landmarks[to];
if (!start || !end || (start.visibility ?? 1) < 0.25 || (end.visibility ?? 1) < 0.25) return;
ctx.beginPath();
ctx.moveTo(start.x * canvas.width, start.y * canvas.height);
ctx.lineTo(end.x * canvas.width, end.y * canvas.height);
ctx.moveTo(start.x * width, start.y * height);
ctx.lineTo(end.x * width, end.y * height);
ctx.stroke();
});
@@ -724,7 +728,17 @@ export function drawLiveCameraOverlay(
if ((point.visibility ?? 1) < 0.25) return;
ctx.fillStyle = index >= 11 && index <= 16 ? "rgba(253, 224, 71, 0.95)" : "rgba(255,255,255,0.88)";
ctx.beginPath();
ctx.arc(point.x * canvas.width, point.y * canvas.height, index >= 11 && index <= 16 ? 5 : 4, 0, Math.PI * 2);
ctx.arc(point.x * width, point.y * height, index >= 11 && index <= 16 ? 5 : 4, 0, Math.PI * 2);
ctx.fill();
});
}
export function drawLiveCameraOverlay(
canvas: HTMLCanvasElement | null,
landmarks: PosePoint[] | undefined,
avatarState?: AvatarRenderState,
) {
const ctx = canvas?.getContext("2d");
if (!canvas || !ctx) return;
renderLiveCameraOverlayToContext(ctx, canvas.width, canvas.height, landmarks, avatarState, { clear: true });
}