Spaces:
Sleeping
Sleeping
tyrwh
commited on
Commit
·
85022a6
1
Parent(s):
70d6e34
Finishing up commits before moving to Github as main repo
Browse files- Dockerfile +29 -12
- app.py +27 -14
- requirements.txt +1 -0
Dockerfile
CHANGED
|
@@ -1,10 +1,8 @@
|
|
| 1 |
# Use an official Python runtime as a parent image
|
| 2 |
FROM python:3.12
|
|
|
|
| 3 |
|
| 4 |
-
#
|
| 5 |
-
WORKDIR /app
|
| 6 |
-
|
| 7 |
-
# Install system dependencies required by OpenCV and other packages, plus Redis
|
| 8 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 9 |
libgl1 \
|
| 10 |
libglib2.0-0 \
|
|
@@ -13,21 +11,40 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
| 13 |
libxext6 \
|
| 14 |
&& rm -rf /var/lib/apt/lists/*
|
| 15 |
|
| 16 |
-
#
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
# Install any needed packages specified in requirements.txt
|
| 20 |
# --no-cache-dir: Disables the cache to reduce image size.
|
| 21 |
# -r requirements.txt: Specifies the file containing the list of packages to install.
|
| 22 |
-
RUN pip install --no-cache-dir -r requirements.txt
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
#
|
| 25 |
-
|
| 26 |
-
chmod 777 /app/uploads /app/results /app/annotated
|
| 27 |
|
| 28 |
# Copy the rest of the application code into the container at /app
|
| 29 |
# This includes app.py, nemaquant.py, templates/, static/, etc.
|
| 30 |
-
COPY . .
|
| 31 |
|
| 32 |
# Make port 7860 available to the world outside this container
|
| 33 |
# This is the port Flask will run on (as configured in app.py)
|
|
@@ -41,4 +58,4 @@ EXPOSE 7860
|
|
| 41 |
# Use gunicorn for production deployment if preferred over Flask's development server
|
| 42 |
# CMD ["gunicorn", "--bind", "0.0.0.0:7860", "app:app"]
|
| 43 |
# For simplicity during development and typical HF Spaces use:
|
| 44 |
-
CMD python app.py
|
|
|
|
| 1 |
# Use an official Python runtime as a parent image
|
| 2 |
FROM python:3.12
|
| 3 |
+
# FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04
|
| 4 |
|
| 5 |
+
# run updates before switching over to non-root user
|
|
|
|
|
|
|
|
|
|
| 6 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 7 |
libgl1 \
|
| 8 |
libglib2.0-0 \
|
|
|
|
| 11 |
libxext6 \
|
| 12 |
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
|
| 14 |
+
# add new user with ID 1000 to avoid permission issues on HF spaces
|
| 15 |
+
RUN useradd -m -u 1000 user
|
| 16 |
+
USER user
|
| 17 |
+
|
| 18 |
+
# Set home to user's home dir and add local bin to PATH
|
| 19 |
+
ENV HOME=/home/user \
|
| 20 |
+
PATH=/user/user/.local/bin:$PATH
|
| 21 |
+
|
| 22 |
+
# Set the working directory in the container
|
| 23 |
+
WORKDIR $HOME/app
|
| 24 |
+
|
| 25 |
+
# Try and run pip command after setting the user with `USER user` to avoid permission issues with Python
|
| 26 |
+
# NOTE - this is from the HF Spaces docs, not sure if necessary
|
| 27 |
+
COPY --chown=user ./requirements.txt .
|
| 28 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 29 |
+
|
| 30 |
+
# Copy the current directory contents into the container at $HOME/app setting the owner to the user
|
| 31 |
+
COPY --chown=user . $HOME/app
|
| 32 |
|
| 33 |
# Install any needed packages specified in requirements.txt
|
| 34 |
# --no-cache-dir: Disables the cache to reduce image size.
|
| 35 |
# -r requirements.txt: Specifies the file containing the list of packages to install.
|
| 36 |
+
# RUN pip install --no-cache-dir -r requirements.txt
|
| 37 |
+
|
| 38 |
+
# Create the necessary dirs
|
| 39 |
+
# we should not need to chown, since we are using USER user above
|
| 40 |
+
RUN mkdir -p uploads results annotated .yolo_config
|
| 41 |
|
| 42 |
+
# set the env var for YOLO user config directory
|
| 43 |
+
ENV YOLO_CONFIG_DIR=.yolo_config
|
|
|
|
| 44 |
|
| 45 |
# Copy the rest of the application code into the container at /app
|
| 46 |
# This includes app.py, nemaquant.py, templates/, static/, etc.
|
| 47 |
+
# COPY . .
|
| 48 |
|
| 49 |
# Make port 7860 available to the world outside this container
|
| 50 |
# This is the port Flask will run on (as configured in app.py)
|
|
|
|
| 58 |
# Use gunicorn for production deployment if preferred over Flask's development server
|
| 59 |
# CMD ["gunicorn", "--bind", "0.0.0.0:7860", "app:app"]
|
| 60 |
# For simplicity during development and typical HF Spaces use:
|
| 61 |
+
CMD ["python", "app.py"]
|
app.py
CHANGED
|
@@ -9,8 +9,9 @@ import cv2
|
|
| 9 |
import csv
|
| 10 |
import pickle
|
| 11 |
import shutil
|
|
|
|
| 12 |
from ultralytics import YOLO
|
| 13 |
-
from ultralytics.utils import ThreadingLocked
|
| 14 |
import numpy as np
|
| 15 |
import pandas as pd
|
| 16 |
from torch import cuda
|
|
@@ -23,11 +24,14 @@ from datetime import datetime
|
|
| 23 |
from werkzeug.utils import secure_filename
|
| 24 |
from yolo_utils import detect_in_image
|
| 25 |
|
| 26 |
-
set_start_method('spawn')
|
| 27 |
-
|
| 28 |
app = Flask(__name__)
|
| 29 |
app.secret_key = os.environ.get('FLASK_SECRET_KEY', str(uuid.uuid4())) # For session security
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
APP_ROOT = Path(__file__).parent
|
| 32 |
UPLOAD_FOLDER = APP_ROOT / 'uploads'
|
| 33 |
RESULTS_FOLDER = APP_ROOT / 'results'
|
|
@@ -44,6 +48,9 @@ app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'tif', 'tiff'}
|
|
| 44 |
# RESULTS_FOLDER.mkdir(parents=True, exist_ok=True)
|
| 45 |
# ANNOT_FOLDER.mkdir(parents=True, exist_ok=True)
|
| 46 |
|
|
|
|
|
|
|
|
|
|
| 47 |
# need a global dict to hold async results objects
|
| 48 |
# so you can check the progress of an abr
|
| 49 |
# maybe there's a better way around this?
|
|
@@ -62,9 +69,6 @@ def handle_exception(e):
|
|
| 62 |
def index():
|
| 63 |
return render_template('index.html')
|
| 64 |
|
| 65 |
-
# Load model once at startup, use CUDA if available
|
| 66 |
-
MODEL_DEVICE = 'cuda' if cuda.is_available() else 'cpu'
|
| 67 |
-
|
| 68 |
# save the uploaded files
|
| 69 |
@app.route('/uploads', methods=['POST'])
|
| 70 |
def upload_files():
|
|
@@ -389,6 +393,15 @@ def export_csv():
|
|
| 389 |
print(error_message)
|
| 390 |
return jsonify({"error": "Server error", "log": error_message}), 500
|
| 391 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
|
| 393 |
def print_startup_info():
|
| 394 |
print("----- NemaQuant Flask App Starting -----")
|
|
@@ -406,7 +419,7 @@ def print_startup_info():
|
|
| 406 |
|
| 407 |
is_container = Path('/.dockerenv').exists() or 'DOCKER_HOST' in os.environ
|
| 408 |
print(f"Running in container: {is_container}")
|
| 409 |
-
|
| 410 |
if is_container:
|
| 411 |
try:
|
| 412 |
user_info = f"{os.getuid()}:{os.getgid()}"
|
|
@@ -425,15 +438,10 @@ def print_startup_info():
|
|
| 425 |
else:
|
| 426 |
print(f"Directory {path_str} does not exist.")
|
| 427 |
|
| 428 |
-
|
| 429 |
-
@app.before_request
|
| 430 |
-
def startup_routine():
|
| 431 |
-
# Ensure session id exists
|
| 432 |
-
if 'id' not in session:
|
| 433 |
-
session['id'] = str(uuid.uuid4())
|
| 434 |
print('Running periodic cleanup of old sessions...')
|
| 435 |
# Cleanup old session folders
|
| 436 |
-
max_age_hours =
|
| 437 |
now = time.time()
|
| 438 |
for base_dir in [UPLOAD_FOLDER, RESULTS_FOLDER, ANNOT_FOLDER]:
|
| 439 |
for session_dir in Path(base_dir).iterdir():
|
|
@@ -441,6 +449,11 @@ def startup_routine():
|
|
| 441 |
mtime = session_dir.stat().st_mtime
|
| 442 |
if now - mtime > max_age_hours * 3600:
|
| 443 |
shutil.rmtree(session_dir)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
|
| 445 |
if __name__ == '__main__':
|
| 446 |
print_startup_info()
|
|
|
|
| 9 |
import csv
|
| 10 |
import pickle
|
| 11 |
import shutil
|
| 12 |
+
import logging
|
| 13 |
from ultralytics import YOLO
|
| 14 |
+
# from ultralytics.utils import ThreadingLocked
|
| 15 |
import numpy as np
|
| 16 |
import pandas as pd
|
| 17 |
from torch import cuda
|
|
|
|
| 24 |
from werkzeug.utils import secure_filename
|
| 25 |
from yolo_utils import detect_in_image
|
| 26 |
|
|
|
|
|
|
|
| 27 |
app = Flask(__name__)
|
| 28 |
app.secret_key = os.environ.get('FLASK_SECRET_KEY', str(uuid.uuid4())) # For session security
|
| 29 |
|
| 30 |
+
# disable werkzeug logging - too noisy
|
| 31 |
+
# comment out these lines if you want to see full logs
|
| 32 |
+
log = logging.getLogger('werkzeug')
|
| 33 |
+
log.setLevel(logging.ERROR)
|
| 34 |
+
|
| 35 |
APP_ROOT = Path(__file__).parent
|
| 36 |
UPLOAD_FOLDER = APP_ROOT / 'uploads'
|
| 37 |
RESULTS_FOLDER = APP_ROOT / 'results'
|
|
|
|
| 48 |
# RESULTS_FOLDER.mkdir(parents=True, exist_ok=True)
|
| 49 |
# ANNOT_FOLDER.mkdir(parents=True, exist_ok=True)
|
| 50 |
|
| 51 |
+
# Load model once at startup, use CUDA if available
|
| 52 |
+
MODEL_DEVICE = 'cuda' if cuda.is_available() else 'cpu'
|
| 53 |
+
|
| 54 |
# need a global dict to hold async results objects
|
| 55 |
# so you can check the progress of an abr
|
| 56 |
# maybe there's a better way around this?
|
|
|
|
| 69 |
def index():
|
| 70 |
return render_template('index.html')
|
| 71 |
|
|
|
|
|
|
|
|
|
|
| 72 |
# save the uploaded files
|
| 73 |
@app.route('/uploads', methods=['POST'])
|
| 74 |
def upload_files():
|
|
|
|
| 393 |
print(error_message)
|
| 394 |
return jsonify({"error": "Server error", "log": error_message}), 500
|
| 395 |
|
| 396 |
+
@app.before_request
|
| 397 |
+
def ensure_session():
|
| 398 |
+
if 'id' not in session:
|
| 399 |
+
session['id'] = uuid.uuid4().hex
|
| 400 |
+
print(f"New session started: {session['id']}")
|
| 401 |
+
else:
|
| 402 |
+
pass
|
| 403 |
+
# print(f"Existing session: {session['id']}")
|
| 404 |
+
|
| 405 |
|
| 406 |
def print_startup_info():
|
| 407 |
print("----- NemaQuant Flask App Starting -----")
|
|
|
|
| 419 |
|
| 420 |
is_container = Path('/.dockerenv').exists() or 'DOCKER_HOST' in os.environ
|
| 421 |
print(f"Running in container: {is_container}")
|
| 422 |
+
|
| 423 |
if is_container:
|
| 424 |
try:
|
| 425 |
user_info = f"{os.getuid()}:{os.getgid()}"
|
|
|
|
| 438 |
else:
|
| 439 |
print(f"Directory {path_str} does not exist.")
|
| 440 |
|
| 441 |
+
# some cleanup steps - not sure quite where to put these
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 442 |
print('Running periodic cleanup of old sessions...')
|
| 443 |
# Cleanup old session folders
|
| 444 |
+
max_age_hours = 4
|
| 445 |
now = time.time()
|
| 446 |
for base_dir in [UPLOAD_FOLDER, RESULTS_FOLDER, ANNOT_FOLDER]:
|
| 447 |
for session_dir in Path(base_dir).iterdir():
|
|
|
|
| 449 |
mtime = session_dir.stat().st_mtime
|
| 450 |
if now - mtime > max_age_hours * 3600:
|
| 451 |
shutil.rmtree(session_dir)
|
| 452 |
+
|
| 453 |
+
print('App is running at the following local addresses:',
|
| 454 |
+
'http://127.0.0.1:7860',
|
| 455 |
+
'http://localhost:7860',
|
| 456 |
+
sep='\n')
|
| 457 |
|
| 458 |
if __name__ == '__main__':
|
| 459 |
print_startup_info()
|
requirements.txt
CHANGED
|
@@ -5,5 +5,6 @@ pandas==2.3.1
|
|
| 5 |
Pillow==11.3.0
|
| 6 |
torch==2.7.1
|
| 7 |
ultralytics==8.3.170
|
|
|
|
| 8 |
Werkzeug==3.1.3
|
| 9 |
gunicorn==21.2.0
|
|
|
|
| 5 |
Pillow==11.3.0
|
| 6 |
torch==2.7.1
|
| 7 |
ultralytics==8.3.170
|
| 8 |
+
watchdog==6.0.0
|
| 9 |
Werkzeug==3.1.3
|
| 10 |
gunicorn==21.2.0
|