This commit is contained in:
Linrador 2026-06-24 11:05:31 +02:00
parent c6c6bad6cf
commit 83120174a6
7 changed files with 94 additions and 10 deletions

View File

@ -1607,12 +1607,21 @@ def predict_batch(req: PredictBatchRequest):
"error": "no paths supplied",
}
imgsz = int(req.imageSize or _IMGSZ or 640)
current_model = get_model()
if current_model is None:
predictions = [empty_prediction("detector_model_missing") for _ in paths]
if not req.detectorOnly:
apply_pose_batch_to_predictions(paths, predictions, imgsz)
return {
"ok": True,
"predictions": [empty_prediction("model_missing") for _ in paths],
"error": _MODEL_ERROR or f"YOLO model not found: {DEFAULT_MODEL_PATH}",
"available": False,
"modelAvailable": False,
"predictions": predictions,
"modelError": _MODEL_ERROR,
"expectedModel": str(DEFAULT_MODEL_PATH),
}
if DETECTION_LABELS_PATH is None or _LABEL_ERROR:
@ -1622,8 +1631,6 @@ def predict_batch(req: PredictBatchRequest):
"error": f"detection labels missing: {_LABEL_ERROR}",
}
imgsz = int(req.imageSize or _IMGSZ or 640)
try:
results = current_model.predict(
source=paths,
@ -1642,6 +1649,8 @@ def predict_batch(req: PredictBatchRequest):
return {
"ok": True,
"available": True,
"modelAvailable": True,
"predictions": predictions,
}
@ -1703,7 +1712,7 @@ def health():
status_payload = {
"ok": True,
"ready": current_model is not None,
"ready": True,
"modelAvailable": current_model is not None,
"model": _MODEL_PATH,
"modelError": _MODEL_ERROR,
@ -1751,8 +1760,8 @@ def reload_model():
names = getattr(current_model, "names", {}) or {} if current_model is not None else {}
status_payload = {
"ok": current_model is not None,
"ready": current_model is not None,
"ok": True,
"ready": True,
"modelAvailable": current_model is not None,
"model": _MODEL_PATH,
"modelError": _MODEL_ERROR,

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -9,6 +9,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
@ -112,11 +113,22 @@ func mlPythonSetupConfigFromEnv() mlPythonSetupConfig {
appDir: filepath.Clean(appDir),
requirementsPath: filepath.Clean(requirementsPath),
venvDir: filepath.Clean(venvDir),
venvPython: filepath.Join(filepath.Clean(venvDir), "bin", "python"),
venvPython: mlPythonVenvPythonPath(venvDir),
requirementsMark: filepath.Join(filepath.Clean(venvDir), ".requirements-ml.txt"),
}
}
func mlPythonVenvPythonPath(venvDir string) string {
venvDir = filepath.Clean(strings.TrimSpace(venvDir))
if venvDir == "" {
return ""
}
if runtime.GOOS == "windows" {
return filepath.Join(venvDir, "Scripts", "python.exe")
}
return filepath.Join(venvDir, "bin", "python")
}
func defaultMLPythonVenvDir() string {
base := strings.TrimSpace(os.TempDir())
if base == "" {

View File

@ -206,6 +206,10 @@ func aiServerPythonPath() string {
return raw
}
if venvPython := configuredMLPythonVenvPythonPath(); venvPython != "" {
return venvPython
}
// Windows py launcher zuerst versuchen.
if runtime.GOOS == "windows" {
if _, err := exec.LookPath("py"); err == nil {
@ -224,6 +228,34 @@ func aiServerPythonPath() string {
return "python"
}
func configuredMLPythonVenvPythonPath() string {
if strings.TrimSpace(os.Getenv("NSFWAPP_SKIP_ML_SETUP")) == "1" {
return ""
}
candidates := []string{}
if venvDir := strings.TrimSpace(os.Getenv("NSFWAPP_ML_VENV")); venvDir != "" {
candidates = append(candidates, venvDir)
}
if strings.TrimSpace(os.Getenv("NSFWAPP_ML_SETUP")) == "1" {
candidates = append(candidates, defaultMLPythonVenvDir())
}
seen := map[string]bool{}
for _, dir := range candidates {
path := mlPythonVenvPythonPath(dir)
key := filepath.Clean(path)
if path == "" || seen[key] {
continue
}
seen[key] = true
if info, err := os.Stat(path); err == nil && info != nil && !info.IsDir() {
return path
}
}
return ""
}
func findAIServerScriptDir() (string, error) {
cwd, _ := os.Getwd()
@ -544,7 +576,7 @@ func startAIServer(ctx context.Context) (*aiServerProcess, error) {
// Modell ebenfalls standardmäßig aus generated/training nehmen,
// aber YOLO_MODEL darf weiterhin extern überschrieben werden.
if strings.TrimSpace(os.Getenv("YOLO_MODEL")) == "" {
if strings.TrimSpace(os.Getenv("YOLO_MODEL")) == "" && defaultModel.EffectiveExists {
env = upsertEnv(env, "YOLO_MODEL", defaultModelPath)
}

View File

@ -2557,7 +2557,38 @@ func trainingCancelHandler(w http.ResponseWriter, r *http.Request) {
}
func trainingRunJob(ctx context.Context, root string, count int) {
if err := ensureMLPythonSetup(ctx); err != nil {
if errors.Is(err, context.Canceled) {
trainingFinishCancelled(root)
return
}
appLogln("ML-Python setup for training failed:", err)
trainingSetJobStatus(func(s *TrainingJobStatus) {
finishedAt := time.Now().UTC()
var durationMs int64
if startedAt, parseErr := time.Parse(time.RFC3339, strings.TrimSpace(s.StartedAt)); parseErr == nil {
durationMs = finishedAt.Sub(startedAt).Milliseconds()
if durationMs < 0 {
durationMs = 0
}
}
s.Running = false
s.Progress = 100
s.Step = "Training fehlgeschlagen."
s.Message = "ML-Python-Umgebung konnte nicht vorbereitet werden."
s.Error = err.Error()
s.FinishedAt = finishedAt.Format(time.RFC3339)
s.DurationMs = durationMs
s.PreviewURL = ""
})
trainingClearJobCancel()
return
}
python := trainingPythonExe()
appLogln("ML-Python für Training:", python)
cleanOutput := func(text string) string {
out := strings.TrimSpace(text)
@ -6504,7 +6535,7 @@ func trainingPythonExe() string {
if v != "" {
return v
}
return "python"
return aiServerPythonPath()
}
func trainingProjectRoot() string {