Download app/main.py from PhonePixelGhost/Image_Classification_Service: direct link, hf CLI and curl.
- Browser
- Download file 6.39 kB
-
https://huggingface.co/spaces/PhonePixelGhost/Image_Classification_Service/resolve/main/app/main.py
- Command line
-
hf download hf://spaces/PhonePixelGhost/Image_Classification_Service/app/main.py
-
curl -L -o main.py https://huggingface.co/spaces/PhonePixelGhost/Image_Classification_Service/resolve/main/app/main.py
6.39 kB
| from fastapi import FastAPI, File, UploadFile, HTTPException | |
| from fastapi.responses import HTMLResponse | |
| from concurrent.futures import ProcessPoolExecutor | |
| import asyncio | |
| from app.model import run_inference | |
| from app.schemas import PredictionResponse | |
| from PIL import UnidentifiedImageError | |
| app = FastAPI(title="ResNet-18 Image Classifier", version="1.0.0") | |
| executor = ProcessPoolExecutor(max_workers=4) | |
| MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB | |
| ALLOWED_CONTENT_TYPES = {"image/jpeg", "image/png", "image/webp", "image/gif"} | |
| async def demo_ui(): | |
| # ... (HTML UI code remains the same) | |
| return """ | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>ResNet-18 Image Classifier</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet"> | |
| <style> | |
| body { font-family: 'Inter', sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; margin: 0; color: #333; } | |
| .card { background: white; padding: 2rem; border-radius: 1.5rem; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); width: 100%; max-width: 450px; text-align: center; } | |
| h1 { color: #1a202c; margin-bottom: 1.5rem; font-size: 1.5rem; } | |
| #preview { width: 100%; height: 250px; border: 2px dashed #e2e8f0; border-radius: 1rem; object-fit: cover; margin-bottom: 1.5rem; display: none; } | |
| .upload-btn-wrapper { position: relative; overflow: hidden; display: inline-block; width: 100%; } | |
| .btn { border: none; color: white; background: #667eea; padding: 0.75rem 2rem; border-radius: 0.75rem; font-weight: 600; cursor: pointer; transition: 0.3s; width: 100%; font-size: 1rem; } | |
| .btn:hover { background: #5a67d8; transform: translateY(-2px); } | |
| input[type=file] { font-size: 100px; position: absolute; left: 0; top: 0; opacity: 0; cursor: pointer; } | |
| #result { margin-top: 1.5rem; padding: 1rem; border-radius: 1rem; background: #f7fafc; display: none; text-align: left; } | |
| .label { font-weight: 600; color: #4a5568; } | |
| .value { color: #2d3748; float: right; } | |
| .loading { display: none; margin-top: 1rem; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="card"> | |
| <h1>ResNet-18 Quantized</h1> | |
| <img id="preview" src="#" alt="Preview"> | |
| <div class="upload-btn-wrapper"> | |
| <button class="btn" id="btn-text">Select Image to Predict</button> | |
| <input type="file" id="fileInput" accept="image/*"> | |
| </div> | |
| <div id="loading" class="loading">⌛ Predicting...</div> | |
| <div id="result"> | |
| <div><span class="label">Label:</span> <span class="value" id="res-label">-</span></div> | |
| <div style="margin-top:0.5rem"><span class="label">Confidence:</span> <span class="value" id="res-score">-</span></div> | |
| <div style="margin-top:0.5rem"><span class="label">Inference:</span> <span class="value" id="res-time">-</span></div> | |
| </div> | |
| </div> | |
| <script> | |
| const fileInput = document.getElementById('fileInput'); | |
| const preview = document.getElementById('preview'); | |
| const resultDiv = document.getElementById('result'); | |
| const loading = document.getElementById('loading'); | |
| const btnText = document.getElementById('btn-text'); | |
| fileInput.onchange = evt => { | |
| const [file] = fileInput.files; | |
| if (file) { | |
| preview.src = URL.createObjectURL(file); | |
| preview.style.display = 'block'; | |
| predict(file); | |
| } | |
| } | |
| async function predict(file) { | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| loading.style.display = 'block'; | |
| resultDiv.style.display = 'none'; | |
| btnText.disabled = true; | |
| try { | |
| const response = await fetch('/predict', { method: 'POST', body: formData }); | |
| const data = await response.json(); | |
| if (response.status !== 200) { | |
| alert(data.detail || 'Prediction failed'); | |
| return; | |
| } | |
| document.getElementById('res-label').innerText = data.label; | |
| document.getElementById('res-score').innerText = (data.score * 100).toFixed(2) + '%'; | |
| document.getElementById('res-time').innerText = data.inference_time_ms.toFixed(2) + ' ms'; | |
| resultDiv.style.display = 'block'; | |
| } catch (e) { | |
| alert('Error predicting image'); | |
| } finally { | |
| loading.style.display = 'none'; | |
| btnText.disabled = false; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| async def health(): | |
| return {"status": "ok"} | |
| async def predict(file: UploadFile = File(...)): | |
| # 1. ตรวจสอบ Content Type | |
| if file.content_type not in ALLOWED_CONTENT_TYPES: | |
| raise HTTPException(status_code=415, detail="Unsupported media type") | |
| # 2. อ่านข้อมูล | |
| image_bytes = await file.read() | |
| # 3. ตรวจสอบขนาดไฟล์ (Fix สำหรับ test_predict_rejects_oversized_file) | |
| if len(image_bytes) > MAX_FILE_SIZE: | |
| raise HTTPException(status_code=413, detail="File too large") | |
| # 4. รัน Inference และดักจับ Error (Fix สำหรับ test_predict_rejects_corrupted_file) | |
| loop = asyncio.get_event_loop() | |
| try: | |
| result = await loop.run_in_executor(executor, run_inference, image_bytes) | |
| return result | |
| except UnidentifiedImageError: | |
| raise HTTPException(status_code=400, detail="Invalid image file") | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Inference error: {str(e)}") | |