fix: resolve TypeScript compilation errors

Fixes:
- VideoPlayer.vue: restore allTags, filteredMarks, marksByTag (removed by mistake)
- PeopleView.vue: fix frameTime scope in playFaceDetailVideo catch block
- Both errors caused white screen on build failure

Root cause:
1. visibleTags duplicate declaration (old + new logic)
2. frameTime undefined in catch block (scope issue)

Solution:
1. Keep allTags/filteredMarks/marksByTag (needed in template)
2. Keep visibleTags (new logic for processor toggles)
3. Move frameTime calculation to correct scope in catch block
This commit is contained in:
2026-07-24 23:33:40 +08:00
parent 48975ef247
commit cb7e1ba0ba
37 changed files with 6875 additions and 205 deletions

60
src/utils/mockPoseData.ts Normal file
View File

@@ -0,0 +1,60 @@
import type { PoseResponse, AppearanceResponse } from '@/api/types'
export const mockPose: PoseResponse = {
frame: 100,
keypoints: [
{ name: 'nose', x: 100, y: 50, confidence: 0.9 },
{ name: 'left_eye', x: 90, y: 45, confidence: 0.95 },
{ name: 'right_eye', x: 110, y: 45, confidence: 0.95 },
{ name: 'left_ear', x: 80, y: 50, confidence: 0.85 },
{ name: 'right_ear', x: 120, y: 50, confidence: 0.85 },
{ name: 'left_shoulder', x: 60, y: 100, confidence: 0.9 },
{ name: 'right_shoulder', x: 140, y: 100, confidence: 0.9 },
{ name: 'left_elbow', x: 50, y: 150, confidence: 0.85 },
{ name: 'right_elbow', x: 150, y: 150, confidence: 0.85 },
{ name: 'left_wrist', x: 45, y: 190, confidence: 0.8 },
{ name: 'right_wrist', x: 155, y: 190, confidence: 0.8 },
{ name: 'left_hip', x: 70, y: 200, confidence: 0.9 },
{ name: 'right_hip', x: 130, y: 200, confidence: 0.9 },
{ name: 'left_knee', x: 65, y: 280, confidence: 0.85 },
{ name: 'right_knee', x: 135, y: 280, confidence: 0.85 },
{ name: 'left_ankle', x: 60, y: 350, confidence: 0.8 },
{ name: 'right_ankle', x: 140, y: 350, confidence: 0.8 },
],
pose_class: 'standing',
confidence: 0.92
}
export const mockAppearance: AppearanceResponse = {
frame: 100,
dominant_colors: [
{ rgb: [255, 100, 50], percentage: 0.35 },
{ rgb: [50, 150, 200], percentage: 0.25 },
{ rgb: [100, 200, 100], percentage: 0.15 },
]
}
export const mockSittingPose: PoseResponse = {
frame: 200,
keypoints: [
{ name: 'nose', x: 100, y: 60, confidence: 0.88 },
{ name: 'left_eye', x: 90, y: 55, confidence: 0.92 },
{ name: 'right_eye', x: 110, y: 55, confidence: 0.92 },
{ name: 'left_ear', x: 80, y: 60, confidence: 0.82 },
{ name: 'right_ear', x: 120, y: 60, confidence: 0.82 },
{ name: 'left_shoulder', x: 60, y: 110, confidence: 0.88 },
{ name: 'right_shoulder', x: 140, y: 110, confidence: 0.88 },
{ name: 'left_elbow', x: 55, y: 140, confidence: 0.83 },
{ name: 'right_elbow', x: 145, y: 140, confidence: 0.83 },
{ name: 'left_wrist', x: 50, y: 160, confidence: 0.78 },
{ name: 'right_wrist', x: 150, y: 160, confidence: 0.78 },
{ name: 'left_hip', x: 70, y: 180, confidence: 0.88 },
{ name: 'right_hip', x: 130, y: 180, confidence: 0.88 },
{ name: 'left_knee', x: 90, y: 200, confidence: 0.83 },
{ name: 'right_knee', x: 110, y: 200, confidence: 0.83 },
{ name: 'left_ankle', x: 85, y: 240, confidence: 0.78 },
{ name: 'right_ankle', x: 115, y: 240, confidence: 0.78 },
],
pose_class: 'sitting',
confidence: 0.87
}

264
src/utils/poseRenderer.ts Normal file
View File

@@ -0,0 +1,264 @@
import type { PoseResponse, AppearanceResponse } from '@/api/types'
const SKELETON_CONNECTIONS: [string, string][] = [
['nose', 'left_eye'],
['nose', 'right_eye'],
['left_eye', 'left_ear'],
['right_eye', 'right_ear'],
['left_shoulder', 'right_shoulder'],
['left_shoulder', 'left_elbow'],
['left_elbow', 'left_wrist'],
['right_shoulder', 'right_elbow'],
['right_elbow', 'right_wrist'],
['left_shoulder', 'left_hip'],
['right_shoulder', 'right_hip'],
['left_hip', 'right_hip'],
['left_hip', 'left_knee'],
['left_knee', 'left_ankle'],
['right_hip', 'right_knee'],
['right_knee', 'right_ankle'],
]
function transformKeypoints(
keypoints: Array<{ name: string; x: number; y: number; confidence?: number }>,
canvasWidth: number,
canvasHeight: number
): Map<string, { x: number; y: number; confidence: number }> {
console.log('[transformKeypoints] input keypoints:', keypoints?.length)
const validKeypoints = keypoints.filter(kp => {
const conf = kp.confidence ?? 0
return conf > 0 && kp.x > 0 && kp.y > 0
})
console.log('[transformKeypoints] valid keypoints:', validKeypoints.length)
if (validKeypoints.length === 0) {
return new Map()
}
let minX = Infinity, maxX = -Infinity
let minY = Infinity, maxY = -Infinity
for (const kp of validKeypoints) {
minX = Math.min(minX, kp.x)
maxX = Math.max(maxX, kp.x)
minY = Math.min(minY, kp.y)
maxY = Math.max(maxY, kp.y)
}
const width = maxX - minX || 1
const height = maxY - minY || 1
console.log('[transformKeypoints] bounding box:', minX, minY, 'to', maxX, maxY)
console.log('[transformKeypoints] bbox size:', width, 'x', height)
const padding = 20
const availableWidth = canvasWidth - 2 * padding
const availableHeight = canvasHeight - 2 * padding
const scaleX = availableWidth / width
const scaleY = availableHeight / height
const scale = Math.min(scaleX, scaleY)
const scaledWidth = width * scale
const scaledHeight = height * scale
const offsetX = (canvasWidth - scaledWidth) / 2 - minX * scale
const offsetY = (canvasHeight - scaledHeight) / 2 - minY * scale
console.log('[transformKeypoints] scale:', scale, 'offset:', offsetX, offsetY)
const keypointMap = new Map<string, { x: number; y: number; confidence: number }>()
for (const kp of keypoints) {
const conf = kp.confidence ?? 0
keypointMap.set(kp.name, {
x: kp.x * scale + offsetX,
y: kp.y * scale + offsetY,
confidence: conf
})
}
console.log('[transformKeypoints] First 3 transformed:',
[...keypointMap.entries()].slice(0, 3).map(([k, v]) => ({ name: k, ...v })))
return keypointMap
}
export function drawPoseSkeleton(
canvas: HTMLCanvasElement,
pose: PoseResponse,
options?: {
lineWidth?: number
pointRadius?: number
lineColor?: string
pointColor?: string
videoWidth?: number
videoHeight?: number
noTransform?: boolean
}
) {
console.log('[drawPoseSkeleton] canvas size:', canvas.width, 'x', canvas.height)
console.log('[drawPoseSkeleton] pose:', pose)
const ctx = canvas.getContext('2d')
if (!ctx) {
console.error('[drawPoseSkeleton] No context')
return
}
const {
lineWidth = 2,
pointRadius = 4,
lineColor = '#00ff00',
pointColor = '#ff0000',
noTransform = false
} = options || {}
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 不轉換,直接畫原始座標(用於 debug)
if (noTransform) {
console.log('[drawPoseSkeleton] Drawing raw keypoints (no transform)')
const validKeypoints = pose.keypoints.filter(kp => kp.confidence > 0 && kp.x > 0 && kp.y > 0)
if (validKeypoints.length === 0) {
ctx.fillStyle = '#9aa0a6'
ctx.font = '12px sans-serif'
ctx.textAlign = 'center'
ctx.fillText('No valid keypoints', canvas.width / 2, canvas.height / 2)
return
}
// 找 bounding box
let minX = Infinity, maxX = -Infinity
let minY = Infinity, maxY = -Infinity
for (const kp of validKeypoints) {
minX = Math.min(minX, kp.x)
maxX = Math.max(maxX, kp.x)
minY = Math.min(minY, kp.y)
maxY = Math.max(maxY, kp.y)
}
console.log('[drawPoseSkeleton] Raw bbox:', minX, minY, 'to', maxX, maxY)
// 繪製
ctx.fillStyle = pointColor
for (const kp of validKeypoints) {
ctx.beginPath()
ctx.arc(kp.x, kp.y, pointRadius, 0, Math.PI * 2)
ctx.fill()
}
return
}
const keypointMap = transformKeypoints(pose.keypoints, canvas.width, canvas.height)
console.log('[drawPoseSkeleton] transformed keypoints:', keypointMap.size)
if (keypointMap.size === 0) {
console.log('[drawPoseSkeleton] No valid keypoints')
ctx.fillStyle = '#9aa0a6'
ctx.font = '12px sans-serif'
ctx.textAlign = 'center'
ctx.fillText('No pose detected', canvas.width / 2, canvas.height / 2)
return
}
ctx.strokeStyle = lineColor
ctx.lineWidth = lineWidth
for (const [from, to] of SKELETON_CONNECTIONS) {
const fromPt = keypointMap.get(from)
const toPt = keypointMap.get(to)
if (fromPt && toPt && fromPt.confidence > 0 && toPt.confidence > 0) {
ctx.beginPath()
ctx.moveTo(fromPt.x, fromPt.y)
ctx.lineTo(toPt.x, toPt.y)
ctx.stroke()
}
}
ctx.fillStyle = pointColor
for (const kp of pose.keypoints) {
const transformed = keypointMap.get(kp.name)
if (transformed && transformed.confidence > 0) {
ctx.beginPath()
ctx.arc(transformed.x, transformed.y, pointRadius, 0, Math.PI * 2)
ctx.fill()
}
}
}
export function drawAppearanceOverlay(
canvas: HTMLCanvasElement,
appearance: AppearanceResponse,
pose?: PoseResponse
) {
const ctx = canvas.getContext('2d')
if (!ctx) return
ctx.clearRect(0, 0, canvas.width, canvas.height)
if (!appearance.dominant_colors?.length) {
if (appearance.hsv_histogram && appearance.hsv_histogram.length > 0) {
ctx.fillStyle = '#f8f9fa'
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = '#5f6368'
ctx.font = '12px sans-serif'
ctx.textAlign = 'center'
ctx.fillText('HSV histogram', canvas.width / 2, canvas.height / 2 - 10)
ctx.fillText(`(${appearance.hsv_histogram.length} channels)`, canvas.width / 2, canvas.height / 2 + 10)
}
return
}
if (!pose) {
const swatchWidth = canvas.width / appearance.dominant_colors.length
appearance.dominant_colors.forEach((color, i) => {
ctx.fillStyle = `rgb(${color.rgb.join(',')})`
ctx.fillRect(i * swatchWidth, 0, swatchWidth, canvas.height)
if (color.percentage) {
ctx.fillStyle = '#fff'
ctx.font = '12px sans-serif'
ctx.fillText(`${Math.round(color.percentage * 100)}%`, i * swatchWidth + 4, canvas.height - 8)
}
})
return
}
const keypointMap = transformKeypoints(pose.keypoints, canvas.width, canvas.height)
if (keypointMap.size === 0) return
ctx.globalAlpha = 0.4
const upperBodyPts = ['left_shoulder', 'right_shoulder', 'left_elbow', 'right_elbow']
const lowerBodyPts = ['left_hip', 'right_hip', 'left_knee', 'right_knee']
if (appearance.dominant_colors[0]) {
const color = appearance.dominant_colors[0]
ctx.fillStyle = `rgb(${color.rgb.join(',')})`
for (const name of upperBodyPts) {
const pt = keypointMap.get(name)
if (pt && pt.confidence > 0) {
ctx.beginPath()
ctx.arc(pt.x, pt.y, 15, 0, Math.PI * 2)
ctx.fill()
}
}
}
if (appearance.dominant_colors[1]) {
const color = appearance.dominant_colors[1]
ctx.fillStyle = `rgb(${color.rgb.join(',')})`
for (const name of lowerBodyPts) {
const pt = keypointMap.get(name)
if (pt && pt.confidence > 0) {
ctx.beginPath()
ctx.arc(pt.x, pt.y, 15, 0, Math.PI * 2)
ctx.fill()
}
}
}
ctx.globalAlpha = 1.0
}