diff --git a/backend/dist/nsfwapp-linux-amd64 b/backend/dist/nsfwapp-linux-amd64 index 1ee06e3..9cc9ccf 100644 Binary files a/backend/dist/nsfwapp-linux-amd64 and b/backend/dist/nsfwapp-linux-amd64 differ diff --git a/backend/dist/nsfwapp.exe b/backend/dist/nsfwapp.exe index 3319ad5..13b6629 100644 Binary files a/backend/dist/nsfwapp.exe and b/backend/dist/nsfwapp.exe differ diff --git a/backend/dist/nsfwapp_amd64.deb b/backend/dist/nsfwapp_amd64.deb index 3e55eb8..28407f6 100644 Binary files a/backend/dist/nsfwapp_amd64.deb and b/backend/dist/nsfwapp_amd64.deb differ diff --git a/backend/training.go b/backend/training.go index 8ed09c9..119b731 100644 --- a/backend/training.go +++ b/backend/training.go @@ -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() diff --git a/backend/training_label_loader.go b/backend/training_label_loader.go index 3368637..7fda992 100644 --- a/backend/training_label_loader.go +++ b/backend/training_label_loader.go @@ -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 diff --git a/backend/training_test.go b/backend/training_test.go index bd129af..26c6711 100644 --- a/backend/training_test.go +++ b/backend/training_test.go @@ -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", diff --git a/frontend/src/components/ui/TrainingTab.tsx b/frontend/src/components/ui/TrainingTab.tsx index fbce326..351fa39 100644 --- a/frontend/src/components/ui/TrainingTab.tsx +++ b/frontend/src/components/ui/TrainingTab.tsx @@ -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).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" >