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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user