Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HW9 PR3 #33

Merged
merged 7 commits into from
Nov 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/test_fastapi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Run FastApi Tests

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.9'

- name: Set up AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt # Ensure you have this file in the specified directory
pip install awscli httpx python-multipart
working-directory: serving/fastapi_server

- name: Run AWS S3 Copy Command
run: |
aws s3 cp s3://mlp-data-2024/rtdetr_model/ ./rtdetr_model --recursive
working-directory: serving/gradio_server

- name: Run tests
run: |
python -m pytest tests # This will run all tests in the specified directory
working-directory: serving/fastapi_server
46 changes: 46 additions & 0 deletions .github/workflows/test_gradio.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Run FastApi Tests

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.9'

- name: Set up AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt # Ensure you have this file in the specified directory
pip install awscli
working-directory: serving/gradio_server

- name: Run AWS S3 Copy Command
run: |
aws s3 cp s3://mlp-data-2024/rtdetr_model/ ./rtdetr_model --recursive
working-directory: serving/gradio_server

- name: Run tests
run: |
python -m pytest tests # This will run all tests in the specified directory
working-directory: serving/gradio_server
45 changes: 45 additions & 0 deletions .github/workflows/test_streamlit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Run FastApi Tests

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.9'

- name: Set up AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt # Ensure you have this file in the specified directory
pip install awscli
working-directory: serving/streamlit_server

- name: Run AWS S3 Copy Command
run: |
aws s3 cp s3://mlp-data-2024/rtdetr_model/ ./rtdetr_model --recursive

- name: Run tests
run: |
python -m pytest tests # This will run all tests in the specified directory
working-directory: serving/streamlit_server
68 changes: 68 additions & 0 deletions serving/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Streamlit


## Local Deployment
```bash
streamlit run streamlit_ui.py -- --model_path path/to/model
```


## Build Container
```bash
docker build \
--build-arg AWS_ACCESS_KEY_ID=key \
--build-arg AWS_SECRET_ACCESS_KEY="secret_key" \
-t streamlit_app:latest .
```

Run:
```bash
docker run -it --rm -p 8501:8501 streamlit_app:latest
```


# Gradio

## Local Deployment
```bash
python gradio_ui.py --model_path path/to/model
```

## Build Container
```bash
docker build \
--build-arg AWS_ACCESS_KEY_ID=key \
--build-arg AWS_SECRET_ACCESS_KEY="secret_key" \
-t gradio_app:latest .
```

Run:
```bash
docker run -it --rm -p 7860:7860 gradio_app:latest
```


# FastAPI
## Local Deployment
```bash
pip install -r requirements.txt
python fastapi_server.py
```

## Build Container
```bash
docker build \
--build-arg AWS_ACCESS_KEY_ID=key \
--build-arg AWS_SECRET_ACCESS_KEY="secret_key" \
-t fastapi_app:latest .
```

Run:
```bash
docker run -it --rm -p 8000:8000 fastapi_app:latest
```

How to make a request:
```bash
curl -X POST "http://localhost:8000/predict/" -F "image=@/path/to/image.jpg" -F "threshold=0.5"
```
22 changes: 22 additions & 0 deletions serving/fastapi_server/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt requirements.txt

RUN pip install -r requirements.txt
RUN pip install awscli
RUN pip install python-multipart

COPY . /app

EXPOSE 8000

ARG AWS_SECRET_ACCESS_KEY
ARG AWS_ACCESS_KEY_ID
ENV AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
ENV AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}

RUN aws s3 cp s3://mlp-data-2024/rtdetr_model/ ./rtdetr_model --recursive

CMD ["python", "fastapi_server.py"]
77 changes: 77 additions & 0 deletions serving/fastapi_server/fastapi_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import os
from fastapi import FastAPI, UploadFile, File
from fastapi.responses import JSONResponse
from PIL import Image
import torch
from transformers import AutoModelForObjectDetection, AutoImageProcessor
from io import BytesIO
from http import HTTPStatus
from typing import Dict

from fastapi import HTTPException
from PIL import UnidentifiedImageError

device = torch.device("mps" if torch.backends.mps.is_available() else "cpu")

app = FastAPI()


model_path = "./rtdetr_model"

try:
model = AutoModelForObjectDetection.from_pretrained(model_path).to(device).eval()
processor = AutoImageProcessor.from_pretrained(model_path)
model_loaded = True
except Exception as e:
model_loaded = False
print(f"Error loading model: {e}")

MODEL_LABEL_MAPPING = {0: "person", 1: "car", 2: "pet"}

@app.get("/")
def _index() -> Dict:
"""Health check."""
response = {
"message": HTTPStatus.OK.phrase,
"status_code": HTTPStatus.OK,
"data": {"model_loaded": model_loaded},
}
return response


def predict(image: Image.Image, threshold: float):
inputs = processor(images=image, return_tensors="pt")
inputs = {k: v.to(device) for k, v in inputs.items()}

outputs = model(**inputs)

target_sizes = torch.tensor([image.size[::-1]]) # target size in (height, width)
results = processor.post_process_object_detection(outputs, target_sizes=target_sizes, threshold=threshold)[0]
results = {k: v.detach().cpu() for k, v in results.items()}

return results

@app.post("/predict/")
async def inference(image: UploadFile = File(...), threshold: float = 0.5):
try:
image_data = await image.read()
image = Image.open(BytesIO(image_data))
except UnidentifiedImageError:
raise HTTPException(status_code=400, detail="Invalid image file")

results = predict(image, threshold)

output_data = []
for score, label, box in zip(results["scores"], results["labels"], results["boxes"]):
output_data.append({
"class": MODEL_LABEL_MAPPING[label.item()],
"score": score.item(),
"box": [box[0].item(), box[1].item(), box[2].item(), box[3].item()]
})

return JSONResponse(content={"predictions": output_data})


if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
18 changes: 18 additions & 0 deletions serving/fastapi_server/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
fastapi==0.115.0
uvicorn===0.31.0
torch==2.4.0
torchvision==0.19.0
transformers==4.44.2
supervision==0.22.0
huggingface==0.0.1
accelerate==0.33.0
torchmetrics==1.4.1
albumentations==1.4.14
pillow==10.4.0
datasets==2.21.0
PyYAML==6.0.2
wandb==0.17.7
pytest==8.3.3
pycocotools==2.0.8
python-dotenv==1.0.1
# boto3==1.34.158
54 changes: 54 additions & 0 deletions serving/fastapi_server/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import os
import pytest
from fastapi.testclient import TestClient
from PIL import Image
import io
from fastapi_server import app # Adjust if your FastAPI app is in a different file

client = TestClient(app)

@pytest.fixture
def test_image():
# Create a simple 100x100 red image for testing
image = Image.new("RGB", (100, 100), color="red")
img_byte_arr = io.BytesIO()
image.save(img_byte_arr, format='JPEG')
img_byte_arr.seek(0)
return img_byte_arr

def test_index():
"""Test the health check endpoint."""
response = client.get("/")
assert response.status_code == 200
data = response.json()
assert data["status_code"] == 200
assert data["message"] == "OK"
assert "model_loaded" in data["data"]

@pytest.mark.skipif(os.environ.get("MODEL_AVAILABLE") != "1", reason="Model not available")
def test_predict(test_image):
"""Test the prediction endpoint with an example image."""
# Simulate sending the image as form data
files = {'image': ('test_image.jpg', test_image, 'image/jpeg')}
response = client.post("/predict/", files=files, data={"threshold": "0.5"})

assert response.status_code == 200
data = response.json()

# Ensure the response contains the predictions
assert "predictions" in data
for prediction in data["predictions"]:
assert "class" in prediction
assert "score" in prediction
assert "box" in prediction
assert len(prediction["box"]) == 4 # Ensure the box has 4 coordinates


def test_predict_invalid_file():
"""Test prediction with invalid file input."""
files = {'image': ('test_image.txt', io.BytesIO(b"not an image"), 'text/plain')}
response = client.post("/predict/", files=files, data={"threshold": "0.5"})

# Expecting a 400 Bad Request for invalid image input
assert response.status_code == 400
assert response.json()["detail"] == "Invalid image file"
Loading
Loading