bugfixes
This commit is contained in:
parent
518a8d5162
commit
885a22e206
BIN
backend/dist/nsfwapp-linux-amd64
vendored
BIN
backend/dist/nsfwapp-linux-amd64
vendored
Binary file not shown.
BIN
backend/dist/nsfwapp.exe
vendored
BIN
backend/dist/nsfwapp.exe
vendored
Binary file not shown.
BIN
backend/dist/nsfwapp_amd64.deb
vendored
BIN
backend/dist/nsfwapp_amd64.deb
vendored
Binary file not shown.
@ -104,6 +104,7 @@ func registerRoutes(mux *http.ServeMux, auth *AuthManager) *ModelStore {
|
||||
api.HandleFunc("/api/training/delete-all", trainingDeleteAllHandler)
|
||||
api.HandleFunc("/api/training/skip", trainingSkipHandler)
|
||||
api.HandleFunc("/api/training/import-video", trainingImportVideoHandler)
|
||||
api.HandleFunc("/api/training/import-video/status", trainingImportVideoStatusHandler)
|
||||
api.HandleFunc("/api/training/video-preview", trainingVideoPreviewHandler)
|
||||
|
||||
api.HandleFunc("/api/chaturbate/online", chaturbateOnlineHandler)
|
||||
|
||||
@ -1232,13 +1232,36 @@ type TrainingImportVideoRequest struct {
|
||||
}
|
||||
|
||||
type TrainingImportVideoResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
Count int `json:"count"`
|
||||
Sample *TrainingSample `json:"sample,omitempty"`
|
||||
Samples []TrainingSample `json:"samples,omitempty"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
OK bool `json:"ok"`
|
||||
Accepted bool `json:"accepted,omitempty"`
|
||||
Running bool `json:"running,omitempty"`
|
||||
RequestID string `json:"requestId,omitempty"`
|
||||
Count int `json:"count"`
|
||||
Sample *TrainingSample `json:"sample,omitempty"`
|
||||
Samples []TrainingSample `json:"samples,omitempty"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type trainingImportVideoJobState struct {
|
||||
requestID string
|
||||
startedAt time.Time
|
||||
finishedAt time.Time
|
||||
running bool
|
||||
statusCode int
|
||||
response *TrainingImportVideoResponse
|
||||
errorText string
|
||||
}
|
||||
|
||||
var trainingImportVideoJobs = struct {
|
||||
sync.Mutex
|
||||
items map[string]*trainingImportVideoJobState
|
||||
}{
|
||||
items: map[string]*trainingImportVideoJobState{},
|
||||
}
|
||||
|
||||
const trainingImportVideoJobTTL = 2 * time.Hour
|
||||
|
||||
func trainingCleanImportVideoCount(count int) int {
|
||||
if count <= 0 {
|
||||
return trainingImportVideoDefaultFrameCount
|
||||
@ -1512,6 +1535,295 @@ func trainingFrameSecondsForVideo(duration float64, count int) []float64 {
|
||||
return out
|
||||
}
|
||||
|
||||
func trainingRunImportVideoRequest(req TrainingImportVideoRequest) (TrainingImportVideoResponse, int, string) {
|
||||
outPath := strings.TrimSpace(req.Output)
|
||||
if outPath == "" {
|
||||
msg := "output missing"
|
||||
return TrainingImportVideoResponse{OK: false, Error: msg}, http.StatusBadRequest, msg
|
||||
}
|
||||
|
||||
if !trainingSupportedImportVideo(outPath) {
|
||||
msg := "unsupported video type"
|
||||
return TrainingImportVideoResponse{OK: false, Error: msg}, http.StatusBadRequest, msg
|
||||
}
|
||||
|
||||
fi, err := os.Stat(outPath)
|
||||
if err != nil || fi == nil || fi.IsDir() || fi.Size() <= 0 {
|
||||
if err == nil {
|
||||
err = errors.New("video file missing or empty")
|
||||
}
|
||||
|
||||
msg := "video not found: " + err.Error()
|
||||
return TrainingImportVideoResponse{OK: false, Error: msg}, http.StatusBadRequest, msg
|
||||
}
|
||||
|
||||
duration := trainingProbeDurationSeconds(outPath)
|
||||
if duration <= 0 {
|
||||
msg := "Videolaenge konnte nicht bestimmt werden"
|
||||
return TrainingImportVideoResponse{OK: false, Error: msg}, http.StatusBadRequest, msg
|
||||
}
|
||||
|
||||
root, err := trainingRootDir()
|
||||
if err != nil {
|
||||
msg := err.Error()
|
||||
return TrainingImportVideoResponse{OK: false, Error: msg}, http.StatusInternalServerError, msg
|
||||
}
|
||||
|
||||
if err := trainingEnsureDetectorDirs(root); err != nil {
|
||||
msg := err.Error()
|
||||
return TrainingImportVideoResponse{OK: false, Error: msg}, http.StatusInternalServerError, msg
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(root, "frames"), 0755); err != nil {
|
||||
msg := err.Error()
|
||||
return TrainingImportVideoResponse{OK: false, Error: msg}, http.StatusInternalServerError, msg
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(root, "samples"), 0755); err != nil {
|
||||
msg := err.Error()
|
||||
return TrainingImportVideoResponse{OK: false, Error: msg}, http.StatusInternalServerError, msg
|
||||
}
|
||||
|
||||
seconds := trainingFrameSecondsForVideo(duration, req.Count)
|
||||
sourceFile := filepath.Base(outPath)
|
||||
previewURL := ""
|
||||
sourceFileWithFrame := func(index int) string {
|
||||
return fmt.Sprintf("%s (%d / %d)", sourceFile, index+1, len(seconds))
|
||||
}
|
||||
|
||||
requestID := strings.TrimSpace(req.AnalysisRequestID)
|
||||
if requestID == "" {
|
||||
requestID = trainingMakeSampleID(outPath, float64(time.Now().UnixNano()))
|
||||
}
|
||||
|
||||
totalSteps := len(seconds) * 3
|
||||
if totalSteps < 1 {
|
||||
totalSteps = 1
|
||||
}
|
||||
|
||||
startedAtMs := trainingPublishAnalysisStartedWithPreview(
|
||||
requestID,
|
||||
totalSteps,
|
||||
sourceFile,
|
||||
previewURL,
|
||||
"Video wird ins Training uebernommen...",
|
||||
)
|
||||
|
||||
var sourceSizeBytes int64
|
||||
if st, err := os.Stat(outPath); err == nil && st != nil && !st.IsDir() {
|
||||
sourceSizeBytes = st.Size()
|
||||
}
|
||||
|
||||
samples := make([]TrainingSample, 0, len(seconds))
|
||||
errs := []string{}
|
||||
|
||||
for i, second := range seconds {
|
||||
stepBase := i * 3
|
||||
|
||||
trainingPublishAnalysisStepWithPreview(
|
||||
requestID,
|
||||
startedAtMs,
|
||||
stepBase+1,
|
||||
totalSteps,
|
||||
sourceFileWithFrame(i),
|
||||
previewURL,
|
||||
fmt.Sprintf("Frame %d/%d wird extrahiert...", i+1, len(seconds)),
|
||||
)
|
||||
|
||||
id := trainingMakeSampleID(outPath, second)
|
||||
framePath := filepath.Join(root, "frames", id+".jpg")
|
||||
|
||||
if err := trainingExtractFrame(outPath, framePath, second); err != nil {
|
||||
errs = append(errs, fmt.Sprintf("Frame bei %.1fs: %v", second, err))
|
||||
continue
|
||||
}
|
||||
|
||||
previewURL = "/api/training/frame?id=" + url.QueryEscape(id)
|
||||
|
||||
trainingPublishAnalysisStepWithPreview(
|
||||
requestID,
|
||||
startedAtMs,
|
||||
stepBase+2,
|
||||
totalSteps,
|
||||
sourceFileWithFrame(i),
|
||||
previewURL,
|
||||
fmt.Sprintf("Frame %d/%d wird analysiert...", i+1, len(seconds)),
|
||||
)
|
||||
|
||||
prediction := trainingPredictFrame(framePath)
|
||||
|
||||
sample := &TrainingSample{
|
||||
SampleID: id,
|
||||
FrameURL: "/api/training/frame?id=" + id,
|
||||
SourceFile: sourceFile,
|
||||
SourcePath: outPath,
|
||||
SourceSizeBytes: sourceSizeBytes,
|
||||
Second: second,
|
||||
CreatedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
Prediction: prediction,
|
||||
}
|
||||
|
||||
trainingPublishAnalysisStepWithPreview(
|
||||
requestID,
|
||||
startedAtMs,
|
||||
stepBase+3,
|
||||
totalSteps,
|
||||
sourceFileWithFrame(i),
|
||||
previewURL,
|
||||
fmt.Sprintf("Frame %d/%d wird gespeichert...", i+1, len(seconds)),
|
||||
)
|
||||
|
||||
if err := trainingWriteSample(root, sample); err != nil {
|
||||
_ = os.Remove(framePath)
|
||||
errs = append(errs, fmt.Sprintf("Frame bei %.1fs speichern: %v", second, err))
|
||||
continue
|
||||
}
|
||||
|
||||
samples = append(samples, *sample)
|
||||
}
|
||||
|
||||
if len(samples) == 0 {
|
||||
msg := "keine Trainingsframes erzeugt"
|
||||
if len(errs) > 0 {
|
||||
msg += ": " + strings.Join(errs, "; ")
|
||||
}
|
||||
|
||||
err := errors.New(msg)
|
||||
trainingPublishAnalysisError(
|
||||
requestID,
|
||||
startedAtMs,
|
||||
sourceFile,
|
||||
"Video konnte nicht ins Training uebernommen werden.",
|
||||
err,
|
||||
)
|
||||
|
||||
return TrainingImportVideoResponse{OK: false, RequestID: requestID, Error: msg}, http.StatusInternalServerError, msg
|
||||
}
|
||||
|
||||
trainingPublishAnalysisFinished(
|
||||
requestID,
|
||||
startedAtMs,
|
||||
totalSteps,
|
||||
sourceFile,
|
||||
fmt.Sprintf("%d Frames ins Training uebernommen.", len(samples)),
|
||||
)
|
||||
|
||||
return TrainingImportVideoResponse{
|
||||
OK: true,
|
||||
RequestID: requestID,
|
||||
Count: len(samples),
|
||||
Sample: &samples[0],
|
||||
Samples: samples,
|
||||
Errors: errs,
|
||||
}, http.StatusOK, ""
|
||||
}
|
||||
|
||||
func trainingPruneImportVideoJobsLocked(now time.Time) {
|
||||
for requestID, job := range trainingImportVideoJobs.items {
|
||||
if job == nil || job.running {
|
||||
continue
|
||||
}
|
||||
if !job.finishedAt.IsZero() && now.Sub(job.finishedAt) > trainingImportVideoJobTTL {
|
||||
delete(trainingImportVideoJobs.items, requestID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func trainingStartImportVideoJob(req TrainingImportVideoRequest) {
|
||||
requestID := strings.TrimSpace(req.AnalysisRequestID)
|
||||
if requestID == "" {
|
||||
requestID = trainingMakeSampleID(req.Output, float64(time.Now().UnixNano()))
|
||||
req.AnalysisRequestID = requestID
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
trainingImportVideoJobs.Lock()
|
||||
trainingPruneImportVideoJobsLocked(now)
|
||||
if _, exists := trainingImportVideoJobs.items[requestID]; exists {
|
||||
trainingImportVideoJobs.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
trainingImportVideoJobs.items[requestID] = &trainingImportVideoJobState{
|
||||
requestID: requestID,
|
||||
startedAt: now,
|
||||
running: true,
|
||||
statusCode: http.StatusAccepted,
|
||||
}
|
||||
trainingImportVideoJobs.Unlock()
|
||||
|
||||
go func() {
|
||||
resp, statusCode, errorText := trainingRunImportVideoRequest(req)
|
||||
resp.RequestID = requestID
|
||||
|
||||
trainingImportVideoJobs.Lock()
|
||||
if job := trainingImportVideoJobs.items[requestID]; job != nil {
|
||||
job.running = false
|
||||
job.finishedAt = time.Now().UTC()
|
||||
job.statusCode = statusCode
|
||||
job.response = &resp
|
||||
job.errorText = strings.TrimSpace(errorText)
|
||||
}
|
||||
trainingImportVideoJobs.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
func trainingImportVideoJobResponse(requestID string) (TrainingImportVideoResponse, int) {
|
||||
requestID = strings.TrimSpace(requestID)
|
||||
if requestID == "" {
|
||||
return TrainingImportVideoResponse{OK: false, Error: "requestId missing"}, http.StatusBadRequest
|
||||
}
|
||||
|
||||
trainingImportVideoJobs.Lock()
|
||||
job := trainingImportVideoJobs.items[requestID]
|
||||
trainingImportVideoJobs.Unlock()
|
||||
|
||||
if job == nil {
|
||||
return TrainingImportVideoResponse{OK: false, RequestID: requestID, Error: "Import-Job nicht gefunden."}, http.StatusNotFound
|
||||
}
|
||||
|
||||
if job.running {
|
||||
return TrainingImportVideoResponse{
|
||||
OK: true,
|
||||
Accepted: true,
|
||||
Running: true,
|
||||
RequestID: requestID,
|
||||
}, http.StatusAccepted
|
||||
}
|
||||
|
||||
if job.response != nil {
|
||||
resp := *job.response
|
||||
resp.Running = false
|
||||
resp.RequestID = requestID
|
||||
if resp.Error == "" {
|
||||
resp.Error = job.errorText
|
||||
}
|
||||
statusCode := job.statusCode
|
||||
if statusCode < 100 {
|
||||
statusCode = http.StatusOK
|
||||
}
|
||||
return resp, statusCode
|
||||
}
|
||||
|
||||
return TrainingImportVideoResponse{OK: false, RequestID: requestID, Error: job.errorText}, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
func trainingImportVideoStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
trainingWriteError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
requestID := strings.TrimSpace(r.URL.Query().Get("requestId"))
|
||||
if requestID == "" {
|
||||
requestID = strings.TrimSpace(r.URL.Query().Get("id"))
|
||||
}
|
||||
|
||||
resp, statusCode := trainingImportVideoJobResponse(requestID)
|
||||
trainingWriteJSON(w, statusCode, resp)
|
||||
}
|
||||
|
||||
func trainingImportVideoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
trainingWriteError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
@ -1524,6 +1836,17 @@ func trainingImportVideoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
analysisRequestID := strings.TrimSpace(req.AnalysisRequestID)
|
||||
if analysisRequestID == "" {
|
||||
analysisRequestID = trainingMakeSampleID(req.Output, float64(time.Now().UnixNano()))
|
||||
req.AnalysisRequestID = analysisRequestID
|
||||
}
|
||||
|
||||
trainingStartImportVideoJob(req)
|
||||
resp, statusCode := trainingImportVideoJobResponse(analysisRequestID)
|
||||
trainingWriteJSON(w, statusCode, resp)
|
||||
return
|
||||
|
||||
outPath := strings.TrimSpace(req.Output)
|
||||
if outPath == "" {
|
||||
trainingWriteError(w, http.StatusBadRequest, "output missing")
|
||||
|
||||
@ -2751,6 +2751,8 @@ const FEEDBACK_PAGE_SIZE = 10
|
||||
|
||||
const TRAINING_INFO_DISMISSED_STORAGE_KEY = 'training:info-dismissed-key'
|
||||
const TRAINING_IMAGE_EXPANDED_STORAGE_KEY = 'training:image-expanded'
|
||||
const TRAINING_PENDING_IMPORT_VIDEO_STORAGE_KEY = 'training:pending-import-video'
|
||||
const TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY = 'training:active-import-video'
|
||||
|
||||
export default function TrainingTab(props: {
|
||||
active?: boolean
|
||||
@ -2766,6 +2768,7 @@ export default function TrainingTab(props: {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [analysisProgress, setAnalysisProgress] = useState(0)
|
||||
const [analysisStep, setAnalysisStep] = useState('')
|
||||
const [analysisSourceFile, setAnalysisSourceFile] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [training, setTraining] = useState(false)
|
||||
const [trainingStatus, setTrainingStatus] = useState<TrainingStatus | null>(null)
|
||||
@ -2794,6 +2797,7 @@ export default function TrainingTab(props: {
|
||||
|
||||
const [importedSampleQueue, setImportedSampleQueue] = useState<QueuedTrainingSample[]>([])
|
||||
const importedSampleQueueRef = useRef<QueuedTrainingSample[]>([])
|
||||
const feedbackEditReturnSampleRef = useRef<QueuedTrainingSample | null>(null)
|
||||
|
||||
const [feedbackModalOpen, setFeedbackModalOpen] = useState(false)
|
||||
const [feedbackItems, setFeedbackItems] = useState<TrainingAnnotation[]>([])
|
||||
@ -2972,6 +2976,21 @@ export default function TrainingTab(props: {
|
||||
])
|
||||
|
||||
const editFeedbackItem = useCallback((item: TrainingAnnotation) => {
|
||||
const currentSample = sampleRef.current
|
||||
|
||||
if (
|
||||
!editingFeedback &&
|
||||
!feedbackEditReturnSampleRef.current &&
|
||||
currentSample &&
|
||||
currentSample.sampleId !== item.sampleId
|
||||
) {
|
||||
feedbackEditReturnSampleRef.current = {
|
||||
sample: currentSample,
|
||||
correction: cloneCorrectionState(correctionRef.current),
|
||||
manualCorrection: hasManualCorrectionRef.current,
|
||||
}
|
||||
}
|
||||
|
||||
const nextSample = annotationToTrainingSample(item)
|
||||
const nextCorrection = annotationToCorrectionState(item)
|
||||
const nextManualCorrection = !item.accepted
|
||||
@ -3001,7 +3020,7 @@ export default function TrainingTab(props: {
|
||||
behavior: 'smooth',
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
}, [editingFeedback])
|
||||
|
||||
const getImageContentRect = useCallback((): ImageContentRect | null => {
|
||||
const imgEl = frameImageRef.current
|
||||
@ -3738,6 +3757,7 @@ export default function TrainingTab(props: {
|
||||
setLoadingPreviewFallbackUrl(previewUrl)
|
||||
setLoadingPreviewCandidate(previewUrl)
|
||||
setLoading(true)
|
||||
setAnalysisSourceFile('')
|
||||
setAnalysisProgress(8)
|
||||
setAnalysisStep(
|
||||
opts?.refreshPrediction
|
||||
@ -3793,7 +3813,15 @@ export default function TrainingTab(props: {
|
||||
loadTrainingSampleIntoTab(data as TrainingSample)
|
||||
} catch (e) {
|
||||
if (isCurrentRequest()) {
|
||||
setError(e instanceof Error ? e.message : String(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. Beim Zurückkehren wird das nächste offene Trainingsbild wieder geladen.')
|
||||
} else {
|
||||
setError(msg)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (!isCurrentRequest()) {
|
||||
@ -3810,6 +3838,7 @@ export default function TrainingTab(props: {
|
||||
|
||||
activeAnalysisRequestIdRef.current = null
|
||||
setLoading(false)
|
||||
setAnalysisSourceFile('')
|
||||
setAnalysisProgress(0)
|
||||
setAnalysisStep('')
|
||||
}, 500)
|
||||
@ -3838,6 +3867,64 @@ export default function TrainingTab(props: {
|
||||
applyTrainingStatus(data)
|
||||
}, [applyTrainingStatus])
|
||||
|
||||
const completeVideoImportFromData = useCallback(async (data: any) => {
|
||||
const samples: TrainingSample[] = Array.isArray(data?.samples)
|
||||
? data.samples
|
||||
: data?.sample
|
||||
? [data.sample]
|
||||
: []
|
||||
|
||||
if (samples.length === 0) {
|
||||
throw new Error('Es wurden keine Trainingsframes erzeugt.')
|
||||
}
|
||||
|
||||
const deferredCurrentSample = Boolean(sampleRef.current)
|
||||
|
||||
loadPriorityTrainingSamples(samples, {
|
||||
deferCurrentSampleToQueueEnd: deferredCurrentSample,
|
||||
})
|
||||
setImageReloadKey((value) => value + 1)
|
||||
|
||||
await loadTrainingStatus()
|
||||
|
||||
const errorCount = Array.isArray(data?.errors) ? data.errors.length : 0
|
||||
const baseMessage = errorCount > 0
|
||||
? `${samples.length} Frames ins Training übernommen, ${errorCount} Frames fehlgeschlagen.`
|
||||
: `${samples.length} Frames ins Training übernommen.`
|
||||
|
||||
setMessage(
|
||||
deferredCurrentSample
|
||||
? `${baseMessage} Das aktuelle Bild wurde ans Ende der Queue gelegt.`
|
||||
: baseMessage
|
||||
)
|
||||
|
||||
return true
|
||||
}, [loadPriorityTrainingSamples, loadTrainingStatus])
|
||||
|
||||
const waitForVideoImportResult = useCallback(async (requestId: string) => {
|
||||
const id = String(requestId || '').trim()
|
||||
if (!id) throw new Error('requestId fehlt.')
|
||||
|
||||
for (;;) {
|
||||
const res = await fetch(
|
||||
`/api/training/import-video/status?requestId=${encodeURIComponent(id)}`,
|
||||
{ cache: 'no-store' }
|
||||
)
|
||||
const data = await res.json().catch(() => null)
|
||||
|
||||
if (res.status === 202 || data?.running) {
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 1200))
|
||||
continue
|
||||
}
|
||||
|
||||
if (!res.ok || !data?.ok) {
|
||||
throw new Error(backendText(data, `HTTP ${res.status}`))
|
||||
}
|
||||
|
||||
return completeVideoImportFromData(data)
|
||||
}
|
||||
}, [completeVideoImportFromData])
|
||||
|
||||
const importVideoIntoTraining = useCallback(async (raw: any) => {
|
||||
const output = String(raw?.output || '').trim()
|
||||
if (!output) return false
|
||||
@ -3868,6 +3955,7 @@ export default function TrainingTab(props: {
|
||||
loadingRef.current = true
|
||||
|
||||
setLoading(true)
|
||||
setAnalysisSourceFile(detail.sourceFile || detail.output.split(/[\\/]/).pop() || '')
|
||||
setAnalysisProgress(5)
|
||||
setAnalysisStep('Video wird ins Training übernommen…')
|
||||
setError(null)
|
||||
@ -3875,7 +3963,11 @@ export default function TrainingTab(props: {
|
||||
|
||||
try {
|
||||
try {
|
||||
window.sessionStorage.removeItem('training:pending-import-video')
|
||||
window.sessionStorage.removeItem(TRAINING_PENDING_IMPORT_VIDEO_STORAGE_KEY)
|
||||
window.sessionStorage.setItem(
|
||||
TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY,
|
||||
JSON.stringify({ requestId, importKey, detail })
|
||||
)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@ -3898,6 +3990,18 @@ export default function TrainingTab(props: {
|
||||
throw new Error(backendText(data, `HTTP ${res.status}`))
|
||||
}
|
||||
|
||||
if (res.status === 202 || data?.accepted || data?.running) {
|
||||
await waitForVideoImportResult(String(data?.requestId || requestId))
|
||||
|
||||
try {
|
||||
window.sessionStorage.removeItem(TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const samples: TrainingSample[] = Array.isArray(data.samples)
|
||||
? data.samples
|
||||
: data.sample
|
||||
@ -3935,7 +4039,20 @@ export default function TrainingTab(props: {
|
||||
|
||||
return true
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(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 {
|
||||
try {
|
||||
window.sessionStorage.removeItem(TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
setError(msg)
|
||||
}
|
||||
return false
|
||||
} finally {
|
||||
setAnalysisProgress(100)
|
||||
@ -3948,6 +4065,7 @@ export default function TrainingTab(props: {
|
||||
activeAnalysisRequestIdRef.current = null
|
||||
loadingRef.current = false
|
||||
setLoading(false)
|
||||
setAnalysisSourceFile('')
|
||||
setAnalysisProgress(0)
|
||||
setAnalysisStep('')
|
||||
}
|
||||
@ -3961,6 +4079,7 @@ export default function TrainingTab(props: {
|
||||
loadPriorityTrainingSamples,
|
||||
loadTrainingStatus,
|
||||
setLoadingPreviewCandidate,
|
||||
waitForVideoImportResult,
|
||||
])
|
||||
|
||||
const loadTrainingStats = useCallback(async () => {
|
||||
@ -4160,6 +4279,16 @@ export default function TrainingTab(props: {
|
||||
''
|
||||
).trim()
|
||||
|
||||
const sourceFile = String(
|
||||
data?.sourceFile ||
|
||||
data?.analysis?.sourceFile ||
|
||||
''
|
||||
).trim()
|
||||
|
||||
if (sourceFile) {
|
||||
setAnalysisSourceFile(sourceFile)
|
||||
}
|
||||
|
||||
const previewUrl = String(
|
||||
data?.previewUrl ||
|
||||
data?.analysis?.previewUrl ||
|
||||
@ -4359,8 +4488,67 @@ export default function TrainingTab(props: {
|
||||
|
||||
window.addEventListener('training:import-video', onImportVideo as EventListener)
|
||||
|
||||
let resumedActiveImport = false
|
||||
|
||||
try {
|
||||
const raw = window.sessionStorage.getItem('training:pending-import-video')
|
||||
const activeRaw = window.sessionStorage.getItem(TRAINING_ACTIVE_IMPORT_VIDEO_STORAGE_KEY)
|
||||
|
||||
if (activeRaw) {
|
||||
const active = JSON.parse(activeRaw)
|
||||
const requestId = String(active?.requestId || '').trim()
|
||||
const importKey = String(active?.importKey || '').trim()
|
||||
|
||||
if (requestId) {
|
||||
if (importKey) {
|
||||
videoImportInFlightKeyRef.current = importKey
|
||||
}
|
||||
|
||||
activeAnalysisRequestIdRef.current = requestId
|
||||
loadingRef.current = true
|
||||
setLoading(true)
|
||||
setAnalysisSourceFile(String(active?.detail?.sourceFile || active?.detail?.output || '').split(/[\\/]/).pop() || '')
|
||||
setAnalysisProgress(5)
|
||||
setAnalysisStep('Video-Import wird fortgesetzt…')
|
||||
setError(null)
|
||||
|
||||
void waitForVideoImportResult(requestId)
|
||||
.then(() => {
|
||||
window.sessionStorage.removeItem(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 {
|
||||
window.sessionStorage.removeItem(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 (importKey && videoImportInFlightKeyRef.current === importKey) {
|
||||
videoImportInFlightKeyRef.current = null
|
||||
}
|
||||
})
|
||||
|
||||
resumedActiveImport = true
|
||||
}
|
||||
}
|
||||
|
||||
const raw = resumedActiveImport
|
||||
? ''
|
||||
: window.sessionStorage.getItem(TRAINING_PENDING_IMPORT_VIDEO_STORAGE_KEY)
|
||||
|
||||
if (raw) {
|
||||
const detail = JSON.parse(raw)
|
||||
@ -4373,7 +4561,7 @@ export default function TrainingTab(props: {
|
||||
return () => {
|
||||
window.removeEventListener('training:import-video', onImportVideo as EventListener)
|
||||
}
|
||||
}, [importVideoIntoTraining])
|
||||
}, [importVideoIntoTraining, waitForVideoImportResult])
|
||||
|
||||
useEffect(() => {
|
||||
if (!tabActive || initializedRef.current) return
|
||||
@ -4632,6 +4820,21 @@ export default function TrainingTab(props: {
|
||||
|
||||
await loadTrainingStatus()
|
||||
|
||||
if (wasEditingFeedback) {
|
||||
const returnItem = feedbackEditReturnSampleRef.current
|
||||
feedbackEditReturnSampleRef.current = null
|
||||
|
||||
if (returnItem) {
|
||||
loadQueuedTrainingSample(returnItem)
|
||||
return
|
||||
}
|
||||
|
||||
if (!loadNextImportedQueuedSample()) {
|
||||
await loadNext({ preserveNotice: true })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!loadNextImportedQueuedSample()) {
|
||||
await loadNext({
|
||||
forceNew: true,
|
||||
@ -4657,6 +4860,7 @@ export default function TrainingTab(props: {
|
||||
editingFeedback,
|
||||
loadNext,
|
||||
loadTrainingStatus,
|
||||
loadQueuedTrainingSample,
|
||||
loadNextImportedQueuedSample,
|
||||
]
|
||||
)
|
||||
@ -4664,6 +4868,21 @@ export default function TrainingTab(props: {
|
||||
const skipCurrentSample = useCallback(async () => {
|
||||
if (!sample) return
|
||||
|
||||
if (editingFeedback) {
|
||||
const returnItem = feedbackEditReturnSampleRef.current
|
||||
feedbackEditReturnSampleRef.current = null
|
||||
|
||||
setEditingFeedback(null)
|
||||
setError(null)
|
||||
setMessage('Feedback-Bearbeitung abgebrochen.')
|
||||
|
||||
if (returnItem) {
|
||||
loadQueuedTrainingSample(returnItem)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const skippedSampleId = sample.sampleId
|
||||
|
||||
setSaving(true)
|
||||
@ -4706,7 +4925,13 @@ export default function TrainingTab(props: {
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}, [sample, loadNext, loadNextImportedQueuedSample])
|
||||
}, [
|
||||
sample,
|
||||
editingFeedback,
|
||||
loadNext,
|
||||
loadQueuedTrainingSample,
|
||||
loadNextImportedQueuedSample,
|
||||
])
|
||||
|
||||
const startTraining = useCallback(async () => {
|
||||
shownTrainingCompletionRef.current = null
|
||||
@ -6382,9 +6607,13 @@ export default function TrainingTab(props: {
|
||||
const stageOverlayMode: 'training' | 'analysis' =
|
||||
trainingRunning ? 'training' : 'analysis'
|
||||
|
||||
const analysisOverlayText = analysisSourceFile
|
||||
? `${analysisSourceFile} · ${analysisStep || 'Bild wird geladen…'}`
|
||||
: analysisStep || 'Bild wird geladen…'
|
||||
|
||||
const stageOverlayText = trainingRunning
|
||||
? shownTrainingStep || 'Aktuelles Bild wird geladen…'
|
||||
: analysisStep || 'Bild wird geladen…'
|
||||
: analysisOverlayText
|
||||
|
||||
const stageOverlayProgress = trainingRunning
|
||||
? shownTrainingProgress
|
||||
@ -7391,14 +7620,20 @@ export default function TrainingTab(props: {
|
||||
onClick={() => void skipCurrentSample()}
|
||||
className="w-full justify-center px-2 text-xs sm:text-sm"
|
||||
title={
|
||||
trainingSampleMode === 'uncertain'
|
||||
? 'Dieses Bild löschen und eine andere unsichere Prediction laden.'
|
||||
: 'Dieses Bild löschen und ein anderes laden.'
|
||||
editingFeedback
|
||||
? 'Feedback-Bearbeitung abbrechen und zum vorherigen Trainingsbild zurückkehren.'
|
||||
: trainingSampleMode === 'uncertain'
|
||||
? 'Dieses Bild löschen und eine andere unsichere Prediction laden.'
|
||||
: 'Dieses Bild löschen und ein anderes laden.'
|
||||
}
|
||||
>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<ForwardIcon className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
Überspringen
|
||||
{editingFeedback ? (
|
||||
<XCircleIcon className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
) : (
|
||||
<ForwardIcon className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
)}
|
||||
{editingFeedback ? 'Bearbeitung abbrechen' : 'Überspringen'}
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user