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/delete-all", trainingDeleteAllHandler)
|
||||||
api.HandleFunc("/api/training/skip", trainingSkipHandler)
|
api.HandleFunc("/api/training/skip", trainingSkipHandler)
|
||||||
api.HandleFunc("/api/training/import-video", trainingImportVideoHandler)
|
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/training/video-preview", trainingVideoPreviewHandler)
|
||||||
|
|
||||||
api.HandleFunc("/api/chaturbate/online", chaturbateOnlineHandler)
|
api.HandleFunc("/api/chaturbate/online", chaturbateOnlineHandler)
|
||||||
|
|||||||
@ -1232,13 +1232,36 @@ type TrainingImportVideoRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TrainingImportVideoResponse struct {
|
type TrainingImportVideoResponse struct {
|
||||||
OK bool `json:"ok"`
|
OK bool `json:"ok"`
|
||||||
Count int `json:"count"`
|
Accepted bool `json:"accepted,omitempty"`
|
||||||
Sample *TrainingSample `json:"sample,omitempty"`
|
Running bool `json:"running,omitempty"`
|
||||||
Samples []TrainingSample `json:"samples,omitempty"`
|
RequestID string `json:"requestId,omitempty"`
|
||||||
Errors []string `json:"errors,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 {
|
func trainingCleanImportVideoCount(count int) int {
|
||||||
if count <= 0 {
|
if count <= 0 {
|
||||||
return trainingImportVideoDefaultFrameCount
|
return trainingImportVideoDefaultFrameCount
|
||||||
@ -1512,6 +1535,295 @@ func trainingFrameSecondsForVideo(duration float64, count int) []float64 {
|
|||||||
return out
|
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) {
|
func trainingImportVideoHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
trainingWriteError(w, http.StatusMethodNotAllowed, "method not allowed")
|
trainingWriteError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
@ -1524,6 +1836,17 @@ func trainingImportVideoHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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)
|
outPath := strings.TrimSpace(req.Output)
|
||||||
if outPath == "" {
|
if outPath == "" {
|
||||||
trainingWriteError(w, http.StatusBadRequest, "output missing")
|
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_INFO_DISMISSED_STORAGE_KEY = 'training:info-dismissed-key'
|
||||||
const TRAINING_IMAGE_EXPANDED_STORAGE_KEY = 'training:image-expanded'
|
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: {
|
export default function TrainingTab(props: {
|
||||||
active?: boolean
|
active?: boolean
|
||||||
@ -2766,6 +2768,7 @@ export default function TrainingTab(props: {
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [analysisProgress, setAnalysisProgress] = useState(0)
|
const [analysisProgress, setAnalysisProgress] = useState(0)
|
||||||
const [analysisStep, setAnalysisStep] = useState('')
|
const [analysisStep, setAnalysisStep] = useState('')
|
||||||
|
const [analysisSourceFile, setAnalysisSourceFile] = useState('')
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [training, setTraining] = useState(false)
|
const [training, setTraining] = useState(false)
|
||||||
const [trainingStatus, setTrainingStatus] = useState<TrainingStatus | null>(null)
|
const [trainingStatus, setTrainingStatus] = useState<TrainingStatus | null>(null)
|
||||||
@ -2794,6 +2797,7 @@ export default function TrainingTab(props: {
|
|||||||
|
|
||||||
const [importedSampleQueue, setImportedSampleQueue] = useState<QueuedTrainingSample[]>([])
|
const [importedSampleQueue, setImportedSampleQueue] = useState<QueuedTrainingSample[]>([])
|
||||||
const importedSampleQueueRef = useRef<QueuedTrainingSample[]>([])
|
const importedSampleQueueRef = useRef<QueuedTrainingSample[]>([])
|
||||||
|
const feedbackEditReturnSampleRef = useRef<QueuedTrainingSample | null>(null)
|
||||||
|
|
||||||
const [feedbackModalOpen, setFeedbackModalOpen] = useState(false)
|
const [feedbackModalOpen, setFeedbackModalOpen] = useState(false)
|
||||||
const [feedbackItems, setFeedbackItems] = useState<TrainingAnnotation[]>([])
|
const [feedbackItems, setFeedbackItems] = useState<TrainingAnnotation[]>([])
|
||||||
@ -2972,6 +2976,21 @@ export default function TrainingTab(props: {
|
|||||||
])
|
])
|
||||||
|
|
||||||
const editFeedbackItem = useCallback((item: TrainingAnnotation) => {
|
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 nextSample = annotationToTrainingSample(item)
|
||||||
const nextCorrection = annotationToCorrectionState(item)
|
const nextCorrection = annotationToCorrectionState(item)
|
||||||
const nextManualCorrection = !item.accepted
|
const nextManualCorrection = !item.accepted
|
||||||
@ -3001,7 +3020,7 @@ export default function TrainingTab(props: {
|
|||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}, [])
|
}, [editingFeedback])
|
||||||
|
|
||||||
const getImageContentRect = useCallback((): ImageContentRect | null => {
|
const getImageContentRect = useCallback((): ImageContentRect | null => {
|
||||||
const imgEl = frameImageRef.current
|
const imgEl = frameImageRef.current
|
||||||
@ -3738,6 +3757,7 @@ export default function TrainingTab(props: {
|
|||||||
setLoadingPreviewFallbackUrl(previewUrl)
|
setLoadingPreviewFallbackUrl(previewUrl)
|
||||||
setLoadingPreviewCandidate(previewUrl)
|
setLoadingPreviewCandidate(previewUrl)
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
setAnalysisSourceFile('')
|
||||||
setAnalysisProgress(8)
|
setAnalysisProgress(8)
|
||||||
setAnalysisStep(
|
setAnalysisStep(
|
||||||
opts?.refreshPrediction
|
opts?.refreshPrediction
|
||||||
@ -3793,7 +3813,15 @@ export default function TrainingTab(props: {
|
|||||||
loadTrainingSampleIntoTab(data as TrainingSample)
|
loadTrainingSampleIntoTab(data as TrainingSample)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (isCurrentRequest()) {
|
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 {
|
} finally {
|
||||||
if (!isCurrentRequest()) {
|
if (!isCurrentRequest()) {
|
||||||
@ -3810,6 +3838,7 @@ export default function TrainingTab(props: {
|
|||||||
|
|
||||||
activeAnalysisRequestIdRef.current = null
|
activeAnalysisRequestIdRef.current = null
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
setAnalysisSourceFile('')
|
||||||
setAnalysisProgress(0)
|
setAnalysisProgress(0)
|
||||||
setAnalysisStep('')
|
setAnalysisStep('')
|
||||||
}, 500)
|
}, 500)
|
||||||
@ -3838,6 +3867,64 @@ export default function TrainingTab(props: {
|
|||||||
applyTrainingStatus(data)
|
applyTrainingStatus(data)
|
||||||
}, [applyTrainingStatus])
|
}, [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 importVideoIntoTraining = useCallback(async (raw: any) => {
|
||||||
const output = String(raw?.output || '').trim()
|
const output = String(raw?.output || '').trim()
|
||||||
if (!output) return false
|
if (!output) return false
|
||||||
@ -3868,6 +3955,7 @@ export default function TrainingTab(props: {
|
|||||||
loadingRef.current = true
|
loadingRef.current = true
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
setAnalysisSourceFile(detail.sourceFile || detail.output.split(/[\\/]/).pop() || '')
|
||||||
setAnalysisProgress(5)
|
setAnalysisProgress(5)
|
||||||
setAnalysisStep('Video wird ins Training übernommen…')
|
setAnalysisStep('Video wird ins Training übernommen…')
|
||||||
setError(null)
|
setError(null)
|
||||||
@ -3875,7 +3963,11 @@ export default function TrainingTab(props: {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
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 {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
@ -3898,6 +3990,18 @@ export default function TrainingTab(props: {
|
|||||||
throw new Error(backendText(data, `HTTP ${res.status}`))
|
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)
|
const samples: TrainingSample[] = Array.isArray(data.samples)
|
||||||
? data.samples
|
? data.samples
|
||||||
: data.sample
|
: data.sample
|
||||||
@ -3935,7 +4039,20 @@ export default function TrainingTab(props: {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
} catch (e) {
|
} 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
|
return false
|
||||||
} finally {
|
} finally {
|
||||||
setAnalysisProgress(100)
|
setAnalysisProgress(100)
|
||||||
@ -3948,6 +4065,7 @@ export default function TrainingTab(props: {
|
|||||||
activeAnalysisRequestIdRef.current = null
|
activeAnalysisRequestIdRef.current = null
|
||||||
loadingRef.current = false
|
loadingRef.current = false
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
setAnalysisSourceFile('')
|
||||||
setAnalysisProgress(0)
|
setAnalysisProgress(0)
|
||||||
setAnalysisStep('')
|
setAnalysisStep('')
|
||||||
}
|
}
|
||||||
@ -3961,6 +4079,7 @@ export default function TrainingTab(props: {
|
|||||||
loadPriorityTrainingSamples,
|
loadPriorityTrainingSamples,
|
||||||
loadTrainingStatus,
|
loadTrainingStatus,
|
||||||
setLoadingPreviewCandidate,
|
setLoadingPreviewCandidate,
|
||||||
|
waitForVideoImportResult,
|
||||||
])
|
])
|
||||||
|
|
||||||
const loadTrainingStats = useCallback(async () => {
|
const loadTrainingStats = useCallback(async () => {
|
||||||
@ -4160,6 +4279,16 @@ export default function TrainingTab(props: {
|
|||||||
''
|
''
|
||||||
).trim()
|
).trim()
|
||||||
|
|
||||||
|
const sourceFile = String(
|
||||||
|
data?.sourceFile ||
|
||||||
|
data?.analysis?.sourceFile ||
|
||||||
|
''
|
||||||
|
).trim()
|
||||||
|
|
||||||
|
if (sourceFile) {
|
||||||
|
setAnalysisSourceFile(sourceFile)
|
||||||
|
}
|
||||||
|
|
||||||
const previewUrl = String(
|
const previewUrl = String(
|
||||||
data?.previewUrl ||
|
data?.previewUrl ||
|
||||||
data?.analysis?.previewUrl ||
|
data?.analysis?.previewUrl ||
|
||||||
@ -4359,8 +4488,67 @@ export default function TrainingTab(props: {
|
|||||||
|
|
||||||
window.addEventListener('training:import-video', onImportVideo as EventListener)
|
window.addEventListener('training:import-video', onImportVideo as EventListener)
|
||||||
|
|
||||||
|
let resumedActiveImport = false
|
||||||
|
|
||||||
try {
|
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) {
|
if (raw) {
|
||||||
const detail = JSON.parse(raw)
|
const detail = JSON.parse(raw)
|
||||||
@ -4373,7 +4561,7 @@ export default function TrainingTab(props: {
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('training:import-video', onImportVideo as EventListener)
|
window.removeEventListener('training:import-video', onImportVideo as EventListener)
|
||||||
}
|
}
|
||||||
}, [importVideoIntoTraining])
|
}, [importVideoIntoTraining, waitForVideoImportResult])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!tabActive || initializedRef.current) return
|
if (!tabActive || initializedRef.current) return
|
||||||
@ -4632,6 +4820,21 @@ export default function TrainingTab(props: {
|
|||||||
|
|
||||||
await loadTrainingStatus()
|
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()) {
|
if (!loadNextImportedQueuedSample()) {
|
||||||
await loadNext({
|
await loadNext({
|
||||||
forceNew: true,
|
forceNew: true,
|
||||||
@ -4657,6 +4860,7 @@ export default function TrainingTab(props: {
|
|||||||
editingFeedback,
|
editingFeedback,
|
||||||
loadNext,
|
loadNext,
|
||||||
loadTrainingStatus,
|
loadTrainingStatus,
|
||||||
|
loadQueuedTrainingSample,
|
||||||
loadNextImportedQueuedSample,
|
loadNextImportedQueuedSample,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@ -4664,6 +4868,21 @@ export default function TrainingTab(props: {
|
|||||||
const skipCurrentSample = useCallback(async () => {
|
const skipCurrentSample = useCallback(async () => {
|
||||||
if (!sample) return
|
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
|
const skippedSampleId = sample.sampleId
|
||||||
|
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
@ -4706,7 +4925,13 @@ export default function TrainingTab(props: {
|
|||||||
} finally {
|
} finally {
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
}
|
}
|
||||||
}, [sample, loadNext, loadNextImportedQueuedSample])
|
}, [
|
||||||
|
sample,
|
||||||
|
editingFeedback,
|
||||||
|
loadNext,
|
||||||
|
loadQueuedTrainingSample,
|
||||||
|
loadNextImportedQueuedSample,
|
||||||
|
])
|
||||||
|
|
||||||
const startTraining = useCallback(async () => {
|
const startTraining = useCallback(async () => {
|
||||||
shownTrainingCompletionRef.current = null
|
shownTrainingCompletionRef.current = null
|
||||||
@ -6382,9 +6607,13 @@ export default function TrainingTab(props: {
|
|||||||
const stageOverlayMode: 'training' | 'analysis' =
|
const stageOverlayMode: 'training' | 'analysis' =
|
||||||
trainingRunning ? 'training' : 'analysis'
|
trainingRunning ? 'training' : 'analysis'
|
||||||
|
|
||||||
|
const analysisOverlayText = analysisSourceFile
|
||||||
|
? `${analysisSourceFile} · ${analysisStep || 'Bild wird geladen…'}`
|
||||||
|
: analysisStep || 'Bild wird geladen…'
|
||||||
|
|
||||||
const stageOverlayText = trainingRunning
|
const stageOverlayText = trainingRunning
|
||||||
? shownTrainingStep || 'Aktuelles Bild wird geladen…'
|
? shownTrainingStep || 'Aktuelles Bild wird geladen…'
|
||||||
: analysisStep || 'Bild wird geladen…'
|
: analysisOverlayText
|
||||||
|
|
||||||
const stageOverlayProgress = trainingRunning
|
const stageOverlayProgress = trainingRunning
|
||||||
? shownTrainingProgress
|
? shownTrainingProgress
|
||||||
@ -7391,14 +7620,20 @@ export default function TrainingTab(props: {
|
|||||||
onClick={() => void skipCurrentSample()}
|
onClick={() => void skipCurrentSample()}
|
||||||
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={
|
title={
|
||||||
trainingSampleMode === 'uncertain'
|
editingFeedback
|
||||||
? 'Dieses Bild löschen und eine andere unsichere Prediction laden.'
|
? 'Feedback-Bearbeitung abbrechen und zum vorherigen Trainingsbild zurückkehren.'
|
||||||
: 'Dieses Bild löschen und ein anderes laden.'
|
: 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">
|
<span className="inline-flex items-center gap-1.5">
|
||||||
<ForwardIcon className="h-3.5 w-3.5" aria-hidden="true" />
|
{editingFeedback ? (
|
||||||
Überspringen
|
<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>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user