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:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -49,6 +49,8 @@ datasets/**/inbox/
|
|||||||
datasets/**/archive/
|
datasets/**/archive/
|
||||||
datasets/**/sources/
|
datasets/**/sources/
|
||||||
datasets/**/DATASET/
|
datasets/**/DATASET/
|
||||||
|
datasets/**/versions/
|
||||||
|
datasets/**/manifests/ingest_log.jsonl
|
||||||
datasets/**/*.jpg
|
datasets/**/*.jpg
|
||||||
datasets/**/*.jpeg
|
datasets/**/*.jpeg
|
||||||
datasets/**/*.png
|
datasets/**/*.png
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
# ADAS MOON-3D 训练包 `adas_moon3d_v1`
|
# ADAS 前向训练包 `adas_v1`
|
||||||
|
|
||||||
|
> **2026-07 更新**:pack 已从 `adas_moon3d_v1` 重命名为 `adas_v1`(见 `workflow.registry.yaml`)。
|
||||||
|
> 新增 YOLO HBB 导出路径和 2D 矩形标注支持。
|
||||||
|
|
||||||
## 目录结构
|
## 目录结构
|
||||||
|
|
||||||
```text
|
```text
|
||||||
datasets/adas/packs/adas_moon3d_v1/
|
datasets/adas/packs/adas_v1/
|
||||||
├── sources/{batch}/
|
├── sources/{batch}/
|
||||||
│ ├── images/
|
│ ├── images/
|
||||||
│ ├── calib/
|
│ ├── calib/
|
||||||
│ └── labels/quaternion_json/
|
│ └── labels/
|
||||||
|
│ ├── quaternion_json/ # 3D cuboid 导出(有 calib 时优先)
|
||||||
|
│ └── yolo-hbb/ # 2D YOLO HBB 导出(无 calib 回退)
|
||||||
├── lists/
|
├── lists/
|
||||||
│ ├── train_stems.txt
|
│ ├── train_stems.txt
|
||||||
│ └── val_stems.txt
|
│ └── val_stems.txt
|
||||||
@@ -32,20 +37,34 @@ datasets/adas/packs/adas_moon3d_v1/
|
|||||||
## 管线
|
## 管线
|
||||||
|
|
||||||
1. **labeling_export** — CVAT ls_annotations → `labels/quaternion_json/*.json`
|
1. **labeling_export** — CVAT ls_annotations → `labels/quaternion_json/*.json`
|
||||||
|
- 支持 **cuboid**(3D 框)和 **rectanglelabels**(2D 矩形)两种标注类型
|
||||||
|
- 2D 矩形自动转为 quaternion_json detection(`fit_ok=false`, `source="2d_rect"`)
|
||||||
2. **cuboid_fit_3d**(有 calib 时自动触发)— 补全 3D 字段
|
2. **cuboid_fit_3d**(有 calib 时自动触发)— 补全 3D 字段
|
||||||
3. **build_adas**(审核)— `promote_batch` 复制到 pack + 刷新 stem 列表
|
3. **build_adas**(审核)— `promote_batch` 复制到 pack + 刷新 stem 列表
|
||||||
|
- 优先使用 quaternion_json,无 cuboid 标注时回退到 YOLO HBB
|
||||||
|
|
||||||
|
### 导出路径说明
|
||||||
|
|
||||||
|
| 标注类型 | 导出产物 | 条件 |
|
||||||
|
|---------|---------|------|
|
||||||
|
| cuboid (3D) | `labels/quaternion_json/*.json` | CVAT cuboid 标注 |
|
||||||
|
| rectanglelabels (2D) | `labels/quaternion_json/*.json` | 2D 矩形自动桥接 |
|
||||||
|
| YOLO HBB | `labels/yolo-hbb/*.txt` | 无 cuboid 时使用 `export_adas_yolo` |
|
||||||
|
|
||||||
## CLI
|
## CLI
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 导出(平台 Job 或脚本)
|
# cuboid 导出(平台 Job)
|
||||||
PYTHONPATH=platform python3 -c "from as_platform.labeling.export_cuboid_batch import export_batch; ..."
|
PYTHONPATH=platform python3 -c "from as_platform.labeling.export_cuboid_batch import export_batch; ..."
|
||||||
|
|
||||||
|
# YOLO HBB 导出(平台 Job,无 cuboid 时自动选择)
|
||||||
|
# 由 jobs/runner.py 调用 datasets/adas/scripts/export_ls_to_yolo.py
|
||||||
|
|
||||||
# 3D 拟合
|
# 3D 拟合
|
||||||
PYTHONPATH=platform python3 -c "from as_platform.labeling.fit_cuboid_batch import fit_batch; ..."
|
PYTHONPATH=platform python3 -c "from as_platform.labeling.fit_cuboid_batch import fit_batch; ..."
|
||||||
|
|
||||||
# 入包
|
# 入包
|
||||||
python as.py build adas cuboid_7cls --batch val_front6mm_pilot --pack adas_moon3d_v1
|
python as.py build adas cuboid_7cls --batch val_front6mm_pilot --pack adas_v1
|
||||||
```
|
```
|
||||||
|
|
||||||
## Smoke
|
## Smoke
|
||||||
@@ -54,7 +73,6 @@ python as.py build adas cuboid_7cls --batch val_front6mm_pilot --pack adas_moon3
|
|||||||
bash scripts/smoke_adas_promote.sh
|
bash scripts/smoke_adas_promote.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## 与 dms/packs/adas_v1 的区别
|
## 数据目录
|
||||||
|
|
||||||
- `dms/packs/adas_v1`:2D YOLO 历史包([`scripts/organize_adas.py`](../scripts/organize_adas.py))
|
ADAS 前向数据在 **数据目录** → **前向** 视图中展示,按 pack → batch 层级显示图片数、标注数和类别分布。
|
||||||
- `datasets/adas/packs/adas_moon3d_v1`:MOON-3D quaternion_json 3D GT
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
- 数据候选:`platform/as_platform/db/models.py` → `DatasetCandidate`
|
- 数据候选:`platform/as_platform/db/models.py` → `DatasetCandidate`
|
||||||
- 上传 API:`server.py` → `upload/file`、`inspect-upload`
|
- 上传 API:`server.py` → `upload/file`、`inspect-upload`
|
||||||
- 审核:`audit/queue.py`、`/api/v1/approvals/*`
|
- 审核:`audit/queue.py`、`/api/v1/system/audit/*`
|
||||||
- 入湖 CLI:`as.py build` / `add` + `ingest_incremental.py`
|
- 入湖 CLI:`as.py build` / `add` + `ingest_incremental.py`
|
||||||
|
|
||||||
## 建议下一里程碑(未在本汇总 plan 全量实现)
|
## 建议下一里程碑(未在本汇总 plan 全量实现)
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ bash scripts/smoke_dms_e2e_2img.sh setup
|
|||||||
### Commit `0b8ade0` — Unified Ingest SDK
|
### Commit `0b8ade0` — Unified Ingest SDK
|
||||||
|
|
||||||
- `platform/as_platform/data/promote/` — DMS/ADAS promote_batch
|
- `platform/as_platform/data/promote/` — DMS/ADAS promote_batch
|
||||||
- ADAS cuboid export / fit / `adas_moon3d_v1`
|
- ADAS cuboid export / fit / `adas_v1`
|
||||||
- `platform/as_platform/tests/test_unified_ingest_sdk.py`(7 项单元测试)
|
- `platform/as_platform/tests/test_unified_ingest_sdk.py`(7 项单元测试)
|
||||||
- `scripts/smoke_adas_promote.sh`
|
- `scripts/smoke_adas_promote.sh`
|
||||||
- `scripts/smoke_labeling_api.sh` 已接入上述测试
|
- `scripts/smoke_labeling_api.sh` 已接入上述测试
|
||||||
|
|||||||
@@ -350,7 +350,7 @@ python as.py train dms dam --track platform # 平台训练(需审核)
|
|||||||
| `/api/v1/system` | `system_routes.py` | 审核队列、任务监控、执行日志 |
|
| `/api/v1/system` | `system_routes.py` | 审核队列、任务监控、执行日志 |
|
||||||
| `/api/v1/delivery` | `delivery_routes.py` | 批次交付、Bitable 集成 |
|
| `/api/v1/delivery` | `delivery_routes.py` | 批次交付、Bitable 集成 |
|
||||||
| `/api/v1/feishu` | `feishu_routes.py` | Bitable Webhook 回调 |
|
| `/api/v1/feishu` | `feishu_routes.py` | Bitable Webhook 回调 |
|
||||||
| `/api/v1/` (根) | `server.py` | health, dashboard, catalog, approvals, simulate, agents |
|
| `/api/v1/` (根) | `server.py` | health, dashboard, catalog, simulate, agents |
|
||||||
|
|
||||||
### 6.2 核心端点速查
|
### 6.2 核心端点速查
|
||||||
|
|
||||||
@@ -366,10 +366,10 @@ GET /api/v1/catalog
|
|||||||
POST /api/v1/catalog
|
POST /api/v1/catalog
|
||||||
|
|
||||||
# 审核流程
|
# 审核流程
|
||||||
GET /api/v1/approvals
|
GET /api/v1/system/audit
|
||||||
POST /api/v1/approvals/{id}/approve
|
POST /api/v1/system/audit/{id}/approve
|
||||||
POST /api/v1/approvals/{id}/reject
|
POST /api/v1/system/audit/{id}/reject
|
||||||
GET /api/v1/approvals/{id}/preview
|
GET /api/v1/system/audit/{id}/preview
|
||||||
|
|
||||||
# 任务队列
|
# 任务队列
|
||||||
GET /api/v1/jobs
|
GET /api/v1/jobs
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
**协调员 QA 门禁路径(推荐):** 标完 → **提交质检** (`in_review`) → 质检通过 (`labeling_submitted`) → **执行导出** → `returned` → **提交 build**(审核)→ `ingested`。
|
**协调员 QA 门禁路径(推荐):** 标完 → **提交质检** (`in_review`) → 质检通过 (`labeling_submitted`) → **执行导出** → `returned` → **提交 build**(审核)→ `ingested`。
|
||||||
|
|
||||||
ADAS cuboid 详见 [ADAS_MOON3D_PACK.md](./ADAS_MOON3D_PACK.md):导出 → 可选 `cuboid_fit_3d` → `build_adas` 进 `adas_moon3d_v1`。
|
ADAS cuboid 详见 [ADAS_MOON3D_PACK.md](./ADAS_MOON3D_PACK.md):导出(支持 cuboid 3D / 2D 矩形 / YOLO HBB 回退)→ 可选 `cuboid_fit_3d` → `build_adas` 进 `adas_v1`。
|
||||||
|
|
||||||
**登记 meta**(`更多操作` 内)只写入 `batch.meta.yaml`,**不等于**已送标。
|
**登记 meta**(`更多操作` 内)只写入 `batch.meta.yaml`,**不等于**已送标。
|
||||||
|
|
||||||
@@ -191,6 +191,7 @@ datasets/dms/inbox/dam/batch_0516/
|
|||||||
|------|--------|--------|------|
|
|------|--------|--------|------|
|
||||||
| **训练包**(默认) | `dms_v1` 等 | `dam` / `addw_face` / `前向·粗检测` / `前向·细分类` … | 该包下各任务的 train/val 快照;前向在 registry 为单任务双 mode,目录按 mode 拆开显示 |
|
| **训练包**(默认) | `dms_v1` 等 | `dam` / `addw_face` / `前向·粗检测` / `前向·细分类` … | 该包下各任务的 train/val 快照;前向在 registry 为单任务双 mode,目录按 mode 拆开显示 |
|
||||||
| **采集批次** | 任务(如 `dam`) | `0516 批次` 等 mode | 单波送标质量 |
|
| **采集批次** | 任务(如 `dam`) | `0516 批次` 等 mode | 单波送标质量 |
|
||||||
|
| **前向** | `adas_v1` 等 pack | 批次列表 | ADAS 3D/2D 标注批次,含图片数、标注数、类别分布;支持 quaternion_json 和 YOLO HBB |
|
||||||
| **车道线** | lane pack | — | Lane 列表统计 |
|
| **车道线** | lane pack | — | Lane 列表统计 |
|
||||||
|
|
||||||
增量条(合并前)仍在磁盘 `packs/<pack>/<task>/sources/<批次名>/`;合并用 `python HSAP/as.py build dms dam --pack dms_v1 --all-sources`。
|
增量条(合并前)仍在磁盘 `packs/<pack>/<task>/sources/<批次名>/`;合并用 `python HSAP/as.py build dms dam --pack dms_v1 --all-sources`。
|
||||||
|
|||||||
@@ -68,7 +68,13 @@ def _class_names_for_campaign(camp) -> dict[int, str]:
|
|||||||
import yaml
|
import yaml
|
||||||
from as_platform.data.core import load_wf, proj_root
|
from as_platform.data.core import load_wf, proj_root
|
||||||
|
|
||||||
if not camp or camp.project != "dms":
|
if not camp:
|
||||||
|
return {}
|
||||||
|
if camp.project == "adas":
|
||||||
|
from as_platform.labeling.class_map import load_adas_class_names
|
||||||
|
names = load_adas_class_names()
|
||||||
|
return {i: n for i, n in enumerate(names)}
|
||||||
|
if camp.project != "dms":
|
||||||
return {}
|
return {}
|
||||||
wf = load_wf()
|
wf = load_wf()
|
||||||
root = proj_root(wf, "dms")
|
root = proj_root(wf, "dms")
|
||||||
@@ -114,21 +120,42 @@ def _parse_ls_annotations(path: Path, class_names: dict[int, str]) -> list[dict[
|
|||||||
return []
|
return []
|
||||||
out: list[dict[str, Any]] = []
|
out: list[dict[str, Any]] = []
|
||||||
for item in data.get("result") or []:
|
for item in data.get("result") or []:
|
||||||
if item.get("type") not in ("rectanglelabels", "rectangle"):
|
item_type = item.get("type")
|
||||||
continue
|
if item_type == "cuboid":
|
||||||
val = item.get("value") or {}
|
# Cuboid: 8 vertices in pixel coords → compute 2D bbox
|
||||||
w_pct = float(val.get("width") or 0)
|
points = item.get("points") or []
|
||||||
h_pct = float(val.get("height") or 0)
|
if len(points) < 16:
|
||||||
if w_pct <= 0 or h_pct <= 0:
|
continue
|
||||||
continue
|
label = item.get("label") or "unknown"
|
||||||
x_pct = float(val.get("x") or 0)
|
cid = _name_to_class_id(str(label), class_names)
|
||||||
y_pct = float(val.get("y") or 0)
|
# Compute min/max from 8 (x,y) pairs
|
||||||
labels = val.get("rectanglelabels") or val.get("labels") or []
|
xs = [points[i] for i in range(0, len(points), 2)]
|
||||||
label = labels[0] if labels else "unknown"
|
ys = [points[i] for i in range(1, len(points), 2)]
|
||||||
cid = _name_to_class_id(str(label), class_names)
|
min_x, max_x = min(xs), max(xs)
|
||||||
cx = (x_pct + w_pct / 2) / 100.0
|
min_y, max_y = min(ys), max(ys)
|
||||||
cy = (y_pct + h_pct / 2) / 100.0
|
if max_x <= min_x or max_y <= min_y:
|
||||||
out.append({"class_id": cid, "bbox": (cx, cy, w_pct / 100.0, h_pct / 100.0)})
|
continue
|
||||||
|
orig_w = float(item.get("original_width") or 1920)
|
||||||
|
orig_h = float(item.get("original_height") or 1080)
|
||||||
|
bw = (max_x - min_x) / orig_w
|
||||||
|
bh = (max_y - min_y) / orig_h
|
||||||
|
cx = (min_x + max_x) / 2.0 / orig_w
|
||||||
|
cy = (min_y + max_y) / 2.0 / orig_h
|
||||||
|
out.append({"class_id": cid, "bbox": (cx, cy, bw, bh)})
|
||||||
|
elif item_type in ("rectanglelabels", "rectangle"):
|
||||||
|
val = item.get("value") or {}
|
||||||
|
w_pct = float(val.get("width") or 0)
|
||||||
|
h_pct = float(val.get("height") or 0)
|
||||||
|
if w_pct <= 0 or h_pct <= 0:
|
||||||
|
continue
|
||||||
|
x_pct = float(val.get("x") or 0)
|
||||||
|
y_pct = float(val.get("y") or 0)
|
||||||
|
labels = val.get("rectanglelabels") or val.get("labels") or []
|
||||||
|
label = labels[0] if labels else "unknown"
|
||||||
|
cid = _name_to_class_id(str(label), class_names)
|
||||||
|
cx = (x_pct + w_pct / 2) / 100.0
|
||||||
|
cy = (y_pct + h_pct / 2) / 100.0
|
||||||
|
out.append({"class_id": cid, "bbox": (cx, cy, w_pct / 100.0, h_pct / 100.0)})
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ def build_catalog_signature(wf: dict, proj_root_fn) -> dict[str, Any]:
|
|||||||
files.append({"path": str(p), "missing": True})
|
files.append({"path": str(p), "missing": True})
|
||||||
|
|
||||||
dirs: list[dict[str, Any]] = []
|
dirs: list[dict[str, Any]] = []
|
||||||
for pname in ("dms", "lane"):
|
for pname in ("dms", "lane", "adas"):
|
||||||
root = proj_root_fn(wf, pname)
|
root = proj_root_fn(wf, pname)
|
||||||
dirs.append(_dir_fingerprint(root, scan_children=False))
|
dirs.append(_dir_fingerprint(root, scan_children=False))
|
||||||
dirs.append(_dir_fingerprint(root / "inbox"))
|
dirs.append(_dir_fingerprint(root / "inbox"))
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from typing import Any
|
|||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from as_platform.config import WORKSPACE, WORKSPACE_ROOT, LANE_DATA_VIZ_ENABLED
|
from as_platform.config import WORKSPACE, WORKSPACE_ROOT, LANE_DATA_VIZ_ENABLED
|
||||||
from as_platform.data.batch import META_FILENAME, dms_has_images, enrich_batch, write_meta
|
from as_platform.data.batch import META_FILENAME, dms_has_images, enrich_batch, read_meta, write_meta
|
||||||
from as_platform.data.catalog_cache import (
|
from as_platform.data.catalog_cache import (
|
||||||
build_catalog_signature,
|
build_catalog_signature,
|
||||||
get_cached_catalog,
|
get_cached_catalog,
|
||||||
@@ -811,7 +811,7 @@ def _normalize_catalog_dms(dms: dict[str, Any], reg: dict) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def _build_catalog(wf: dict, *, prefer_reports: bool = True) -> tuple[dict[str, Any], str]:
|
def _build_catalog(wf: dict, *, prefer_reports: bool = True) -> tuple[dict[str, Any], str]:
|
||||||
out: dict[str, Any] = {"workspace": str(WORKSPACE), "dms": {}, "lane": {}}
|
out: dict[str, Any] = {"workspace": str(WORKSPACE), "dms": {}, "lane": {}, "adas": {}}
|
||||||
build_source = "scan"
|
build_source = "scan"
|
||||||
|
|
||||||
reports = load_dms_reports()
|
reports = load_dms_reports()
|
||||||
@@ -966,6 +966,71 @@ def _build_catalog(wf: dict, *, prefer_reports: bool = True) -> tuple[dict[str,
|
|||||||
"add_template": "python as.py add lane --src <archive> --engineer <name> --date YYYYMMDD",
|
"add_template": "python as.py add lane --src <archive> --engineer <name> --date YYYYMMDD",
|
||||||
"quality": lane_quality,
|
"quality": lane_quality,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── ADAS catalog ──
|
||||||
|
adas_root = proj_root(wf, "adas")
|
||||||
|
out["adas"] = {}
|
||||||
|
adas_packs_dir = adas_root / "packs"
|
||||||
|
if adas_packs_dir.is_dir():
|
||||||
|
for pack_dir in sorted(adas_packs_dir.iterdir()):
|
||||||
|
if pack_dir.name.startswith(".") or not pack_dir.is_dir():
|
||||||
|
continue
|
||||||
|
pack_name = pack_dir.name
|
||||||
|
sources_dir = pack_dir / "sources"
|
||||||
|
pack_entry: dict[str, Any] = {
|
||||||
|
"name": pack_name,
|
||||||
|
"path": str(pack_dir.relative_to(adas_root)),
|
||||||
|
"enabled": pack_name in wf["projects"]["adas"].get("active_packs", []),
|
||||||
|
"batches": [],
|
||||||
|
"total_images": 0,
|
||||||
|
"total_labels": 0,
|
||||||
|
"class_counts": {},
|
||||||
|
}
|
||||||
|
if sources_dir.is_dir():
|
||||||
|
for batch_dir in sorted(sources_dir.iterdir()):
|
||||||
|
if batch_dir.name.startswith(".") or not batch_dir.is_dir():
|
||||||
|
continue
|
||||||
|
batch_name = batch_dir.name
|
||||||
|
meta = read_meta(batch_dir) or {}
|
||||||
|
stage = meta.get("stage", "unknown")
|
||||||
|
task = meta.get("task", "")
|
||||||
|
counts = meta.get("counts", {})
|
||||||
|
img_count = counts.get("images", 0)
|
||||||
|
# Count quaternion_json detections
|
||||||
|
qdir = batch_dir / "labels" / "quaternion_json"
|
||||||
|
label_count = 0
|
||||||
|
class_counts: dict[str, int] = {}
|
||||||
|
if qdir.is_dir():
|
||||||
|
for qf in sorted(qdir.glob("*.json")):
|
||||||
|
try:
|
||||||
|
qdata = json.loads(qf.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, json.JSONDecodeError):
|
||||||
|
continue
|
||||||
|
for det in qdata.get("detections") or []:
|
||||||
|
label_count += 1
|
||||||
|
cls = str(det.get("class_name") or det.get("class_id", "unknown"))
|
||||||
|
class_counts[cls] = class_counts.get(cls, 0) + 1
|
||||||
|
# Count images from directory if meta is missing
|
||||||
|
if img_count == 0:
|
||||||
|
imgs_dir = batch_dir / "images"
|
||||||
|
if imgs_dir.is_dir():
|
||||||
|
img_count = sum(1 for f in imgs_dir.rglob("*") if f.suffix.lower() in IMG_EXTS)
|
||||||
|
batch_entry = {
|
||||||
|
"batch": batch_name,
|
||||||
|
"task": task,
|
||||||
|
"stage": stage,
|
||||||
|
"images": img_count,
|
||||||
|
"labels": label_count,
|
||||||
|
"class_counts": class_counts,
|
||||||
|
"ingested_at": meta.get("ingested_at"),
|
||||||
|
}
|
||||||
|
pack_entry["batches"].append(batch_entry)
|
||||||
|
pack_entry["total_images"] += img_count
|
||||||
|
pack_entry["total_labels"] += label_count
|
||||||
|
for cls, n in class_counts.items():
|
||||||
|
pack_entry["class_counts"][cls] = pack_entry["class_counts"].get(cls, 0) + n
|
||||||
|
out["adas"][pack_name] = pack_entry
|
||||||
|
|
||||||
return out, build_source
|
return out, build_source
|
||||||
|
|
||||||
|
|
||||||
@@ -996,7 +1061,9 @@ def get_catalog(
|
|||||||
result = {"task": task_or_pack, **(full_catalog.get("dms", {}).get(task_or_pack, {}))}
|
result = {"task": task_or_pack, **(full_catalog.get("dms", {}).get(task_or_pack, {}))}
|
||||||
elif project == "lane" and task_or_pack:
|
elif project == "lane" and task_or_pack:
|
||||||
result = {"pack": task_or_pack, **(full_catalog.get("lane", {}).get(task_or_pack, {}))}
|
result = {"pack": task_or_pack, **(full_catalog.get("lane", {}).get(task_or_pack, {}))}
|
||||||
elif project in ("dms", "lane"):
|
elif project == "adas" and task_or_pack:
|
||||||
|
result = {"pack": task_or_pack, **(full_catalog.get("adas", {}).get(task_or_pack, {}))}
|
||||||
|
elif project in ("dms", "lane", "adas"):
|
||||||
result = {"workspace": full_catalog.get("workspace", str(WORKSPACE)), project: full_catalog.get(project, {})}
|
result = {"workspace": full_catalog.get("workspace", str(WORKSPACE)), project: full_catalog.get(project, {})}
|
||||||
else:
|
else:
|
||||||
result = dict(full_catalog)
|
result = dict(full_catalog)
|
||||||
|
|||||||
@@ -86,14 +86,20 @@ class AdasCuboidPromoteAdapter(PackPromoteAdapter):
|
|||||||
def promote(self, ctx: PromoteContext) -> PromoteResult:
|
def promote(self, ctx: PromoteContext) -> PromoteResult:
|
||||||
warnings = list(ctx.extra.get("validate_warnings") or [])
|
warnings = list(ctx.extra.get("validate_warnings") or [])
|
||||||
qdir = ctx.batch_dir / "labels" / "quaternion_json"
|
qdir = ctx.batch_dir / "labels" / "quaternion_json"
|
||||||
if not qdir.is_dir() or not any(qdir.glob("*.json")):
|
has_quat = qdir.is_dir() and any(qdir.glob("*.json"))
|
||||||
|
|
||||||
|
# YOLO HBB 回退
|
||||||
|
labels_dir = ctx.batch_dir / "labels"
|
||||||
|
has_yolo = (labels_dir / "yolo-hbb").is_dir() and any((labels_dir / "yolo-hbb").glob("*.txt"))
|
||||||
|
|
||||||
|
if not has_quat and not has_yolo:
|
||||||
return PromoteResult(
|
return PromoteResult(
|
||||||
ok=False,
|
ok=False,
|
||||||
project=ctx.project,
|
project=ctx.project,
|
||||||
task=ctx.task,
|
task=ctx.task,
|
||||||
batch=ctx.batch,
|
batch=ctx.batch,
|
||||||
pack=ctx.pack,
|
pack=ctx.pack,
|
||||||
warnings=["missing quaternion_json export"],
|
warnings=["missing quaternion_json or YOLO HBB export"],
|
||||||
)
|
)
|
||||||
|
|
||||||
pack_dir = ctx.project_root / "packs" / ctx.pack
|
pack_dir = ctx.project_root / "packs" / ctx.pack
|
||||||
@@ -119,7 +125,9 @@ class AdasCuboidPromoteAdapter(PackPromoteAdapter):
|
|||||||
if src_sub.is_dir():
|
if src_sub.is_dir():
|
||||||
copied += _sync_tree(src_sub, dest / sub)
|
copied += _sync_tree(src_sub, dest / sub)
|
||||||
|
|
||||||
normalized = _normalize_quaternion_json(dest)
|
normalized = _normalize_quaternion_json(dest) if has_quat else 0
|
||||||
|
if has_yolo and not has_quat:
|
||||||
|
warnings.append("using YOLO HBB labels (no quaternion_json)")
|
||||||
|
|
||||||
meta = read_meta(ctx.batch_dir) or {}
|
meta = read_meta(ctx.batch_dir) or {}
|
||||||
meta.update({
|
meta.update({
|
||||||
@@ -130,6 +138,7 @@ class AdasCuboidPromoteAdapter(PackPromoteAdapter):
|
|||||||
"pack": ctx.pack,
|
"pack": ctx.pack,
|
||||||
"ingested_at": datetime.now(timezone.utc).isoformat(),
|
"ingested_at": datetime.now(timezone.utc).isoformat(),
|
||||||
"pipeline_version": 2,
|
"pipeline_version": 2,
|
||||||
|
"label_format": "quaternion_json" if has_quat else "yolo_hbb",
|
||||||
})
|
})
|
||||||
write_meta(dest, meta)
|
write_meta(dest, meta)
|
||||||
write_meta(ctx.batch_dir, meta)
|
write_meta(ctx.batch_dir, meta)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""ADAS cuboid batch validation before promote."""
|
"""ADAS batch validation before promote(支持 quaternion_json 和 YOLO HBB)。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -7,20 +7,87 @@ from pathlib import Path
|
|||||||
from as_platform.labeling.class_map import load_adas_class_names
|
from as_platform.labeling.class_map import load_adas_class_names
|
||||||
|
|
||||||
|
|
||||||
|
def _has_yolo_hbb_labels(batch_dir: Path) -> bool:
|
||||||
|
"""检查是否有 YOLO HBB txt 标注文件。"""
|
||||||
|
yolo_dir = batch_dir / "labels" / "yolo-hbb"
|
||||||
|
return yolo_dir.is_dir() and any(yolo_dir.glob("*.txt"))
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_yolo_hbb(batch_dir: Path, expected_names: list[str]) -> tuple[list[str], list[str], dict]:
|
||||||
|
"""校验 YOLO HBB 格式标注。"""
|
||||||
|
errors: list[str] = []
|
||||||
|
warnings: list[str] = []
|
||||||
|
n_classes = len(expected_names)
|
||||||
|
|
||||||
|
labels_dir = batch_dir / "labels" / "yolo-hbb"
|
||||||
|
txt_files = sorted(labels_dir.glob("*.txt")) if labels_dir.is_dir() else []
|
||||||
|
|
||||||
|
total_boxes = 0
|
||||||
|
files_with_boxes = 0
|
||||||
|
for p in txt_files:
|
||||||
|
try:
|
||||||
|
lines = p.read_text(encoding="utf-8").strip().splitlines()
|
||||||
|
except OSError as e:
|
||||||
|
errors.append(f"{p.name}: read error ({e})")
|
||||||
|
continue
|
||||||
|
if not lines:
|
||||||
|
continue
|
||||||
|
has_valid = False
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) < 5:
|
||||||
|
warnings.append(f"{p.name}: invalid YOLO line '{line[:40]}...'")
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
cid = int(parts[0])
|
||||||
|
if cid < 0 or cid >= n_classes:
|
||||||
|
errors.append(f"{p.name}: class_id {cid} out of range (0-{n_classes - 1})")
|
||||||
|
continue
|
||||||
|
# 验证 bbox 值在 [0,1]
|
||||||
|
for v in parts[1:5]:
|
||||||
|
fv = float(v)
|
||||||
|
if fv < 0 or fv > 1:
|
||||||
|
warnings.append(f"{p.name}: bbox value {fv} out of [0,1]")
|
||||||
|
except ValueError:
|
||||||
|
warnings.append(f"{p.name}: non-numeric value in '{line[:40]}...'")
|
||||||
|
continue
|
||||||
|
total_boxes += 1
|
||||||
|
has_valid = True
|
||||||
|
if has_valid:
|
||||||
|
files_with_boxes += 1
|
||||||
|
|
||||||
|
if files_with_boxes == 0:
|
||||||
|
errors.append("no valid YOLO HBB label files")
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
"format": "yolo_hbb",
|
||||||
|
"label_files": len(txt_files),
|
||||||
|
"files_with_boxes": files_with_boxes,
|
||||||
|
"total_boxes": total_boxes,
|
||||||
|
}
|
||||||
|
return errors, warnings, stats
|
||||||
|
|
||||||
|
|
||||||
def validate_adas_cuboid_batch(
|
def validate_adas_cuboid_batch(
|
||||||
batch_dir: Path,
|
batch_dir: Path,
|
||||||
*,
|
*,
|
||||||
allow_partial_3d: bool = False,
|
allow_partial_3d: bool = False,
|
||||||
min_fit_ratio: float = 0.8,
|
min_fit_ratio: float = 0.8,
|
||||||
) -> tuple[list[str], list[str], dict]:
|
) -> tuple[list[str], list[str], dict]:
|
||||||
"""Return (errors, warnings, stats)."""
|
"""Return (errors, warnings, stats). 优先 quaternion_json,其次 YOLO HBB。"""
|
||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
warnings: list[str] = []
|
warnings: list[str] = []
|
||||||
qdir = batch_dir / "labels" / "quaternion_json"
|
qdir = batch_dir / "labels" / "quaternion_json"
|
||||||
expected_names = load_adas_class_names()
|
expected_names = load_adas_class_names()
|
||||||
|
|
||||||
if not qdir.is_dir():
|
if not qdir.is_dir():
|
||||||
errors.append(f"missing labels/quaternion_json under {batch_dir}")
|
# 回退到 YOLO HBB 格式
|
||||||
|
if _has_yolo_hbb_labels(batch_dir):
|
||||||
|
return _validate_yolo_hbb(batch_dir, expected_names)
|
||||||
|
errors.append(f"missing labels/quaternion_json or YOLO HBB labels under {batch_dir}")
|
||||||
return errors, warnings, {}
|
return errors, warnings, {}
|
||||||
|
|
||||||
files = sorted(qdir.glob("*.json"))
|
files = sorted(qdir.glob("*.json"))
|
||||||
|
|||||||
@@ -34,7 +34,13 @@ def _scan_project_inbox(project: str, wf: dict | None = None) -> list[dict[str,
|
|||||||
|
|
||||||
with session_scope() as db:
|
with session_scope() as db:
|
||||||
deliveries = {
|
deliveries = {
|
||||||
(r.project, r.task or "", r.mode or "", r.batch_name): r
|
(r.project, r.task or "", r.mode or "", r.batch_name): {
|
||||||
|
"id": r.id,
|
||||||
|
"status": r.status,
|
||||||
|
"collection_start": r.collection_start,
|
||||||
|
"collection_end": r.collection_end,
|
||||||
|
"created_at": r.created_at,
|
||||||
|
}
|
||||||
for r in db.query(BatchDelivery).filter(BatchDelivery.project == project).all()
|
for r in db.query(BatchDelivery).filter(BatchDelivery.project == project).all()
|
||||||
}
|
}
|
||||||
indexed = {
|
indexed = {
|
||||||
@@ -78,13 +84,13 @@ def _scan_project_inbox(project: str, wf: dict | None = None) -> list[dict[str,
|
|||||||
"has_labels": has_labels,
|
"has_labels": has_labels,
|
||||||
"stage_hint": stage_hint,
|
"stage_hint": stage_hint,
|
||||||
"source_type": "inbox_scan",
|
"source_type": "inbox_scan",
|
||||||
"delivery_id": delivery.id if delivery else None,
|
"delivery_id": delivery["id"] if delivery else None,
|
||||||
"delivery_status": delivery.status if delivery else None,
|
"delivery_status": delivery["status"] if delivery else None,
|
||||||
"in_ledger": delivery is not None,
|
"in_ledger": delivery is not None,
|
||||||
"in_workbench": in_index,
|
"in_workbench": in_index,
|
||||||
"collection_start": delivery.collection_start if delivery else _dir_mtime_iso(batch_dir),
|
"collection_start": delivery["collection_start"] if delivery else _dir_mtime_iso(batch_dir),
|
||||||
"collection_end": delivery.collection_end if delivery else None,
|
"collection_end": delivery["collection_end"] if delivery else None,
|
||||||
"created_at": delivery.created_at.isoformat() if delivery and delivery.created_at else None,
|
"created_at": delivery["created_at"].isoformat() if delivery and delivery["created_at"] else None,
|
||||||
"needs_ledger": delivery is None,
|
"needs_ledger": delivery is None,
|
||||||
"needs_workbench": not in_index,
|
"needs_workbench": not in_index,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -277,6 +277,24 @@ def execute_action(action: str, params: dict[str, Any]) -> dict[str, Any]:
|
|||||||
f"skipped_empty={conv.get('skipped_empty')} missing_ann={conv.get('missing_ann')}"
|
f"skipped_empty={conv.get('skipped_empty')} missing_ann={conv.get('missing_ann')}"
|
||||||
)
|
)
|
||||||
return {"ok": True, "stdout": json.dumps(conv, ensure_ascii=False), "stderr": "", "export_convert": conv}
|
return {"ok": True, "stdout": json.dumps(conv, ensure_ascii=False), "stderr": "", "export_convert": conv}
|
||||||
|
if row.get("project") == "adas" and export == "yolo":
|
||||||
|
scripts_dir = WORKSPACE / "datasets" / "adas" / "scripts"
|
||||||
|
if str(scripts_dir) not in sys.path:
|
||||||
|
sys.path.insert(0, str(scripts_dir))
|
||||||
|
from export_ls_to_yolo import export_batch as export_adas_yolo
|
||||||
|
|
||||||
|
with session_scope() as db:
|
||||||
|
camp = db.get(LabelingCampaign, campaign_id)
|
||||||
|
if not camp:
|
||||||
|
raise ValueError("campaign not found")
|
||||||
|
batch_dir = resolve_campaign_batch_dir(camp)
|
||||||
|
conv = export_adas_yolo(batch_dir, task=task, out_subdir="labels/yolo-hbb")
|
||||||
|
if conv.get("written", 0) == 0:
|
||||||
|
raise ValueError(
|
||||||
|
"export_adas_yolo: 无有效 YOLO HBB 标注可导出 (written=0); "
|
||||||
|
f"skipped_empty={conv.get('skipped_empty')} missing_ann={conv.get('missing_ann')}"
|
||||||
|
)
|
||||||
|
return {"ok": True, "stdout": json.dumps(conv, ensure_ascii=False), "stderr": "", "export_convert": conv}
|
||||||
if row.get("project") == "adas" and export == "cvat_cuboid":
|
if row.get("project") == "adas" and export == "cvat_cuboid":
|
||||||
from as_platform.labeling.export_cuboid_batch import export_batch as export_cuboid_batch
|
from as_platform.labeling.export_cuboid_batch import export_batch as export_cuboid_batch
|
||||||
|
|
||||||
|
|||||||
@@ -119,8 +119,8 @@ def on_labeling_export_job_succeeded(job: dict) -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return
|
return
|
||||||
project = camp.project or "dms"
|
project = camp.project or "dms"
|
||||||
if _batch_has_export_labels(project, batch_dir):
|
# job 已成功,无论是否有 CLI 导出产物,均进入 returned 阶段
|
||||||
_advance_campaign_stage(str(cid), "returned")
|
_advance_campaign_stage(str(cid), "returned")
|
||||||
if project == "adas" and _batch_has_calib(batch_dir):
|
if project == "adas" and _batch_has_calib(batch_dir):
|
||||||
from as_platform.jobs.queue import enqueue_job
|
from as_platform.jobs.queue import enqueue_job
|
||||||
|
|
||||||
|
|||||||
@@ -102,8 +102,45 @@ def _resolve_image_for_ann(data: dict[str, Any], batch_dir: Path, task_id: str)
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _rect_item_to_detection(item: dict[str, Any], class_map: dict[str, int]) -> dict[str, Any] | None:
|
||||||
|
"""2D rectanglelabels 条目 → quaternion_json detection(无 3D 信息,仅 bbox2d)。"""
|
||||||
|
value = item.get("value") or {}
|
||||||
|
w_pct = float(value.get("width") or 0)
|
||||||
|
h_pct = float(value.get("height") or 0)
|
||||||
|
if w_pct <= 0 or h_pct <= 0:
|
||||||
|
return None
|
||||||
|
labels = value.get("rectanglelabels") or []
|
||||||
|
label = str(labels[0]) if labels else "unknown"
|
||||||
|
class_id = class_map.get(label)
|
||||||
|
if class_id is None:
|
||||||
|
for name, cid in class_map.items():
|
||||||
|
if name.lower() == label.lower():
|
||||||
|
class_id = cid
|
||||||
|
break
|
||||||
|
if class_id is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
img_w = int(item.get("original_width") or 1920)
|
||||||
|
img_h = int(item.get("original_height") or 1080)
|
||||||
|
x_pct = float(value.get("x") or 0)
|
||||||
|
y_pct = float(value.get("y") or 0)
|
||||||
|
x1 = int(x_pct / 100.0 * img_w)
|
||||||
|
y1 = int(y_pct / 100.0 * img_h)
|
||||||
|
x2 = int((x_pct + w_pct) / 100.0 * img_w)
|
||||||
|
y2 = int((y_pct + h_pct) / 100.0 * img_h)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"class_id": class_id,
|
||||||
|
"class_name": label,
|
||||||
|
"score": 1.0,
|
||||||
|
"box2d_xyxy": [x1, y1, x2, y2],
|
||||||
|
"fit_ok": False,
|
||||||
|
"source": "2d_rect",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def export_batch(batch_dir: Path) -> dict[str, Any]:
|
def export_batch(batch_dir: Path) -> dict[str, Any]:
|
||||||
"""导出 cuboid ls_annotations → quaternion_json。"""
|
"""导出 ls_annotations(cuboid + 2D rectangle)→ quaternion_json。"""
|
||||||
batch_dir = batch_dir.resolve()
|
batch_dir = batch_dir.resolve()
|
||||||
class_map = _load_cuboid_class_map()
|
class_map = _load_cuboid_class_map()
|
||||||
calib_path, K, calib_size = _find_calib(batch_dir)
|
calib_path, K, calib_size = _find_calib(batch_dir)
|
||||||
@@ -124,7 +161,9 @@ def export_batch(batch_dir: Path) -> dict[str, Any]:
|
|||||||
continue
|
continue
|
||||||
regions = _extract_result_regions(data)
|
regions = _extract_result_regions(data)
|
||||||
cuboids = [r for r in regions if r.get("type") == "cuboid"]
|
cuboids = [r for r in regions if r.get("type") == "cuboid"]
|
||||||
if not cuboids:
|
rectangles = [r for r in regions if r.get("type") == "rectanglelabels"]
|
||||||
|
|
||||||
|
if not cuboids and not rectangles:
|
||||||
skipped_empty += 1
|
skipped_empty += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -134,16 +173,24 @@ def export_batch(batch_dir: Path) -> dict[str, Any]:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
detections: list[dict[str, Any]] = []
|
detections: list[dict[str, Any]] = []
|
||||||
|
# 处理 cuboid 标注
|
||||||
for item in cuboids:
|
for item in cuboids:
|
||||||
det = cuboid_item_to_detection(item, class_map, K=K)
|
det = cuboid_item_to_detection(item, class_map, K=K)
|
||||||
if det:
|
if det:
|
||||||
detections.append(det)
|
detections.append(det)
|
||||||
|
# 处理 2D 矩形标注 → quaternion_json detection
|
||||||
|
for item in rectangles:
|
||||||
|
det = _rect_item_to_detection(item, class_map)
|
||||||
|
if det:
|
||||||
|
detections.append(det)
|
||||||
if not detections:
|
if not detections:
|
||||||
skipped_empty += 1
|
skipped_empty += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
img_w = int((cuboids[0].get("original_width") or (calib_size or [1920, 1080])[0]))
|
# 获取图片尺寸:优先 cuboid,其次 rectangle
|
||||||
img_h = int((cuboids[0].get("original_height") or (calib_size or [1920, 1080])[1]))
|
first_region = cuboids[0] if cuboids else rectangles[0]
|
||||||
|
img_w = int((first_region.get("original_width") or (calib_size or [1920, 1080])[0]))
|
||||||
|
img_h = int((first_region.get("original_height") or (calib_size or [1920, 1080])[1]))
|
||||||
|
|
||||||
payload: dict[str, Any] = {
|
payload: dict[str, Any] = {
|
||||||
"image": str(image_path),
|
"image": str(image_path),
|
||||||
|
|||||||
@@ -238,10 +238,14 @@ def cvat_shapes_to_export_regions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if stype == "rectangle":
|
if stype == "rectangle":
|
||||||
xtl = float(shape.get("xtl", 0))
|
pts = shape.get("points") or []
|
||||||
ytl = float(shape.get("ytl", 0))
|
if len(pts) >= 4:
|
||||||
xbr = float(shape.get("xbr", 0))
|
xtl, ytl, xbr, ybr = float(pts[0]), float(pts[1]), float(pts[2]), float(pts[3])
|
||||||
ybr = float(shape.get("ybr", 0))
|
else:
|
||||||
|
xtl = float(shape.get("xtl", 0))
|
||||||
|
ytl = float(shape.get("ytl", 0))
|
||||||
|
xbr = float(shape.get("xbr", 0))
|
||||||
|
ybr = float(shape.get("ybr", 0))
|
||||||
regions.append({
|
regions.append({
|
||||||
**base,
|
**base,
|
||||||
"type": "rectanglelabels",
|
"type": "rectanglelabels",
|
||||||
|
|||||||
@@ -186,9 +186,9 @@ def open_campaign(
|
|||||||
else:
|
else:
|
||||||
camp.status = "in_progress"
|
camp.status = "in_progress"
|
||||||
camp.updated_at = now
|
camp.updated_at = now
|
||||||
if cvat_task_id and not camp.cvat_task_id:
|
# 每次开标都创建新 CVAT Task,始终更新 task_id
|
||||||
camp.cvat_task_id = cvat_task_id
|
camp.cvat_task_id = cvat_task_id
|
||||||
camp.cvat_job_url = cvat_job_url
|
camp.cvat_job_url = cvat_job_url
|
||||||
if ann_types and not camp.annotation_types:
|
if ann_types and not camp.annotation_types:
|
||||||
camp.annotation_types = ann_types
|
camp.annotation_types = ann_types
|
||||||
db.flush()
|
db.flush()
|
||||||
@@ -531,6 +531,7 @@ def _cvat_upload_thread(cvat_task_id: int, image_paths: list, campaign_id: str |
|
|||||||
with session_scope() as db:
|
with session_scope() as db:
|
||||||
camp = db.get(LabelingCampaign, campaign_id)
|
camp = db.get(LabelingCampaign, campaign_id)
|
||||||
if camp and camp.cvat_job_url is None:
|
if camp and camp.cvat_job_url is None:
|
||||||
|
camp.cvat_task_id = cvat_task_id
|
||||||
camp.cvat_job_url = f"_UPLOAD_FAILED_: {e}"
|
camp.cvat_job_url = f"_UPLOAD_FAILED_: {e}"
|
||||||
camp.status = "upload_failed"
|
camp.status = "upload_failed"
|
||||||
updated = True
|
updated = True
|
||||||
@@ -542,12 +543,13 @@ def _cvat_upload_thread(cvat_task_id: int, image_paths: list, campaign_id: str |
|
|||||||
try:
|
try:
|
||||||
with session_scope() as db:
|
with session_scope() as db:
|
||||||
camp = db.get(LabelingCampaign, campaign_id)
|
camp = db.get(LabelingCampaign, campaign_id)
|
||||||
# 上传成功后,尝试回填 job_url
|
# 上传成功后,回填 job_url 和 task_id
|
||||||
if camp and camp.cvat_job_url is None:
|
if camp and camp.cvat_job_url is None:
|
||||||
from as_platform.labeling.cvat_client import get_cvat_client
|
from as_platform.labeling.cvat_client import get_cvat_client
|
||||||
cvat = get_cvat_client()
|
cvat = get_cvat_client()
|
||||||
task = cvat.get_task(cvat_task_id)
|
task = cvat.get_task(cvat_task_id)
|
||||||
if task.job_url:
|
if task.job_url:
|
||||||
|
camp.cvat_task_id = cvat_task_id
|
||||||
camp.cvat_job_url = task.job_url
|
camp.cvat_job_url = task.job_url
|
||||||
camp.status = "in_progress"
|
camp.status = "in_progress"
|
||||||
updated = True
|
updated = True
|
||||||
@@ -657,7 +659,11 @@ def _build_class_map(camp, reg: dict | None) -> dict[str, int]:
|
|||||||
|
|
||||||
|
|
||||||
def get_cvat_status(campaign_id: str) -> dict[str, Any]:
|
def get_cvat_status(campaign_id: str) -> dict[str, Any]:
|
||||||
"""查询 CVAT 侧 Task 状态,包含上传进度信息。"""
|
"""查询 CVAT 侧 Task 状态,包含上传进度信息。
|
||||||
|
|
||||||
|
自动恢复:当检测到 cvat_job_url 为 None 且 CVAT Task 无数据时,
|
||||||
|
说明上传线程已丢失(如容器重启),自动重新触发上传。
|
||||||
|
"""
|
||||||
with session_scope() as db:
|
with session_scope() as db:
|
||||||
camp = db.get(LabelingCampaign, campaign_id)
|
camp = db.get(LabelingCampaign, campaign_id)
|
||||||
if not camp:
|
if not camp:
|
||||||
@@ -690,12 +696,31 @@ def get_cvat_status(campaign_id: str) -> dict[str, Any]:
|
|||||||
images = _iter_batch_images(batch_dir)
|
images = _iter_batch_images(batch_dir)
|
||||||
image_count = len(images)
|
image_count = len(images)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
images = []
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# 自动恢复:cvat_job_url 为空且 CVAT Task 无 Job → 上传线程已丢失
|
||||||
|
resolved_url = task.job_url or camp.cvat_job_url
|
||||||
|
if not resolved_url and image_count > 0:
|
||||||
|
# CVAT Task 存在但无数据,重新触发上传
|
||||||
|
import threading
|
||||||
|
_needs_recovery = True
|
||||||
|
try:
|
||||||
|
r = cvat._session.get(f"{cvat._api}/tasks/{camp.cvat_task_id}/data/meta")
|
||||||
|
_needs_recovery = r.status_code >= 400 # 400 = 未上传数据
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if _needs_recovery:
|
||||||
|
print(f"[CVAT] 检测到断线任务,自动恢复上传 task={camp.cvat_task_id} campaign={campaign_id}")
|
||||||
|
uploader = _cvat_upload_thread(camp.cvat_task_id, images, campaign_id=campaign_id)
|
||||||
|
threading.Thread(target=uploader, daemon=True).start()
|
||||||
|
camp.cvat_job_url = None # 重置,等待线程回填
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"cvat_available": True,
|
"cvat_available": True,
|
||||||
"campaign_id": campaign_id,
|
"campaign_id": campaign_id,
|
||||||
"cvat_task_id": camp.cvat_task_id,
|
"cvat_task_id": camp.cvat_task_id,
|
||||||
"cvat_job_url": task.job_url or camp.cvat_job_url,
|
"cvat_job_url": resolved_url,
|
||||||
"cvat_status": task.status,
|
"cvat_status": task.status,
|
||||||
"image_count": image_count,
|
"image_count": image_count,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -396,18 +396,18 @@ export const hsapApi = {
|
|||||||
if (opts?.status) p.set("status", opts.status);
|
if (opts?.status) p.set("status", opts.status);
|
||||||
if (opts?.offset != null) p.set("offset", String(opts.offset));
|
if (opts?.offset != null) p.set("offset", String(opts.offset));
|
||||||
if (opts?.limit != null) p.set("limit", String(opts.limit));
|
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}`),
|
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/approvals/${id}/preview`),
|
getApprovalPreview: (id: string) => fetchJson<Record<string, unknown>>(`${API_BASE}/api/v1/system/audit/${id}/preview`),
|
||||||
|
|
||||||
listApprovalImages: (id: string, offset = 0, limit = 60) =>
|
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) => {
|
fetchApprovalImageBlob: async (approvalId: string, imageId: string, thumb = true) => {
|
||||||
const q = thumb ? "?thumb=true" : "?thumb=false";
|
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());
|
if (!res.ok) throw new Error(await res.text());
|
||||||
return URL.createObjectURL(await res.blob());
|
return URL.createObjectURL(await res.blob());
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
export type CatalogReport = Record<string, unknown> & {
|
export type CatalogReport = Record<string, unknown> & {
|
||||||
dms?: Record<string, DmsTaskEntry>;
|
dms?: Record<string, DmsTaskEntry>;
|
||||||
lane?: Record<string, Record<string, unknown>>;
|
lane?: Record<string, Record<string, unknown>>;
|
||||||
|
adas?: Record<string, AdasPackEntry>;
|
||||||
projects?: { dms?: { active_packs?: string[] }; lane?: { active_packs?: string[] } };
|
projects?: { dms?: { active_packs?: string[] }; lane?: { active_packs?: string[] } };
|
||||||
_cache?: Record<string, unknown>;
|
_cache?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
@@ -19,6 +20,28 @@ export type DmsPackRow = {
|
|||||||
total_boxes?: number; label_files?: number; sampled?: boolean;
|
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 type SplitCounts = { train: number; val: number; test: number };
|
||||||
|
|
||||||
export function aggregateSplitCounts(packs: DmsPackRow[]): SplitCounts {
|
export function aggregateSplitCounts(packs: DmsPackRow[]): SplitCounts {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type { LabelingBatchRow } from "@/lib/types";
|
|||||||
type ExportBatchTableProps = {
|
type ExportBatchTableProps = {
|
||||||
batches: LabelingBatchRow[];
|
batches: LabelingBatchRow[];
|
||||||
importingId: string | null;
|
importingId: string | null;
|
||||||
|
exportingId: string | null;
|
||||||
buildingId: string | null;
|
buildingId: string | null;
|
||||||
onExport: (campaignId: string) => void;
|
onExport: (campaignId: string) => void;
|
||||||
onImportVendor: (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> = ({
|
export const ExportBatchTable: React.FC<ExportBatchTableProps> = ({
|
||||||
batches,
|
batches,
|
||||||
importingId,
|
importingId,
|
||||||
|
exportingId,
|
||||||
buildingId,
|
buildingId,
|
||||||
onExport,
|
onExport,
|
||||||
onImportVendor,
|
onImportVendor,
|
||||||
@@ -67,7 +69,7 @@ export const ExportBatchTable: React.FC<ExportBatchTableProps> = ({
|
|||||||
<td className="py-2 px-2 whitespace-nowrap w-[7.5rem]">
|
<td className="py-2 px-2 whitespace-nowrap w-[7.5rem]">
|
||||||
{cid && isExport && (
|
{cid && isExport && (
|
||||||
<div className="inline-flex items-center justify-center gap-0.5">
|
<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>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import {
|
|||||||
buildDmsPackTree, dmsPacks, dmsTaskModes,
|
buildDmsPackTree, dmsPacks, dmsTaskModes,
|
||||||
findPackRow, isDmsCatalogScope, packTaskKey, parseCatalogScope,
|
findPackRow, isDmsCatalogScope, packTaskKey, parseCatalogScope,
|
||||||
parsePackTaskKey, primaryPack, scopeKeyFromSelection, selectionFromScopeKey,
|
parsePackTaskKey, primaryPack, scopeKeyFromSelection, selectionFromScopeKey,
|
||||||
splitCountsFromPack, type CatalogReport, type CatalogUiSelection,
|
splitCountsFromPack, type AdasBatchEntry, type AdasPackEntry,
|
||||||
|
type CatalogReport, type CatalogUiSelection,
|
||||||
type CatalogViewKind, type DmsPackRow, type DmsTaskEntry,
|
type CatalogViewKind, type DmsPackRow, type DmsTaskEntry,
|
||||||
} from "@/lib/dmsCatalog";
|
} from "@/lib/dmsCatalog";
|
||||||
|
|
||||||
@@ -183,8 +184,8 @@ export const CatalogPage: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* DMS / Forward sub-views */}
|
{/* DMS sub-views */}
|
||||||
{domain !== "lane" && (
|
{domain === "dms" && (
|
||||||
<div className="flex flex-wrap gap-2 mb-4 text-sm">
|
<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>
|
<p className="text-xs text-gray-500 m-0 self-center mr-2">视图</p>
|
||||||
{(["pack", "batch"] as const).map((v) => (
|
{(["pack", "batch"] as const).map((v) => (
|
||||||
@@ -198,8 +199,8 @@ export const CatalogPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pack selector */}
|
{/* Pack selector — only for DMS */}
|
||||||
{domain !== "lane" && subView === "pack" && packTree.length > 0 && (
|
{domain === "dms" && subView === "pack" && packTree.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-3 items-end mb-4">
|
<div className="flex flex-wrap gap-3 items-end mb-4">
|
||||||
<label className="flex-1 min-w-[8rem] text-xs text-gray-500">训练包
|
<label className="flex-1 min-w-[8rem] text-xs text-gray-500">训练包
|
||||||
<select className={selectClass} value={ui.pack} onChange={(e) => setPack(e.target.value)}>
|
<select className={selectClass} value={ui.pack} onChange={(e) => setPack(e.target.value)}>
|
||||||
@@ -214,8 +215,75 @@ export const CatalogPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* DMS/Forward: Charts */}
|
{/* ADAS 前向:Pack + Batch 表格 */}
|
||||||
{domain !== "lane" && taskEntry && isDmsCatalogScope(scope) && (
|
{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="rounded-xl border border-gray-200 bg-white p-3 mb-4">
|
||||||
<div className="flex flex-wrap gap-2 items-center mb-2">
|
<div className="flex flex-wrap gap-2 items-center mb-2">
|
||||||
<span className="text-base font-semibold">{activePackTask?.label || taskEntry.label || scope.task}</span>
|
<span className="text-base font-semibold">{activePackTask?.label || taskEntry.label || scope.task}</span>
|
||||||
@@ -253,8 +321,8 @@ export const CatalogPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Empty state */}
|
{/* Empty state for DMS */}
|
||||||
{domain !== "lane" && !taskEntry && (
|
{domain === "dms" && !taskEntry && (
|
||||||
<div className="card text-center py-12 text-gray-400">
|
<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-lg mb-2">暂无 {DOMAIN_TABS.find((t) => t.key === domain)?.label} 数据</p>
|
||||||
<p className="text-sm">请先在送标工作台中扫描入库,或检查数据集链接是否正确</p>
|
<p className="text-sm">请先在送标工作台中扫描入库,或检查数据集链接是否正确</p>
|
||||||
@@ -266,8 +334,8 @@ export const CatalogPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pack table */}
|
{/* DMS pack table — only for DMS domain */}
|
||||||
{domain !== "lane" && isDmsCatalogScope(scope) && taskEntry && tablePacks.length > 0 && (
|
{domain === "dms" && isDmsCatalogScope(scope) && taskEntry && tablePacks.length > 0 && (
|
||||||
<div className="rounded-xl border border-gray-200 bg-white overflow-hidden">
|
<div className="rounded-xl border border-gray-200 bg-white overflow-hidden">
|
||||||
<table className="w-full text-sm">
|
<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>
|
<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 [error, setError] = useState<string | null>(null);
|
||||||
const [info, setInfo] = useState<string | null>(null);
|
const [info, setInfo] = useState<string | null>(null);
|
||||||
const [importingId, setImportingId] = 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 [buildingId, setBuildingId] = useState<string | null>(null);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [stageFilter, setStageFilter] = useState("");
|
const [stageFilter, setStageFilter] = useState("");
|
||||||
@@ -60,8 +61,11 @@ export const ExportPage: React.FC = () => {
|
|||||||
const reloadCurrent = useCallback(() => load(offset, limit), [load, offset, limit]);
|
const reloadCurrent = useCallback(() => load(offset, limit), [load, offset, limit]);
|
||||||
|
|
||||||
const handleExport = async (campaignId: string) => {
|
const handleExport = async (campaignId: string) => {
|
||||||
|
setExportingId(campaignId);
|
||||||
|
setError(null);
|
||||||
try { await hsapApi.labelingExport(campaignId); setInfo("导出任务已提交"); reloadCurrent(); }
|
try { await hsapApi.labelingExport(campaignId); setInfo("导出任务已提交"); reloadCurrent(); }
|
||||||
catch (e) { setError(String(e)); }
|
catch (e) { setError(String(e)); }
|
||||||
|
setExportingId(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmitBuild = async (b: LabelingBatchRow) => {
|
const handleSubmitBuild = async (b: LabelingBatchRow) => {
|
||||||
@@ -135,6 +139,7 @@ export const ExportPage: React.FC = () => {
|
|||||||
<ExportBatchTable
|
<ExportBatchTable
|
||||||
batches={batches}
|
batches={batches}
|
||||||
importingId={importingId}
|
importingId={importingId}
|
||||||
|
exportingId={exportingId}
|
||||||
buildingId={buildingId}
|
buildingId={buildingId}
|
||||||
onExport={handleExport}
|
onExport={handleExport}
|
||||||
onImportVendor={handleImportVendor}
|
onImportVendor={handleImportVendor}
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ projects:
|
|||||||
# 数据湖:宿主机 DATA/data/送标/adas → inbox/{task}/{batch}/images/
|
# 数据湖:宿主机 DATA/data/送标/adas → inbox/{task}/{batch}/images/
|
||||||
registry: adas.registry.yaml
|
registry: adas.registry.yaml
|
||||||
packs_registry: data_packs.yaml
|
packs_registry: data_packs.yaml
|
||||||
base_pack: adas_moon3d_v1
|
base_pack: adas_v1
|
||||||
active_packs:
|
active_packs:
|
||||||
- adas_moon3d_v1
|
- adas_v1
|
||||||
merge:
|
merge:
|
||||||
train: lists/train_stems.txt
|
train: lists/train_stems.txt
|
||||||
val: lists/val_stems.txt
|
val: lists/val_stems.txt
|
||||||
|
|||||||
Reference in New Issue
Block a user