This commit is contained in:
Linrador 2026-05-06 21:45:29 +02:00
parent cb7f32d24d
commit e4635bb1f1
3 changed files with 144 additions and 45 deletions

View File

@ -1842,6 +1842,9 @@ func trainingCreateNextSample() (*TrainingSample, error) {
func trainingCreateUncertainNextSampleWithProgress(startedAtMs int64) (*TrainingSample, error) {
const attempts = 8
const stepsPerAttempt = 4
totalSteps := attempts*stepsPerAttempt + 1
root, err := trainingRootDir()
if err != nil {
@ -1853,22 +1856,47 @@ func trainingCreateUncertainNextSampleWithProgress(startedAtMs int64) (*Training
errs := []string{}
for i := 0; i < attempts; i++ {
publishAnalysisStep(
attempt := i + 1
stepStart := i*stepsPerAttempt + 1
prefix := fmt.Sprintf("Kandidat %d/%d: ", attempt, attempts)
sample, err := trainingCreateNextSampleWithProgressRange(
startedAtMs,
i+1,
attempts+1,
"",
fmt.Sprintf("Unsicherer Kandidat %d/%d wird analysiert…", i+1, attempts),
stepStart,
totalSteps,
prefix,
)
sample, err := trainingCreateNextSample()
if err != nil {
errs = append(errs, err.Error())
publishAnalysisStep(
startedAtMs,
stepStart+stepsPerAttempt-1,
totalSteps,
"",
fmt.Sprintf("Kandidat %d/%d fehlgeschlagen…", attempt, attempts),
)
continue
}
score := trainingPredictionUncertaintyScore(sample.Prediction)
publishAnalysisStep(
startedAtMs,
stepStart+stepsPerAttempt-1,
totalSteps,
sample.SourceFile,
fmt.Sprintf(
"Kandidat %d/%d bewertet · Unsicherheit %.0f%%",
attempt,
attempts,
score*100,
),
)
if score > bestScore {
if best != nil {
trainingDeleteSampleFiles(root, best.SampleID)
@ -1891,8 +1919,8 @@ func trainingCreateUncertainNextSampleWithProgress(startedAtMs int64) (*Training
publishAnalysisStep(
startedAtMs,
attempts+1,
attempts+1,
totalSteps,
totalSteps,
best.SourceFile,
fmt.Sprintf("Unsicherer Kandidat gewählt · Score %.0f%%", bestScore*100),
)
@ -1988,7 +2016,31 @@ func trainingPredictionUncertaintyScore(pred TrainingPrediction) float64 {
}
func trainingCreateNextSampleWithProgress(startedAtMs int64) (*TrainingSample, error) {
publishAnalysisStep(startedAtMs, 1, 4, "", "Video wird ausgewählt…")
return trainingCreateNextSampleWithProgressRange(
startedAtMs,
1,
4,
"",
)
}
func trainingCreateNextSampleWithProgressRange(
startedAtMs int64,
stepStart int,
stepTotal int,
prefix string,
) (*TrainingSample, error) {
publishStep := func(localStep int, sourceFile string, message string) {
publishAnalysisStep(
startedAtMs,
stepStart+localStep-1,
stepTotal,
sourceFile,
prefix+message,
)
}
publishStep(1, "", "Video wird ausgewählt…")
settings := getSettings()
@ -2002,7 +2054,9 @@ func trainingCreateNextSampleWithProgress(startedAtMs int64) (*TrainingSample, e
return nil, err
}
publishAnalysisStep(startedAtMs, 2, 4, filepath.Base(videoPath), "Bild wird extrahiert…")
sourceFile := filepath.Base(videoPath)
publishStep(2, sourceFile, "Bild wird extrahiert…")
duration := trainingProbeDurationSeconds(videoPath)
second := trainingRandomSecond(duration)
@ -2019,6 +2073,7 @@ func trainingCreateNextSampleWithProgress(startedAtMs int64) (*TrainingSample, e
if err := os.MkdirAll(filepath.Join(root, "frames"), 0755); err != nil {
return nil, err
}
if err := os.MkdirAll(filepath.Join(root, "samples"), 0755); err != nil {
return nil, err
}
@ -2027,6 +2082,8 @@ func trainingCreateNextSampleWithProgress(startedAtMs int64) (*TrainingSample, e
framePath := filepath.Join(root, "frames", id+".jpg")
if err := trainingExtractFrame(videoPath, framePath, second); err != nil {
publishStep(2, sourceFile, "Bild wird erneut bei Sekunde 0 extrahiert…")
second = 0
id = trainingMakeSampleID(videoPath, second)
framePath = filepath.Join(root, "frames", id+".jpg")
@ -2036,7 +2093,7 @@ func trainingCreateNextSampleWithProgress(startedAtMs int64) (*TrainingSample, e
}
}
publishAnalysisStep(startedAtMs, 3, 4, filepath.Base(videoPath), "Bild wird analysiert…")
publishStep(3, sourceFile, "Bild wird analysiert…")
prediction := trainingPredictFrame(framePath)
@ -2048,7 +2105,7 @@ func trainingCreateNextSampleWithProgress(startedAtMs int64) (*TrainingSample, e
sample := &TrainingSample{
SampleID: id,
FrameURL: "/api/training/frame?id=" + id,
SourceFile: filepath.Base(videoPath),
SourceFile: sourceFile,
SourcePath: videoPath,
SourceSizeBytes: sourceSizeBytes,
Second: second,
@ -2056,7 +2113,7 @@ func trainingCreateNextSampleWithProgress(startedAtMs int64) (*TrainingSample, e
Prediction: prediction,
}
publishAnalysisStep(startedAtMs, 4, 4, filepath.Base(videoPath), "Analyse-Ergebnis wird gespeichert…")
publishStep(4, sourceFile, "Analyse-Ergebnis wird gespeichert…")
if err := trainingWriteSample(root, sample); err != nil {
return nil, err

View File

@ -2741,7 +2741,6 @@ export default function App() {
if (!authed) return
let es: EventSource | null = null
let autostartEs: EventSource | null = null
let fallbackTimer: number | null = null
const stopFallbackPoll = () => {
@ -2844,6 +2843,12 @@ export default function App() {
if (msg?.type !== 'analysis_progress') return
window.dispatchEvent(
new CustomEvent('app:sse:analysis', {
detail: msg,
})
)
const file = baseName(String(msg?.file ?? '').trim())
if (!file) return
@ -2929,6 +2934,12 @@ export default function App() {
if (data?.type !== 'training_status') return
setTrainingTabRunning(Boolean(data?.training?.running))
window.dispatchEvent(
new CustomEvent('app:sse:training', {
detail: data,
})
)
} catch {
// ignore
}
@ -2944,20 +2955,6 @@ export default function App() {
eventSourceRef.current = es
modelEventNamesRef.current = new Set()
autostartEs = new EventSource('/api/autostart/state/stream')
autostartEs.addEventListener('autostart', (ev) => {
try {
applyAutostartState(JSON.parse(String((ev as MessageEvent).data ?? 'null')))
} catch {
// ignore
}
})
autostartEs.onerror = () => {
// Falls der separate Stream ausfällt, wenigstens beim Fokus/Visibility wieder korrekt ziehen.
}
es.onopen = () => {
stopFallbackPoll()
void loadTaskStateOnce()
@ -3010,10 +3007,6 @@ export default function App() {
es.close()
}
if (autostartEs) {
autostartEs.close()
}
eventSourceRef.current = null
modelEventNamesRef.current = new Set()
}

View File

@ -2326,10 +2326,10 @@ export default function TrainingTab(props: {
const url = `/api/training/next${params.toString() ? `?${params.toString()}` : ''}`
setAnalysisProgress(25)
setAnalysisProgress(uncertainMode ? 5 : 25)
setAnalysisStep(
uncertainMode
? 'Mehrere Kandidaten werden bewertet…'
? 'Mehrere Kandidaten werden vorbereitet…'
: 'Bild wird vorbereitet…'
)
@ -2441,11 +2441,9 @@ export default function TrainingTab(props: {
}, [])
useEffect(() => {
const es = new EventSource('/api/events/stream')
const onTraining = (ev: MessageEvent) => {
const onTraining = (event: Event) => {
try {
const data = JSON.parse(String(ev.data ?? 'null'))
const data = (event as CustomEvent<any>).detail
if (data?.type !== 'training_status') return
@ -2457,18 +2455,69 @@ export default function TrainingTab(props: {
}
}
es.addEventListener('training', onTraining as EventListener)
es.onerror = () => {
// Optional: Polling-Fallback bleibt separat bestehen.
}
window.addEventListener('app:sse:training', onTraining as EventListener)
return () => {
es.removeEventListener('training', onTraining as EventListener)
es.close()
window.removeEventListener('app:sse:training', onTraining as EventListener)
}
}, [applyTrainingStatus])
useEffect(() => {
const onAnalysis = (event: Event) => {
try {
const data = (event as CustomEvent<any>).detail
const message = String(
data?.message ||
data?.step ||
data?.title ||
''
).trim()
const current = Number(data?.current ?? data?.stepIndex ?? data?.index)
const total = Number(data?.total ?? data?.steps ?? data?.stepTotal)
const rawProgress = Number(
data?.progress ??
data?.percent ??
data?.analysis?.progress
)
let nextProgress: number | null = null
if (
Number.isFinite(current) &&
Number.isFinite(total) &&
total > 0
) {
nextProgress = (current / total) * 100
} else if (Number.isFinite(rawProgress)) {
nextProgress = rawProgress <= 1
? rawProgress * 100
: rawProgress
}
if (message) {
setAnalysisStep(message)
}
if (nextProgress !== null) {
setAnalysisProgress((prev) =>
Math.max(prev, clampPercent(nextProgress))
)
}
} catch {
// ignore
}
}
window.addEventListener('app:sse:analysis', onAnalysis as EventListener)
return () => {
window.removeEventListener('app:sse:analysis', onAnalysis as EventListener)
}
}, [])
useEffect(() => {
const draggingBox = Boolean(drawingBox || boxInteraction)