feat: Add support for ADAS project in labeling and export processes

- Implemented class name loading for ADAS campaigns in `_class_names_for_campaign`.
- Enhanced annotation parsing to support cuboid and 2D rectangle labels in `_parse_ls_annotations`.
- Updated catalog building to include ADAS packs and batches in `build_catalog_signature`.
- Added ADAS cuboid promotion logic in `adas_cuboid.py`, supporting both quaternion_json and YOLO HBB formats.
- Introduced validation for ADAS batches to check for YOLO HBB labels in `adas_cuboid.py`.
- Modified delivery scanning to include ADAS project deliveries in `scan.py`.
- Extended job execution to handle ADAS exports for YOLO format in `runner.py`.
- Updated labeling export logic to accommodate ADAS project in `export_cuboid_batch.py`.
- Enhanced format conversion to support rectangle labels in `format_converter.py`.
- Improved CVAT task handling in `service.py` to ensure task ID updates on each labeling job.
- Updated web API endpoints to reflect ADAS project changes in `hsap-api.ts`.
- Added ADAS catalog types and UI components for displaying ADAS packs and batches in `dmsCatalog.ts`, `CatalogPage.tsx`, and `ExportPage.tsx`.
- Adjusted workflow registry to align with new ADAS project structure.
This commit is contained in:
2026-07-16 16:03:04 +08:00
parent be8a885b07
commit 1bde0fe430
23 changed files with 472 additions and 83 deletions

View File

@@ -396,18 +396,18 @@ export const hsapApi = {
if (opts?.status) p.set("status", opts.status);
if (opts?.offset != null) p.set("offset", String(opts.offset));
if (opts?.limit != null) p.set("limit", String(opts.limit));
return fetchJson<PagedResult<Record<string, unknown>>>(`${API_BASE}/api/v1/approvals?${p}`);
return fetchJson<PagedResult<Record<string, unknown>>>(`${API_BASE}/api/v1/system/audit?${p}`);
},
getApproval: (id: string) => fetchJson<Record<string, unknown>>(`${API_BASE}/api/v1/approvals/${id}`),
getApprovalPreview: (id: string) => fetchJson<Record<string, unknown>>(`${API_BASE}/api/v1/approvals/${id}/preview`),
getApproval: (id: string) => fetchJson<Record<string, unknown>>(`${API_BASE}/api/v1/system/audit/${id}`),
getApprovalPreview: (id: string) => fetchJson<Record<string, unknown>>(`${API_BASE}/api/v1/system/audit/${id}/preview`),
listApprovalImages: (id: string, offset = 0, limit = 60) =>
fetchJson<{ total: number; items: { id: string }[] }>(`${API_BASE}/api/v1/approvals/${id}/images?offset=${offset}&limit=${limit}`),
fetchJson<{ total: number; items: { id: string }[] }>(`${API_BASE}/api/v1/system/audit/${id}/images?offset=${offset}&limit=${limit}`),
fetchApprovalImageBlob: async (approvalId: string, imageId: string, thumb = true) => {
const q = thumb ? "?thumb=true" : "?thumb=false";
const res = await fetch(`${API_BASE}/api/v1/approvals/${approvalId}/images/${imageId}${q}`, { headers: authHeaders(), cache: "no-store" });
const res = await fetch(`${API_BASE}/api/v1/system/audit/${approvalId}/images/${imageId}${q}`, { headers: authHeaders(), cache: "no-store" });
if (!res.ok) throw new Error(await res.text());
return URL.createObjectURL(await res.blob());
},

View File

@@ -2,6 +2,7 @@
export type CatalogReport = Record<string, unknown> & {
dms?: Record<string, DmsTaskEntry>;
lane?: Record<string, Record<string, unknown>>;
adas?: Record<string, AdasPackEntry>;
projects?: { dms?: { active_packs?: string[] }; lane?: { active_packs?: string[] } };
_cache?: Record<string, unknown>;
};
@@ -19,6 +20,28 @@ export type DmsPackRow = {
total_boxes?: number; label_files?: number; sampled?: boolean;
};
// ── ADAS catalog types ──
export type AdasPackEntry = {
name: string;
path: string;
enabled: boolean;
total_images: number;
total_labels: number;
class_counts: Record<string, number>;
batches: AdasBatchEntry[];
};
export type AdasBatchEntry = {
batch: string;
task: string;
stage: string;
images: number;
labels: number;
class_counts: Record<string, number>;
ingested_at?: string;
};
export type SplitCounts = { train: number; val: number; test: number };
export function aggregateSplitCounts(packs: DmsPackRow[]): SplitCounts {

View File

@@ -10,6 +10,7 @@ import type { LabelingBatchRow } from "@/lib/types";
type ExportBatchTableProps = {
batches: LabelingBatchRow[];
importingId: string | null;
exportingId: string | null;
buildingId: string | null;
onExport: (campaignId: string) => void;
onImportVendor: (campaignId: string) => void;
@@ -21,6 +22,7 @@ const COLS = ["34%", "14%", "8%", "10%", "auto", "7.5rem"];
export const ExportBatchTable: React.FC<ExportBatchTableProps> = ({
batches,
importingId,
exportingId,
buildingId,
onExport,
onImportVendor,
@@ -67,7 +69,7 @@ export const ExportBatchTable: React.FC<ExportBatchTableProps> = ({
<td className="py-2 px-2 whitespace-nowrap w-[7.5rem]">
{cid && isExport && (
<div className="inline-flex items-center justify-center gap-0.5">
<Button size="small" variant="primary" className="!px-2" onClick={() => onExport(cid)}>
<Button size="small" variant="primary" className="!px-2" loading={exportingId === cid} onClick={() => onExport(cid)}>
</Button>
<Button

View File

@@ -13,7 +13,8 @@ import {
buildDmsPackTree, dmsPacks, dmsTaskModes,
findPackRow, isDmsCatalogScope, packTaskKey, parseCatalogScope,
parsePackTaskKey, primaryPack, scopeKeyFromSelection, selectionFromScopeKey,
splitCountsFromPack, type CatalogReport, type CatalogUiSelection,
splitCountsFromPack, type AdasBatchEntry, type AdasPackEntry,
type CatalogReport, type CatalogUiSelection,
type CatalogViewKind, type DmsPackRow, type DmsTaskEntry,
} from "@/lib/dmsCatalog";
@@ -183,8 +184,8 @@ export const CatalogPage: React.FC = () => {
))}
</div>
{/* DMS / Forward sub-views */}
{domain !== "lane" && (
{/* DMS sub-views */}
{domain === "dms" && (
<div className="flex flex-wrap gap-2 mb-4 text-sm">
<p className="text-xs text-gray-500 m-0 self-center mr-2"></p>
{(["pack", "batch"] as const).map((v) => (
@@ -198,8 +199,8 @@ export const CatalogPage: React.FC = () => {
</div>
)}
{/* Pack selector */}
{domain !== "lane" && subView === "pack" && packTree.length > 0 && (
{/* Pack selector — only for DMS */}
{domain === "dms" && subView === "pack" && packTree.length > 0 && (
<div className="flex flex-wrap gap-3 items-end mb-4">
<label className="flex-1 min-w-[8rem] text-xs text-gray-500">
<select className={selectClass} value={ui.pack} onChange={(e) => setPack(e.target.value)}>
@@ -214,8 +215,75 @@ export const CatalogPage: React.FC = () => {
</div>
)}
{/* DMS/Forward: Charts */}
{domain !== "lane" && taskEntry && isDmsCatalogScope(scope) && (
{/* ADAS 前向Pack + Batch 表格 */}
{domain === "forward" && cat?.adas && Object.keys(cat.adas).length > 0 && (
<div className="space-y-4">
{Object.entries(cat.adas as Record<string, AdasPackEntry>).map(([packName, pack]) => (
<div key={packName} className="rounded-xl border border-gray-200 bg-white overflow-hidden">
<div className="bg-gray-50 px-4 py-3 border-b border-gray-200 flex items-center gap-3">
<span className="font-semibold text-base">{packName}</span>
{pack.enabled && <Badge variant="success"></Badge>}
<span className="text-sm text-gray-500 ml-auto">
{pack.total_images} · {pack.total_labels} · {pack.batches.length}
</span>
</div>
{pack.batches.length > 0 ? (
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 text-left border-b border-gray-200">
<th className="px-4 py-2"></th>
<th className="px-4 py-2"></th>
<th className="px-4 py-2"></th>
<th className="px-4 py-2"></th>
<th className="px-4 py-2"></th>
<th className="px-4 py-2"></th>
<th className="px-4 py-2"></th>
</tr>
</thead>
<tbody>
{pack.batches.map((b: AdasBatchEntry) => (
<tr key={b.batch} className="border-b border-gray-100 hover:bg-gray-50">
<td className="px-4 py-2 font-mono text-xs">{b.batch}</td>
<td className="px-4 py-2">{b.task}</td>
<td className="px-4 py-2">
<Badge variant={b.stage === "ingested" ? "success" : "default"} size="small">{b.stage}</Badge>
</td>
<td className="px-4 py-2">{b.images}</td>
<td className="px-4 py-2">{b.labels}</td>
<td className="px-4 py-2">
<div className="flex flex-wrap gap-1">
{Object.entries(b.class_counts).map(([cls, n]) => (
<span key={cls} className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-blue-50 text-blue-700 text-xs">
{cls} <span className="font-medium">{n}</span>
</span>
))}
</div>
</td>
<td className="px-4 py-2 text-xs text-gray-400">
{b.ingested_at ? new Date(b.ingested_at).toLocaleString("zh-CN") : "—"}
</td>
</tr>
))}
</tbody>
</table>
) : (
<div className="px-4 py-8 text-center text-gray-400 text-sm"></div>
)}
</div>
))}
</div>
)}
{/* Empty state for forward */}
{domain === "forward" && (!cat?.adas || Object.keys(cat.adas).length === 0) && (
<div className="card text-center py-12 text-gray-400">
<p className="text-lg mb-2"> ADAS </p>
<p className="text-sm"> quaternion_json </p>
</div>
)}
{/* DMS/Forward: Charts — only for DMS domain */}
{domain === "dms" && taskEntry && isDmsCatalogScope(scope) && (
<div className="rounded-xl border border-gray-200 bg-white p-3 mb-4">
<div className="flex flex-wrap gap-2 items-center mb-2">
<span className="text-base font-semibold">{activePackTask?.label || taskEntry.label || scope.task}</span>
@@ -253,8 +321,8 @@ export const CatalogPage: React.FC = () => {
</div>
)}
{/* Empty state */}
{domain !== "lane" && !taskEntry && (
{/* Empty state for DMS */}
{domain === "dms" && !taskEntry && (
<div className="card text-center py-12 text-gray-400">
<p className="text-lg mb-2"> {DOMAIN_TABS.find((t) => t.key === domain)?.label} </p>
<p className="text-sm"></p>
@@ -266,8 +334,8 @@ export const CatalogPage: React.FC = () => {
</div>
)}
{/* Pack table */}
{domain !== "lane" && isDmsCatalogScope(scope) && taskEntry && tablePacks.length > 0 && (
{/* DMS pack table — only for DMS domain */}
{domain === "dms" && isDmsCatalogScope(scope) && taskEntry && tablePacks.length > 0 && (
<div className="rounded-xl border border-gray-200 bg-white overflow-hidden">
<table className="w-full text-sm">
<thead><tr className="bg-gray-50 text-left border-b border-gray-200"><th className="px-4 py-2"></th><th></th><th>train</th><th>val</th><th>test</th></tr></thead>

View File

@@ -19,6 +19,7 @@ export const ExportPage: React.FC = () => {
const [error, setError] = useState<string | null>(null);
const [info, setInfo] = useState<string | null>(null);
const [importingId, setImportingId] = useState<string | null>(null);
const [exportingId, setExportingId] = useState<string | null>(null);
const [buildingId, setBuildingId] = useState<string | null>(null);
const [search, setSearch] = useState("");
const [stageFilter, setStageFilter] = useState("");
@@ -60,8 +61,11 @@ export const ExportPage: React.FC = () => {
const reloadCurrent = useCallback(() => load(offset, limit), [load, offset, limit]);
const handleExport = async (campaignId: string) => {
setExportingId(campaignId);
setError(null);
try { await hsapApi.labelingExport(campaignId); setInfo("导出任务已提交"); reloadCurrent(); }
catch (e) { setError(String(e)); }
setExportingId(null);
};
const handleSubmitBuild = async (b: LabelingBatchRow) => {
@@ -135,6 +139,7 @@ export const ExportPage: React.FC = () => {
<ExportBatchTable
batches={batches}
importingId={importingId}
exportingId={exportingId}
buildingId={buildingId}
onExport={handleExport}
onImportVendor={handleImportVendor}