diff --git a/apps/ml-yolo/Dockerfile b/apps/ml-yolo/Dockerfile index 1f3b991f..0eb23a6f 100644 --- a/apps/ml-yolo/Dockerfile +++ b/apps/ml-yolo/Dockerfile @@ -51,8 +51,7 @@ RUN python3 -m venv /opt/tools-venv && \ /opt/tools-venv/bin/pip install --no-cache-dir --upgrade pip && \ /opt/tools-venv/bin/pip install --no-cache-dir --timeout=120 --retries=5 \ --index-url https://download.pytorch.org/whl/cpu \ - --extra-index-url https://pypi.org/simple \ - torch==2.8.0+cpu torchvision==0.23.0 && \ + torch==2.8.0+cpu torchvision==0.23.0+cpu && \ git clone https://github.com/luxonis/tools.git /tmp/luxonis-tools && \ cd /tmp/luxonis-tools && \ git checkout edbe7da1a7f75833a71d65caf1028036faa81061 && \ @@ -131,15 +130,14 @@ ENV IN_DOCKER=1 ENV ROBOPIPE_MODELCONVERTER_BIN=/opt/modelconverter-venv/bin/modelconverter ENV ROBOPIPE_SNPE_ROOT=/opt/snpe -# Pre-download pretrained weights into the image. Ultralytics' YOLO(name) -# constructor calls safe_download() which has a known silent-failure path: if -# a retry deletes a partial file and the loop exits without raising, -# attempt_download_asset() returns a Path to a non-existent file and the next -# torch.load() raises FileNotFoundError. By baking the weights at /app (the -# WORKDIR), YOLO()'s first existence check (`Path(file).exists()` against cwd) -# hits and skips the download entirely. -# YOLO11 weights at v8.3.0; YOLO26 weights at v8.4.0 (first release that -# ships the yolo26 family). +# Pre-download all yolo11 detection + segmentation pretrained weights into the +# image. Ultralytics' YOLO(name) constructor calls safe_download() which has a +# known silent-failure path: if a retry deletes a partial file and the loop +# exits without raising, attempt_download_asset() returns a Path to a +# non-existent file and the next torch.load() raises FileNotFoundError. By +# baking the weights at /app (the WORKDIR), YOLO()'s first existence check +# (`Path(file).exists()` against cwd) hits and skips the download entirely. +# Pin to v8.3.0 — the release tag that hosts the yolo11 family weights. RUN cd /app && for v in \ yolo11n yolo11s yolo11m yolo11l yolo11x \ yolo11n-seg yolo11s-seg yolo11m-seg yolo11l-seg yolo11x-seg \ @@ -147,13 +145,6 @@ RUN cd /app && for v in \ curl -fL --retry 3 -o ${v}.pt \ https://github.com/ultralytics/assets/releases/download/v8.3.0/${v}.pt; \ done -RUN cd /app && for v in \ - yolo26n yolo26s yolo26m yolo26l yolo26x \ - yolo26n-seg yolo26s-seg yolo26m-seg yolo26l-seg yolo26x-seg \ - ; do \ - curl -fL --retry 3 -o ${v}.pt \ - https://github.com/ultralytics/assets/releases/download/v8.4.0/${v}.pt; \ - done COPY app/ ./app/ diff --git a/apps/ml-yolo/app/ml/archive_patch.py b/apps/ml-yolo/app/ml/archive_patch.py index ddd9f9c8..ee140af0 100644 --- a/apps/ml-yolo/app/ml/archive_patch.py +++ b/apps/ml-yolo/app/ml/archive_patch.py @@ -24,13 +24,6 @@ from ..models.model_type import ModelType -def _yolo_subtype(model_variant: str) -> str: - stem = Path(model_variant).stem.lower() - if stem.startswith("yolo26"): - return "yolo26" - return "yolov8" - - # Pre-NMS thresholds the on-device DetectionParser uses. The dashboard # does its own confidence filtering on top # (sensor.dashboard_config.confidenceThreshold), so this is a coarse @@ -50,7 +43,6 @@ def patch_nn_archive_heads( archive_path: str | Path, model_type: ModelType, label_ids: list[int], - model_variant: str = "", ) -> None: """Mutate the NN archive at `archive_path` in-place to ensure it has a valid `heads` block. No-op when the file isn't an NN archive, when @@ -126,7 +118,7 @@ def patch_nn_archive_heads( "conf_threshold": _DEFAULT_CONF_THRESHOLD, "max_det": _DEFAULT_MAX_DET, "anchors": None, - "subtype": _yolo_subtype(model_variant), + "subtype": "yolov8", "yolo_outputs": output_names, }, "outputs": output_names, diff --git a/apps/ml-yolo/app/ml/train_model.py b/apps/ml-yolo/app/ml/train_model.py index 5aab628c..a9b7d423 100644 --- a/apps/ml-yolo/app/ml/train_model.py +++ b/apps/ml-yolo/app/ml/train_model.py @@ -397,7 +397,6 @@ def upload_for(t: ModelOutputType) -> OutputUpload: converted_path, config.type, config.training_config.dataset_config.label_ids, - model_variant=get_model_variant(config), ) conv_upload = upload_for(output_type) _upload_to_signed_url(conv_upload, converted_path) diff --git a/apps/ml-yolo/app/ml/ultralytics_config.py b/apps/ml-yolo/app/ml/ultralytics_config.py index 6610a386..21fda86c 100644 --- a/apps/ml-yolo/app/ml/ultralytics_config.py +++ b/apps/ml-yolo/app/ml/ultralytics_config.py @@ -10,8 +10,6 @@ dfl, hsv_h, hsv_s, hsv_v, degrees, translate, scale, shear, perspective, flipud, fliplr, mosaic, mixup, copy_paste, optimizer, cos_lr, patience, imgsz, workers, device, amp -Note: `dfl` is silently stripped for YOLO26 variants (DFL removed in YOLO26). -YOLO26 variants: yolo26[n|s|m|l|x][.pt] and yolo26[n|s|m|l|x]-seg[.pt] Plus two ml-yolo-specific keys stripped before passthrough: backend — consumed by the API dispatcher model_variant — pretrained weights filename (e.g. yolo11m.pt) @@ -37,10 +35,6 @@ } -def _is_yolo26(variant: str) -> bool: - return Path(variant).stem.lower().startswith("yolo26") - - def get_model_variant(config: ModelConfig) -> str: custom = config.training_config.custom_hyperparams or {} variant = custom.get("model_variant") @@ -108,6 +102,4 @@ def build_train_kwargs( # custom_hyperparams wins over defaults but not over the dispatch kwargs above. for key, value in custom.items(): kwargs[key] = value - if _is_yolo26(get_model_variant(config)): - kwargs.pop("dfl", None) return kwargs diff --git a/apps/ml-yolo/requirements.txt b/apps/ml-yolo/requirements.txt index 3de5d062..71664133 100644 --- a/apps/ml-yolo/requirements.txt +++ b/apps/ml-yolo/requirements.txt @@ -11,8 +11,9 @@ pydantic-settings==2.7.1 python-multipart==0.0.17 # ML training (no luxonis-train — this is the Ultralytics-only service). -# 8.4.53: adds MuSGD optimizer (YOLO26 default). 8.4.44 had YOLO26 weights -# but MuSGD was absent — auto picked AdamW. YOLO11 unaffected by the bump. +# 8.3.160 decouples the confusion matrix update from args.plots (so the CM +# is populated even without plot files) and fixes the epoch-47 validation +# plot broadcast crash. Still numpy-1.x compatible. # # torch is PINNED to 2.8.0 (not 2.9). In torch 2.9, `torch.onnx.export` # hard-imports `onnxscript` even on the legacy `dynamo=False` path, and @@ -22,7 +23,7 @@ python-multipart==0.0.17 # it triggers the crash. torch 2.8's legacy exporter is pure TorchScript # with zero onnxscript involvement, sidestepping the trap entirely. Bump # only when ultralytics or torch fix the upstream interaction. -ultralytics==8.4.53 +ultralytics==8.3.160 torch==2.8.0 torchvision==0.23.0 diff --git a/apps/web/src/modules/dashboard/components/DashboardPage/DashboardPage.tsx b/apps/web/src/modules/dashboard/components/DashboardPage/DashboardPage.tsx index e365506f..62ca21bd 100644 --- a/apps/web/src/modules/dashboard/components/DashboardPage/DashboardPage.tsx +++ b/apps/web/src/modules/dashboard/components/DashboardPage/DashboardPage.tsx @@ -1,13 +1,13 @@ +import { cn } from "@/lib/utils"; import { useGetProjectQuery } from "@/modules/project/services/projectApi"; import { Button } from "@/modules/shadcn/ui/button"; import { Input } from "@/modules/shadcn/ui/input"; import { Label } from "@/modules/shadcn/ui/label"; -import { cn } from "@/lib/utils"; import { DashboardConfiguration } from "@repo/schema"; import { Link } from "lucide-react"; import { useEffect, useState } from "react"; -import { toast } from "sonner"; import { useParams } from "react-router"; +import { toast } from "sonner"; import { useCreateDashboardConfigMutation, useDeleteDashboardConfigMutation, @@ -15,8 +15,10 @@ import { useUpdateDashboardConfigMutation, } from "../../services/dashboardConfigApi"; -import { EvaluationThresholdsPage } from "@/modules/evaluation"; -import { TestCasesOverviewPage } from "@/modules/evaluation"; +import { + EvaluationThresholdsPage, + TestCasesOverviewPage, +} from "@/modules/evaluation"; import { ReportsPage } from "@/modules/reports"; import { DashboardRuntimePage } from "../DashboardRuntimePage"; @@ -59,7 +61,7 @@ export const DashboardPage = ({ const activeConfigId = showMultipleConfigs ? selectedConfigId - : configs[0]?.id ?? null; + : (configs[0]?.id ?? null); // Notify parent of active config changes useEffect(() => { @@ -164,7 +166,7 @@ export const DashboardPage = ({ key={config.id} className={cn( "group cursor-pointer rounded-md px-3 py-2.5 transition-colors hover:bg-gray-100", - isSelected && "border border-emerald-200 bg-emerald-50" + isSelected && "border border-emerald-200 bg-emerald-50", )} onClick={() => setSelectedConfigId(config.id)} > @@ -219,7 +221,7 @@ export const DashboardPage = ({
Model is running!
++ Model is running! +
- {message ?? - "You cannot use capture while a model is running. Disable it first."} + You cannot use capture while a model is running. Disable it first.
- {!hideButton && ( - -