feat: add Face bbox and Pose indicator to VideoPlayer

Features:
- Face bbox overlay with identity label
- Pose indicator (top-right corner)
- Extended loadProcessorMarks for all processors
- ASRX speaker:text format

Overlays:
- OCR: orange bbox + text label
- Face: blue bbox + identity name
- Pose: purple indicator badge

New props:
- showFace: boolean
- showPose: boolean

Data formats:
- Face: { frames: [{ frame, faces: [{ bbox, identity_name }] }] }
- Pose: { frames: [{ frame, pose }] }
- ASRX: { segments: [{ start_frame, end_frame, speaker, text }] }
This commit is contained in:
2026-07-24 23:17:31 +08:00
parent b1a9c44778
commit 6a34a862f4

View File

@@ -41,6 +41,31 @@
</div>
</div>
<!-- Face bbox overlay -->
<div v-if="currentFaceMarks.length" class="ms-video-face-overlay">
<div
v-for="mark in currentFaceMarks"
:key="mark.id"
class="ms-face-bbox"
:style="{
left: (mark.metadata?.bbox?.x || 0) + 'px',
top: (mark.metadata?.bbox?.y || 0) + 'px',
width: (mark.metadata?.bbox?.width || 100) + 'px',
height: (mark.metadata?.bbox?.height || 100) + 'px'
}"
>
<span class="ms-face-label">{{ mark.note }}</span>
</div>
</div>
<!-- Pose indicator -->
<div v-if="currentPoseMarks.length" class="ms-video-pose-overlay">
<div class="ms-pose-indicator">
<span class="ms-pose-icon">🏃</span>
<span class="ms-pose-text">Pose detected</span>
</div>
</div>
<!-- ASRX subtitle bar -->
<div v-if="currentAsrxMark" class="ms-video-subtitle-bar">
<div class="ms-subtitle-text">{{ currentAsrxMark.note }}</div>
@@ -193,6 +218,8 @@ const props = withDefaults(defineProps<{
useOriginal?: boolean
showAsrx?: boolean
showOcr?: boolean
showFace?: boolean
showPose?: boolean
}>(), {
startFrame: 0,
endFrame: 0,
@@ -204,6 +231,8 @@ const props = withDefaults(defineProps<{
useOriginal: false,
showAsrx: false,
showOcr: false,
showFace: false,
showPose: false,
})
const emit = defineEmits(['close', 'trace-change', 'segment-change'])
@@ -264,7 +293,7 @@ function loadTracesAsMarks() {
}
}
// Load ASRX/OCR data as marks
// Load ASRX/OCR/Face/Pose data as marks
async function loadProcessorMarks() {
if (!props.fileUuid || props.simple) return
@@ -274,13 +303,16 @@ async function loadProcessorMarks() {
const asrxData: any = await apiCall('get_processor_json', { fileHash: props.fileUuid, processor: 'asrx' })
if (asrxData && asrxData.segments) {
for (const seg of asrxData.segments) {
const speaker = seg.speaker || 'Speaker'
const text = seg.text || ''
marks.value.push({
id: nextMarkId++,
startFrame: seg.start_frame || 0,
endFrame: seg.end_frame,
tag: 'asrx',
note: seg.text,
source: 'asrx'
note: `${speaker}: ${text}`,
source: 'asrx',
metadata: { speaker: seg.speaker }
})
}
}
@@ -302,6 +334,47 @@ async function loadProcessorMarks() {
}
}
}
// Load Face
if (props.showFace) {
const faceData: any = await apiCall('get_processor_json', { fileHash: props.fileUuid, processor: 'face' })
if (faceData && faceData.frames) {
for (const frame of faceData.frames) {
const faces = frame.faces || []
for (const face of faces) {
marks.value.push({
id: nextMarkId++,
startFrame: frame.frame,
tag: 'face',
note: face.identity_name || face.person_name || `Face #${face.face_id}`,
source: 'face',
metadata: {
bbox: face.bbox,
face_id: face.face_id,
confidence: face.confidence
}
})
}
}
}
}
// Load Pose
if (props.showPose) {
const poseData: any = await apiCall('get_processor_json', { fileHash: props.fileUuid, processor: 'pose' })
if (poseData && poseData.frames) {
for (const frame of poseData.frames) {
marks.value.push({
id: nextMarkId++,
startFrame: frame.frame,
tag: 'pose',
note: 'Pose detected',
source: 'pose',
metadata: { pose: frame.pose }
})
}
}
}
} catch (e) {
console.error('[loadProcessorMarks] Error:', e)
}
@@ -347,6 +420,20 @@ const currentOcrMarks = computed(() => {
)
})
const currentFaceMarks = computed(() => {
const frame = currentFrame.value
return marks.value.filter(m =>
m.tag === 'face' && m.startFrame === frame
)
})
const currentPoseMarks = computed(() => {
const frame = currentFrame.value
return marks.value.filter(m =>
m.tag === 'pose' && m.startFrame === frame
)
})
function startMark() {
markingStart.value = currentFrame.value
}
@@ -1081,6 +1168,60 @@ onUnmounted(() => {
text-overflow: ellipsis;
}
.ms-video-face-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 60px;
pointer-events: none;
z-index: 11;
}
.ms-face-bbox {
position: absolute;
border: 2px solid #4285f4;
background: rgba(66, 133, 244, 0.1);
border-radius: 6px;
}
.ms-face-label {
position: absolute;
bottom: -20px;
left: 0;
background: #4285f4;
color: #fff;
padding: 2px 8px;
border-radius: 3px;
font-size: 10px;
white-space: nowrap;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
}
.ms-video-pose-overlay {
position: absolute;
top: 12px;
right: 12px;
z-index: 12;
}
.ms-pose-indicator {
background: rgba(156, 39, 176, 0.9);
color: #fff;
padding: 6px 12px;
border-radius: 6px;
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
}
.ms-pose-icon {
font-size: 14px;
}
.ms-video-subtitle-bar {
background: rgba(0, 0, 0, 0.92);
padding: 10px 20px;