From 900a9640cac0b9f8a9defbaaf113a79081a5fd59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Berke=C5=A1?= Date: Wed, 20 May 2026 08:30:00 +0200 Subject: [PATCH 1/6] Revert "Disable reports" This reverts commit d41b05f047a85ba3ef011849d37c6912c016b589. --- .../DashboardPage/DashboardPage.tsx | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/apps/web/src/modules/dashboard/components/DashboardPage/DashboardPage.tsx b/apps/web/src/modules/dashboard/components/DashboardPage/DashboardPage.tsx index 62ca21bd..ce4ee384 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 { useParams } from "react-router"; import { toast } from "sonner"; +import { useParams } from "react-router"; import { useCreateDashboardConfigMutation, useDeleteDashboardConfigMutation, @@ -15,10 +15,8 @@ import { useUpdateDashboardConfigMutation, } from "../../services/dashboardConfigApi"; -import { - EvaluationThresholdsPage, - TestCasesOverviewPage, -} from "@/modules/evaluation"; +import { EvaluationThresholdsPage } from "@/modules/evaluation"; +import { TestCasesOverviewPage } from "@/modules/evaluation"; import { ReportsPage } from "@/modules/reports"; import { DashboardRuntimePage } from "../DashboardRuntimePage"; @@ -61,7 +59,7 @@ export const DashboardPage = ({ const activeConfigId = showMultipleConfigs ? selectedConfigId - : (configs[0]?.id ?? null); + : configs[0]?.id ?? null; // Notify parent of active config changes useEffect(() => { @@ -166,7 +164,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)} > @@ -221,7 +219,7 @@ export const DashboardPage = ({
diff --git a/apps/web/src/modules/reports/components/CreateReportForm/CreateReportForm.tsx b/apps/web/src/modules/reports/components/CreateReportForm/CreateReportForm.tsx index 4b35d97c..6ee870cb 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 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 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 pad = (n: number) => n.toString().padStart(2, "0"); -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 formatUtcDateTime = (date: Date): string => + `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}T${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}`; const isIncompleteDateTime = (value: string): boolean => value !== "" && !DATETIME_LOCAL_PATTERN.test(value); @@ -33,19 +33,27 @@ export const CreateReportForm = ({ onSubmit, isSubmitting = false, }: CreateReportFormProps) => { - const { startOfToday, nowLocal } = useMemo(() => { + const { startOfTodayUtc, nowUtc } = useMemo(() => { const now = new Date(); return { - startOfToday: formatLocalDateTime( - new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0), + startOfTodayUtc: formatUtcDateTime( + new Date( + Date.UTC( + now.getUTCFullYear(), + now.getUTCMonth(), + now.getUTCDate(), + 0, + 0, + ), + ), ), - nowLocal: formatLocalDateTime(now), + nowUtc: formatUtcDateTime(now), }; }, []); - const maxAllowed = nowLocal; + const maxAllowed = nowUtc; - const [start, setStart] = useState(startOfToday); - const [end, setEnd] = useState(nowLocal); + const [start, setStart] = useState(startOfTodayUtc); + const [end, setEnd] = useState(nowUtc); const startRef = useRef(null); const endRef = useRef(null); @@ -73,9 +81,9 @@ export const CreateReportForm = ({ } try { - await onSubmit({ start: toIsoOrNull(start), end: toIsoOrNull(end) }); - setStart(startOfToday); - setEnd(nowLocal); + await onSubmit({ start: toUtcIsoOrNull(start), end: toUtcIsoOrNull(end) }); + setStart(startOfTodayUtc); + setEnd(nowUtc); } catch { // parent surfaces the error; keep the user's input so they can retry } @@ -88,7 +96,7 @@ export const CreateReportForm = ({ Create new report
- +
- + { +export const ReportsPage = ({ dashboardId, projectId }: 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, @@ -51,6 +65,15 @@ export const ReportsPage = ({ dashboardId }: ReportsPageProps) => { return ; } + if (isModelRunning) { + return ( + + ); + } + if (!isSuccess) { return (
diff --git a/apps/web/src/modules/ui/components/ModelRunning/ModelRunning.tsx b/apps/web/src/modules/ui/components/ModelRunning/ModelRunning.tsx index e179fe35..461405d8 100644 --- a/apps/web/src/modules/ui/components/ModelRunning/ModelRunning.tsx +++ b/apps/web/src/modules/ui/components/ModelRunning/ModelRunning.tsx @@ -5,9 +5,10 @@ import { Link } from "react-router"; export interface ModelRunningProps { projectId: number; + message?: string; } -export const ModelRunning = ({ projectId }: ModelRunningProps) => { +export const ModelRunning = ({ projectId, message }: ModelRunningProps) => { const runHref = webRoutes.run.replace(":projectId", String(projectId)); return ( @@ -21,7 +22,7 @@ export const ModelRunning = ({ projectId }: ModelRunningProps) => {

- You cannot use capture while a model is running. Disable it first. + {message ?? "You cannot use capture while a model is running. Disable it first."}

From 8089af1ec5f61b38e9dab09886067102a1fcbed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Berke=C5=A1?= Date: Sat, 23 May 2026 10:46:55 +0200 Subject: [PATCH 4/6] Hide go to run button in reports --- .../components/ReportsPage/ReportsPage.tsx | 1 + .../components/ModelRunning/ModelRunning.tsx | 22 ++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/apps/web/src/modules/reports/components/ReportsPage/ReportsPage.tsx b/apps/web/src/modules/reports/components/ReportsPage/ReportsPage.tsx index 3e5a0ba8..e9de1a3b 100644 --- a/apps/web/src/modules/reports/components/ReportsPage/ReportsPage.tsx +++ b/apps/web/src/modules/reports/components/ReportsPage/ReportsPage.tsx @@ -70,6 +70,7 @@ export const ReportsPage = ({ dashboardId, projectId }: ReportsPageProps) => { ); } diff --git a/apps/web/src/modules/ui/components/ModelRunning/ModelRunning.tsx b/apps/web/src/modules/ui/components/ModelRunning/ModelRunning.tsx index 461405d8..b0d3c504 100644 --- a/apps/web/src/modules/ui/components/ModelRunning/ModelRunning.tsx +++ b/apps/web/src/modules/ui/components/ModelRunning/ModelRunning.tsx @@ -6,9 +6,14 @@ import { Link } from "react-router"; export interface ModelRunningProps { projectId: number; message?: string; + hideButton?: boolean; } -export const ModelRunning = ({ projectId, message }: ModelRunningProps) => { +export const ModelRunning = ({ + projectId, + message, + hideButton, +}: ModelRunningProps) => { const runHref = webRoutes.run.replace(":projectId", String(projectId)); return ( @@ -17,17 +22,18 @@ export const ModelRunning = ({ projectId, message }: ModelRunningProps) => {
-

- Model is running! -

+

Model is running!

- {message ?? "You cannot use capture while a model is running. Disable it first."} + {message ?? + "You cannot use capture while a model is running. Disable it first."}

- - - + {!hideButton && ( + + + + )}
); }; From 348e50f7fde666d1252dcf2f03f794a241f0d74c Mon Sep 17 00:00:00 2001 From: David Stranava Date: Sun, 24 May 2026 15:10:28 +0200 Subject: [PATCH 5/6] Yolo26 --- 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 ++- .../components/AdvancedSettings/presets.ts | 53 +++++++++++++++++++ 6 files changed, 92 insertions(+), 14 deletions(-) diff --git a/apps/ml-yolo/Dockerfile b/apps/ml-yolo/Dockerfile index 0eb23a6f..1f3b991f 100644 --- a/apps/ml-yolo/Dockerfile +++ b/apps/ml-yolo/Dockerfile @@ -51,7 +51,8 @@ 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 \ - torch==2.8.0+cpu torchvision==0.23.0+cpu && \ + --extra-index-url https://pypi.org/simple \ + torch==2.8.0+cpu torchvision==0.23.0 && \ git clone https://github.com/luxonis/tools.git /tmp/luxonis-tools && \ cd /tmp/luxonis-tools && \ git checkout edbe7da1a7f75833a71d65caf1028036faa81061 && \ @@ -130,14 +131,15 @@ ENV IN_DOCKER=1 ENV ROBOPIPE_MODELCONVERTER_BIN=/opt/modelconverter-venv/bin/modelconverter ENV ROBOPIPE_SNPE_ROOT=/opt/snpe -# 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. +# 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). RUN cd /app && for v in \ yolo11n yolo11s yolo11m yolo11l yolo11x \ yolo11n-seg yolo11s-seg yolo11m-seg yolo11l-seg yolo11x-seg \ @@ -145,6 +147,13 @@ 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 ee140af0..ddd9f9c8 100644 --- a/apps/ml-yolo/app/ml/archive_patch.py +++ b/apps/ml-yolo/app/ml/archive_patch.py @@ -24,6 +24,13 @@ 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 @@ -43,6 +50,7 @@ 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 @@ -118,7 +126,7 @@ def patch_nn_archive_heads( "conf_threshold": _DEFAULT_CONF_THRESHOLD, "max_det": _DEFAULT_MAX_DET, "anchors": None, - "subtype": "yolov8", + "subtype": _yolo_subtype(model_variant), "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 0e979a56..98e04870 100644 --- a/apps/ml-yolo/app/ml/train_model.py +++ b/apps/ml-yolo/app/ml/train_model.py @@ -396,6 +396,7 @@ 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 21fda86c..6610a386 100644 --- a/apps/ml-yolo/app/ml/ultralytics_config.py +++ b/apps/ml-yolo/app/ml/ultralytics_config.py @@ -10,6 +10,8 @@ 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) @@ -35,6 +37,10 @@ } +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") @@ -102,4 +108,6 @@ 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 71664133..b63863d3 100644 --- a/apps/ml-yolo/requirements.txt +++ b/apps/ml-yolo/requirements.txt @@ -11,9 +11,8 @@ pydantic-settings==2.7.1 python-multipart==0.0.17 # ML training (no luxonis-train — this is the Ultralytics-only service). -# 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. +# 8.4.44: first release series shipping yolo26*.pt weights (YOLO26 support). +# YOLO11 trains identically on 8.4.x — the 8.3→8.4 bump is additive only. # # 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 @@ -23,7 +22,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.3.160 +ultralytics==8.4.44 torch==2.8.0 torchvision==0.23.0 diff --git a/apps/web/src/modules/model/components/AdvancedSettings/presets.ts b/apps/web/src/modules/model/components/AdvancedSettings/presets.ts index c748a897..b36afc3d 100644 --- a/apps/web/src/modules/model/components/AdvancedSettings/presets.ts +++ b/apps/web/src/modules/model/components/AdvancedSettings/presets.ts @@ -110,6 +110,59 @@ 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 = ( From 298c59b8938957105f19627c3eb1efc4d5c86f79 Mon Sep 17 00:00:00 2001 From: David Stranava Date: Sun, 24 May 2026 16:21:21 +0200 Subject: [PATCH 6/6] Added support for MuSGD optimizer --- apps/ml-yolo/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/ml-yolo/requirements.txt b/apps/ml-yolo/requirements.txt index b63863d3..3de5d062 100644 --- a/apps/ml-yolo/requirements.txt +++ b/apps/ml-yolo/requirements.txt @@ -11,8 +11,8 @@ pydantic-settings==2.7.1 python-multipart==0.0.17 # ML training (no luxonis-train — this is the Ultralytics-only service). -# 8.4.44: first release series shipping yolo26*.pt weights (YOLO26 support). -# YOLO11 trains identically on 8.4.x — the 8.3→8.4 bump is additive only. +# 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. # # 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 +22,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.44 +ultralytics==8.4.53 torch==2.8.0 torchvision==0.23.0