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:
@@ -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());
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user