feat: initial HSAP platform

Huaxu Sentinel Active Safety Platform with embedded algorithm code,
Docker Compose setup, and vendored dataset scaffolds for clone-and-run.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-25 16:59:59 +08:00
commit 7c43b44c57
1619 changed files with 373355 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
from as_platform.data.ingest.base import IngestContext, IngestAdapter, NormalizedDataset
from as_platform.data.ingest.registry import (
UnknownFormatError,
available_formats,
detect_adapter,
inspect_uploaded_dataset,
)
__all__ = [
"IngestContext",
"IngestAdapter",
"NormalizedDataset",
"UnknownFormatError",
"available_formats",
"detect_adapter",
"inspect_uploaded_dataset",
]

View File

@@ -0,0 +1,46 @@
"""Data ingest adapter base abstractions."""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any
@dataclass
class IngestContext:
project: str
task: str | None
source_path: Path
@dataclass
class NormalizedDataset:
format_id: str
project: str
task: str | None
source_path: str
split_counts: dict[str, int] = field(default_factory=dict)
sample_count: int = 0
annotation_count: int = 0
artifacts: list[str] = field(default_factory=list)
warnings: list[str] = field(default_factory=list)
extra: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class IngestAdapter(ABC):
"""Adapter interface for task-specific upload formats."""
format_id: str = "unknown"
projects: tuple[str, ...] = ()
@abstractmethod
def can_handle(self, ctx: IngestContext) -> bool:
raise NotImplementedError
@abstractmethod
def inspect(self, ctx: IngestContext) -> NormalizedDataset:
raise NotImplementedError

View File

@@ -0,0 +1,88 @@
"""DMS COCO-format adapter."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from as_platform.data.ingest.base import IngestAdapter, IngestContext, NormalizedDataset
COCO_NAMES = ("instances_train.json", "instances_val.json", "instances_test.json", "annotations.json")
def _read_json(path: Path) -> dict[str, Any] | None:
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return None
class DmsCocoAdapter(IngestAdapter):
format_id = "dms_coco"
projects = ("dms",)
def _find_coco_files(self, root: Path) -> list[Path]:
files: list[Path] = []
for name in COCO_NAMES:
p = root / "annotations" / name
if p.is_file():
files.append(p)
for name in COCO_NAMES:
p = root / name
if p.is_file():
files.append(p)
return files
def can_handle(self, ctx: IngestContext) -> bool:
root = ctx.source_path
return len(self._find_coco_files(root)) > 0
def inspect(self, ctx: IngestContext) -> NormalizedDataset:
root = ctx.source_path
files = self._find_coco_files(root)
split_counts = {"train": 0, "val": 0, "test": 0}
ann_count = 0
categories: set[str] = set()
warnings: list[str] = []
for f in files:
data = _read_json(f)
if not data:
warnings.append(f"failed to parse {f.name}")
continue
images = data.get("images") or []
anns = data.get("annotations") or []
cats = data.get("categories") or []
ann_count += len(anns)
for c in cats:
name = c.get("name")
if isinstance(name, str):
categories.add(name)
lower = f.name.lower()
if "train" in lower:
split_counts["train"] += len(images)
elif "val" in lower:
split_counts["val"] += len(images)
elif "test" in lower:
split_counts["test"] += len(images)
else:
split_counts["train"] += len(images)
return NormalizedDataset(
format_id=self.format_id,
project=ctx.project,
task=ctx.task,
source_path=str(root),
split_counts=split_counts,
sample_count=sum(split_counts.values()),
annotation_count=ann_count,
artifacts=[self._artifact_name(root, f) for f in files],
warnings=warnings,
extra={"categories": sorted(categories)},
)
@staticmethod
def _artifact_name(root: Path, path: Path) -> str:
try:
return str(path.relative_to(root))
except ValueError:
return path.name

View File

@@ -0,0 +1,67 @@
"""DMS YOLO-style dataset adapter."""
from __future__ import annotations
from pathlib import Path
from as_platform.data.ingest.base import IngestAdapter, IngestContext, NormalizedDataset
IMG_EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".JPG", ".JPEG", ".PNG"}
def _count_images(path: Path) -> int:
if not path.is_dir():
return 0
return sum(1 for p in path.rglob("*") if p.is_file() and p.suffix in IMG_EXTS)
def _count_txt(path: Path) -> int:
if not path.is_dir():
return 0
return sum(1 for p in path.rglob("*.txt") if p.is_file())
class DmsYoloAdapter(IngestAdapter):
format_id = "dms_yolo"
projects = ("dms",)
def can_handle(self, ctx: IngestContext) -> bool:
root = ctx.source_path
return (
(root / "images").is_dir()
and (root / "labels").is_dir()
) or (
(root / "images" / "train").is_dir()
and (root / "labels" / "train").is_dir()
)
def inspect(self, ctx: IngestContext) -> NormalizedDataset:
root = ctx.source_path
train_images = _count_images(root / "images" / "train")
val_images = _count_images(root / "images" / "val")
test_images = _count_images(root / "images" / "test")
if train_images + val_images + test_images == 0:
# fallback single-folder dataset
train_images = _count_images(root / "images")
train_labels = _count_txt(root / "labels" / "train")
val_labels = _count_txt(root / "labels" / "val")
test_labels = _count_txt(root / "labels" / "test")
if train_labels + val_labels + test_labels == 0:
train_labels = _count_txt(root / "labels")
warnings: list[str] = []
if train_images == 0:
warnings.append("train split has no images")
if train_labels == 0:
warnings.append("train split has no labels")
return NormalizedDataset(
format_id=self.format_id,
project=ctx.project,
task=ctx.task,
source_path=str(root),
split_counts={"train": train_images, "val": val_images, "test": test_images},
sample_count=train_images + val_images + test_images,
annotation_count=train_labels + val_labels + test_labels,
artifacts=["images/", "labels/"],
warnings=warnings,
)

View File

@@ -0,0 +1,34 @@
"""Lane .lines.txt adapter."""
from __future__ import annotations
from pathlib import Path
from as_platform.data.ingest.base import IngestAdapter, IngestContext, NormalizedDataset
class LaneLinesAdapter(IngestAdapter):
format_id = "lane_lines"
projects = ("lane",)
def can_handle(self, ctx: IngestContext) -> bool:
root = ctx.source_path
return any(root.rglob("*.lines.txt"))
def inspect(self, ctx: IngestContext) -> NormalizedDataset:
root = ctx.source_path
line_files = list(root.rglob("*.lines.txt"))
split_counts = {"train": len(line_files), "val": 0, "test": 0}
warnings: list[str] = []
if not line_files:
warnings.append("no *.lines.txt found")
return NormalizedDataset(
format_id=self.format_id,
project=ctx.project,
task=ctx.task,
source_path=str(root),
split_counts=split_counts,
sample_count=len(line_files),
annotation_count=len(line_files),
artifacts=["*.lines.txt"],
warnings=warnings,
)

View File

@@ -0,0 +1,48 @@
"""Lane mask + list txt adapter."""
from __future__ import annotations
from pathlib import Path
from as_platform.data.ingest.base import IngestAdapter, IngestContext, NormalizedDataset
def _line_count(path: Path) -> int:
if not path.is_file():
return 0
try:
return sum(1 for _ in path.open(encoding="utf-8", errors="ignore"))
except OSError:
return 0
class LaneMaskAdapter(IngestAdapter):
format_id = "lane_mask"
projects = ("lane",)
def can_handle(self, ctx: IngestContext) -> bool:
root = ctx.source_path
return (root / "list" / "train_gt.txt").is_file() or (root / "train_val_gt.txt").is_file()
def inspect(self, ctx: IngestContext) -> NormalizedDataset:
root = ctx.source_path
train = _line_count(root / "list" / "train_gt.txt")
val = _line_count(root / "list" / "val_gt.txt")
test = _line_count(root / "list" / "test_gt.txt")
if train == 0 and (root / "train_val_gt.txt").is_file():
train = _line_count(root / "train_val_gt.txt")
warnings: list[str] = []
if train == 0:
warnings.append("train split list is empty")
return NormalizedDataset(
format_id=self.format_id,
project=ctx.project,
task=ctx.task,
source_path=str(root),
split_counts={"train": train, "val": val, "test": test},
sample_count=train + val + test,
annotation_count=train + val + test,
artifacts=["list/train_gt.txt", "list/val_gt.txt", "list/test_gt.txt"],
warnings=warnings,
)

View File

@@ -0,0 +1,49 @@
"""Adapter registry and auto detection for uploaded datasets."""
from __future__ import annotations
from pathlib import Path
from as_platform.data.ingest.base import IngestAdapter, IngestContext, NormalizedDataset
from as_platform.data.ingest.dms_coco import DmsCocoAdapter
from as_platform.data.ingest.dms_yolo import DmsYoloAdapter
from as_platform.data.ingest.lane_lines import LaneLinesAdapter
from as_platform.data.ingest.lane_mask import LaneMaskAdapter
class UnknownFormatError(ValueError):
pass
ADAPTERS: tuple[IngestAdapter, ...] = (
DmsYoloAdapter(),
DmsCocoAdapter(),
LaneMaskAdapter(),
LaneLinesAdapter(),
)
def available_formats(project: str) -> list[str]:
return [a.format_id for a in ADAPTERS if project in a.projects]
def detect_adapter(ctx: IngestContext) -> IngestAdapter:
for adapter in ADAPTERS:
if ctx.project not in adapter.projects:
continue
if adapter.can_handle(ctx):
return adapter
raise UnknownFormatError(
f"unable to detect format for project={ctx.project}, task={ctx.task}, "
f"source={ctx.source_path}. supported={available_formats(ctx.project)}"
)
def inspect_uploaded_dataset(project: str, task: str | None, source_path: str | Path) -> NormalizedDataset:
ctx = IngestContext(project=project, task=task, source_path=Path(source_path).resolve())
if not ctx.source_path.exists():
raise FileNotFoundError(f"source path not found: {ctx.source_path}")
adapter = detect_adapter(ctx)
out = adapter.inspect(ctx)
# Ensure adapter id is always reflected in output.
out.format_id = adapter.format_id
return out