Skip to content

Commit fd04573

Browse files
authored
Merge pull request #4 from Roalkege/main
This PR improves the out-of-the-box experience for running `light-object-detect`, especially in Docker / on Unraid.
2 parents c0f9a9b + 50009ee commit fd04573

5 files changed

Lines changed: 247 additions & 92 deletions

File tree

Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ RUN python -m pip install --upgrade pip \
3434

3535
COPY . .
3636

37+
# Bake the default TFLite model into the image (can be disabled at build time).
38+
# Usage: docker build --build-arg DOWNLOAD_DEFAULT_MODEL=0 ...
39+
ARG DOWNLOAD_DEFAULT_MODEL=1
40+
RUN if [ "$DOWNLOAD_DEFAULT_MODEL" = "1" ]; then python scripts/download_model.py; fi
41+
3742
EXPOSE 8000
3843

3944
# .env is optional; Unraid can mount it to /app/.env

README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ A lightweight Python API for object detection with pluggable backends. This API
2828
pipenv install
2929
```
3030

31-
3. Download a sample TFLite model:
31+
3. (Optional) Pre-download the default TFLite model:
3232
```bash
3333
pipenv run python scripts/download_model.py
3434
```
3535

36+
If you skip this step, the API will try to download the default model on startup when the `tflite` backend is enabled (requires internet access). Docker builds download the default model by default.
37+
3638
## Usage
3739

3840
1. Start the API server using the provided script:
@@ -49,23 +51,26 @@ A lightweight Python API for object detection with pluggable backends. This API
4951

5052
3. Access the API documentation at http://localhost:9001/docs
5153

52-
## Docker (z.B. Unraid / lightNVR)
54+
## Docker (e.g. Unraid / lightNVR)
5355

5456
### Build
5557

5658
```bash
5759
docker build -t light-object-detect:local .
5860
```
5961

62+
By default, the image downloads a small reference TFLite model at build time so the `tflite` backend works out of the box.
63+
To disable this, build with `--build-arg DOWNLOAD_DEFAULT_MODEL=0`.
64+
6065
### Run
6166

62-
Option A: ohne `.env` (Defaults aus `config.py`):
67+
Option A: without `.env` (uses defaults from `config.py`):
6368

6469
```bash
6570
docker run --rm -p 8000:8000 --name light-object-detect light-object-detect:local
6671
```
6772

68-
Option B: mit `.env` (empfohlen, z.B. Backend/Model-Pfade):
73+
Option B: with `.env` (recommended, e.g. for backend/model paths):
6974

7075
```bash
7176
docker run --rm -p 8000:8000 --name light-object-detect \
@@ -86,7 +91,7 @@ docker run --rm -p 8000:8000 --name light-object-detect `
8691

8792
### lightNVR Integration
8893

89-
In lightNVR als API-URL typischerweise:
94+
In lightNVR, the API URL is typically:
9095

9196
- `http://<docker-host>:8000/api/v1/detect`
9297

main.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from api.router import router as api_router
88
from config import settings
99
from backends.factory import get_backend
10+
from utils.model_download import ensure_tflite_ssd_mobilenet_v1, ModelDownloadError
1011

1112
# Configure logging
1213
logging.basicConfig(
@@ -30,6 +31,25 @@ async def startup_event():
3031
logger.info(f"Available backends: {settings.AVAILABLE_BACKENDS}")
3132
logger.info(f"Default backend: {settings.DEFAULT_BACKEND}")
3233

34+
# Ensure the default TFLite model is present if TFLite is enabled.
35+
# This keeps the out-of-the-box experience working, while still allowing
36+
# users to override paths via .env.
37+
if "tflite" in settings.AVAILABLE_BACKENDS:
38+
try:
39+
ensure_result = ensure_tflite_ssd_mobilenet_v1(
40+
model_path=settings.TFLITE_MODEL_PATH,
41+
labels_path=settings.TFLITE_LABELS_PATH,
42+
force=False,
43+
)
44+
if ensure_result.did_download:
45+
logger.info(f"✓ Downloaded default TFLite model: {ensure_result.message}")
46+
except ModelDownloadError as e:
47+
logger.error(
48+
"✗ TFLite model is missing and could not be downloaded. "
49+
f"Reason: {e}. "
50+
"Fix: run 'python scripts/download_model.py' or set TFLITE_MODEL_PATH to an existing model."
51+
)
52+
3353
# Test each backend
3454
for backend_name in settings.AVAILABLE_BACKENDS:
3555
try:

scripts/download_model.py

Lines changed: 23 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,39 @@
11
#!/usr/bin/env python3
22
"""
3-
Script to download a sample TFLite model for object detection.
3+
Script to download a default TFLite model for object detection.
4+
5+
This is mainly intended for local development; Docker builds can also run it
6+
to bake the model into the image.
47
"""
58

6-
import os
9+
from __future__ import annotations
10+
711
import sys
8-
import urllib.request
9-
import zipfile
10-
import shutil
1112
from pathlib import Path
1213

13-
# Add parent directory to path to import config
14+
# Add parent directory to path to import config + utils
1415
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
15-
from config import settings
16-
17-
18-
def download_file(url, destination):
19-
"""Download a file from a URL to a destination."""
20-
print(f"Downloading {url} to {destination}...")
21-
22-
# Create directory if it doesn't exist
23-
os.makedirs(os.path.dirname(destination), exist_ok=True)
24-
25-
# Download the file
26-
urllib.request.urlretrieve(url, destination)
27-
28-
print(f"Downloaded {destination}")
2916

30-
31-
def extract_zip(zip_path, extract_dir):
32-
"""Extract a zip file to a directory."""
33-
print(f"Extracting {zip_path} to {extract_dir}...")
34-
35-
# Create directory if it doesn't exist
36-
os.makedirs(extract_dir, exist_ok=True)
37-
38-
# Extract the zip file
39-
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
40-
zip_ref.extractall(extract_dir)
41-
42-
print(f"Extracted {zip_path}")
43-
44-
45-
def download_ssd_mobilenet():
46-
"""Download SSD MobileNet v1 model."""
47-
model_url = "https://storage.googleapis.com/download.tensorflow.org/models/tflite/coco_ssd_mobilenet_v1_1.0_quant_2018_06_29.zip"
48-
zip_path = "/tmp/ssd_mobilenet.zip"
49-
extract_dir = "/tmp/ssd_mobilenet"
50-
model_dir = os.path.dirname(settings.TFLITE_MODEL_PATH)
51-
52-
# Download the model
53-
download_file(model_url, zip_path)
54-
55-
# Extract the zip file
56-
extract_zip(zip_path, extract_dir)
57-
58-
# Create model directory if it doesn't exist
59-
os.makedirs(model_dir, exist_ok=True)
60-
61-
# Copy the model file
62-
shutil.copy(
63-
os.path.join(extract_dir, "detect.tflite"),
64-
settings.TFLITE_MODEL_PATH
65-
)
66-
67-
# Create a labelmap file
68-
with open(settings.TFLITE_LABELS_PATH, 'w') as f:
69-
# COCO dataset labels
70-
labels = [
71-
"person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat",
72-
"traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat",
73-
"dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack",
74-
"umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball",
75-
"kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket",
76-
"bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple",
77-
"sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair",
78-
"couch", "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse", "remote",
79-
"keyboard", "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator", "book",
80-
"clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush"
81-
]
82-
for label in labels:
83-
f.write(f"{label}\n")
84-
85-
# Clean up
86-
os.remove(zip_path)
87-
shutil.rmtree(extract_dir)
88-
89-
print(f"Model saved to {settings.TFLITE_MODEL_PATH}")
90-
print(f"Labels saved to {settings.TFLITE_LABELS_PATH}")
17+
from config import settings
18+
from utils.model_download import ensure_tflite_ssd_mobilenet_v1, ModelDownloadError
9119

9220

9321
def main():
9422
"""Main function."""
95-
print("Downloading TFLite model for object detection...")
96-
97-
# Download SSD MobileNet model
98-
download_ssd_mobilenet()
99-
100-
print("Done!")
23+
print("Ensuring default TFLite model for object detection...")
24+
25+
try:
26+
result = ensure_tflite_ssd_mobilenet_v1(
27+
model_path=settings.TFLITE_MODEL_PATH,
28+
labels_path=settings.TFLITE_LABELS_PATH,
29+
force=False,
30+
)
31+
except ModelDownloadError as e:
32+
print(f"ERROR: {e}")
33+
raise SystemExit(1) from e
34+
35+
print(result.message)
36+
print("Done.")
10137

10238

10339
if __name__ == "__main__":

0 commit comments

Comments
 (0)