File size: 7,097 Bytes
b6de416
 
 
a930b94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7896889
 
b6de416
67a7d05
7896889
 
a930b94
 
 
 
 
 
 
 
 
 
 
b6de416
 
 
 
7896889
b6de416
 
 
 
 
a930b94
 
b6de416
7896889
a930b94
 
 
 
 
 
 
 
 
b6de416
 
a930b94
b6de416
2f89a36
a930b94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67a7d05
a930b94
67a7d05
a930b94
 
 
 
67a7d05
a930b94
 
 
67a7d05
2f89a36
67a7d05
a930b94
 
2f89a36
b6de416
7896889
a930b94
b6de416
 
67a7d05
a930b94
b6de416
a930b94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67a7d05
a930b94
 
 
 
 
 
 
 
 
 
67a7d05
a930b94
 
 
 
 
 
 
 
 
 
b6de416
67a7d05
a930b94
 
b6de416
a930b94
 
 
 
 
 
 
 
b6de416
 
a930b94
 
 
 
 
b6de416
a930b94
 
 
 
 
 
 
 
b6de416
 
 
 
a930b94
 
b6de416
 
 
67a7d05
b6de416
 
a930b94
 
 
 
 
 
 
 
 
b6de416
 
a930b94
b6de416
 
7896889
b6de416
 
ec10275
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
import uuid
from pathlib import Path
from datetime import datetime
from fastapi import FastAPI, UploadFile, File, HTTPException, Request
from fastapi.responses import FileResponse

from . import configs as config
from .models import UploadResponse, ClassifyRequest, ClassifyResponse, SegmentRequest, SegmentResponse
from .services.predictor import classify_image
from .services.segmenter import segment_image
from .storage import (
    save_image,
    get_image_path,
    get_image_bytes,
    save_classification_result,
    get_classification_result,
    save_segmentation_result,
    get_segmentation_result,
)
from .monitoring.metrics import MetricsMiddleware, metrics_endpoint

app = FastAPI(
    title="AI Image Classification API",
    version="3.2.0"
)

app.add_middleware(MetricsMiddleware)

ALLOWED_CONTENT_TYPES = {"image/jpeg", "image/png", "image/jpg"}
MAX_FILE_SIZE = 10 * 1024 * 1024


@app.get("/metrics")
async def prometheus_metrics(request: Request):
    return metrics_endpoint(request)


@app.get("/models/status")
async def models_status():
    classification_model = config.get_classification_model()
    segmentation_model = config.get_segmentation_model()

    return {
        "classification_loaded": classification_model is not None,
        "segmentation_loaded": segmentation_model is not None,
        "storage_paths": {
            "images": str(config.IMAGES_DIR),
            "segments": str(config.SEGMENTS_DIR),
        },
    }


@app.post("/api/upload", status_code=201, response_model=UploadResponse)
async def upload_image(file: UploadFile = File(...)):
    if file.content_type not in ALLOWED_CONTENT_TYPES:
        raise HTTPException(
            status_code=400,
            detail=f"Unsupported content type: {file.content_type}. Allowed: {ALLOWED_CONTENT_TYPES}",
        )

    file_bytes = await file.read()

    if len(file_bytes) > MAX_FILE_SIZE:
        raise HTTPException(status_code=400, detail="File too large (10MB max)")

    ext = Path(file.filename or "image.jpg").suffix or ".jpg"
    image_id = str(uuid.uuid4())

    save_image(image_id, file_bytes, ext)

    return UploadResponse(
        image_id=image_id,
        filename=file.filename or "image.jpg",
        size_bytes=len(file_bytes),
        content_type=file.content_type,
        uploaded_at=datetime.now().isoformat(),
        url=f"/api/images/{image_id}",
    )


@app.get("/api/images/{image_id}")
async def get_image(image_id: str):
    image_path = get_image_path(image_id)
    if image_path is None:
        raise HTTPException(status_code=404, detail="Image not found")

    media_type = "image/jpeg"
    if image_path.suffix.lower() in {".png"}:
        media_type = "image/png"

    return FileResponse(path=image_path, media_type=media_type)


@app.post("/api/classify", response_model=ClassifyResponse)
async def classify(body: ClassifyRequest):
    image_bytes = get_image_bytes(body.image_id)
    if image_bytes is None:
        raise HTTPException(status_code=404, detail="Image not found")

    # ✅ Check for cached result
    existing = get_classification_result(body.image_id)
    if existing:
        return ClassifyResponse(
            image_id=body.image_id,
            prediction=existing["prediction"],
            confidence=existing["confidence"],
            model_version=existing.get("model_version", "hf_savedmodel"),
            status=existing.get("status", "completed"),
        )

    # ✅ Classify without model_version
    try:
        prediction, confidence = classify_image(image_bytes)
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

    result = {
        "prediction": prediction,
        "confidence": confidence,
        "model_version": "hf_savedmodel",
        "status": "completed",
    }
    save_classification_result(body.image_id, result)

    return ClassifyResponse(
        image_id=body.image_id,
        **result,
    )


@app.get("/api/classify/{image_id}", response_model=ClassifyResponse)
async def get_classify_result(image_id: str):
    result = get_classification_result(image_id)
    if result is None:
        raise HTTPException(status_code=404, detail="Classification result not found")

    return ClassifyResponse(
        image_id=image_id,
        prediction=result["prediction"],
        confidence=result["confidence"],
        model_version=result.get("model_version", "hf_savedmodel"),
        status=result.get("status", "completed"),
    )


@app.post("/api/segment", response_model=SegmentResponse)
async def segment(body: SegmentRequest):
    image_bytes = get_image_bytes(body.image_id)
    if image_bytes is None:
        raise HTTPException(status_code=404, detail="Image not found")

    # ✅ Check for cached result
    existing = get_segmentation_result(body.image_id)
    if existing:
        return SegmentResponse(
            image_id=body.image_id,
            status=existing.get("status", "completed"),
            masks_shape=existing.get("masks_shape"),
            max_confidence=existing.get("max_confidence"),
            result_url=existing.get("result_path"),
            error=existing.get("error"),
        )

    # ✅ Run segmentation
    seg_result = segment_image(image_bytes)
    seg_path = save_segmentation_result(body.image_id, seg_result)

    return SegmentResponse(
        image_id=body.image_id,
        status=seg_result.get("status", "completed"),
        masks_shape=seg_result.get("masks_shape"),
        max_confidence=seg_result.get("max_confidence"),
        result_url=seg_path,
        error=seg_result.get("error"),
    )


@app.get("/api/segment/{image_id}", response_model=SegmentResponse)
async def get_segment_result(image_id: str):
    result = get_segmentation_result(image_id)
    if result is None:
        raise HTTPException(status_code=404, detail="Segmentation result not found")

    return SegmentResponse(
        image_id=image_id,
        status=result.get("status", "completed"),
        masks_shape=result.get("masks_shape"),
        max_confidence=result.get("max_confidence"),
        result_url=result.get("result_path"),
        error=result.get("error"),
    )


@app.get("/")
async def root():
    class_ready = "READY" if config.get_classification_model() else "REQUIRED"
    seg_ready = "READY" if config.get_segmentation_model() else "OPTIONAL"

    return {
        "service": "AI Image Classification API",
        "version": "3.2.0",
        "classification": class_ready,
        "segmentation": seg_ready,
        "endpoints": {
            "upload": "POST /api/upload",
            "classify": "POST /api/classify",
            "segment": "POST /api/segment",
            "health": "GET /health",
            "metrics": "GET /metrics",
            "docs": "/docs",
        },
        "models_status": "/models/status",
    }


@app.get("/health")
async def health():
    return {
        "status": "healthy",
        "predict_ready": config.get_classification_model() is not None,
    }