bugfixes
This commit is contained in:
parent
96b81fc89d
commit
cf5888d9e5
@ -65,8 +65,8 @@ const (
|
|||||||
// Sprite-Modus ist aktuell deaktiviert. Analyse läuft über Video-Frames.
|
// Sprite-Modus ist aktuell deaktiviert. Analyse läuft über Video-Frames.
|
||||||
analyzeMaxSpriteCandidates = 24
|
analyzeMaxSpriteCandidates = 24
|
||||||
|
|
||||||
// Video-Modus: schnelle Vorschau. Für bessere Trefferquote später 24.
|
// Video-Modus: extrahiert 1 Frame alle N Sekunden.
|
||||||
// neu, falls du später alle N Sekunden willst
|
// 1 = jeder Sekunde, 3 = alle 3 Sekunden, 5 = alle 5 Sekunden.
|
||||||
analyzeVideoFrameIntervalSeconds = 3
|
analyzeVideoFrameIntervalSeconds = 3
|
||||||
|
|
||||||
// AI-Server nicht mit tausenden Pfaden auf einmal fluten.
|
// AI-Server nicht mit tausenden Pfaden auf einmal fluten.
|
||||||
@ -668,12 +668,18 @@ func trainingPredictFramePathsBatchForAnalyze(
|
|||||||
|
|
||||||
res, err := client.Do(req)
|
res, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||||
|
return nil, ctxErr
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
var parsed analyzeBatchPredictResp
|
var parsed analyzeBatchPredictResp
|
||||||
if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil {
|
if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil {
|
||||||
|
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||||
|
return nil, ctxErr
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1785,15 +1791,23 @@ func analyzeVideoFromFramesForGoal(
|
|||||||
"Analyse 0%",
|
"Analyse 0%",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
publishAnalysisError(startedAtMs, file, "Analyse abgebrochen", err)
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
durationSec, _ := durationSecondsForAnalyze(ctx, outPath)
|
durationSec, _ := durationSecondsForAnalyze(ctx, outPath)
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
publishAnalysisError(startedAtMs, file, "Analyse abgebrochen", err)
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if durationSec <= 0 {
|
if durationSec <= 0 {
|
||||||
err := appErrorf("videolänge konnte nicht bestimmt werden")
|
err := appErrorf("videolänge konnte nicht bestimmt werden")
|
||||||
publishAnalysisError(startedAtMs, file, "Analyse fehlgeschlagen", err)
|
publishAnalysisError(startedAtMs, file, "Analyse fehlgeschlagen", err)
|
||||||
return nil, nil, 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) {
|
publishPercentRange := func(lastPercent *int, nextPercent int, current int, total int, extractPhase bool) {
|
||||||
if total <= 0 {
|
if total <= 0 {
|
||||||
total = 1
|
total = 1
|
||||||
@ -1814,7 +1828,6 @@ func analyzeVideoFromFramesForGoal(
|
|||||||
label := fmt.Sprintf("Analyse %d%%", p)
|
label := fmt.Sprintf("Analyse %d%%", p)
|
||||||
|
|
||||||
if extractPhase {
|
if extractPhase {
|
||||||
// p läuft global 0..50, publishAnalyzeExtractProgress erwartet aber 0..1.
|
|
||||||
ratio := float64(p) / 50.0
|
ratio := float64(p) / 50.0
|
||||||
if ratio < 0 {
|
if ratio < 0 {
|
||||||
ratio = 0
|
ratio = 0
|
||||||
@ -1830,9 +1843,6 @@ func analyzeVideoFromFramesForGoal(
|
|||||||
label,
|
label,
|
||||||
)
|
)
|
||||||
} else {
|
} 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
|
inferenceCurrent := current
|
||||||
inferenceTotal := total
|
inferenceTotal := total
|
||||||
|
|
||||||
@ -1854,6 +1864,16 @@ func analyzeVideoFromFramesForGoal(
|
|||||||
*lastPercent = nextPercent
|
*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
|
lastExtractPercent := 0
|
||||||
|
|
||||||
samples, cleanup, err := extractVideoFramesBatch(
|
samples, cleanup, err := extractVideoFramesBatch(
|
||||||
@ -1887,6 +1907,14 @@ func analyzeVideoFromFramesForGoal(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if cleanup != nil {
|
||||||
|
defer cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return failCancelled()
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
publishAnalysisError(startedAtMs, file, "Frames konnten nicht extrahiert werden", err)
|
publishAnalysisError(startedAtMs, file, "Frames konnten nicht extrahiert werden", err)
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@ -1910,8 +1938,16 @@ func analyzeVideoFromFramesForGoal(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return failCancelled()
|
||||||
|
}
|
||||||
|
|
||||||
paths := make([]string, 0, len(samples))
|
paths := make([]string, 0, len(samples))
|
||||||
for _, sample := range samples {
|
for _, sample := range samples {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return failCancelled()
|
||||||
|
}
|
||||||
|
|
||||||
paths = append(paths, sample.Path)
|
paths = append(paths, sample.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1925,6 +1961,10 @@ func analyzeVideoFromFramesForGoal(
|
|||||||
"Analyse 50%",
|
"Analyse 50%",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return failCancelled()
|
||||||
|
}
|
||||||
|
|
||||||
// Schneller AI-Server-Batch-Pfad für nsfw, highlights und all.
|
// Schneller AI-Server-Batch-Pfad für nsfw, highlights und all.
|
||||||
// Wichtig: ensureAnalyzeAllGoalsForVideoCtx ruft goal="all" auf.
|
// Wichtig: ensureAnalyzeAllGoalsForVideoCtx ruft goal="all" auf.
|
||||||
// Ohne diesen Block fällt "all" auf die sehr langsame Einzelbild-Analyse zurück.
|
// Ohne diesen Block fällt "all" auf die sehr langsame Einzelbild-Analyse zurück.
|
||||||
@ -1937,6 +1977,10 @@ func analyzeVideoFromFramesForGoal(
|
|||||||
detectorOnly := false
|
detectorOnly := false
|
||||||
|
|
||||||
for startIdx := 0; startIdx < len(samples); startIdx += analyzeFramePredictBatchSize {
|
for startIdx := 0; startIdx < len(samples); startIdx += analyzeFramePredictBatchSize {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return failCancelled()
|
||||||
|
}
|
||||||
|
|
||||||
endIdx := startIdx + analyzeFramePredictBatchSize
|
endIdx := startIdx + analyzeFramePredictBatchSize
|
||||||
if endIdx > len(samples) {
|
if endIdx > len(samples) {
|
||||||
endIdx = len(samples)
|
endIdx = len(samples)
|
||||||
@ -1948,6 +1992,10 @@ func analyzeVideoFromFramesForGoal(
|
|||||||
detectorOnly,
|
detectorOnly,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return failCancelled()
|
||||||
|
}
|
||||||
|
|
||||||
if batchErr != nil || len(predictions) < endIdx-startIdx {
|
if batchErr != nil || len(predictions) < endIdx-startIdx {
|
||||||
appLogln("⚠️ video batch analyse fehlgeschlagen, fallback auf einzelbild-analyse:", batchErr)
|
appLogln("⚠️ video batch analyse fehlgeschlagen, fallback auf einzelbild-analyse:", batchErr)
|
||||||
|
|
||||||
@ -1968,6 +2016,10 @@ func analyzeVideoFromFramesForGoal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < endIdx-startIdx; i++ {
|
for i := 0; i < endIdx-startIdx; i++ {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return failCancelled()
|
||||||
|
}
|
||||||
|
|
||||||
sample := samples[startIdx+i]
|
sample := samples[startIdx+i]
|
||||||
pred := predictions[i]
|
pred := predictions[i]
|
||||||
|
|
||||||
@ -1999,6 +2051,10 @@ func analyzeVideoFromFramesForGoal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if batchOK {
|
if batchOK {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return failCancelled()
|
||||||
|
}
|
||||||
|
|
||||||
if lastInferencePercent < 100 {
|
if lastInferencePercent < 100 {
|
||||||
publishPercentRange(
|
publishPercentRange(
|
||||||
&lastInferencePercent,
|
&lastInferencePercent,
|
||||||
@ -2012,8 +2068,6 @@ func analyzeVideoFromFramesForGoal(
|
|||||||
cleanNSFWHits := mergeAnalyzeHits(nsfwHits)
|
cleanNSFWHits := mergeAnalyzeHits(nsfwHits)
|
||||||
cleanHighlightHits := mergeAnalyzeHits(highlightHits)
|
cleanHighlightHits := mergeAnalyzeHits(highlightHits)
|
||||||
|
|
||||||
cleanup()
|
|
||||||
|
|
||||||
publishAnalysisFinished(startedAtMs, total, file, "Analyse abgeschlossen")
|
publishAnalysisFinished(startedAtMs, total, file, "Analyse abgeschlossen")
|
||||||
return cleanNSFWHits, cleanHighlightHits, nil
|
return cleanNSFWHits, cleanHighlightHits, nil
|
||||||
}
|
}
|
||||||
@ -2022,12 +2076,21 @@ func analyzeVideoFromFramesForGoal(
|
|||||||
// Fallback: langsame Einzelbild-Analyse.
|
// Fallback: langsame Einzelbild-Analyse.
|
||||||
// Dieser Pfad sollte nur laufen, wenn der AI-Server-Batch fehlschlägt.
|
// Dieser Pfad sollte nur laufen, wenn der AI-Server-Batch fehlschlägt.
|
||||||
for i, sample := range samples {
|
for i, sample := range samples {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return failCancelled()
|
||||||
|
}
|
||||||
|
|
||||||
t := sample.Time
|
t := sample.Time
|
||||||
|
|
||||||
switch goal {
|
switch goal {
|
||||||
case "nsfw":
|
case "nsfw":
|
||||||
res, err := classifyFramePathForAnalyze(ctx, sample.Path)
|
res, frameErr := classifyFramePathForAnalyze(ctx, sample.Path)
|
||||||
if err == nil {
|
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return failCancelled()
|
||||||
|
}
|
||||||
|
|
||||||
|
if frameErr == nil {
|
||||||
bestLabel, bestScore := pickBestNSFWResult(res.Results)
|
bestLabel, bestScore := pickBestNSFWResult(res.Results)
|
||||||
if bestLabel != "" && bestScore >= nsfwThresholdForLabel(bestLabel) {
|
if bestLabel != "" && bestScore >= nsfwThresholdForLabel(bestLabel) {
|
||||||
nsfwHits = append(nsfwHits, analyzeHit{
|
nsfwHits = append(nsfwHits, analyzeHit{
|
||||||
@ -2042,10 +2105,20 @@ func analyzeVideoFromFramesForGoal(
|
|||||||
|
|
||||||
case "highlights":
|
case "highlights":
|
||||||
pred := predictFramePathForAnalyze(ctx, sample.Path)
|
pred := predictFramePathForAnalyze(ctx, sample.Path)
|
||||||
|
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return failCancelled()
|
||||||
|
}
|
||||||
|
|
||||||
highlightHits = appendHighlightHitsFromPrediction(highlightHits, pred, t)
|
highlightHits = appendHighlightHitsFromPrediction(highlightHits, pred, t)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
pred := predictFramePathForAnalyze(ctx, sample.Path)
|
pred := predictFramePathForAnalyze(ctx, sample.Path)
|
||||||
|
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return failCancelled()
|
||||||
|
}
|
||||||
|
|
||||||
nsfwHits = appendNSFWHitFromPrediction(nsfwHits, pred, t)
|
nsfwHits = appendNSFWHitFromPrediction(nsfwHits, pred, t)
|
||||||
highlightHits = appendHighlightHitsFromPrediction(highlightHits, pred, t)
|
highlightHits = appendHighlightHitsFromPrediction(highlightHits, pred, t)
|
||||||
}
|
}
|
||||||
@ -2066,6 +2139,10 @@ func analyzeVideoFromFramesForGoal(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return failCancelled()
|
||||||
|
}
|
||||||
|
|
||||||
if lastInferencePercent < 100 {
|
if lastInferencePercent < 100 {
|
||||||
publishPercentRange(
|
publishPercentRange(
|
||||||
&lastInferencePercent,
|
&lastInferencePercent,
|
||||||
@ -2079,8 +2156,6 @@ func analyzeVideoFromFramesForGoal(
|
|||||||
cleanNSFWHits := mergeAnalyzeHits(nsfwHits)
|
cleanNSFWHits := mergeAnalyzeHits(nsfwHits)
|
||||||
cleanHighlightHits := mergeAnalyzeHits(highlightHits)
|
cleanHighlightHits := mergeAnalyzeHits(highlightHits)
|
||||||
|
|
||||||
cleanup()
|
|
||||||
|
|
||||||
publishAnalysisFinished(startedAtMs, total, file, "Analyse abgeschlossen")
|
publishAnalysisFinished(startedAtMs, total, file, "Analyse abgeschlossen")
|
||||||
|
|
||||||
return cleanNSFWHits, cleanHighlightHits, nil
|
return cleanNSFWHits, cleanHighlightHits, nil
|
||||||
|
|||||||
@ -492,6 +492,45 @@ func runRegenerateAssetsJob(job *regenerateAssetsJob) {
|
|||||||
removeRegenerateAssetsJobLater(file, 3*time.Second)
|
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) {
|
func handleRegenerateAssets(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost && r.Method != http.MethodDelete {
|
if r.Method != http.MethodPost && r.Method != http.MethodDelete {
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
@ -520,13 +559,7 @@ func handleRegenerateAssets(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
regenerateAssetsMu.Lock()
|
cancelRegenerateAssetsJob(file)
|
||||||
job := regenerateAssetsJobs[file]
|
|
||||||
regenerateAssetsMu.Unlock()
|
|
||||||
|
|
||||||
if job != nil && job.Cancel != nil {
|
|
||||||
job.Cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
|||||||
@ -933,7 +933,7 @@ function sortTrainingLabels(input: Partial<TrainingLabels> | null | undefined):
|
|||||||
function TrainingOverlay(props: { step: string; progress: number }) {
|
function TrainingOverlay(props: { step: string; progress: number }) {
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 z-20 flex items-center justify-center text-center text-white">
|
<div className="absolute inset-0 z-20 flex items-center justify-center text-center text-white">
|
||||||
<div className="absolute -inset-2 bg-black/45 backdrop-blur-[8px] shadow-[inset_0_0_72px_30px_rgba(0,0,0,0.75)]" />
|
<div className="absolute inset-0 rounded-lg bg-black/45 backdrop-blur-[8px] shadow-[inset_0_0_72px_30px_rgba(0,0,0,0.75)]" />
|
||||||
|
|
||||||
<div className="relative z-10 flex flex-col items-center justify-center">
|
<div className="relative z-10 flex flex-col items-center justify-center">
|
||||||
<LoadingSpinner
|
<LoadingSpinner
|
||||||
@ -973,7 +973,7 @@ function LoadingImageOverlay(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 z-20 flex items-center justify-center text-center text-white">
|
<div className="absolute inset-0 z-20 flex items-center justify-center text-center text-white">
|
||||||
<div className="absolute -inset-2 rounded-xl bg-black/45 backdrop-blur-[8px] shadow-[inset_0_0_72px_30px_rgba(0,0,0,0.75)]" />
|
<div className="absolute inset-0 rounded-lg bg-black/45 backdrop-blur-[8px] shadow-[inset_0_0_72px_30px_rgba(0,0,0,0.75)]" />
|
||||||
|
|
||||||
<div className="relative z-10 flex flex-col items-center justify-center">
|
<div className="relative z-10 flex flex-col items-center justify-center">
|
||||||
<LoadingSpinner
|
<LoadingSpinner
|
||||||
@ -2062,6 +2062,8 @@ export default function TrainingTab(props: {
|
|||||||
const wasTrainingRunningRef = useRef(false)
|
const wasTrainingRunningRef = useRef(false)
|
||||||
const shownTrainingCompletionRef = useRef<string | null>(null)
|
const shownTrainingCompletionRef = useRef<string | null>(null)
|
||||||
|
|
||||||
|
const [frameImageLoaded, setFrameImageLoaded] = useState(false)
|
||||||
|
|
||||||
const imageBoxRef = useRef<HTMLDivElement | null>(null)
|
const imageBoxRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
const detectorBoxesScrollRef = useRef<HTMLDivElement | null>(null)
|
const detectorBoxesScrollRef = useRef<HTMLDivElement | null>(null)
|
||||||
@ -2231,6 +2233,15 @@ export default function TrainingTab(props: {
|
|||||||
return `${sample.frameUrl}&t=${encodeURIComponent(sample.sampleId)}&r=${imageReloadKey}`
|
return `${sample.frameUrl}&t=${encodeURIComponent(sample.sampleId)}&r=${imageReloadKey}`
|
||||||
}, [sample, imageReloadKey])
|
}, [sample, imageReloadKey])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!imageSrc) {
|
||||||
|
setFrameImageLoaded(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setFrameImageLoaded(false)
|
||||||
|
}, [imageSrc])
|
||||||
|
|
||||||
const canStartTraining = Boolean(trainingStatus?.canTrain)
|
const canStartTraining = Boolean(trainingStatus?.canTrain)
|
||||||
const feedbackCount = trainingStatus?.feedbackCount ?? 0
|
const feedbackCount = trainingStatus?.feedbackCount ?? 0
|
||||||
const requiredCount = trainingStatus?.requiredCount ?? 5
|
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 shownTrainingDurationMs = useMemo(() => {
|
||||||
const job = trainingStatus?.training
|
const job = trainingStatus?.training
|
||||||
@ -3428,7 +3441,7 @@ export default function TrainingTab(props: {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
'overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm dark:border-white/10 dark:bg-gray-900/70',
|
'relative z-0 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm dark:border-white/10 dark:bg-gray-900/70',
|
||||||
compact ? 'p-2' : 'p-3',
|
compact ? 'p-2' : 'p-3',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
@ -3580,14 +3593,17 @@ export default function TrainingTab(props: {
|
|||||||
|
|
||||||
const detectorBoxesPanel = (opts?: {
|
const detectorBoxesPanel = (opts?: {
|
||||||
compact?: boolean
|
compact?: boolean
|
||||||
|
stretch?: boolean
|
||||||
maxHeightClassName?: string
|
maxHeightClassName?: string
|
||||||
}) => {
|
}) => {
|
||||||
const compact = Boolean(opts?.compact)
|
const compact = Boolean(opts?.compact)
|
||||||
|
const stretch = Boolean(opts?.stretch)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
'rounded-lg bg-gray-50 ring-1 ring-black/5 dark:bg-white/5 dark:ring-white/10',
|
'relative z-0 flex min-h-0 flex-col overflow-hidden rounded-lg bg-gray-50 ring-1 ring-black/5 dark:bg-white/5 dark:ring-white/10',
|
||||||
|
stretch ? 'h-full max-h-full flex-1' : 'max-h-full',
|
||||||
compact ? 'p-1.5' : 'p-2',
|
compact ? 'p-1.5' : 'p-2',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
@ -3612,11 +3628,11 @@ export default function TrainingTab(props: {
|
|||||||
<div
|
<div
|
||||||
ref={detectorBoxesScrollRef}
|
ref={detectorBoxesScrollRef}
|
||||||
className={[
|
className={[
|
||||||
'mt-2 min-h-6 overflow-y-auto pr-1 overscroll-contain scroll-smooth',
|
'mt-2 min-h-0 flex-1 overflow-y-auto pr-1 overscroll-contain scroll-smooth',
|
||||||
compact
|
compact
|
||||||
? 'max-h-40 space-y-1'
|
? 'max-h-40 space-y-1'
|
||||||
: [
|
: [
|
||||||
'max-h-32 space-y-1',
|
'space-y-1',
|
||||||
opts?.maxHeightClassName || 'lg:max-h-[32dvh]',
|
opts?.maxHeightClassName || 'lg:max-h-[32dvh]',
|
||||||
].join(' '),
|
].join(' '),
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
@ -3657,7 +3673,7 @@ export default function TrainingTab(props: {
|
|||||||
<span className="flex min-w-0 items-center gap-1.5">
|
<span className="flex min-w-0 items-center gap-1.5">
|
||||||
<Icon
|
<Icon
|
||||||
className={[
|
className={[
|
||||||
'shrink-0',
|
'relative z-0 shrink-0',
|
||||||
compact ? 'h-3 w-3' : 'h-3.5 w-3.5',
|
compact ? 'h-3 w-3' : 'h-3.5 w-3.5',
|
||||||
isActive
|
isActive
|
||||||
? 'text-blue-600 dark:text-blue-300'
|
? 'text-blue-600 dark:text-blue-300'
|
||||||
@ -3695,7 +3711,7 @@ export default function TrainingTab(props: {
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={uiLocked}
|
disabled={uiLocked}
|
||||||
className={[
|
className={[
|
||||||
'group/delete shrink-0 inline-flex items-center justify-center rounded-md transition',
|
'group/delete relative z-0 shrink-0 inline-flex items-center justify-center rounded-md transition',
|
||||||
compact ? 'h-6 w-6' : 'h-7 w-7',
|
compact ? 'h-6 w-6' : 'h-7 w-7',
|
||||||
'text-red-600 hover:bg-red-50 hover:text-red-700',
|
'text-red-600 hover:bg-red-50 hover:text-red-700',
|
||||||
'focus:outline-none focus:ring-2 focus:ring-red-500/30',
|
'focus:outline-none focus:ring-2 focus:ring-red-500/30',
|
||||||
@ -3711,7 +3727,7 @@ export default function TrainingTab(props: {
|
|||||||
>
|
>
|
||||||
<TrashIcon
|
<TrashIcon
|
||||||
className={[
|
className={[
|
||||||
'transition group-hover/delete:scale-110',
|
'relative z-0 transition group-hover/delete:scale-110',
|
||||||
compact ? 'h-3.5 w-3.5' : 'h-4 w-4',
|
compact ? 'h-3.5 w-3.5' : 'h-4 w-4',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@ -3738,7 +3754,7 @@ export default function TrainingTab(props: {
|
|||||||
<Button
|
<Button
|
||||||
size="md"
|
size="md"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className={compact ? 'mt-1.5 w-full text-xs' : 'mt-2 w-full'}
|
className={compact ? 'mt-1.5 w-full text-xs' : 'mt-2 w-full shrink-0'}
|
||||||
disabled={uiLocked}
|
disabled={uiLocked}
|
||||||
onClick={clearBoxes}
|
onClick={clearBoxes}
|
||||||
>
|
>
|
||||||
@ -3802,9 +3818,12 @@ export default function TrainingTab(props: {
|
|||||||
Bestätige oder korrigiere die Analyse. Jede Antwort wird als Trainingsdatenpunkt gespeichert.
|
Bestätige oder korrigiere die Analyse. Jede Antwort wird als Trainingsdatenpunkt gespeichert.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 flex min-h-0 flex-1 flex-col gap-3">
|
<div className="mt-3 flex min-h-0 flex-1 flex-col gap-3 overflow-hidden">
|
||||||
<div className="min-h-0">
|
<div className="min-h-0 flex-1 overflow-hidden">
|
||||||
{detectorBoxesPanel({ maxHeightClassName: 'lg:max-h-[28dvh]' })}
|
{detectorBoxesPanel({
|
||||||
|
stretch: true,
|
||||||
|
maxHeightClassName: 'lg:max-h-none',
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
@ -3815,7 +3834,7 @@ export default function TrainingTab(props: {
|
|||||||
|
|
||||||
{/* Mitte */}
|
{/* Mitte */}
|
||||||
<section className="min-w-0 rounded-xl border border-gray-200 bg-white p-2 shadow-sm dark:border-white/10 dark:bg-gray-900/60 sm:p-3 lg:self-start">
|
<section className="min-w-0 rounded-xl border border-gray-200 bg-white p-2 shadow-sm dark:border-white/10 dark:bg-gray-900/60 sm:p-3 lg:self-start">
|
||||||
<div className="relative flex min-h-[180px] flex-1 items-center justify-center overflow-visible rounded-xl bg-black p-2 sm:p-3 lg:min-h-[300px]">
|
<div className="relative flex min-h-[180px] flex-1 items-center justify-center overflow-visible rounded-lg bg-black p-2 sm:p-3 lg:min-h-[300px]">
|
||||||
{imageSrc ? (
|
{imageSrc ? (
|
||||||
<div className="relative flex h-full w-full items-center justify-center">
|
<div className="relative flex h-full w-full items-center justify-center">
|
||||||
<div
|
<div
|
||||||
@ -3836,10 +3855,12 @@ export default function TrainingTab(props: {
|
|||||||
src={imageSrc}
|
src={imageSrc}
|
||||||
alt="Training Frame"
|
alt="Training Frame"
|
||||||
draggable={false}
|
draggable={false}
|
||||||
|
onLoad={() => setFrameImageLoaded(true)}
|
||||||
|
onError={() => setFrameImageLoaded(true)}
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
onDragStart={(e) => e.preventDefault()}
|
onDragStart={(e) => e.preventDefault()}
|
||||||
className={[
|
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',
|
'select-none',
|
||||||
imageTouchClass,
|
imageTouchClass,
|
||||||
'[-webkit-user-drag:none] [-webkit-touch-callout:none]',
|
'[-webkit-user-drag:none] [-webkit-touch-callout:none]',
|
||||||
@ -4228,10 +4249,10 @@ export default function TrainingTab(props: {
|
|||||||
step={shownTrainingStep}
|
step={shownTrainingStep}
|
||||||
progress={shownTrainingProgress}
|
progress={shownTrainingProgress}
|
||||||
/>
|
/>
|
||||||
) : loading ? (
|
) : frameBusy ? (
|
||||||
<LoadingImageOverlay
|
<LoadingImageOverlay
|
||||||
text={analysisStep}
|
text={analysisStep || 'Frame wird geladen…'}
|
||||||
progress={analysisProgress}
|
progress={loading ? analysisProgress : 100}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@ -4250,13 +4271,14 @@ export default function TrainingTab(props: {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sticky bottom-0 z-40 mt-2 grid grid-cols-2 gap-2 sm:static">
|
<div className="sticky bottom-0 z-40 mt-3 grid grid-cols-2 gap-2 sm:static sm:mt-4">
|
||||||
<Button
|
<Button
|
||||||
size="md"
|
size="md"
|
||||||
variant={hasManualCorrection ? 'primary' : 'soft'}
|
variant={hasManualCorrection ? 'primary' : 'soft'}
|
||||||
color={hasManualCorrection ? undefined : 'emerald'}
|
color={hasManualCorrection ? undefined : 'emerald'}
|
||||||
disabled={
|
disabled={
|
||||||
uiLocked ||
|
uiLocked ||
|
||||||
|
frameBusy ||
|
||||||
!sample ||
|
!sample ||
|
||||||
(!hasManualCorrection && !sample.prediction.modelAvailable)
|
(!hasManualCorrection && !sample.prediction.modelAvailable)
|
||||||
}
|
}
|
||||||
@ -4288,7 +4310,7 @@ export default function TrainingTab(props: {
|
|||||||
<Button
|
<Button
|
||||||
size="md"
|
size="md"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
disabled={uiLocked || !sample}
|
disabled={uiLocked || frameBusy || !sample}
|
||||||
onClick={() => void loadNext({ forceNew: true })}
|
onClick={() => void loadNext({ forceNew: true })}
|
||||||
className="w-full justify-center px-2 text-xs sm:text-sm"
|
className="w-full justify-center px-2 text-xs sm:text-sm"
|
||||||
title="Dieses Bild nicht bewerten und ein anderes laden."
|
title="Dieses Bild nicht bewerten und ein anderes laden."
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user