Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 9 additions & 18 deletions apps/ml-yolo/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 && \
Expand Down Expand Up @@ -131,29 +130,21 @@ 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 \
; do \
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/

Expand Down
10 changes: 1 addition & 9 deletions apps/ml-yolo/app/ml/archive_patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
1 change: 0 additions & 1 deletion apps/ml-yolo/app/ml/train_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 0 additions & 8 deletions apps/ml-yolo/app/ml/ultralytics_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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")
Expand Down Expand Up @@ -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
7 changes: 4 additions & 3 deletions apps/ml-yolo/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
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,
useGetDashboardConfigsQuery,
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";

Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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)}
>
Expand Down Expand Up @@ -219,7 +221,7 @@ export const DashboardPage = ({
<div
className={cn(
"flex shrink-0 flex-row gap-2 opacity-0 transition-opacity group-hover:opacity-100",
isSelected && "opacity-100"
isSelected && "opacity-100",
)}
>
<button
Expand Down Expand Up @@ -270,7 +272,7 @@ export const DashboardPage = ({
{ key: "custom", label: "Custom dashboard" },
{ key: "test-cases", label: "Test cases" },
{ key: "evaluation", label: "Evaluation" },
{ key: "reports", label: "Reports" },
// { key: "reports", label: "Reports" },
] as const
).map((tab) => (
<button
Expand All @@ -279,7 +281,7 @@ export const DashboardPage = ({
className={cn(
"relative cursor-pointer border-none bg-none px-0 py-3 text-gray-500 hover:text-gray-700",
rightTab === tab.key &&
"text-primary after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-primary after:content-['']"
"text-primary after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-primary after:content-['']",
)}
onClick={() => setRightTab(tab.key)}
>
Expand Down Expand Up @@ -327,10 +329,7 @@ export const DashboardPage = ({
/>
)}
{rightTab === "reports" && (
<ReportsPage
dashboardId={activeConfigId}
projectId={projectId}
/>
<ReportsPage dashboardId={activeConfigId} />
)}
</div>
</div>
Expand Down
53 changes: 0 additions & 53 deletions apps/web/src/modules/model/components/AdvancedSettings/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<HTMLInputElement>(null);
const endRef = useRef<HTMLInputElement>(null);

Expand Down Expand Up @@ -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
}
Expand All @@ -96,7 +88,7 @@ export const CreateReportForm = ({
<span className="text-sm font-bold">Create new report</span>
<div className="flex flex-row flex-wrap items-end gap-3">
<div className="flex flex-col gap-1.5">
<Label htmlFor="report-start">Start (UTC, optional)</Label>
<Label htmlFor="report-start">Start (optional)</Label>
<Input
ref={startRef}
id="report-start"
Expand All @@ -108,7 +100,7 @@ export const CreateReportForm = ({
/>
</div>
<div className="flex flex-col gap-1.5">
<Label htmlFor="report-end">End (UTC, optional)</Label>
<Label htmlFor="report-end">End (optional)</Label>
<Input
ref={endRef}
id="report-end"
Expand Down
Loading