From cf5888d9e5600af7caa0abb1b0c4738c493c73f8 Mon Sep 17 00:00:00 2001 From: Linrador <68631622+Linrador@users.noreply.github.com> Date: Tue, 5 May 2026 15:06:59 +0200 Subject: [PATCH] bugfixes --- backend/analyze.go | 103 ++++++++++++++++++--- backend/tasks_regenerate_assets.go | 47 ++++++++-- frontend/src/components/ui/TrainingTab.tsx | 64 ++++++++----- 3 files changed, 172 insertions(+), 42 deletions(-) diff --git a/backend/analyze.go b/backend/analyze.go index 4eb0a38..c3321e3 100644 --- a/backend/analyze.go +++ b/backend/analyze.go @@ -65,8 +65,8 @@ const ( // Sprite-Modus ist aktuell deaktiviert. Analyse läuft über Video-Frames. analyzeMaxSpriteCandidates = 24 - // Video-Modus: schnelle Vorschau. Für bessere Trefferquote später 24. - // neu, falls du später alle N Sekunden willst + // Video-Modus: extrahiert 1 Frame alle N Sekunden. + // 1 = jeder Sekunde, 3 = alle 3 Sekunden, 5 = alle 5 Sekunden. analyzeVideoFrameIntervalSeconds = 3 // AI-Server nicht mit tausenden Pfaden auf einmal fluten. @@ -668,12 +668,18 @@ func trainingPredictFramePathsBatchForAnalyze( res, err := client.Do(req) if err != nil { + if ctxErr := ctx.Err(); ctxErr != nil { + return nil, ctxErr + } return nil, err } defer res.Body.Close() var parsed analyzeBatchPredictResp if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil { + if ctxErr := ctx.Err(); ctxErr != nil { + return nil, ctxErr + } return nil, err } @@ -1785,15 +1791,23 @@ func analyzeVideoFromFramesForGoal( "Analyse 0%", ) + if err := ctx.Err(); err != nil { + publishAnalysisError(startedAtMs, file, "Analyse abgebrochen", err) + return nil, nil, err + } + durationSec, _ := durationSecondsForAnalyze(ctx, outPath) + if err := ctx.Err(); err != nil { + publishAnalysisError(startedAtMs, file, "Analyse abgebrochen", err) + return nil, nil, err + } + if durationSec <= 0 { err := appErrorf("videolänge konnte nicht bestimmt werden") publishAnalysisError(startedAtMs, file, "Analyse fehlgeschlagen", err) return nil, nil, err } - // Hilfsfunktion: garantiert, dass keine Prozentpunkte übersprungen werden. - // Beispiel: letzter Stand 12, neuer Stand 17 -> sendet 13,14,15,16,17. publishPercentRange := func(lastPercent *int, nextPercent int, current int, total int, extractPhase bool) { if total <= 0 { total = 1 @@ -1814,7 +1828,6 @@ func analyzeVideoFromFramesForGoal( label := fmt.Sprintf("Analyse %d%%", p) if extractPhase { - // p läuft global 0..50, publishAnalyzeExtractProgress erwartet aber 0..1. ratio := float64(p) / 50.0 if ratio < 0 { ratio = 0 @@ -1830,9 +1843,6 @@ func analyzeVideoFromFramesForGoal( label, ) } else { - // Für die Inferenzphase current/total so wählen, dass der globale - // Fortschritt in publishAnalyzeInferenceProgress wieder exakt p ergibt: - // current/total = (p - 50) / 50 inferenceCurrent := current inferenceTotal := total @@ -1854,6 +1864,16 @@ func analyzeVideoFromFramesForGoal( *lastPercent = nextPercent } + failCancelled := func() ([]analyzeHit, []analyzeHit, error) { + err := ctx.Err() + if err == nil { + err = context.Canceled + } + + publishAnalysisError(startedAtMs, file, "Analyse abgebrochen", err) + return nil, nil, err + } + lastExtractPercent := 0 samples, cleanup, err := extractVideoFramesBatch( @@ -1887,6 +1907,14 @@ func analyzeVideoFromFramesForGoal( }, ) + if cleanup != nil { + defer cleanup() + } + + if ctx.Err() != nil { + return failCancelled() + } + if err != nil { publishAnalysisError(startedAtMs, file, "Frames konnten nicht extrahiert werden", err) return nil, nil, err @@ -1910,8 +1938,16 @@ func analyzeVideoFromFramesForGoal( ) } + if ctx.Err() != nil { + return failCancelled() + } + paths := make([]string, 0, len(samples)) for _, sample := range samples { + if err := ctx.Err(); err != nil { + return failCancelled() + } + paths = append(paths, sample.Path) } @@ -1925,6 +1961,10 @@ func analyzeVideoFromFramesForGoal( "Analyse 50%", ) + if ctx.Err() != nil { + return failCancelled() + } + // Schneller AI-Server-Batch-Pfad für nsfw, highlights und all. // Wichtig: ensureAnalyzeAllGoalsForVideoCtx ruft goal="all" auf. // Ohne diesen Block fällt "all" auf die sehr langsame Einzelbild-Analyse zurück. @@ -1937,6 +1977,10 @@ func analyzeVideoFromFramesForGoal( detectorOnly := false for startIdx := 0; startIdx < len(samples); startIdx += analyzeFramePredictBatchSize { + if ctx.Err() != nil { + return failCancelled() + } + endIdx := startIdx + analyzeFramePredictBatchSize if endIdx > len(samples) { endIdx = len(samples) @@ -1948,6 +1992,10 @@ func analyzeVideoFromFramesForGoal( detectorOnly, ) + if ctx.Err() != nil { + return failCancelled() + } + if batchErr != nil || len(predictions) < endIdx-startIdx { appLogln("⚠️ video batch analyse fehlgeschlagen, fallback auf einzelbild-analyse:", batchErr) @@ -1968,6 +2016,10 @@ func analyzeVideoFromFramesForGoal( } for i := 0; i < endIdx-startIdx; i++ { + if ctx.Err() != nil { + return failCancelled() + } + sample := samples[startIdx+i] pred := predictions[i] @@ -1999,6 +2051,10 @@ func analyzeVideoFromFramesForGoal( } if batchOK { + if ctx.Err() != nil { + return failCancelled() + } + if lastInferencePercent < 100 { publishPercentRange( &lastInferencePercent, @@ -2012,8 +2068,6 @@ func analyzeVideoFromFramesForGoal( cleanNSFWHits := mergeAnalyzeHits(nsfwHits) cleanHighlightHits := mergeAnalyzeHits(highlightHits) - cleanup() - publishAnalysisFinished(startedAtMs, total, file, "Analyse abgeschlossen") return cleanNSFWHits, cleanHighlightHits, nil } @@ -2022,12 +2076,21 @@ func analyzeVideoFromFramesForGoal( // Fallback: langsame Einzelbild-Analyse. // Dieser Pfad sollte nur laufen, wenn der AI-Server-Batch fehlschlägt. for i, sample := range samples { + if ctx.Err() != nil { + return failCancelled() + } + t := sample.Time switch goal { case "nsfw": - res, err := classifyFramePathForAnalyze(ctx, sample.Path) - if err == nil { + res, frameErr := classifyFramePathForAnalyze(ctx, sample.Path) + + if ctx.Err() != nil { + return failCancelled() + } + + if frameErr == nil { bestLabel, bestScore := pickBestNSFWResult(res.Results) if bestLabel != "" && bestScore >= nsfwThresholdForLabel(bestLabel) { nsfwHits = append(nsfwHits, analyzeHit{ @@ -2042,10 +2105,20 @@ func analyzeVideoFromFramesForGoal( case "highlights": pred := predictFramePathForAnalyze(ctx, sample.Path) + + if ctx.Err() != nil { + return failCancelled() + } + highlightHits = appendHighlightHitsFromPrediction(highlightHits, pred, t) default: pred := predictFramePathForAnalyze(ctx, sample.Path) + + if ctx.Err() != nil { + return failCancelled() + } + nsfwHits = appendNSFWHitFromPrediction(nsfwHits, pred, t) highlightHits = appendHighlightHitsFromPrediction(highlightHits, pred, t) } @@ -2066,6 +2139,10 @@ func analyzeVideoFromFramesForGoal( ) } + if ctx.Err() != nil { + return failCancelled() + } + if lastInferencePercent < 100 { publishPercentRange( &lastInferencePercent, @@ -2079,8 +2156,6 @@ func analyzeVideoFromFramesForGoal( cleanNSFWHits := mergeAnalyzeHits(nsfwHits) cleanHighlightHits := mergeAnalyzeHits(highlightHits) - cleanup() - publishAnalysisFinished(startedAtMs, total, file, "Analyse abgeschlossen") return cleanNSFWHits, cleanHighlightHits, nil diff --git a/backend/tasks_regenerate_assets.go b/backend/tasks_regenerate_assets.go index d17362f..7e86c25 100644 --- a/backend/tasks_regenerate_assets.go +++ b/backend/tasks_regenerate_assets.go @@ -492,6 +492,45 @@ func runRegenerateAssetsJob(job *regenerateAssetsJob) { removeRegenerateAssetsJobLater(file, 3*time.Second) } +func cancelRegenerateAssetsJob(file string) bool { + file = strings.TrimSpace(file) + if file == "" { + return false + } + + regenerateAssetsMu.Lock() + job := regenerateAssetsJobs[file] + if job == nil { + regenerateAssetsMu.Unlock() + return false + } + + id := job.AssetID + cancel := job.Cancel + + if job.State == "queued" || job.State == "running" { + job.State = "cancelled" + job.Error = "Abgebrochen" + job.EndedAt = time.Now() + } + regenerateAssetsMu.Unlock() + + if cancel != nil { + cancel() + } + + setRegenerateAssetsTaskError(file, "Abgebrochen") + + publishFinishedPostworkPhase(file, id, "postwork", "meta", "missing", "", "") + publishFinishedPostworkPhase(file, id, "postwork", "thumb", "missing", "", "") + publishFinishedPostworkPhase(file, id, "postwork", "teaser", "missing", "", "") + publishFinishedPostworkPhase(file, id, "postwork", "sprites", "missing", "", "") + publishFinishedPostworkPhase(file, id, "enrich", "analyze", "missing", "", "") + + removeRegenerateAssetsJobLater(file, 2*time.Second) + return true +} + func handleRegenerateAssets(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost && r.Method != http.MethodDelete { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) @@ -520,13 +559,7 @@ func handleRegenerateAssets(w http.ResponseWriter, r *http.Request) { return } - regenerateAssetsMu.Lock() - job := regenerateAssetsJobs[file] - regenerateAssetsMu.Unlock() - - if job != nil && job.Cancel != nil { - job.Cancel() - } + cancelRegenerateAssetsJob(file) w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ diff --git a/frontend/src/components/ui/TrainingTab.tsx b/frontend/src/components/ui/TrainingTab.tsx index 6b62fcf..f0abca8 100644 --- a/frontend/src/components/ui/TrainingTab.tsx +++ b/frontend/src/components/ui/TrainingTab.tsx @@ -933,7 +933,7 @@ function sortTrainingLabels(input: Partial | null | undefined): function TrainingOverlay(props: { step: string; progress: number }) { return (
-
+
-
+
(null) + const [frameImageLoaded, setFrameImageLoaded] = useState(false) + const imageBoxRef = useRef(null) const detectorBoxesScrollRef = useRef(null) @@ -2231,6 +2233,15 @@ export default function TrainingTab(props: { return `${sample.frameUrl}&t=${encodeURIComponent(sample.sampleId)}&r=${imageReloadKey}` }, [sample, imageReloadKey]) + useEffect(() => { + if (!imageSrc) { + setFrameImageLoaded(false) + return + } + + setFrameImageLoaded(false) + }, [imageSrc]) + const canStartTraining = Boolean(trainingStatus?.canTrain) const feedbackCount = trainingStatus?.feedbackCount ?? 0 const requiredCount = trainingStatus?.requiredCount ?? 5 @@ -3150,7 +3161,9 @@ export default function TrainingTab(props: { }) }, []) - const showImageBoxes = !loading && !trainingRunning + const frameBusy = loading || (!!imageSrc && !frameImageLoaded) + + const showImageBoxes = !frameBusy && !trainingRunning const shownTrainingDurationMs = useMemo(() => { const job = trainingStatus?.training @@ -3428,7 +3441,7 @@ export default function TrainingTab(props: { return (
@@ -3580,14 +3593,17 @@ export default function TrainingTab(props: { const detectorBoxesPanel = (opts?: { compact?: boolean + stretch?: boolean maxHeightClassName?: string }) => { const compact = Boolean(opts?.compact) + const stretch = Boolean(opts?.stretch) return (
@@ -3612,11 +3628,11 @@ export default function TrainingTab(props: {
-
-
- {detectorBoxesPanel({ maxHeightClassName: 'lg:max-h-[28dvh]' })} +
+
+ {detectorBoxesPanel({ + stretch: true, + maxHeightClassName: 'lg:max-h-none', + })}
@@ -3815,7 +3834,7 @@ export default function TrainingTab(props: { {/* Mitte */}
-
+
{imageSrc ? (
setFrameImageLoaded(true)} + onError={() => setFrameImageLoaded(true)} onContextMenu={(e) => e.preventDefault()} onDragStart={(e) => e.preventDefault()} className={[ - 'block max-h-[52dvh] max-w-full object-contain sm:max-h-[60dvh] lg:max-h-[72dvh]', + 'block rounded-lg max-h-[52dvh] max-w-full object-contain sm:max-h-[60dvh] lg:max-h-[72dvh]', 'select-none', imageTouchClass, '[-webkit-user-drag:none] [-webkit-touch-callout:none]', @@ -4228,10 +4249,10 @@ export default function TrainingTab(props: { step={shownTrainingStep} progress={shownTrainingProgress} /> - ) : loading ? ( + ) : frameBusy ? ( ) : null}
@@ -4250,13 +4271,14 @@ export default function TrainingTab(props: { )}
-
+