Skip to content

Commit 9b85095

Browse files
committed
fix: commit yolo-seg(#6)
1 parent b722fd7 commit 9b85095

16 files changed

Lines changed: 154 additions & 25 deletions

File tree

‎backend/alembic/versions/5baeca04aab3_initial_schema.py‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111

1212
from alembic import op
1313

14+
# Use PostgreSQL's efficient JSONB where available, while retaining support for
15+
# the zero-config SQLite development database.
16+
JSON_TYPE = sa.JSON().with_variant(postgresql.JSONB(), "postgresql")
17+
1418
# revision identifiers
1519
revision: str = '5baeca04aab3'
1620
down_revision: str | None = None
@@ -24,7 +28,7 @@ def upgrade() -> None:
2428
sa.Column('id', sa.UUID(), nullable=False),
2529
sa.Column('image_path', sa.Text(), nullable=False),
2630
sa.Column('image_name', sa.String(length=512), nullable=False),
27-
sa.Column('categories', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
31+
sa.Column('categories', JSON_TYPE, nullable=False),
2832
sa.Column('model_name', sa.String(length=256), nullable=False),
2933
sa.Column('image_width', sa.Integer(), nullable=False),
3034
sa.Column('image_height', sa.Integer(), nullable=False),
@@ -43,9 +47,9 @@ def upgrade() -> None:
4347
sa.Column('epochs', sa.Integer(), nullable=False),
4448
sa.Column('imgsz', sa.Integer(), nullable=False),
4549
sa.Column('batch', sa.Integer(), nullable=False),
46-
sa.Column('class_map', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
50+
sa.Column('class_map', JSON_TYPE, nullable=True),
4751
sa.Column('status', sa.String(length=32), nullable=False),
48-
sa.Column('metrics', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
52+
sa.Column('metrics', JSON_TYPE, nullable=True),
4953
sa.Column('model_path', sa.Text(), nullable=True),
5054
sa.Column('error_message', sa.Text(), nullable=True),
5155
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),

‎backend/alembic/versions/c3d4e5f6a7b8_add_mask_polygon_to_boxes.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@
1111

1212
from alembic import op
1313

14+
15+
JSON_TYPE = sa.JSON().with_variant(postgresql.JSONB(), "postgresql")
16+
1417
revision: str = 'c3d4e5f6a7b8'
1518
down_revision: str | None = 'b2c3d4e5f6a7'
1619
branch_labels: str | Sequence[str] | None = None
1720
depends_on: str | Sequence[str] | None = None
1821

1922

2023
def upgrade() -> None:
21-
op.add_column('detection_boxes', sa.Column('mask_polygon', postgresql.JSONB(), nullable=True))
24+
op.add_column('detection_boxes', sa.Column('mask_polygon', JSON_TYPE, nullable=True))
2225

2326

2427
def downgrade() -> None:

‎backend/app/api/routes/export.py‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
from starlette.responses import Response
1010

1111
from ...core.database import get_db
12-
from ...schemas.detection import ExportBatchIn
13-
from ...services.export import FORMAT_LABELS, export_batch, export_single
12+
from ...schemas.detection import ExportAllIn, ExportBatchIn
13+
from ...services.export import FORMAT_LABELS, export_all, export_batch, export_single
1414

1515
logger = logging.getLogger(__name__)
1616
router = APIRouter(prefix="/api/v1", tags=["export"])
@@ -47,3 +47,21 @@ def download_batch_yolo(
4747
media_type="application/zip",
4848
headers={"Content-Disposition": f'attachment; filename="{label}_dataset.zip"'},
4949
)
50+
51+
52+
@router.post("/detections/export-all")
53+
def download_all_detections(
54+
body: ExportAllIn,
55+
db: Session = Depends(get_db),
56+
) -> Response:
57+
"""Download every saved detection as one dataset archive."""
58+
try:
59+
data = export_all(db, format=body.format)
60+
except ValueError as exc:
61+
raise HTTPException(400, detail=str(exc)) from exc
62+
label = FORMAT_LABELS.get(body.format, body.format)
63+
return Response(
64+
content=data,
65+
media_type="application/zip",
66+
headers={"Content-Disposition": f'attachment; filename="{label}_all_dataset.zip"'},
67+
)

‎backend/app/core/gpu_memory.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ def resolve_max_long_side(self) -> int:
7373
return 1333
7474
if total_mb >= 12 * 1024:
7575
return 1024
76-
return 800 # < 12 GB, conservative
76+
if total_mb >= 10 * 1024:
77+
return 800
78+
return 640 # preserve small-object detail on 8 GB cards
7779

7880
def resolve_attn_impl(self) -> str:
7981
try:

‎backend/app/repositories/detection.py‎

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,17 @@
66
from __future__ import annotations
77

88
from collections.abc import Sequence
9+
import uuid
910

1011
from sqlalchemy.orm import Session
1112

1213
from ..models.detection import Detection, DetectionBox
1314

1415

16+
def _as_uuid(value: str | uuid.UUID) -> uuid.UUID:
17+
return value if isinstance(value, uuid.UUID) else uuid.UUID(value)
18+
19+
1520
class DetectionRepository:
1621
def __init__(self, db: Session) -> None:
1722
self.db = db
@@ -46,12 +51,13 @@ def create(
4651
return det
4752

4853
def add_boxes(
49-
self, detection_id: str, boxes: list[dict], commit: bool = False
54+
self, detection_id: str | uuid.UUID, boxes: list[dict], commit: bool = False
5055
) -> list[DetectionBox]:
5156
"""boxes: [{"x1","y1","x2","y2","class_name","confidence","mask_polygon"}, ...]"""
57+
detection_uuid = _as_uuid(detection_id)
5258
entities = [
5359
DetectionBox(
54-
detection_id=detection_id,
60+
detection_id=detection_uuid,
5561
class_name=b["class_name"],
5662
x1=b["x1"],
5763
y1=b["y1"],
@@ -70,18 +76,19 @@ def add_boxes(
7076
return entities
7177

7278
def replace_boxes(
73-
self, detection_id: str, boxes: list[dict], commit: bool = False
79+
self, detection_id: str | uuid.UUID, boxes: list[dict], commit: bool = False
7480
) -> list[DetectionBox]:
7581
"""Delete all existing boxes for a detection and insert new ones."""
82+
detection_uuid = _as_uuid(detection_id)
7683
self.db.query(DetectionBox).filter(
77-
DetectionBox.detection_id == detection_id,
84+
DetectionBox.detection_id == detection_uuid,
7885
).delete()
7986
# Note: If commit=True, the flush on delete is skipped, and the subsequent
8087
# add_boxes(commit=True) will commit both the delete and the inserts
8188
# in a single transaction.
8289
if not commit:
8390
self.db.flush()
84-
return self.add_boxes(detection_id, boxes, commit=commit)
91+
return self.add_boxes(detection_uuid, boxes, commit=commit)
8592

8693
def update_detection(self, detection: Detection, commit: bool = False) -> None:
8794
if commit:
@@ -99,8 +106,8 @@ def update_box(self, box: DetectionBox, commit: bool = False) -> None:
99106

100107
# ── read ───────────────────────────────────────────
101108

102-
def get_by_id(self, detection_id: str) -> Detection | None:
103-
return self.db.query(Detection).filter(Detection.id == detection_id).first()
109+
def get_by_id(self, detection_id: str | uuid.UUID) -> Detection | None:
110+
return self.db.query(Detection).filter(Detection.id == _as_uuid(detection_id)).first()
104111

105112
def list(
106113
self,
@@ -131,8 +138,8 @@ def get_box(self, detection_id: str, box_id: str) -> DetectionBox | None:
131138
return (
132139
self.db.query(DetectionBox)
133140
.filter(
134-
DetectionBox.id == box_id,
135-
DetectionBox.detection_id == detection_id,
141+
DetectionBox.id == _as_uuid(box_id),
142+
DetectionBox.detection_id == _as_uuid(detection_id),
136143
)
137144
.first()
138145
)

‎backend/app/schemas/detection.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,7 @@ class DetectionOut(DetectionListItem):
9090
class ExportBatchIn(BaseSchema):
9191
detection_ids: list[str] = Field(..., min_length=1)
9292
format: str = "yolo"
93+
94+
95+
class ExportAllIn(BaseSchema):
96+
format: str = "yolo-seg"

‎backend/app/services/export.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ def export_single_zip(db: Session, detection_id: str, format: str = "yolo") -> b
4343
return export_batch(db, [detection_id], format=format)
4444

4545

46+
def export_all(db: Session, format: str = "yolo-seg") -> bytes:
47+
"""Export every detection in the database as one dataset archive."""
48+
from ..models.detection import Detection
49+
50+
dets = db.query(Detection).order_by(Detection.created_at, Detection.id).all()
51+
if not dets:
52+
raise ValueError("No detections available to export")
53+
return _export_detections(dets, format)
54+
55+
4656
def export_batch(db: Session, detection_ids: list[str], format: str = "yolo") -> bytes:
4757
"""Export multiple detections as a zip file in the requested format."""
4858
from ..models.detection import Detection
@@ -53,6 +63,10 @@ def export_batch(db: Session, detection_ids: list[str], format: str = "yolo") ->
5363
if det:
5464
dets.append(det)
5565

66+
return _export_detections(dets, format)
67+
68+
69+
def _export_detections(dets: list[Detection], format: str) -> bytes:
5670
unified_map = _build_class_map(dets)
5771

5872
if format == "yolo":

‎backend/app/services/locate_anything.py‎

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,15 @@ def _ensure_decord_stub():
2929
if _decord_loaded:
3030
return
3131
if sys.platform in ("darwin", "win32"):
32-
stub_dir = str(Path(settings.resolved_model_dir) / "stubs")
33-
if Path(stub_dir).exists() and stub_dir not in sys.path:
34-
sys.path.insert(0, stub_dir)
32+
try:
33+
import decord # type: ignore[import-not-found] # noqa: F401
34+
except ImportError:
35+
import decord_stub
36+
37+
# Transformers checks optional imports before loading remote model
38+
# code. Register the bundled image-only compatibility shim under
39+
# the package name it expects.
40+
sys.modules["decord"] = decord_stub
3541
_decord_loaded = True
3642

3743

@@ -80,17 +86,34 @@ def __init__(self, model_path: str, device: str = "mps", progress_cb=None):
8086
self._set_progress(progress_cb, "downloading", "model", 50)
8187
attn_impl = gpu_mem.resolve_attn_impl()
8288
logger.info("Loading model to %s (attn=%s)...", device, attn_impl)
89+
load_kwargs = {}
90+
if device == "cuda":
91+
total_mb = torch.cuda.get_device_properties(0).total_memory // (1024 * 1024)
92+
if total_mb < 10 * 1024:
93+
# The BF16 model is over 7 GiB. Keeping it entirely on an 8 GiB
94+
# display GPU leaves no room for activations and triggers very
95+
# slow WDDM shared-memory paging.
96+
load_kwargs = {
97+
"device_map": "auto",
98+
"max_memory": {0: "5GiB", "cpu": "12GiB"},
99+
}
100+
logger.info("Low-VRAM CUDA detected; enabling CPU/GPU weight offload")
83101
self.model = AutoModel.from_pretrained(
84102
model_path,
85103
torch_dtype=self.dtype,
86104
trust_remote_code=True,
87105
attn_implementation=attn_impl,
88106
low_cpu_mem_usage=True,
107+
**load_kwargs,
89108
)
90109

91110
# ── Move to GPU ──
92111
self._set_progress(progress_cb, "loading", "gpu", 90)
93-
self.model = self.model.to(device).eval()
112+
if load_kwargs:
113+
self.model.eval()
114+
logger.info("Model device map: %s", getattr(self.model, "hf_device_map", {}))
115+
else:
116+
self.model = self.model.to(device).eval()
94117

95118
logger.info("LocateAnything model ready on %s", device)
96119
self._set_progress(progress_cb, "loaded", "", 100)
@@ -112,7 +135,7 @@ def _predict(
112135
self,
113136
image: Image.Image,
114137
question: str,
115-
max_new_tokens: int = 512,
138+
max_new_tokens: int = 128,
116139
) -> dict:
117140
messages = [
118141
{
@@ -393,6 +416,7 @@ def detect(image_path: str | Path, categories: list[str]) -> dict:
393416
try:
394417
result = worker.detect(img, categories)
395418
raw_text = result["answer"]
419+
logger.debug("Raw model response: %r", raw_text)
396420
except Exception as exc:
397421
raise InferenceError(f"Model inference failed: {exc}") from exc
398422

‎backend/autolabeling.db‎

188 KB
Binary file not shown.

‎cli.py‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
PIP = VENV / BIN / ("pip.exe" if IS_WIN else "pip")
2626
PIDFILE = ROOT / ".cli_pids.json"
2727

28-
BACKEND_PORT = int(os.environ.get("BACKEND_PORT", 8000))
28+
# 8000 and 8500 are reserved by the local Windows networking stack.
29+
# Keep the environment override for users who need a different port.
30+
BACKEND_PORT = int(os.environ.get("BACKEND_PORT", 9000))
2931
FRONTEND_PORT = int(os.environ.get("FRONTEND_PORT", 5173))
3032

3133
# ── Utilities ──────────────────────────────────────────
@@ -207,10 +209,29 @@ def check_sam3_token():
207209
def download_vlm():
208210
step("VLM model (LocateAnything-3B, ~6GB)...")
209211
model_dir = BACKEND / "model"
210-
if model_dir.exists() and any(model_dir.iterdir()):
212+
required_files = (
213+
"config.json",
214+
"model.safetensors.index.json",
215+
"model-00001-of-00002.safetensors",
216+
"model-00002-of-00002.safetensors",
217+
"modeling_locateanything.py",
218+
"modeling_qwen2.py",
219+
"modeling_vit.py",
220+
"preprocessor_config.json",
221+
"processing_locateanything.py",
222+
"processor_config.json",
223+
"special_tokens_map.json",
224+
"tokenizer_config.json",
225+
"vocab.json",
226+
)
227+
if (
228+
model_dir.exists()
229+
and not any(model_dir.glob("*.segments.json"))
230+
and all((model_dir / name).is_file() for name in required_files)
231+
):
211232
ok("already cached")
212233
return
213-
print(f" Downloading to {model_dir}... (~10-30 min)")
234+
print(f" Downloading missing model files to {model_dir}... (~10-30 min)")
214235
try:
215236
run(
216237
[str(PYTHON), "-c",
@@ -311,7 +332,7 @@ def start_frontend():
311332
warn(f"Port {FRONTEND_PORT} already in use. Try: FRONTEND_PORT={FRONTEND_PORT + 1} python3 cli.py start")
312333
return None
313334
proc = subprocess.Popen(
314-
["pnpm", "dev", "--port", str(FRONTEND_PORT)],
335+
["pnpm.cmd", "dev", "--port", str(FRONTEND_PORT)],
315336
cwd=FRONTEND,
316337
env={**os.environ, "VITE_BACKEND_PORT": str(BACKEND_PORT)},
317338
stdout=subprocess.DEVNULL,

0 commit comments

Comments
 (0)