This commit is contained in:
Linrador 2026-07-01 08:33:52 +02:00
parent 9e1e41fe04
commit 93b51978c5
7 changed files with 366 additions and 30 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -2548,6 +2548,31 @@ func trainingPruneImportVideoJobsLocked(now time.Time) {
}
}
func trainingLatestRunningImportVideoJobRequestID() string {
now := time.Now().UTC()
trainingImportVideoJobs.Lock()
defer trainingImportVideoJobs.Unlock()
trainingPruneImportVideoJobsLocked(now)
requestID := ""
var latestStarted time.Time
for id, job := range trainingImportVideoJobs.items {
if job == nil || !job.running {
continue
}
if requestID == "" || job.startedAt.After(latestStarted) {
requestID = id
latestStarted = job.startedAt
}
}
return requestID
}
func trainingStartImportVideoJob(req TrainingImportVideoRequest) {
requestID := strings.TrimSpace(req.AnalysisRequestID)
if requestID == "" {
@ -2591,7 +2616,11 @@ func trainingStartImportVideoJob(req TrainingImportVideoRequest) {
func trainingImportVideoJobResponse(requestID string) (TrainingImportVideoResponse, int) {
requestID = strings.TrimSpace(requestID)
if requestID == "" {
return TrainingImportVideoResponse{OK: false, Error: "requestId missing"}, http.StatusBadRequest
requestID = trainingLatestRunningImportVideoJobRequestID()
}
if requestID == "" {
return TrainingImportVideoResponse{OK: false, Error: "Kein laufender Import-Job gefunden."}, http.StatusNotFound
}
trainingImportVideoJobs.Lock()
@ -3083,6 +3112,31 @@ func trainingPruneNextJobsLocked(now time.Time) {
}
}
func trainingLatestRunningNextJobRequestID() string {
now := time.Now().UTC()
trainingNextJobs.Lock()
defer trainingNextJobs.Unlock()
trainingPruneNextJobsLocked(now)
requestID := ""
var latestStarted time.Time
for id, job := range trainingNextJobs.items {
if job == nil || !job.running {
continue
}
if requestID == "" || job.startedAt.After(latestStarted) {
requestID = id
latestStarted = job.startedAt
}
}
return requestID
}
func trainingStartNextJob(req TrainingNextRequest) {
requestID := strings.TrimSpace(req.AnalysisRequestID)
if requestID == "" {
@ -3127,7 +3181,11 @@ func trainingStartNextJob(req TrainingNextRequest) {
func trainingNextJobResponse(requestID string) (TrainingNextResponse, int) {
requestID = strings.TrimSpace(requestID)
if requestID == "" {
return TrainingNextResponse{OK: false, Error: "requestId missing"}, http.StatusBadRequest
requestID = trainingLatestRunningNextJobRequestID()
}
if requestID == "" {
return TrainingNextResponse{OK: false, Error: "Kein laufender Analyse-Job gefunden."}, http.StatusNotFound
}
trainingNextJobs.Lock()

View File

@ -22,7 +22,7 @@ const trainingNoSexPositionLabel = "keine"
func isNoSexPositionLabel(label string) bool {
switch strings.ToLower(strings.TrimSpace(label)) {
case "", trainingNoSexPositionLabel:
case "", trainingNoSexPositionLabel, "unknown", "unbekannt", "none", "no_position", "no-position", "no position", "n/a", "na":
return true
default:
return false

View File

@ -132,6 +132,17 @@ func TestTrainingEffectiveCorrectionClearsNegativeAnnotation(t *testing.T) {
}
}
func TestNoSexPositionAliasesTreatUnknownAsNoPosition(t *testing.T) {
for _, value := range []string{"Unknown", "unknown", "unbekannt", "none", "no_position"} {
if !isNoSexPositionLabel(value) {
t.Fatalf("%q should be treated as no sex position", value)
}
if got := normalizeSexPositionLabel(value); got != trainingNoSexPositionLabel {
t.Fatalf("normalizeSexPositionLabel(%q) = %q, want %q", value, got, trainingNoSexPositionLabel)
}
}
}
func trainingTestPoseKeypoints(overrides map[string]TrainingKeypoint) []TrainingKeypoint {
names := []string{
"nose",

View File

@ -1587,6 +1587,14 @@ const NO_SEX_POSITION_LABEL = 'keine'
const NO_SEX_POSITION_ALIASES = new Set([
'',
NO_SEX_POSITION_LABEL,
'unknown',
'unbekannt',
'none',
'no_position',
'no-position',
'no position',
'n/a',
'na',
])
function normalizeSexPositionValue(value?: string | null) {
@ -3980,6 +3988,49 @@ const TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY = 'training:active-import-video'
const TRAINING_ACTIVE_NEXT_STORAGE_KEY = 'training:active-next'
const ALL_TRAINING_TARGETS: TrainingTargetKey[] = ['detector', 'pose', 'videomae']
function readTrainingActiveJobStorage(key: string) {
try {
const raw = window.localStorage.getItem(key)
if (raw) return raw
} catch {
// ignore
}
try {
return window.sessionStorage.getItem(key) || ''
} catch {
return ''
}
}
function writeTrainingActiveJobStorage(key: string, value: string) {
try {
window.localStorage.setItem(key, value)
} catch {
// ignore
}
try {
window.sessionStorage.setItem(key, value)
} catch {
// ignore
}
}
function removeTrainingActiveJobStorage(key: string) {
try {
window.localStorage.removeItem(key)
} catch {
// ignore
}
try {
window.sessionStorage.removeItem(key)
} catch {
// ignore
}
}
export default function TrainingTab(props: {
active?: boolean
onTrainingRunningChange?: (running: boolean) => void
@ -4715,6 +4766,7 @@ export default function TrainingTab(props: {
return correction.posePersons ?? []
}, [correction.posePersons])
const poseTrainingAllowed = !isNoSexPositionValue(correction.sexPosition)
const hasPosePersons = posePersons.some((person) =>
hasVisiblePoseBox(person) || person.keypoints?.some(isPoseKeypointVisible)
)
@ -4909,6 +4961,9 @@ export default function TrainingTab(props: {
const running = Boolean(data?.running)
if (running) {
if (!loadingRef.current) {
resetPoseUiToBoxes()
}
loadingRef.current = true
setLoading(true)
}
@ -4959,7 +5014,7 @@ export default function TrainingTab(props: {
}
return true
}, [setLoadingPreviewCandidate])
}, [resetPoseUiToBoxes, setLoadingPreviewCandidate])
useEffect(() => {
if (!imageSrc) {
@ -5740,11 +5795,11 @@ export default function TrainingTab(props: {
const previewUrl = String(opts?.previewUrl ?? '').trim()
resetPoseUiToBoxes()
setLoadingPreviewFallbackUrl(previewUrl)
setLoadingPreviewCandidate(previewUrl)
loadingRef.current = true
setLoading(true)
resetPoseUiToBoxes()
setAnalysisSourceFile('')
setAnalysisProgress(8)
setAnalysisStep(
@ -5776,7 +5831,7 @@ export default function TrainingTab(props: {
if (uncertainMode) params.set('mode', 'uncertain')
try {
window.sessionStorage.setItem(
writeTrainingActiveJobStorage(
TRAINING_ACTIVE_NEXT_STORAGE_KEY,
JSON.stringify({
requestId,
@ -5839,7 +5894,7 @@ export default function TrainingTab(props: {
setMessage('Analyse läuft im Backend weiter. Beim Zurückkehren wird das nächste offene Trainingsbild wieder geladen.')
} else {
try {
window.sessionStorage.removeItem(TRAINING_ACTIVE_NEXT_STORAGE_KEY)
removeTrainingActiveJobStorage(TRAINING_ACTIVE_NEXT_STORAGE_KEY)
} catch {
// ignore
}
@ -5853,7 +5908,7 @@ export default function TrainingTab(props: {
if (completed) {
try {
window.sessionStorage.removeItem(TRAINING_ACTIVE_NEXT_STORAGE_KEY)
removeTrainingActiveJobStorage(TRAINING_ACTIVE_NEXT_STORAGE_KEY)
} catch {
// ignore
}
@ -5889,6 +5944,7 @@ export default function TrainingTab(props: {
])
const reloadCurrentImage = useCallback(async () => {
resetPoseUiToBoxes()
setDrawingBox(null)
setBoxInteraction(null)
setTouchMagnifier(null)
@ -5899,7 +5955,7 @@ export default function TrainingTab(props: {
previewUrl: imageSrc,
})
setImageReloadKey((value) => value + 1)
}, [imageSrc, loadNext])
}, [imageSrc, loadNext, resetPoseUiToBoxes])
const loadTrainingStatus = useCallback(async () => {
const res = await fetch('/api/training/status', { cache: 'no-store' })
@ -5956,6 +6012,10 @@ export default function TrainingTab(props: {
)
const data = await res.json().catch(() => null)
if (data?.analysis) {
applyTrainingAnalysisEvent(data.analysis)
}
if (res.status === 202 || data?.running) {
await new Promise((resolve) => window.setTimeout(resolve, 1200))
continue
@ -5967,7 +6027,7 @@ export default function TrainingTab(props: {
return completeVideoImportFromData(data)
}
}, [completeVideoImportFromData])
}, [applyTrainingAnalysisEvent, completeVideoImportFromData])
const importVideoIntoTraining = useCallback(async (raw: any) => {
const output = String(raw?.output || '').trim()
@ -5990,7 +6050,7 @@ export default function TrainingTab(props: {
const importKey = `${detail.jobId || ''}|${detail.output}|${detail.count || TRAINING_IMPORT_FRAME_COUNT}`
// Verhindert Doppelimport durch sessionStorage + CustomEvent.
// Verhindert Doppelimport durch persistent gemerkte aktive Jobs + CustomEvent.
if (videoImportInFlightKeyRef.current === importKey) {
return false
}
@ -6002,6 +6062,7 @@ export default function TrainingTab(props: {
activeAnalysisRequestIdRef.current = requestId
loadingRef.current = true
resetPoseUiToBoxes()
setLoading(true)
setAnalysisSourceFile(detail.sourceFile || detail.output.split(/[\\/]/).pop() || '')
setAnalysisProgress(5)
@ -6009,10 +6070,13 @@ export default function TrainingTab(props: {
setError(null)
setMessage(null)
let keepActiveJob = false
let completed = false
try {
try {
window.sessionStorage.removeItem(TRAINING_PENDING_IMPORT_VIDEO_STORAGE_KEY)
window.sessionStorage.setItem(
writeTrainingActiveJobStorage(
TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY,
JSON.stringify({ requestId, importKey, detail })
)
@ -6040,9 +6104,10 @@ export default function TrainingTab(props: {
if (res.status === 202 || data?.accepted || data?.running) {
await waitForVideoImportResult(String(data?.requestId || requestId))
completed = true
try {
window.sessionStorage.removeItem(TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY)
removeTrainingActiveJobStorage(TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY)
} catch {
// ignore
}
@ -6086,6 +6151,7 @@ export default function TrainingTab(props: {
)
}
completed = true
return true
} catch (e) {
const msg = e instanceof Error ? e.message : String(e)
@ -6093,10 +6159,11 @@ export default function TrainingTab(props: {
/load failed|failed to fetch|networkerror|network error/i.test(msg)
if (mayStillRun) {
keepActiveJob = true
setMessage('Video-Import läuft im Backend weiter und wird beim Zurückkehren wieder aufgenommen.')
} else {
try {
window.sessionStorage.removeItem(TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY)
removeTrainingActiveJobStorage(TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY)
} catch {
// ignore
}
@ -6104,6 +6171,14 @@ export default function TrainingTab(props: {
}
return false
} finally {
if (completed) {
removeTrainingActiveJobStorage(TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY)
}
if (keepActiveJob) {
return
}
setAnalysisProgress(100)
setAnalysisStep('Video-Import abgeschlossen.')
@ -6127,6 +6202,7 @@ export default function TrainingTab(props: {
}, [
loadPriorityTrainingSamples,
loadTrainingStatus,
resetPoseUiToBoxes,
setLoadingPreviewCandidate,
trainingRunning,
waitForVideoImportResult,
@ -6480,6 +6556,163 @@ export default function TrainingTab(props: {
})
}, [activeBoxIndex])
const resumeLatestServerAnalysisJob = useCallback(async () => {
if (
activeAnalysisRequestIdRef.current ||
nextAnalysisInFlightRequestIdRef.current ||
videoImportInFlightKeyRef.current
) {
return true
}
const resumeImport = (data: any) => {
const requestId = String(data?.requestId || '').trim()
if (!requestId) return false
const importKey = `server|${requestId}`
videoImportStartedRef.current = true
videoImportInFlightKeyRef.current = importKey
activeAnalysisRequestIdRef.current = requestId
loadingRef.current = true
writeTrainingActiveJobStorage(
TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY,
JSON.stringify({ requestId, importKey, detail: {} })
)
resetPoseUiToBoxes()
setLoading(true)
setAnalysisSourceFile('')
setAnalysisProgress(5)
setAnalysisStep('Video-Import läuft im Backend…')
setError(null)
if (data?.analysis) {
applyTrainingAnalysisEvent(data.analysis)
}
void waitForVideoImportResult(requestId)
.then(() => {
removeTrainingActiveJobStorage(TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY)
})
.catch((e) => {
const msg = e instanceof Error ? e.message : String(e)
const mayStillRun =
/load failed|failed to fetch|networkerror|network error/i.test(msg)
if (mayStillRun) {
setMessage('Video-Import läuft im Backend weiter und wird beim Zurückkehren wieder aufgenommen.')
} else {
removeTrainingActiveJobStorage(TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY)
setError(msg)
}
})
.finally(() => {
if (activeAnalysisRequestIdRef.current === requestId) {
activeAnalysisRequestIdRef.current = null
loadingRef.current = false
setLoading(false)
setAnalysisSourceFile('')
setAnalysisProgress(0)
setAnalysisStep('')
}
if (videoImportInFlightKeyRef.current === importKey) {
videoImportInFlightKeyRef.current = null
}
})
return true
}
const resumeNext = (data: any) => {
const requestId = String(data?.requestId || '').trim()
if (!requestId) return false
activeAnalysisRequestIdRef.current = requestId
nextAnalysisInFlightRequestIdRef.current = requestId
loadingRef.current = true
writeTrainingActiveJobStorage(
TRAINING_ACTIVE_NEXT_STORAGE_KEY,
JSON.stringify({ requestId, opts: {} })
)
resetPoseUiToBoxes()
setLoading(true)
setAnalysisSourceFile('')
setAnalysisProgress(5)
setAnalysisStep('Analyse läuft im Backend…')
setError(null)
if (data?.analysis) {
applyTrainingAnalysisEvent(data.analysis)
}
void waitForNextResult(requestId)
.then(() => {
removeTrainingActiveJobStorage(TRAINING_ACTIVE_NEXT_STORAGE_KEY)
})
.catch((e) => {
const msg = e instanceof Error ? e.message : String(e)
const mayStillRun =
/load failed|failed to fetch|networkerror|network error/i.test(msg)
if (mayStillRun) {
setMessage('Analyse läuft im Backend weiter und wird beim Zurückkehren wieder aufgenommen.')
} else {
removeTrainingActiveJobStorage(TRAINING_ACTIVE_NEXT_STORAGE_KEY)
setError(msg)
}
})
.finally(() => {
if (activeAnalysisRequestIdRef.current === requestId) {
activeAnalysisRequestIdRef.current = null
nextAnalysisInFlightRequestIdRef.current = null
loadingRef.current = false
setLoading(false)
setAnalysisSourceFile('')
setAnalysisProgress(0)
setAnalysisStep('')
}
})
return true
}
try {
const importRes = await fetch('/api/training/import-video/status', { cache: 'no-store' })
const importData = await importRes.json().catch(() => null)
if (
(importRes.status === 202 || importData?.running || importData?.accepted) &&
importData?.ok &&
resumeImport(importData)
) {
return true
}
} catch {
// ignore
}
try {
const nextRes = await fetch('/api/training/next/status', { cache: 'no-store' })
const nextData = await nextRes.json().catch(() => null)
if (
(nextRes.status === 202 || nextData?.running || nextData?.accepted) &&
nextData?.ok &&
resumeNext(nextData)
) {
return true
}
} catch {
// ignore
}
return false
}, [applyTrainingAnalysisEvent, resetPoseUiToBoxes, waitForNextResult, waitForVideoImportResult])
useEffect(() => {
const onImportVideo = (event: Event) => {
const detail = (event as CustomEvent<any>).detail
@ -6492,7 +6725,7 @@ export default function TrainingTab(props: {
let resumedActiveNext = false
try {
const activeRaw = window.sessionStorage.getItem(TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY)
const activeRaw = readTrainingActiveJobStorage(TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY)
if (activeRaw) {
const active = JSON.parse(activeRaw)
@ -6506,6 +6739,7 @@ export default function TrainingTab(props: {
activeAnalysisRequestIdRef.current = requestId
loadingRef.current = true
resetPoseUiToBoxes()
setLoading(true)
setAnalysisSourceFile(String(active?.detail?.sourceFile || active?.detail?.output || '').split(/[\\/]/).pop() || '')
setAnalysisProgress(5)
@ -6514,7 +6748,7 @@ export default function TrainingTab(props: {
void waitForVideoImportResult(requestId)
.then(() => {
window.sessionStorage.removeItem(TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY)
removeTrainingActiveJobStorage(TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY)
})
.catch((e) => {
const msg = e instanceof Error ? e.message : String(e)
@ -6524,7 +6758,7 @@ export default function TrainingTab(props: {
if (mayStillRun) {
setMessage('Video-Import läuft im Backend weiter und wird beim Zurückkehren wieder aufgenommen.')
} else {
window.sessionStorage.removeItem(TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY)
removeTrainingActiveJobStorage(TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY)
setError(msg)
}
})
@ -6549,7 +6783,7 @@ export default function TrainingTab(props: {
const activeNextRaw = resumedActiveImport
? ''
: window.sessionStorage.getItem(TRAINING_ACTIVE_NEXT_STORAGE_KEY)
: readTrainingActiveJobStorage(TRAINING_ACTIVE_NEXT_STORAGE_KEY)
if (activeNextRaw) {
const active = JSON.parse(activeNextRaw)
@ -6560,6 +6794,7 @@ export default function TrainingTab(props: {
activeAnalysisRequestIdRef.current = requestId
nextAnalysisInFlightRequestIdRef.current = requestId
loadingRef.current = true
resetPoseUiToBoxes()
setLoading(true)
setAnalysisSourceFile('')
setAnalysisProgress(5)
@ -6584,7 +6819,7 @@ export default function TrainingTab(props: {
deferCurrentSampleToQueueEnd: Boolean(activeOpts?.deferCurrentSampleToQueueEnd),
})
.then(() => {
window.sessionStorage.removeItem(TRAINING_ACTIVE_NEXT_STORAGE_KEY)
removeTrainingActiveJobStorage(TRAINING_ACTIVE_NEXT_STORAGE_KEY)
})
.catch((e) => {
const msg = e instanceof Error ? e.message : String(e)
@ -6594,7 +6829,7 @@ export default function TrainingTab(props: {
if (mayStillRun) {
setMessage('Analyse läuft im Backend weiter und wird beim Zurückkehren wieder aufgenommen.')
} else {
window.sessionStorage.removeItem(TRAINING_ACTIVE_NEXT_STORAGE_KEY)
removeTrainingActiveJobStorage(TRAINING_ACTIVE_NEXT_STORAGE_KEY)
setError(msg)
}
})
@ -6630,7 +6865,7 @@ export default function TrainingTab(props: {
return () => {
window.removeEventListener('training:import-video', onImportVideo as EventListener)
}
}, [applyTrainingAnalysisEvent, importVideoIntoTraining, waitForNextResult, waitForVideoImportResult])
}, [applyTrainingAnalysisEvent, importVideoIntoTraining, resetPoseUiToBoxes, waitForNextResult, waitForVideoImportResult])
useEffect(() => {
if (!tabActive || initializedRef.current) return
@ -6645,6 +6880,15 @@ export default function TrainingTab(props: {
if (cancelled || initRunIdRef.current !== runId) return
const resumedServerJob = await resumeLatestServerAnalysisJob()
if (cancelled || initRunIdRef.current !== runId) return
if (resumedServerJob) {
initializedRef.current = true
return
}
// Wichtig:
// Wenn gerade ein Video-Import über "Video ins Training übernehmen" läuft,
// darf loadNext() nicht danach ein zufälliges/letztes Sample darüberlegen.
@ -6669,7 +6913,7 @@ export default function TrainingTab(props: {
return () => {
cancelled = true
}
}, [tabActive, loadLabels, loadNext, loadTrainingStatus])
}, [tabActive, loadLabels, loadNext, loadTrainingStatus, resumeLatestServerAnalysisJob])
useEffect(() => {
if (!tabActive || !initializedRef.current) return
@ -6693,7 +6937,7 @@ export default function TrainingTab(props: {
let importRequestId = activeAnalysisRequestIdRef.current || ''
try {
const activeNextRaw = window.sessionStorage.getItem(TRAINING_ACTIVE_NEXT_STORAGE_KEY)
const activeNextRaw = readTrainingActiveJobStorage(TRAINING_ACTIVE_NEXT_STORAGE_KEY)
if (!nextRequestId && activeNextRaw) {
nextRequestId = String(JSON.parse(activeNextRaw)?.requestId || '').trim()
}
@ -6702,7 +6946,7 @@ export default function TrainingTab(props: {
}
try {
const activeImportRaw = window.sessionStorage.getItem(TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY)
const activeImportRaw = readTrainingActiveJobStorage(TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY)
if (activeImportRaw) {
importRequestId = String(JSON.parse(activeImportRaw)?.requestId || importRequestId || '').trim()
}
@ -6869,6 +7113,7 @@ export default function TrainingTab(props: {
) => {
if (!sample || trainingRunning) return
resetPoseUiToBoxes()
setSavingOverlayText(editingFeedback ? 'Feedback wird aktualisiert…' : 'Feedback wird gespeichert…')
setSaving(true)
setError(null)
@ -7029,6 +7274,7 @@ export default function TrainingTab(props: {
loadTrainingStatus,
loadQueuedTrainingSample,
loadNextImportedQueuedSample,
resetPoseUiToBoxes,
trainingRunning,
]
)
@ -7053,6 +7299,7 @@ export default function TrainingTab(props: {
const skippedSampleId = sample.sampleId
resetPoseUiToBoxes()
setSavingOverlayText('Bild wird übersprungen…')
setSaving(true)
setError(null)
@ -7101,6 +7348,7 @@ export default function TrainingTab(props: {
loadNext,
loadQueuedTrainingSample,
loadNextImportedQueuedSample,
resetPoseUiToBoxes,
])
const openTrainingStartModal = useCallback(() => {
@ -7363,6 +7611,16 @@ export default function TrainingTab(props: {
latestGestureBoxRef.current = null
}, [cancelPointerMoveFrame])
const resetBoxDrawingMode = useCallback(() => {
releaseActivePointerCapture()
clearBoxGestureRefs()
setDrawingBox(null)
setBoxInteraction(null)
setTouchMagnifier(null)
setBoxLabel('')
setActiveBoxIndex(null)
}, [clearBoxGestureRefs, releaseActivePointerCapture])
const applyPointerMoveSnapshot = useCallback((snapshot: {
clientX: number
clientY: number
@ -8071,7 +8329,7 @@ export default function TrainingTab(props: {
}, [markManualCorrection])
const addPosePerson = useCallback(() => {
if (uiLocked) return
if (uiLocked || !poseTrainingAllowed) return
markManualCorrection()
@ -8089,8 +8347,7 @@ export default function TrainingTab(props: {
})
setShowPoseSkeleton(true)
setActiveBoxIndex(null)
setBoxLabel('')
resetBoxDrawingMode()
setActivePosePersonIndex(nextPersonIndex)
setPendingPoseKeypoint(null)
setSelectedPoseKeypointAction(null)
@ -8103,7 +8360,7 @@ export default function TrainingTab(props: {
behavior: 'smooth',
})
})
}, [markManualCorrection, switchPoseKeypointMapView, uiLocked])
}, [markManualCorrection, poseTrainingAllowed, resetBoxDrawingMode, switchPoseKeypointMapView, uiLocked])
const removePoseKeypoint = useCallback((personIndex: number, keypointIndex: number) => {
markManualCorrection()
@ -8311,7 +8568,8 @@ export default function TrainingTab(props: {
setLoadingPreviewFailed(false)
}, [loading, frameBusy, loadingPreviewFallbackUrl, loadingPreviewUrl])
const poseModeAvailable = Boolean(sample) || hasPosePersons || originalPosePersonCount > 0
const poseModeAvailable =
poseTrainingAllowed && (Boolean(sample) || hasPosePersons || originalPosePersonCount > 0)
const poseModeActive = showPoseSkeleton && poseModeAvailable
const annotationLayerHidden = frameBusy || trainingRunning || saving
const poseSkeletonVisible = poseModeActive && hasPosePersons && !annotationLayerHidden
@ -8322,6 +8580,15 @@ export default function TrainingTab(props: {
focusedPoseOverlayPersonIndex === null || personIndex === focusedPoseOverlayPersonIndex,
[focusedPoseOverlayPersonIndex]
)
useEffect(() => {
if (poseTrainingAllowed || !showPoseSkeleton) return
setShowPoseSkeleton(false)
setActivePosePersonIndex(null)
setPendingPoseKeypoint(null)
setSelectedPoseKeypointAction(null)
}, [poseTrainingAllowed, showPoseSkeleton])
const poseFaceImageZoomActive =
poseModeActive &&
poseKeypointMapView === 'head' &&
@ -8829,8 +9096,8 @@ export default function TrainingTab(props: {
className={modeButtonClass(poseModeActive)}
aria-pressed={poseModeActive}
onClick={() => {
resetBoxDrawingMode()
setShowPoseSkeleton(true)
setActiveBoxIndex(null)
}}
title="Pose-Skelett korrigieren"
>