feat: enforce 100% frame-based positioning (no time)

Core Principle: Frame is the ONLY standard

Changes:
- VideoPlayer: Remove startTime/endTime props
- VideoPlayer: Add mandatory startFrame/endFrame check
- VideoPlayer: UI shows F123 (51.2s) format
- All Views: Only set startFrame/endFrame
- All Views: Only pass :start-frame/:end-frame
- Remove all startTime/endTime from currentVideo refs

Time is only used for display (auxiliary).
Frame is the source of truth for all positioning.
This commit is contained in:
2026-07-24 22:49:35 +08:00
parent 76c6ec183d
commit c2901338e1
5 changed files with 48 additions and 40 deletions

View File

@@ -563,7 +563,7 @@
</div>
</div>
<VideoPlayer v-if="playing" simple :file-uuid="currentVideo.fileUuid" :start-time="currentVideo.startTime" :end-time="currentVideo.endTime" :title="currentVideo.title" @close="playing = false" />
<VideoPlayer v-if="playing" simple :file-uuid="currentVideo.fileUuid" :start-frame="currentVideo.startFrame" :end-frame="currentVideo.endFrame" :title="currentVideo.title" @close="playing = false" />
</div>
</template>
@@ -588,7 +588,7 @@ const displayFilter = ref('all')
const showFilter = ref(false)
const selectedFiles = ref<string[]>([])
const playing = ref(false)
const currentVideo = ref({ fileUuid: '', startTime: 0, endTime: 0, title: '' })
const currentVideo = ref({ fileUuid: '', startFrame: 0, endFrame: 0, title: '' })
const processorJsonDialog = ref(false)
const processorJsonContent = ref('')
const processorJsonTitle = ref('')
@@ -842,7 +842,7 @@ function toggleSelect(f: any) {
}
function playVideo(f: any) {
currentVideo.value = { fileUuid: f.file_uuid, startTime: 0, endTime: 99999, title: f.file_name }
currentVideo.value = { fileUuid: f.file_uuid, startFrame: 0, endFrame: 99999, title: `F0-F99999 (${f.file_name})` }
playing.value = true
}

View File

@@ -274,7 +274,7 @@
</div>
</div>
<VideoPlayer v-if="playing" :file-uuid="currentVideo.fileUuid" :start-time="currentVideo.startTime" :end-time="currentVideo.endTime" :all-traces="clusterVideoTraces" :merged-segments="clusterVideoSegments" :title="currentVideo.title" @close="playing = false; clusterVideoTraces = []; clusterVideoSegments = []" />
<VideoPlayer v-if="playing" :file-uuid="currentVideo.fileUuid" :start-frame="currentVideo.startFrame" :end-frame="currentVideo.endFrame" :all-traces="clusterVideoTraces" :merged-segments="clusterVideoSegments" :title="currentVideo.title" @close="playing = false; clusterVideoTraces = []; clusterVideoSegments = []" />
<!-- Merge Target Selection Modal -->
<div v-if="showMergeTargetModal" class="ms-modal-overlay show" @click.self="showMergeTargetModal = false">
@@ -488,7 +488,7 @@ const loading = computed(() => refreshing.value)
const faces = ref<any[]>([])
const traces = ref<any[]>([])
const playing = ref(false)
const currentVideo = ref({ fileUuid: '', startTime: 0, endTime: 0, title: '' })
const currentVideo = ref({ fileUuid: '', startFrame: 0, endFrame: 0, title: '' })
const candidateThumbs = faceThumbsCache
const activeFrameCard = ref('')
const activeFrameMode = ref('')
@@ -1118,7 +1118,12 @@ async function assignClusterTrace(trace: any) {
}
function playTrace(t: any) {
currentVideo.value = { fileUuid: t.file_uuid, startTime: t.first_sec, endTime: t.last_sec, title: `Trace F${t.trace_id} - ${formatTime(t.first_sec)}-${formatTime(t.last_sec)}` }
currentVideo.value = {
fileUuid: t.file_uuid,
startFrame: t.first_frame || 0,
endFrame: t.last_frame || (t.first_frame || 0) + 30,
title: `F${t.first_frame || 0}-F${t.last_frame || 0} (Trace ${t.trace_id})`
}
playing.value = true
}
@@ -1354,21 +1359,20 @@ async function playFaceDetailVideo() {
const frameTime = frame / fps
const startTime = Math.max(0, frameTime - 5)
const endTime = frameTime + 5
currentVideo.value = {
currentVideo.value = {
fileUuid: c.file_uuid,
startTime,
endTime,
title: `Face #${frame} (${Math.round(frameTime)}s)`
startFrame: startTime * fps,
endFrame: endTime * fps,
title: `F${Math.round(startTime * fps)}-F${Math.round(endTime * fps)} (${c.name})`
}
playing.value = true
} catch (e) {
console.error('Failed to get file info:', e)
const frameTime = frame / 30
currentVideo.value = {
fileUuid: c.file_uuid,
startTime: Math.max(0, frameTime - 5),
endTime: frameTime + 5,
title: `Face #${frame}`
startFrame: Math.round(Math.max(0, frameTime - 5) * 30),
endFrame: Math.round((frameTime + 5) * 30),
title: `F${Math.round(Math.max(0, frameTime - 5) * 30)}-F${Math.round((frameTime + 5) * 30)}`
}
playing.value = true
}

View File

@@ -270,7 +270,7 @@
</div>
</div>
<VideoPlayer v-if="playing" :file-uuid="currentVideo.fileUuid" :start-time="currentVideo.startTime" :end-time="currentVideo.endTime" :all-traces="allTraces" :merged-segments="mergedSegments" :initial-trace-idx="currentVideo.traceIdx" :title="currentVideo.title" @close="playing = false" />
<VideoPlayer v-if="playing" :file-uuid="currentVideo.fileUuid" :start-frame="currentVideo.startFrame" :end-frame="currentVideo.endFrame" :all-traces="allTraces" :merged-segments="mergedSegments" :initial-trace-idx="currentVideo.traceIdx" :title="currentVideo.title" @close="playing = false" />
<!-- Trace strip context menu -->
<div v-if="traceCtxMenu.show" class="ms-ctx-menu" :style="{ left: traceCtxMenu.x + 'px', top: traceCtxMenu.y + 'px', display: 'block' }" @click.stop @mousedown.stop @pointerdown.stop>
@@ -300,7 +300,7 @@ const allPeople = ref<any[]>([])
const allTraces = ref<any[]>([])
const mergedSegments = ref<any[]>([])
const playing = ref(false)
const currentVideo = ref({ fileUuid: '', startTime: 0, endTime: 0, traceIdx: 0, title: '' })
const currentVideo = ref({ fileUuid: '', startFrame: 0, endFrame: 0, traceIdx: 0, title: '' })
const showCandidates = ref(false)
const showMerge = ref(false)
const mergeSearchQuery = ref('')
@@ -734,10 +734,10 @@ function playAllSegments() {
const t = allTraces.value[0]
currentVideo.value = {
fileUuid: t.file_uuid || '',
startTime: t.first_sec || t.start_time || 0,
endTime: t.last_sec || t.end_time || 0,
startFrame: t.first_frame || 0,
endFrame: t.last_frame || (t.first_frame || 0) + 30,
traceIdx: 0,
title: `${person.value?.name} · ${card.count} segments`,
title: `F${t.first_frame || 0}-F${t.last_frame || 0} (${person.value?.name})`,
}
playing.value = true
}
@@ -747,10 +747,10 @@ function playMerged(m: any) {
if (!t) return
currentVideo.value = {
fileUuid: t.file_uuid || '',
startTime: t.first_sec || t.start_time || 0,
endTime: t.last_sec || t.end_time || 0,
startFrame: t.first_frame || 0,
endFrame: t.last_frame || (t.first_frame || 0) + 30,
traceIdx: m._startIdx,
title: `${person.value?.name} · #${m.start_frame}–#${m.end_frame}`,
title: `F${t.first_frame || 0}-F${t.last_frame || 0} (${person.value?.name})`,
}
playing.value = true
}

View File

@@ -190,8 +190,8 @@
v-if="playing"
simple
:file-uuid="currentVideo.fileUuid"
:start-time="currentVideo.startTime"
:end-time="currentVideo.endTime"
:start-frame="currentVideo.startFrame"
:end-frame="currentVideo.endFrame"
:title="currentVideo.title"
@close="playing = false"
/>
@@ -423,7 +423,7 @@ function ensureMinDuration(r: any) {
}
const playing = ref(false)
const currentVideo = ref({ fileUuid: '', startTime: 0, endTime: 0, startFrame: null as number | null, endFrame: null as number | null, title: '' })
const currentVideo = ref({ fileUuid: '', startFrame: 0, endFrame: 0, title: '' })
const currentModeLabel = computed(() => modes.find(m => m.value === mode.value)?.label || 'Keyword')