From 77ac4873f48c20f35c92ca420c5595f6dd064d95 Mon Sep 17 00:00:00 2001 From: adamberkes Date: Tue, 26 May 2026 11:59:17 +0200 Subject: [PATCH] Revert "Dev -> Prod" --- apps/ml-yolo/Dockerfile | 27 ++++------ apps/ml-yolo/app/ml/archive_patch.py | 10 +--- apps/ml-yolo/app/ml/train_model.py | 1 - apps/ml-yolo/app/ml/ultralytics_config.py | 8 --- apps/ml-yolo/requirements.txt | 7 +-- .../DashboardPage/DashboardPage.tsx | 25 +++++---- .../components/AdvancedSettings/presets.ts | 53 ------------------- .../CreateReportForm/CreateReportForm.tsx | 50 ++++++++--------- .../components/ReportsPage/ReportsPage.tsx | 33 +++--------- .../components/ModelRunning/ModelRunning.tsx | 23 +++----- 10 files changed, 62 insertions(+), 175 deletions(-) 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 = ({
diff --git a/apps/web/src/modules/model/components/AdvancedSettings/presets.ts b/apps/web/src/modules/model/components/AdvancedSettings/presets.ts index b36afc3d..c748a897 100644 --- a/apps/web/src/modules/model/components/AdvancedSettings/presets.ts +++ b/apps/web/src/modules/model/components/AdvancedSettings/presets.ts @@ -110,59 +110,6 @@ const ULTRALYTICS_PRESETS: HyperparamsPreset[] = [ patience: 30, }, }, - { - id: "yolo26-fast", - name: "YOLO26 Fast", - description: "YOLO26 nano — NMS-free, faster CPU inference, quick iteration", - config: { - model_variant: "yolo26n", - imgsz: 640, - optimizer: "AdamW", - lr0: 0.001, - cos_lr: true, - patience: 20, - hsv_s: 0.15, - hsv_v: 0.2, - degrees: 15.0, - cls: 0.8, - }, - }, - { - id: "yolo26-accuracy", - name: "YOLO26 High Accuracy", - description: - "YOLO26 large at 1280px — NMS-free, edge-optimised, tuned for fine-grained classification", - config: { - model_variant: "yolo26l", - imgsz: 1280, - optimizer: "AdamW", - lr0: 0.001, - lrf: 0.01, - momentum: 0.937, - weight_decay: 0.0005, - cos_lr: true, - warmup_epochs: 5, - warmup_momentum: 0.8, - warmup_bias_lr: 0.1, - patience: 50, - box: 9.0, - cls: 0.8, - mosaic: 1.0, - close_mosaic: 25, - copy_paste: 0.2, - erasing: 0.4, - hsv_h: 0.01, - hsv_s: 0.15, - hsv_v: 0.2, - degrees: 15.0, - translate: 0.1, - scale: 0.5, - shear: 2.0, - perspective: 0.0005, - fliplr: 0.5, - amp: true, - }, - }, ]; export const getHyperparamsPresets = ( diff --git a/apps/web/src/modules/reports/components/CreateReportForm/CreateReportForm.tsx b/apps/web/src/modules/reports/components/CreateReportForm/CreateReportForm.tsx index 6ee870cb..4b35d97c 100644 --- a/apps/web/src/modules/reports/components/CreateReportForm/CreateReportForm.tsx +++ b/apps/web/src/modules/reports/components/CreateReportForm/CreateReportForm.tsx @@ -12,19 +12,19 @@ export interface CreateReportFormProps { isSubmitting?: boolean; } -const DATETIME_LOCAL_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?$/; - -// Treat the datetime-local string as literal UTC: send back what the user -// typed, never apply the browser's timezone offset. -const toUtcIsoOrNull = (value: string): string | null => { - if (!value || !DATETIME_LOCAL_PATTERN.test(value)) return null; - return value.length === 19 ? `${value}.000Z` : `${value}:00.000Z`; +const toIsoOrNull = (value: string): string | null => { + if (!value) return null; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return null; + return date.toISOString(); }; const pad = (n: number) => n.toString().padStart(2, "0"); -const formatUtcDateTime = (date: Date): string => - `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}T${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}`; +const formatLocalDateTime = (date: Date): string => + `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; + +const DATETIME_LOCAL_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?$/; const isIncompleteDateTime = (value: string): boolean => value !== "" && !DATETIME_LOCAL_PATTERN.test(value); @@ -33,27 +33,19 @@ export const CreateReportForm = ({ onSubmit, isSubmitting = false, }: CreateReportFormProps) => { - const { startOfTodayUtc, nowUtc } = useMemo(() => { + const { startOfToday, nowLocal } = useMemo(() => { const now = new Date(); return { - startOfTodayUtc: formatUtcDateTime( - new Date( - Date.UTC( - now.getUTCFullYear(), - now.getUTCMonth(), - now.getUTCDate(), - 0, - 0, - ), - ), + startOfToday: formatLocalDateTime( + new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0), ), - nowUtc: formatUtcDateTime(now), + nowLocal: formatLocalDateTime(now), }; }, []); - const maxAllowed = nowUtc; + const maxAllowed = nowLocal; - const [start, setStart] = useState(startOfTodayUtc); - const [end, setEnd] = useState(nowUtc); + const [start, setStart] = useState(startOfToday); + const [end, setEnd] = useState(nowLocal); const startRef = useRef(null); const endRef = useRef(null); @@ -81,9 +73,9 @@ export const CreateReportForm = ({ } try { - await onSubmit({ start: toUtcIsoOrNull(start), end: toUtcIsoOrNull(end) }); - setStart(startOfTodayUtc); - setEnd(nowUtc); + await onSubmit({ start: toIsoOrNull(start), end: toIsoOrNull(end) }); + setStart(startOfToday); + setEnd(nowLocal); } catch { // parent surfaces the error; keep the user's input so they can retry } @@ -96,7 +88,7 @@ export const CreateReportForm = ({ Create new report
- +
- + { +export const ReportsPage = ({ dashboardId }: ReportsPageProps) => { const { url: cameraApiUrl } = useCameraApiUrl(); const [pendingDeleteId, setPendingDeleteId] = useState(null); const [downloadingId, setDownloadingId] = useState(null); const [pollingInterval, setPollingInterval] = useState(0); - const { data: cameras } = useListCamerasQuery(undefined, { - skip: !cameraApiUrl, - }); - const { cameraMxid: selectedCamera, streamName: selectedStream } = - useSelectedCameraStream(cameras); - const { isSuccess: isModelRunning } = useGetDashboardQuery( - { mxid: selectedCamera!, streamName: selectedStream! }, - { skip: !selectedCamera || !selectedStream }, - ); - const { data: reports = [], refetch, @@ -65,16 +51,6 @@ export const ReportsPage = ({ dashboardId, projectId }: ReportsPageProps) => { return ; } - if (isModelRunning) { - return ( - - ); - } - if (!isSuccess) { return (
@@ -133,6 +109,11 @@ export const ReportsPage = ({ dashboardId, projectId }: ReportsPageProps) => {
Existing reports + {inflight && ( + + Refreshing while reports are being generated… + + )}
{reports.length === 0 ? ( diff --git a/apps/web/src/modules/ui/components/ModelRunning/ModelRunning.tsx b/apps/web/src/modules/ui/components/ModelRunning/ModelRunning.tsx index b0d3c504..e179fe35 100644 --- a/apps/web/src/modules/ui/components/ModelRunning/ModelRunning.tsx +++ b/apps/web/src/modules/ui/components/ModelRunning/ModelRunning.tsx @@ -5,15 +5,9 @@ import { Link } from "react-router"; export interface ModelRunningProps { projectId: number; - message?: string; - hideButton?: boolean; } -export const ModelRunning = ({ - projectId, - message, - hideButton, -}: ModelRunningProps) => { +export const ModelRunning = ({ projectId }: ModelRunningProps) => { const runHref = webRoutes.run.replace(":projectId", String(projectId)); return ( @@ -22,18 +16,17 @@ export const ModelRunning = ({
-

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 && ( - - - - )} + + +
); };