b3rian commited on
Commit
51e944e
·
verified ·
1 Parent(s): f2d133b

Upload folder using huggingface_hub

Browse files
.dockerignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python cache
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ .env
6
+ .git/
7
+ .gitignore
8
+ .venv/
9
+ venv/
10
+ *.egg-info/
11
+ *.swp
12
+ .DS_Store
Dockerfile ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use official Python slim image
2
+ FROM python:3.10-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies
8
+ RUN apt-get update && apt-get install -y \
9
+ curl \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Copy Python dependencies
13
+ COPY requirements.txt .
14
+
15
+ # Install Python dependencies
16
+ RUN pip install --no-cache-dir -r requirements.txt
17
+
18
+ # Create models directory and download models from HF
19
+ RUN mkdir -p /app/models && \
20
+ curl -fSL -o /app/models/efficientnet.keras https://huggingface.co/b3rian/resnet/resolve/main/efficientnet.keras && \
21
+ curl -fSL -o /app/models/resnet50_imagenet.keras https://huggingface.co/b3rian/resnet/resolve/main/resnet50_imagenet.keras
22
+
23
+ # Copy API code into the container
24
+ COPY api_backend/ ./api_backend/
25
+
26
+ # Expose the port the app runs on
27
+ EXPOSE 7860
28
+
29
+ # Set environment variables
30
+ ENV MODEL_DIR=/app/models
31
+
32
+ # Run the API
33
+ CMD ["uvicorn", "api_backend.backup:app", "--host", "0.0.0.0", "--port", "7860"]
api_backend/__init__.py ADDED
File without changes
api_backend/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (164 Bytes). View file
 
api_backend/__pycache__/api.cpython-312.pyc ADDED
Binary file (4.42 kB). View file
 
api_backend/__pycache__/backup.cpython-312.pyc ADDED
Binary file (12.7 kB). View file
 
api_backend/__pycache__/configs.cpython-312.pyc ADDED
Binary file (1.3 kB). View file
 
api_backend/__pycache__/endpoints.cpython-312.pyc ADDED
Binary file (12.7 kB). View file
 
api_backend/__pycache__/main.cpython-312.pyc ADDED
Binary file (1.58 kB). View file
 
api_backend/__pycache__/models.cpython-312.pyc ADDED
Binary file (3.44 kB). View file
 
api_backend/__pycache__/predictor.cpython-312.pyc ADDED
Binary file (8.78 kB). View file
 
api_backend/__pycache__/schemas.cpython-312.pyc ADDED
Binary file (1.64 kB). View file
 
api_backend/__pycache__/services.cpython-312.pyc ADDED
Binary file (2.18 kB). View file
 
api_backend/__pycache__/utils.cpython-312.pyc ADDED
Binary file (5.38 kB). View file
 
api_backend/backup.py ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile, HTTPException, Query, Request
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.middleware import Middleware
4
+ from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
5
+ from starlette.responses import JSONResponse
6
+ from pydantic import BaseModel, Field
7
+ from pydantic_settings import BaseSettings
8
+ from typing import List, Callable, Optional
9
+ from enum import Enum
10
+ import numpy as np
11
+ from PIL import Image
12
+ import os
13
+ import io
14
+ import time
15
+ import tensorflow as tf
16
+ import uvicorn
17
+ import logging
18
+ import asyncio
19
+ from concurrent.futures import ThreadPoolExecutor
20
+ from functools import lru_cache
21
+ import datetime
22
+
23
+ # =================== Configuration ===================
24
+ class Settings(BaseSettings):
25
+ models_dir: str = "models"
26
+ allowed_origins: list[str] = ["*"]
27
+ app_name: str = "Image Classifier API"
28
+ app_version: str = "1.1.0"
29
+ log_level: str = "INFO"
30
+ enable_https_redirect: bool = False
31
+
32
+ class Config:
33
+ env_file = ".env"
34
+
35
+ settings = Settings()
36
+
37
+ # =================== Logging Setup ===================
38
+ logging.basicConfig(
39
+ level=settings.log_level,
40
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
41
+ )
42
+ logger = logging.getLogger(__name__)
43
+
44
+ # =================== Model Registry ===================
45
+
46
+ MODEL_DIR = os.getenv("MODEL_DIR", "models")
47
+ resnet_model = os.path.join(MODEL_DIR, "resnet50_imagenet.keras")
48
+ efficientnet_model = os.path.join(MODEL_DIR, "efficientnet.keras")
49
+
50
+ MODEL_REGISTRY = {
51
+ "efficientnet": {
52
+ "path": efficientnet_model,
53
+ "preprocess": tf.keras.applications.efficientnet_v2.preprocess_input,
54
+ "decode": tf.keras.applications.efficientnet_v2.decode_predictions,
55
+ "input_size": (480, 480)
56
+ },
57
+ "resnet": {
58
+ "path": resnet_model,
59
+ "preprocess": tf.keras.applications.resnet50.preprocess_input,
60
+ "decode": tf.keras.applications.resnet50.decode_predictions,
61
+ "input_size": (224, 224)
62
+ }
63
+ }
64
+
65
+ # =================== Custom Exceptions ===================
66
+ class ModelNotFoundError(Exception):
67
+ pass
68
+
69
+ class InvalidImageError(Exception):
70
+ pass
71
+
72
+ # =================== Model Loading ===================
73
+ @lru_cache(maxsize=None)
74
+ def load_model(model_path: str, input_size: tuple) -> tf.keras.Model:
75
+ try:
76
+ model = tf.keras.models.load_model(model_path)
77
+ # Warm up the model
78
+ dummy_input = np.zeros((1, *input_size, 3))
79
+ _ = model.predict(dummy_input)
80
+ logger.info(f"Successfully loaded model from {model_path}")
81
+ return model
82
+ except Exception as e:
83
+ logger.error(f"Failed to load model from {model_path}: {str(e)}")
84
+ raise RuntimeError(f"Failed to load model from {model_path}: {str(e)}")
85
+
86
+ # Initialize models with error handling
87
+ models = {}
88
+ for name, config in MODEL_REGISTRY.items():
89
+ try:
90
+ models[name] = load_model(config["path"], config["input_size"])
91
+ except Exception as e:
92
+ logger.error(f"Could not load model {name}: {str(e)}")
93
+
94
+ # =================== FastAPI Setup ===================
95
+ middleware = [
96
+ Middleware(
97
+ CORSMiddleware,
98
+ allow_origins=settings.allowed_origins,
99
+ allow_methods=["*"],
100
+ allow_headers=["*"],
101
+ )
102
+ ]
103
+
104
+ if settings.enable_https_redirect:
105
+ middleware.append(Middleware(HTTPSRedirectMiddleware))
106
+
107
+ app = FastAPI(
108
+ title=settings.app_name,
109
+ description="FastAPI backend for AI Image Classifier with multiple Keras models",
110
+ version=settings.app_version,
111
+ contact={
112
+ "name": "Your Name",
113
+ "email": "your.email@example.com",
114
+ },
115
+ license_info={
116
+ "name": "MIT",
117
+ },
118
+ openapi_tags=[{
119
+ "name": "predictions",
120
+ "description": "Operations with image predictions",
121
+ }],
122
+ middleware=middleware
123
+ )
124
+
125
+ # =================== Request Logging Middleware ===================
126
+ @app.middleware("http")
127
+ async def log_requests(request: Request, call_next):
128
+ start_time = time.time()
129
+ response = await call_next(request)
130
+ process_time = (time.time() - start_time) * 1000
131
+ logger.info(
132
+ f"Request: {request.method} {request.url} completed in {process_time:.2f}ms"
133
+ )
134
+ return response
135
+
136
+ # =================== Error Handlers ===================
137
+ @app.exception_handler(ModelNotFoundError)
138
+ async def model_not_found_handler(request, exc):
139
+ return JSONResponse(
140
+ status_code=404,
141
+ content={"message": str(exc)},
142
+ )
143
+
144
+ @app.exception_handler(InvalidImageError)
145
+ async def invalid_image_handler(request, exc):
146
+ return JSONResponse(
147
+ status_code=400,
148
+ content={"message": str(exc)},
149
+ )
150
+
151
+ # =================== Response Schemas ===================
152
+ class Prediction(BaseModel):
153
+ label: str
154
+ confidence: float = Field(..., ge=0.0, le=100.0)
155
+
156
+ class ApiResponse(BaseModel):
157
+ predictions: List[Prediction]
158
+ model_version: str
159
+ inference_time: float
160
+ timestamp: str
161
+
162
+ class HealthCheckResponse(BaseModel):
163
+ status: str
164
+ models_loaded: List[str]
165
+ timestamp: str
166
+
167
+ # =================== Model Name Enum ===================
168
+ class ModelName(str, Enum):
169
+ efficientnet = "efficientnet"
170
+ resnet = "resnet"
171
+
172
+ # =================== Image Preprocessing ===================
173
+ def preprocess_image(
174
+ image_bytes: bytes,
175
+ target_size: tuple,
176
+ preprocess_func: Callable[[np.ndarray], np.ndarray]
177
+ ) -> np.ndarray:
178
+ try:
179
+ image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
180
+ image = image.resize(target_size)
181
+ image_array = np.array(image).astype("float32")
182
+ image_array = preprocess_func(image_array)
183
+ return np.expand_dims(image_array, axis=0)
184
+ except Exception as e:
185
+ logger.error(f"Image preprocessing failed: {str(e)}")
186
+ raise InvalidImageError(f"Invalid image file: {str(e)}")
187
+
188
+ # =================== Async Prediction ===================
189
+ executor = ThreadPoolExecutor(max_workers=4)
190
+
191
+ async def async_predict(model: tf.keras.Model, input_tensor: np.ndarray):
192
+ loop = asyncio.get_event_loop()
193
+ return await loop.run_in_executor(executor, model.predict, input_tensor)
194
+
195
+ # =================== Inference Endpoint ===================
196
+ @app.post("/predict", response_model=ApiResponse, tags=["predictions"])
197
+ async def predict(
198
+ request: Request,
199
+ file: UploadFile = File(...),
200
+ model_name: ModelName = Query(..., description="Choose model for inference")
201
+ ):
202
+ if model_name.value not in models:
203
+ logger.error(f"Model '{model_name}' not found in loaded models")
204
+ raise ModelNotFoundError(
205
+ f"Model '{model_name}' not available. Available options: {list(models.keys())}"
206
+ )
207
+
208
+ try:
209
+ model = models[model_name.value]
210
+ config = MODEL_REGISTRY[model_name.value]
211
+ contents = await file.read()
212
+
213
+ # Preprocess
214
+ input_tensor = preprocess_image(contents, config["input_size"], config["preprocess"])
215
+
216
+ # Inference
217
+ start = time.time()
218
+ predictions = await async_predict(model, input_tensor)
219
+ end = time.time()
220
+
221
+ # Decode predictions
222
+ decoded = config["decode"](predictions, top=3)[0]
223
+ results = [
224
+ {"label": label.replace("_", " "), "confidence": round(float(score * 100), 2)}
225
+ for (_, label, score) in decoded
226
+ ]
227
+
228
+ return {
229
+ "predictions": results,
230
+ "model_version": model_name.value,
231
+ "inference_time": round(end - start, 4),
232
+ "timestamp": datetime.datetime.utcnow().isoformat()
233
+ }
234
+ except InvalidImageError as e:
235
+ raise
236
+ except Exception as e:
237
+ logger.error(f"Inference error: {str(e)}", exc_info=True)
238
+ raise HTTPException(status_code=500, detail=f"Inference error: {str(e)}")
239
+
240
+ # =================== Health Check Endpoints ===================
241
+ @app.get("/", include_in_schema=False)
242
+ def root():
243
+ return {"message": "Image Classifier API is running."}
244
+
245
+ @app.get("/health", response_model=HealthCheckResponse, tags=["health"])
246
+ async def health_check():
247
+ return {
248
+ "status": "healthy",
249
+ "models_loaded": list(models.keys()),
250
+ "timestamp": datetime.datetime.utcnow().isoformat()
251
+ }
api_backend/configs.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings
2
+ import logging
3
+
4
+ class Settings(BaseSettings):
5
+ """Application configuration settings."""
6
+ models_dir: str = "models"
7
+ allowed_origins: list[str] = ["*"]
8
+ app_name: str = "Image Classifier API"
9
+ app_version: str = "1.1.0"
10
+ log_level: str = "INFO"
11
+ enable_https_redirect: bool = False
12
+
13
+ class Config:
14
+ env_file = ".env"
15
+
16
+ # Initialize settings and logging
17
+ settings = Settings()
18
+ logging.basicConfig(
19
+ level=settings.log_level,
20
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
21
+ )
22
+ logger = logging.getLogger(__name__)
api_backend/main.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile, HTTPException, Query, Request
2
+ from fastapi.middleware import Middleware
3
+ from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from starlette.responses import JSONResponse
6
+ import datetime
7
+ import time
8
+ from api_backend.configs import settings, logger
9
+ from api_backend.models import models, MODEL_REGISTRY, ModelNotFoundError, InvalidImageError
10
+ from api_backend.schemas import ApiResponse, HealthCheckResponse, ModelName
11
+ from api_backend.services import async_predict, preprocess_image
12
+
13
+ # Setup middleware
14
+ middleware = [
15
+ Middleware(
16
+ CORSMiddleware,
17
+ allow_origins=settings.allowed_origins,
18
+ allow_methods=["*"],
19
+ allow_headers=["*"],
20
+ )
21
+ ]
22
+
23
+ if settings.enable_https_redirect:
24
+ middleware.append(Middleware(HTTPSRedirectMiddleware))
25
+
26
+ # Create FastAPI app
27
+ app = FastAPI(
28
+ title=settings.app_name,
29
+ description="FastAPI backend for AI Image Classifier with multiple Keras models",
30
+ version=settings.app_version,
31
+ contact={
32
+ "name": "Brian",
33
+ "email": "brayann.8189@gmail.com",
34
+ },
35
+ license_info={
36
+ "name": "MIT",
37
+ },
38
+ openapi_tags=[{
39
+ "name": "predictions",
40
+ "description": "Operations with image predictions",
41
+ }],
42
+ middleware=middleware
43
+ )
44
+
45
+ # Middleware
46
+ @app.middleware("http")
47
+ async def log_requests(request: Request, call_next):
48
+ """Middleware to log request processing time."""
49
+ start_time = time.time()
50
+ response = await call_next(request)
51
+ process_time = (time.time() - start_time) * 1000
52
+ logger.info(
53
+ f"Request: {request.method} {request.url} completed in {process_time:.2f}ms"
54
+ )
55
+ return response
56
+
57
+ # Exception Handlers
58
+ @app.exception_handler(ModelNotFoundError)
59
+ async def model_not_found_handler(request, exc):
60
+ return JSONResponse(
61
+ status_code=404,
62
+ content={"message": str(exc)},
63
+ )
64
+
65
+ @app.exception_handler(InvalidImageError)
66
+ async def invalid_image_handler(request, exc):
67
+ return JSONResponse(
68
+ status_code=400,
69
+ content={"message": str(exc)},
70
+ )
71
+
72
+ # Endpoints
73
+ @app.post("/predict", response_model=ApiResponse, tags=["predictions"])
74
+ async def predict(
75
+ request: Request,
76
+ file: UploadFile = File(...),
77
+ model_name: ModelName = Query(..., description="Choose model for inference")
78
+ ):
79
+ if model_name.value not in models:
80
+ logger.error(f"Model '{model_name}' not found in loaded models")
81
+ raise ModelNotFoundError(
82
+ f"Model '{model_name}' not available. Available options: {list(models.keys())}"
83
+ )
84
+
85
+ try:
86
+ model = models[model_name.value]
87
+ config = MODEL_REGISTRY[model_name.value]
88
+ contents = await file.read()
89
+
90
+ # Preprocess
91
+ input_tensor = preprocess_image(contents, config["input_size"], config["preprocess"])
92
+
93
+ # Inference
94
+ start = time.time()
95
+ predictions = await async_predict(model, input_tensor)
96
+ end = time.time()
97
+
98
+ # Decode predictions
99
+ decoded = config["decode"](predictions, top=3)[0]
100
+ results = [
101
+ {"label": label.replace("_", " "), "confidence": round(float(score * 100), 2)}
102
+ for (_, label, score) in decoded
103
+ ]
104
+
105
+ return {
106
+ "predictions": results,
107
+ "model_version": model_name.value,
108
+ "inference_time": round(end - start, 4),
109
+ "timestamp": datetime.datetime.utcnow().isoformat()
110
+ }
111
+ except InvalidImageError as e:
112
+ raise
113
+ except Exception as e:
114
+ logger.error(f"Inference error: {str(e)}", exc_info=True)
115
+ raise HTTPException(status_code=500, detail=f"Inference error: {str(e)}")
116
+
117
+ @app.get("/", include_in_schema=False)
118
+ def root():
119
+ return {"message": "Image Classifier API is running."}
120
+
121
+ @app.get("/health", response_model=HealthCheckResponse, tags=["health"])
122
+ async def health_check():
123
+ return {
124
+ "status": "healthy",
125
+ "models_loaded": list(models.keys()),
126
+ "timestamp": datetime.datetime.utcnow().isoformat()
127
+ }
api_backend/models.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from functools import lru_cache
2
+ import numpy as np
3
+ import tensorflow as tf
4
+ from api_backend.configs import logger
5
+ import os
6
+
7
+ # Get the base directory of the current file
8
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
9
+ MODEL_DIR = os.path.join(BASE_DIR, "models")
10
+
11
+ # Model Registry
12
+ MODEL_REGISTRY = {
13
+ "efficientnet": {
14
+ "path": os.path.join(MODEL_DIR, "efficientnet.keras"),
15
+ "preprocess": tf.keras.applications.efficientnet_v2.preprocess_input,
16
+ "decode": tf.keras.applications.efficientnet_v2.decode_predictions,
17
+ "input_size": (480, 480)
18
+ },
19
+ "resnet": {
20
+ "path": os.path.join(MODEL_DIR, "resnet50_imagenet.keras"),
21
+ "preprocess": tf.keras.applications.resnet50.preprocess_input,
22
+ "decode": tf.keras.applications.resnet50.decode_predictions,
23
+ "input_size": (224, 224)
24
+ }
25
+ }
26
+
27
+ # Exceptions
28
+ class ModelNotFoundError(Exception):
29
+ """Exception raised when a requested model is not found."""
30
+ pass
31
+
32
+ class InvalidImageError(Exception):
33
+ """Exception raised when image processing fails."""
34
+ pass
35
+
36
+ # Model Loading
37
+ @lru_cache(maxsize=None)
38
+ def load_model(model_path: str, input_size: tuple) -> tf.keras.Model:
39
+ """Load and warm up a TensorFlow model with caching."""
40
+ try:
41
+ model = tf.keras.models.load_model(model_path)
42
+ # Warm up the model
43
+ dummy_input = np.zeros((1, *input_size, 3))
44
+ _ = model.predict(dummy_input)
45
+ logger.info(f"Successfully loaded model from {model_path}")
46
+ return model
47
+ except Exception as e:
48
+ logger.error(f"Failed to load model from {model_path}: {str(e)}")
49
+ raise RuntimeError(f"Failed to load model from {model_path}: {str(e)}")
50
+
51
+ # Initialize models
52
+ models = {}
53
+ for name, config in MODEL_REGISTRY.items():
54
+ try:
55
+ models[name] = load_model(config["path"], config["input_size"])
56
+ except Exception as e:
57
+ logger.error(f"Could not load model {name}: {str(e)}")
api_backend/schemas.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from enum import Enum
3
+ from typing import List
4
+ import datetime
5
+
6
+ # Response Models
7
+ class Prediction(BaseModel):
8
+ """Single prediction result schema."""
9
+ label: str
10
+ confidence: float = Field(..., ge=0.0, le=100.0)
11
+
12
+ class ApiResponse(BaseModel):
13
+ """API response schema for prediction endpoint."""
14
+ predictions: List[Prediction]
15
+ model_version: str
16
+ inference_time: float
17
+ timestamp: str
18
+
19
+ class HealthCheckResponse(BaseModel):
20
+ """Health check response schema."""
21
+ status: str
22
+ models_loaded: List[str]
23
+ timestamp: str
24
+
25
+ # Enums
26
+ class ModelName(str, Enum):
27
+ """Supported model names enumeration."""
28
+ efficientnet = "efficientnet"
29
+ resnet = "resnet"
api_backend/services.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from concurrent.futures import ThreadPoolExecutor
3
+ import numpy as np
4
+ from PIL import Image
5
+ from typing import Callable
6
+ import io
7
+ from api_backend.configs import logger
8
+ from api_backend.models import InvalidImageError
9
+
10
+ executor = ThreadPoolExecutor(max_workers=4)
11
+
12
+ async def async_predict(model, input_tensor):
13
+ """Run model prediction in a separate thread."""
14
+ loop = asyncio.get_event_loop()
15
+ return await loop.run_in_executor(executor, model.predict, input_tensor)
16
+
17
+ def preprocess_image(
18
+ image_bytes: bytes,
19
+ target_size: tuple,
20
+ preprocess_func: Callable[[np.ndarray], np.ndarray]
21
+ ) -> np.ndarray:
22
+ """Preprocess image bytes into model input tensor."""
23
+ try:
24
+ image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
25
+ image = image.resize(target_size)
26
+ image_array = np.array(image).astype("float32")
27
+ image_array = preprocess_func(image_array)
28
+ return np.expand_dims(image_array, axis=0)
29
+ except Exception as e:
30
+ logger.error(f"Image preprocessing failed: {str(e)}")
31
+ raise InvalidImageError(f"Invalid image file: {str(e)}")
deploy.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from huggingface_hub import HfApi, upload_folder
2
+ from pathlib import Path
3
+
4
+ # Set your repo info
5
+ username = "b3rian"
6
+ repo_name = "image-classifier-api"
7
+ local_dir = Path(__file__).resolve().parent # Automatically detect current folder
8
+ repo_type = "space"
9
+ space_sdk = "docker"
10
+
11
+ # 1. Create the space (skip if already created)
12
+ api = HfApi()
13
+ api.create_repo(
14
+ repo_id=f"{username}/{repo_name}",
15
+ repo_type=repo_type,
16
+ space_sdk=space_sdk,
17
+ exist_ok=True # Don't fail if it already exists
18
+ )
19
+
20
+ # 2. Upload the entire folder to the space
21
+ upload_folder(
22
+ repo_id=f"{username}/{repo_name}",
23
+ folder_path=local_dir,
24
+ repo_type=repo_type
25
+ )
26
+
27
+ print(f"✅ Deployed to https://huggingface.co/spaces/{username}/{repo_name}")
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.68.0
2
+ uvicorn>=0.15.0
3
+ tensorflow>=2.6.0
4
+ pillow>=8.3.1
5
+ numpy>=1.21.0
6
+ python-multipart>=0.0.5
7
+ pydantic-settings>=2.0.0
8
+ transformers
9
+ huggingface-hub
10
+ git-lfs
11
+ hf_xet