This commit is contained in:
Linrador 2026-04-29 17:45:51 +02:00
parent 13ec4cca54
commit 7d649525d9
4 changed files with 502 additions and 145 deletions

View File

@ -6,6 +6,7 @@ from pathlib import Path
from PIL import Image
from ultralytics import YOLO
import torch
def clamp01(v):
@ -18,6 +19,7 @@ def main():
parser.add_argument("--image", required=True)
parser.add_argument("--conf", type=float, default=0.30)
parser.add_argument("--imgsz", type=int, default=640)
parser.add_argument("--debug-image", action="store_true")
args = parser.parse_args()
root = Path(args.root)
@ -28,33 +30,52 @@ def main():
print(json.dumps({
"available": False,
"source": "detector_missing",
"modelPath": str(model_path),
"boxes": [],
}))
}, ensure_ascii=False))
return
img = Image.open(image_path).convert("RGB")
img_w, img_h = img.size
model = YOLO(str(model_path))
try:
model = YOLO(str(model_path))
except Exception as e:
print(json.dumps({
"available": False,
"source": "detector_load_failed",
"modelPath": str(model_path),
"error": repr(e),
"boxes": [],
}, ensure_ascii=False))
return
device = 0 if torch.cuda.is_available() else "cpu"
results = model.predict(
source=str(image_path),
conf=args.conf,
imgsz=args.imgsz,
conf=float(args.conf),
imgsz=int(args.imgsz),
verbose=False,
device=0 if __import__("torch").cuda.is_available() else "cpu",
device=device,
)
boxes = []
model_names = {}
raw_box_count = 0
if results:
r = results[0]
names = r.names or {}
model_names = {str(k): str(v) for k, v in (r.names or {}).items()}
if r.boxes is not None:
raw_box_count = len(r.boxes)
for b in r.boxes:
cls_id = int(b.cls[0].item())
score = float(b.conf[0].item())
label = str(names.get(cls_id, cls_id))
label = str((r.names or {}).get(cls_id, cls_id))
x1, y1, x2, y2 = [float(v) for v in b.xyxy[0].tolist()]
@ -75,9 +96,25 @@ def main():
"h": h,
})
if args.debug_image:
debug_dir = root / "detector" / "debug"
debug_dir.mkdir(parents=True, exist_ok=True)
out_path = debug_dir / f"{image_path.stem}_conf_{args.conf}.jpg"
plotted = r.plot()
Image.fromarray(plotted).save(out_path)
print(json.dumps({
"available": True,
"source": "yolo_detector",
"modelPath": str(model_path),
"image": str(image_path),
"conf": float(args.conf),
"imgsz": int(args.imgsz),
"device": str(device),
"imageWidth": img_w,
"imageHeight": img_h,
"classNames": model_names,
"rawBoxCount": raw_box_count,
"boxes": boxes,
}, ensure_ascii=False))

View File

@ -158,45 +158,10 @@ def main():
"bodyPartsPresent": weighted_multilabel(top_targets, "bodyPartsPresent", weights),
"objectsPresent": weighted_multilabel(top_targets, "objectsPresent", weights),
"clothingPresent": weighted_multilabel(top_targets, "clothingPresent", weights),
"boxes": predict_boxes(root, image_path),
"boxes": [],
}
print(json.dumps(pred, ensure_ascii=False))
def predict_boxes(root: Path, image_path: Path):
candidates = [
Path(__file__).parent / "predict_detector_model.py",
Path.cwd() / "backend" / "ml" / "predict_detector_model.py",
Path.cwd() / "ml" / "predict_detector_model.py",
]
script = next((p for p in candidates if p.exists()), None)
if script is None:
return []
try:
proc = subprocess.run(
[
sys.executable,
str(script),
"--root",
str(root),
"--image",
str(image_path),
],
capture_output=True,
text=True,
timeout=60,
)
if proc.returncode != 0:
return []
data = json.loads(proc.stdout or "{}")
return data.get("boxes") or []
except Exception:
return []
if __name__ == "__main__":
main()

View File

@ -56,7 +56,7 @@ type TrainingPrediction struct {
BodyPartsPresent []TrainingScoredLabel `json:"bodyPartsPresent"`
ObjectsPresent []TrainingScoredLabel `json:"objectsPresent"`
ClothingPresent []TrainingScoredLabel `json:"clothingPresent"`
Boxes []TrainingBox `json:"boxes,omitempty"`
Boxes []TrainingBox `json:"boxes"`
}
type TrainingCorrection struct {
@ -68,7 +68,7 @@ type TrainingCorrection struct {
BodyPartsPresent []string `json:"bodyPartsPresent"`
ObjectsPresent []string `json:"objectsPresent"`
ClothingPresent []string `json:"clothingPresent"`
Boxes []TrainingBox `json:"boxes,omitempty"`
Boxes []TrainingBox `json:"boxes"`
}
type TrainingSample struct {
@ -104,7 +104,11 @@ type TrainingAnnotation struct {
Notes string `json:"notes,omitempty"`
}
const minTrainingFeedbackCount = 5
type TrainingDetectorPrediction struct {
Available bool `json:"available"`
Source string `json:"source,omitempty"`
Boxes []TrainingBox `json:"boxes"`
}
type TrainingJobStatus struct {
Running bool `json:"running"`
@ -116,6 +120,11 @@ type TrainingJobStatus struct {
FinishedAt string `json:"finishedAt,omitempty"`
}
const minTrainingFeedbackCount = 5
const minDetectorTrainCount = 20
const minDetectorValCount = 3
var trainingJob = struct {
mu sync.Mutex
status TrainingJobStatus
@ -457,17 +466,45 @@ func trainingTrainHandler(w http.ResponseWriter, r *http.Request) {
return
}
feedbackPath := filepath.Join(root, "feedback.jsonl")
count, _ := trainingCountAnnotations(feedbackPath)
// Falls bisher alles zufällig in train gelandet ist, erzeugen wir mindestens
// ein Validation-Sample durch Kopieren. Bei mehr Daten solltest du später
// einen echten 80/20 Split verwenden.
if err := trainingEnsureDetectorValidationSample(root); err != nil {
fmt.Println("⚠️ detector val sample ensure failed:", err)
}
if count < minTrainingFeedbackCount {
feedbackPath := filepath.Join(root, "feedback.jsonl")
feedbackCount, _ := trainingCountAnnotations(feedbackPath)
detectorTrainImages := filepath.Join(root, "detector", "dataset", "images", "train")
detectorTrainLabels := filepath.Join(root, "detector", "dataset", "labels", "train")
detectorValImages := filepath.Join(root, "detector", "dataset", "images", "val")
detectorValLabels := filepath.Join(root, "detector", "dataset", "labels", "val")
detectorDatasetYAML := filepath.Join(root, "detector", "dataset", "dataset.yaml")
trainCount := trainingCountDetectorSamples(detectorTrainImages, detectorTrainLabels)
valCount := trainingCountDetectorSamples(detectorValImages, detectorValLabels)
if !fileExistsNonEmpty(detectorDatasetYAML) {
trainingWriteError(
w,
http.StatusBadRequest,
"YOLO dataset.yaml fehlt oder ist leer. Bitte Detector-Ordner/Dataset prüfen.",
)
return
}
if trainCount < minDetectorTrainCount || valCount < minDetectorValCount {
trainingWriteError(
w,
http.StatusBadRequest,
fmt.Sprintf(
"Zu wenige Bewertungen. Mindestens %d, aktuell %d.",
minTrainingFeedbackCount,
count,
"Zu wenige YOLO-Box-Labels. Benötigt: mindestens %d Train und %d Val. Aktuell: Train=%d, Val=%d. Feedback gesamt: %d.",
minDetectorTrainCount,
minDetectorValCount,
trainCount,
valCount,
feedbackCount,
),
)
return
@ -477,17 +514,27 @@ func trainingTrainHandler(w http.ResponseWriter, r *http.Request) {
*s = TrainingJobStatus{
Running: true,
Progress: 5,
Step: "Training wird vorbereitet…",
Step: "YOLO-Detector-Training wird vorbereitet…",
StartedAt: time.Now().UTC().Format(time.RFC3339),
}
})
go trainingRunJob(root, count)
go trainingRunJob(root, feedbackCount)
trainingWriteJSON(w, http.StatusAccepted, map[string]any{
"ok": true,
"message": "Training gestartet.",
"message": "YOLO-Detector-Training gestartet.",
"training": trainingGetJobStatus(),
"detector": map[string]any{
"trainCount": trainCount,
"valCount": valCount,
"requiredTrain": minDetectorTrainCount,
"requiredVal": minDetectorValCount,
"datasetYAML": detectorDatasetYAML,
"usesSceneKNN": false,
"usesResNet18KNN": false,
"source": "yolo_detector",
},
})
}
@ -495,25 +542,7 @@ func trainingRunJob(root string, count int) {
python := trainingPythonExe()
trainingSetJobStatus(func(s *TrainingJobStatus) {
s.Progress = 15
s.Step = "Scene-Modell wird trainiert…"
})
sceneScript := trainingScriptPath("train_scene_model.py")
sceneOut, err := trainingRunCommand(python, sceneScript, "--root", root)
if err != nil {
trainingSetJobStatus(func(s *TrainingJobStatus) {
s.Running = false
s.Progress = 0
s.Step = ""
s.Error = fmt.Sprintf("scene training failed: %v: %s", err, sceneOut)
s.FinishedAt = time.Now().UTC().Format(time.RFC3339)
})
return
}
trainingSetJobStatus(func(s *TrainingJobStatus) {
s.Progress = 65
s.Progress = 20
s.Step = "Detector-Daten werden geprüft…"
})
@ -526,13 +555,21 @@ func trainingRunJob(root string, count int) {
detectorValImages := filepath.Join(root, "detector", "dataset", "images", "val")
detectorValLabels := filepath.Join(root, "detector", "dataset", "labels", "val")
if err := trainingEnsureDetectorValidationSample(root); err != nil {
fmt.Println("⚠️ detector val sample ensure failed:", err)
}
trainCount := trainingCountDetectorSamples(detectorTrainImages, detectorTrainLabels)
valCount := trainingCountDetectorSamples(detectorValImages, detectorValLabels)
if fileExistsNonEmpty(detectorDatasetYAML) && trainCount >= 5 && valCount >= 1 {
fmt.Printf("🔎 detector data: train=%d val=%d yaml=%v\n", trainCount, valCount, fileExistsNonEmpty(detectorDatasetYAML))
if fileExistsNonEmpty(detectorDatasetYAML) &&
trainCount >= minDetectorTrainCount &&
valCount >= minDetectorValCount {
trainingSetJobStatus(func(s *TrainingJobStatus) {
s.Progress = 75
s.Step = "Detector wird trainiert…"
s.Progress = 35
s.Step = "YOLO-Detector wird trainiert…"
})
detectorScript := trainingScriptPath("train_detector_model.py")
@ -541,7 +578,7 @@ func trainingRunJob(root string, count int) {
detectorScript,
"--root", root,
"--base", "yolo11n.pt",
"--epochs", "20",
"--epochs", "60",
"--imgsz", "640",
)
@ -556,18 +593,25 @@ func trainingRunJob(root string, count int) {
} else {
detectorStatus = "skipped_no_detector_data"
detectorOutput = fmt.Sprintf(
"Detector übersprungen: zu wenige YOLO-Box-Labels. Train=%d, Val=%d. Benötigt: mindestens 5 Train und 1 Val.",
"Detector übersprungen: zu wenige YOLO-Box-Labels. Train=%d, Val=%d. Benötigt: mindestens %d Train und %d Val.",
trainCount,
valCount,
minDetectorTrainCount,
minDetectorValCount,
)
fmt.Println("⚠️", detectorOutput)
}
message := "Training abgeschlossen. Neue Bilder werden jetzt mit dem Scene-Modell analysiert."
message := "Training abgeschlossen."
if detectorStatus == "trained" {
message = "Training abgeschlossen. Scene-Modell und Detector wurden trainiert."
message = "Training abgeschlossen. YOLO-Detector wurde trainiert."
}
if detectorStatus == "failed" {
message = "Scene-Modell wurde trainiert, Detector-Training ist fehlgeschlagen."
message = "YOLO-Detector-Training ist fehlgeschlagen."
}
if detectorStatus == "skipped_no_detector_data" {
message = detectorOutput
}
trainingSetJobStatus(func(s *TrainingJobStatus) {
@ -592,17 +636,65 @@ func trainingStatusHandler(w http.ResponseWriter, r *http.Request) {
return
}
if err := trainingEnsureDetectorDirs(root); err != nil {
trainingWriteError(w, http.StatusInternalServerError, err.Error())
return
}
// Optional, aber praktisch: Status zeigt dann sofort valCount > 0,
// falls mindestens genug train-Daten vorhanden sind.
if err := trainingEnsureDetectorValidationSample(root); err != nil {
fmt.Println("⚠️ detector val sample ensure failed:", err)
}
feedbackPath := filepath.Join(root, "feedback.jsonl")
count, _ := trainingCountAnnotations(feedbackPath)
feedbackCount, _ := trainingCountAnnotations(feedbackPath)
job := trainingGetJobStatus()
detectorDatasetYAML := filepath.Join(root, "detector", "dataset", "dataset.yaml")
detectorTrainImages := filepath.Join(root, "detector", "dataset", "images", "train")
detectorTrainLabels := filepath.Join(root, "detector", "dataset", "labels", "train")
detectorValImages := filepath.Join(root, "detector", "dataset", "images", "val")
detectorValLabels := filepath.Join(root, "detector", "dataset", "labels", "val")
detectorModelPath := filepath.Join(root, "detector", "model", "best.pt")
trainCount := trainingCountDetectorSamples(detectorTrainImages, detectorTrainLabels)
valCount := trainingCountDetectorSamples(detectorValImages, detectorValLabels)
datasetReady := fileExistsNonEmpty(detectorDatasetYAML)
detectorDataReady := datasetReady &&
trainCount >= minDetectorTrainCount &&
valCount >= minDetectorValCount
trainingWriteJSON(w, http.StatusOK, map[string]any{
"ok": true,
"feedbackCount": count,
"feedbackCount": feedbackCount,
"requiredCount": minTrainingFeedbackCount,
"canTrain": count >= minTrainingFeedbackCount,
"training": job,
// Für YOLO-only ist canTrain jetzt bewusst an Box-Labels gekoppelt,
// nicht mehr nur an feedback.jsonl.
"canTrain": detectorDataReady,
"training": job,
"detector": map[string]any{
"source": "yolo_detector",
"usesSceneKNN": false,
"usesResNet18KNN": false,
"trainCount": trainCount,
"valCount": valCount,
"requiredTrain": minDetectorTrainCount,
"requiredVal": minDetectorValCount,
"datasetReady": datasetReady,
"datasetYAML": detectorDatasetYAML,
"dataReady": detectorDataReady,
"modelExists": fileExistsNonEmpty(detectorModelPath),
"modelPath": detectorModelPath,
},
})
}
@ -883,49 +975,260 @@ func trainingPredictFrame(framePath string) TrainingPrediction {
return trainingEmptyPrediction("root_error")
}
python := trainingPythonExe()
script := trainingScriptPath("predict_scene_model.py")
det := trainingPredictDetector(root, framePath)
cmd := exec.Command(
python,
script,
"--root", root,
"--image", framePath,
)
pred := trainingPredictionFromDetector(det)
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: 0x08000000,
fmt.Println("✅ training predict result")
fmt.Println(" modelAvailable:", pred.ModelAvailable)
fmt.Println(" source:", pred.Source)
fmt.Println(" sexPosition:", pred.SexPosition)
fmt.Println(" bodyParts:", len(pred.BodyPartsPresent))
fmt.Println(" objects:", len(pred.ObjectsPresent))
fmt.Println(" clothing:", len(pred.ClothingPresent))
fmt.Println(" boxes:", len(pred.Boxes))
return pred
}
func trainingPredictionFromDetector(det TrainingDetectorPrediction) TrainingPrediction {
boxes := det.Boxes
if boxes == nil {
boxes = []TrainingBox{}
}
out, err := cmd.CombinedOutput()
outText := strings.TrimSpace(string(out))
pred := TrainingPrediction{
ModelAvailable: det.Available,
Source: det.Source,
PeopleCount: 0,
MaleCount: 0,
FemaleCount: 0,
UnknownCount: 0,
SexPosition: "unknown",
SexPositionScore: 0,
BodyPartsPresent: []TrainingScoredLabel{},
ObjectsPresent: []TrainingScoredLabel{},
ClothingPresent: []TrainingScoredLabel{},
Boxes: boxes,
}
if pred.Source == "" {
if det.Available {
pred.Source = "yolo_detector"
} else {
pred.Source = "detector_missing"
}
}
grouped, err := trainingGroupedLabels()
if err != nil {
fmt.Println("⚠️ training predict failed:", err, outText)
return trainingEmptyPrediction("predict_failed")
fmt.Println("⚠️ detector label grouping failed:", err)
return pred
}
var pred TrainingPrediction
if err := json.Unmarshal(out, &pred); err != nil {
fmt.Println("⚠️ training predict json failed:", err, outText)
return trainingEmptyPrediction("predict_json_failed")
pred.BodyPartsPresent = trainingScoredLabelsFromDetectorBoxes(boxes, grouped.BodyParts)
pred.ObjectsPresent = trainingScoredLabelsFromDetectorBoxes(boxes, grouped.Objects)
pred.ClothingPresent = trainingScoredLabelsFromDetectorBoxes(boxes, grouped.Clothing)
return pred
}
func trainingPredictDetector(root string, framePath string) TrainingDetectorPrediction {
python := trainingPythonExe()
script := trainingScriptPath("predict_detector_model.py")
modelPath := filepath.Join(root, "detector", "model", "best.pt")
fmt.Println("🔎 detector predict")
fmt.Println(" python:", python)
fmt.Println(" root:", root)
fmt.Println(" script:", script)
fmt.Println(" image:", framePath)
fmt.Println(" model:", modelPath)
fmt.Println(" modelExists:", fileExistsNonEmpty(modelPath))
if !fileExistsNonEmpty(modelPath) {
return TrainingDetectorPrediction{
Available: false,
Source: "detector_missing",
Boxes: []TrainingBox{},
}
}
if pred.SexPosition == "" {
pred.SexPosition = "unknown"
confValues := []string{"0.30", "0.10", "0.03", "0.01"}
best := TrainingDetectorPrediction{
Available: true,
Source: "yolo_detector",
Boxes: []TrainingBox{},
}
if pred.BodyPartsPresent == nil {
for _, conf := range confValues {
cmd := exec.Command(
python,
script,
"--root", root,
"--image", framePath,
"--conf", conf,
"--imgsz", "640",
)
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: 0x08000000,
}
var stdout strings.Builder
var stderr strings.Builder
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
outText := strings.TrimSpace(stdout.String())
errText := strings.TrimSpace(stderr.String())
if errText != "" {
fmt.Println("🔎 detector stderr:", errText)
}
if err != nil {
fmt.Println("⚠️ detector predict failed")
fmt.Println(" conf:", conf)
fmt.Println(" error:", err)
fmt.Println(" stdout:", outText)
fmt.Println(" stderr:", errText)
continue
}
if outText == "" {
fmt.Println("⚠️ detector predict empty stdout")
fmt.Println(" conf:", conf)
fmt.Println(" stderr:", errText)
continue
}
var det TrainingDetectorPrediction
if err := json.Unmarshal([]byte(outText), &det); err != nil {
fmt.Println("⚠️ detector predict json failed:", err)
fmt.Println(" conf:", conf)
fmt.Println(" stdout:", outText)
fmt.Println(" stderr:", errText)
continue
}
if det.Boxes == nil {
det.Boxes = []TrainingBox{}
}
if det.Source == "" {
det.Source = "yolo_detector"
}
fmt.Println("✅ detector predict result")
fmt.Println(" conf:", conf)
fmt.Println(" available:", det.Available)
fmt.Println(" source:", det.Source)
fmt.Println(" boxes:", len(det.Boxes))
best = det
if len(det.Boxes) > 0 {
return det
}
}
if best.Boxes == nil {
best.Boxes = []TrainingBox{}
}
return best
}
func trainingScoredLabelsFromDetectorBoxes(
boxes []TrainingBox,
group []string,
) []TrainingScoredLabel {
groupSet := map[string]bool{}
for _, label := range group {
clean := strings.TrimSpace(label)
if clean != "" {
groupSet[clean] = true
}
}
best := map[string]float64{}
for _, box := range boxes {
label := strings.TrimSpace(box.Label)
if label == "" || !groupSet[label] {
continue
}
score := box.Score
if score <= 0 {
score = 1
}
if old, ok := best[label]; !ok || score > old {
best[label] = score
}
}
out := make([]TrainingScoredLabel, 0, len(best))
for label, score := range best {
out = append(out, TrainingScoredLabel{
Label: label,
Score: score,
})
}
sort.Slice(out, func(i, j int) bool {
if out[i].Score == out[j].Score {
return out[i].Label < out[j].Label
}
return out[i].Score > out[j].Score
})
return out
}
func trainingApplyDetectorToPrediction(pred TrainingPrediction, det TrainingDetectorPrediction) TrainingPrediction {
boxes := det.Boxes
if boxes == nil {
boxes = []TrainingBox{}
}
grouped, err := trainingGroupedLabels()
if err != nil {
fmt.Println("⚠️ detector label grouping failed:", err)
pred.Boxes = boxes
pred.BodyPartsPresent = []TrainingScoredLabel{}
}
if pred.ObjectsPresent == nil {
pred.ObjectsPresent = []TrainingScoredLabel{}
}
if pred.ClothingPresent == nil {
pred.ClothingPresent = []TrainingScoredLabel{}
return pred
}
if pred.Boxes == nil {
pred.Boxes = []TrainingBox{}
// Wichtig:
// Ab jetzt kommen diese drei Bereiche ausschließlich vom YOLO-Detector.
// Kein Scene-KNN-Fallback, damit keine Labels ohne Box angezeigt werden.
pred.BodyPartsPresent = trainingScoredLabelsFromDetectorBoxes(boxes, grouped.BodyParts)
pred.ObjectsPresent = trainingScoredLabelsFromDetectorBoxes(boxes, grouped.Objects)
pred.ClothingPresent = trainingScoredLabelsFromDetectorBoxes(boxes, grouped.Clothing)
pred.Boxes = boxes
if det.Available {
if pred.Source == "" {
pred.Source = "yolo_detector"
} else {
pred.Source = pred.Source + "+yolo_detector"
}
pred.ModelAvailable = true
}
return pred
@ -946,8 +1249,7 @@ func trainingWriteDetectorSample(root string, sample *TrainingSample, boxes []Tr
return fmt.Errorf("frame missing: %w", err)
}
// Erstmal alles in train schreiben.
// Später kannst du 80/20 train/val splitten.
// Stabiler 80/20 Split: gleicher sampleID landet immer im gleichen Split.
split := trainingStableSplit(sample.SampleID)
imgDir := filepath.Join(root, "detector", "dataset", "images", split)
@ -1010,6 +1312,80 @@ func trainingWriteDetectorSample(root string, sample *TrainingSample, boxes []Tr
return os.WriteFile(labelPath, []byte(strings.Join(lines, "\n")+"\n"), 0644)
}
func trainingEnsureDetectorValidationSample(root string) error {
trainImages := filepath.Join(root, "detector", "dataset", "images", "train")
trainLabels := filepath.Join(root, "detector", "dataset", "labels", "train")
valImages := filepath.Join(root, "detector", "dataset", "images", "val")
valLabels := filepath.Join(root, "detector", "dataset", "labels", "val")
currentVal := trainingCountDetectorSamples(valImages, valLabels)
if currentVal >= minDetectorValCount {
return nil
}
if trainingCountDetectorSamples(trainImages, trainLabels) < minDetectorTrainCount {
return nil
}
entries, err := os.ReadDir(trainImages)
if err != nil {
return nil
}
if err := os.MkdirAll(valImages, 0755); err != nil {
return err
}
if err := os.MkdirAll(valLabels, 0755); err != nil {
return err
}
copied := 0
needed := minDetectorValCount - currentVal
for _, e := range entries {
if copied >= needed {
break
}
if e.IsDir() {
continue
}
ext := strings.ToLower(filepath.Ext(e.Name()))
if ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".webp" {
continue
}
id := strings.TrimSuffix(e.Name(), filepath.Ext(e.Name()))
srcImage := filepath.Join(trainImages, e.Name())
srcLabel := filepath.Join(trainLabels, id+".txt")
if !fileExistsNonEmpty(srcImage) || !fileExistsNonEmpty(srcLabel) {
continue
}
dstImage := filepath.Join(valImages, e.Name())
dstLabel := filepath.Join(valLabels, id+".txt")
if fileExistsNonEmpty(dstImage) && fileExistsNonEmpty(dstLabel) {
continue
}
if err := copyFile(srcImage, dstImage); err != nil {
return err
}
if err := copyFile(srcLabel, dstLabel); err != nil {
return err
}
copied++
fmt.Println("✅ detector val sample duplicated:", id)
}
return nil
}
func trainingStableSplit(sampleID string) string {
sum := sha1.Sum([]byte(sampleID))
if int(sum[0])%5 == 0 {
@ -1057,21 +1433,16 @@ func trainingProjectRoot() string {
return "."
}
// Fall A: Prozess läuft im Projekt-Root:
// ./backend/ml/predict_scene_model.py existiert
if _, err := os.Stat(filepath.Join(wd, "backend", "ml", "predict_scene_model.py")); err == nil {
if _, err := os.Stat(filepath.Join(wd, "backend", "ml", "predict_detector_model.py")); err == nil {
return wd
}
// Fall B: Prozess läuft direkt in /backend:
// ./ml/predict_scene_model.py existiert
if _, err := os.Stat(filepath.Join(wd, "ml", "predict_scene_model.py")); err == nil {
if _, err := os.Stat(filepath.Join(wd, "ml", "predict_detector_model.py")); err == nil {
return filepath.Dir(wd)
}
// Fall C: Fallback: Parent testen
parent := filepath.Dir(wd)
if _, err := os.Stat(filepath.Join(parent, "backend", "ml", "predict_scene_model.py")); err == nil {
if _, err := os.Stat(filepath.Join(parent, "backend", "ml", "predict_detector_model.py")); err == nil {
return parent
}
@ -1123,51 +1494,35 @@ func isTempBuildDir(dir string) bool {
}
func trainingBackendRootDir() (string, error) {
// Fall 1:
// App läuft aus /backend oder EXE liegt neben /ml.
// Dann ist "ml/predict_scene_model.py" app-relativ korrekt.
if script, err := resolvePathRelativeToApp(filepath.Join("ml", "predict_scene_model.py")); err == nil {
if script, err := resolvePathRelativeToApp(filepath.Join("ml", "predict_detector_model.py")); err == nil {
if st, statErr := os.Stat(script); statErr == nil && !st.IsDir() {
return filepath.Dir(filepath.Dir(script)), nil
}
}
// Fall 2:
// Dev-Start aus Projekt-Root.
// Dann ist "backend/ml/predict_scene_model.py" app-relativ korrekt.
if script, err := resolvePathRelativeToApp(filepath.Join("backend", "ml", "predict_scene_model.py")); err == nil {
if script, err := resolvePathRelativeToApp(filepath.Join("backend", "ml", "predict_detector_model.py")); err == nil {
if st, statErr := os.Stat(script); statErr == nil && !st.IsDir() {
return filepath.Dir(filepath.Dir(script)), nil
}
}
// Fall 3:
// Gebaute App mit embedded ML-Scripts.
// Dann gibt es ggf. kein /ml neben der EXE.
// In diesem Fall nehmen wir den EXE-Ordner als App/Backend-Root,
// solange es nicht go-run Temp ist.
if dir, err := exeDir(); err == nil && strings.TrimSpace(dir) != "" && !isTempBuildDir(dir) {
return dir, nil
}
// Fall 4:
// Dev-Fallback über Working Directory.
wd, err := os.Getwd()
if err != nil {
return "", err
}
// Wenn wir direkt in /backend laufen.
if _, err := os.Stat(filepath.Join(wd, "ml", "predict_scene_model.py")); err == nil {
if _, err := os.Stat(filepath.Join(wd, "ml", "predict_detector_model.py")); err == nil {
return wd, nil
}
// Wenn wir im Projekt-Root laufen.
if _, err := os.Stat(filepath.Join(wd, "backend", "ml", "predict_scene_model.py")); err == nil {
if _, err := os.Stat(filepath.Join(wd, "backend", "ml", "predict_detector_model.py")); err == nil {
return filepath.Join(wd, "backend"), nil
}
// Letzter Fallback: bisherige Projekterkennung.
projectRoot := trainingProjectRoot()
return filepath.Join(projectRoot, "backend"), nil
}

View File

@ -516,7 +516,7 @@ export default function TrainingTab() {
}
if (elapsed > 25_000) {
setTrainingStep('Scene-Modell wird trainiert…')
setTrainingStep('Detector wird trainiert…')
return Math.min(prev + 0.8, 80)
}