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>
|
||||||
</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 -->
|
<!-- ASRX subtitle bar -->
|
||||||
<div v-if="currentAsrxMark" class="ms-video-subtitle-bar">
|
<div v-if="currentAsrxMark" class="ms-video-subtitle-bar">
|
||||||
<div class="ms-subtitle-text">{{ currentAsrxMark.note }}</div>
|
<div class="ms-subtitle-text">{{ currentAsrxMark.note }}</div>
|
||||||
@@ -193,6 +218,8 @@ const props = withDefaults(defineProps<{
|
|||||||
useOriginal?: boolean
|
useOriginal?: boolean
|
||||||
showAsrx?: boolean
|
showAsrx?: boolean
|
||||||
showOcr?: boolean
|
showOcr?: boolean
|
||||||
|
showFace?: boolean
|
||||||
|
showPose?: boolean
|
||||||
}>(), {
|
}>(), {
|
||||||
startFrame: 0,
|
startFrame: 0,
|
||||||
endFrame: 0,
|
endFrame: 0,
|
||||||
@@ -204,6 +231,8 @@ const props = withDefaults(defineProps<{
|
|||||||
useOriginal: false,
|
useOriginal: false,
|
||||||
showAsrx: false,
|
showAsrx: false,
|
||||||
showOcr: false,
|
showOcr: false,
|
||||||
|
showFace: false,
|
||||||
|
showPose: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'trace-change', 'segment-change'])
|
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() {
|
async function loadProcessorMarks() {
|
||||||
if (!props.fileUuid || props.simple) return
|
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' })
|
const asrxData: any = await apiCall('get_processor_json', { fileHash: props.fileUuid, processor: 'asrx' })
|
||||||
if (asrxData && asrxData.segments) {
|
if (asrxData && asrxData.segments) {
|
||||||
for (const seg of asrxData.segments) {
|
for (const seg of asrxData.segments) {
|
||||||
|
const speaker = seg.speaker || 'Speaker'
|
||||||
|
const text = seg.text || ''
|
||||||
marks.value.push({
|
marks.value.push({
|
||||||
id: nextMarkId++,
|
id: nextMarkId++,
|
||||||
startFrame: seg.start_frame || 0,
|
startFrame: seg.start_frame || 0,
|
||||||
endFrame: seg.end_frame,
|
endFrame: seg.end_frame,
|
||||||
tag: 'asrx',
|
tag: 'asrx',
|
||||||
note: seg.text,
|
note: `${speaker}: ${text}`,
|
||||||
source: 'asrx'
|
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) {
|
} catch (e) {
|
||||||
console.error('[loadProcessorMarks] Error:', 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() {
|
function startMark() {
|
||||||
markingStart.value = currentFrame.value
|
markingStart.value = currentFrame.value
|
||||||
}
|
}
|
||||||
@@ -1081,6 +1168,60 @@ onUnmounted(() => {
|
|||||||
text-overflow: ellipsis;
|
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 {
|
.ms-video-subtitle-bar {
|
||||||
background: rgba(0, 0, 0, 0.92);
|
background: rgba(0, 0, 0, 0.92);
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
|
|||||||
Reference in New Issue
Block a user