danielle2035 commited on
Commit
01ba07a
·
0 Parent(s):

Initial clean Space deployment

Browse files
Files changed (6) hide show
  1. .gitignore +6 -0
  2. Dockerfile +11 -0
  3. README.md +37 -0
  4. app.py +189 -0
  5. index.html +115 -0
  6. requirements.txt +9 -0
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ .ipynb_checkpoints/
6
+ .netrc
Dockerfile ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt ./
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . ./
9
+
10
+ EXPOSE 7860
11
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Minimal Hugging Face Space for Intel Image Classifier
2
+
3
+ This folder contains a minimal Hugging Face Space app using a custom `index.html` frontend and a Python backend.
4
+
5
+ ## Files
6
+ - `app.py` — FastAPI backend for prediction and frontend delivery
7
+ - `index.html` — frontend UI for uploading images and selecting the model
8
+ - `requirements.txt` — dependencies
9
+ - `models/` — place your trained models here
10
+
11
+ ## Model files required
12
+ Place your trained models in `hf_space/models/`:
13
+
14
+ - `pytorch_model.pth`
15
+ - `model_best.keras`
16
+
17
+ ## Deploying on Hugging Face Spaces
18
+ 1. Create a new Space on Hugging Face
19
+ 2. Select `Python` SDK
20
+ 3. Push the contents of this `hf_space/` directory to the new Space repository
21
+
22
+ The Space will run `app.py` automatically and serve `index.html` as the frontend.
23
+
24
+ ## Docker support
25
+ A `Dockerfile` is included so you can also build and run the app locally in a container.
26
+
27
+ ## Local testing
28
+ From `hf_space/`:
29
+
30
+ ```bash
31
+ pip install -r requirements.txt
32
+ python app.py
33
+ ```
34
+
35
+ Then open:
36
+
37
+ - `http://localhost:7860`
app.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import os
3
+ from pathlib import Path
4
+ from typing import List
5
+
6
+ import numpy as np
7
+ import torch
8
+ import torch.nn as nn
9
+ from torchvision import transforms
10
+ from PIL import Image
11
+ from fastapi import FastAPI, File, Form, HTTPException, UploadFile
12
+ from fastapi.responses import FileResponse, JSONResponse
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from huggingface_hub import hf_hub_download
15
+ import uvicorn
16
+
17
+ BASE_DIR = Path(__file__).resolve().parent
18
+ MODEL_DIR = BASE_DIR / "models"
19
+ PYTORCH_PATH = MODEL_DIR / "pytorch_model.pth"
20
+ TENSORFLOW_PATH = MODEL_DIR / "model_best.keras"
21
+ MODEL_REPO = "danielle2035/intel-classifier-models"
22
+ HF_TOKEN = os.environ.get("HF_TOKEN")
23
+
24
+ CLASSES = ["buildings", "forest", "glacier", "mountain", "sea", "street"]
25
+ CONFIDENCE_THRESHOLD = 0.6
26
+
27
+ app = FastAPI(title="Intel Image Classifier")
28
+ app.add_middleware(
29
+ CORSMiddleware,
30
+ allow_origins=["*"],
31
+ allow_credentials=True,
32
+ allow_methods=["*"],
33
+ allow_headers=["*"],
34
+ )
35
+
36
+
37
+ class CNN(nn.Module):
38
+ def __init__(self, num_classes=6):
39
+ super().__init__()
40
+ self.block1 = self._block(3, 32)
41
+ self.block2 = self._block(32, 64)
42
+ self.block3 = self._block(64, 128)
43
+ self.block4 = self._block(128, 256)
44
+ self.gap = nn.AdaptiveAvgPool2d(1)
45
+ self.fc1 = nn.Linear(256, 128)
46
+ self.fc2 = nn.Linear(128, num_classes)
47
+ self.dropout = nn.Dropout(0.5)
48
+
49
+ def _block(self, in_channels, out_channels):
50
+ return nn.Sequential(
51
+ nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),
52
+ nn.BatchNorm2d(out_channels),
53
+ nn.ReLU(),
54
+ nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
55
+ nn.BatchNorm2d(out_channels),
56
+ nn.ReLU(),
57
+ nn.MaxPool2d(2),
58
+ )
59
+
60
+ def forward(self, x):
61
+ x = self.block1(x)
62
+ x = self.block2(x)
63
+ x = self.block3(x)
64
+ x = self.block4(x)
65
+ x = self.gap(x)
66
+ x = x.view(x.size(0), -1)
67
+ x = self.dropout(torch.relu(self.fc1(x)))
68
+ x = self.fc2(x)
69
+ return x
70
+
71
+
72
+ pytorch_transform = transforms.Compose([
73
+ transforms.Resize((150, 150)),
74
+ transforms.ToTensor(),
75
+ transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
76
+ ])
77
+
78
+ _pytorch_model = None
79
+ _tensorflow_model = None
80
+
81
+
82
+ def download_model_file(filename: str, local_path: Path):
83
+ if local_path.exists():
84
+ return
85
+ MODEL_DIR.mkdir(parents=True, exist_ok=True)
86
+ try:
87
+ hf_hub_download(
88
+ repo_id=MODEL_REPO,
89
+ filename=filename,
90
+ repo_type="model",
91
+ local_dir=str(MODEL_DIR),
92
+ local_dir_use_symlinks=False,
93
+ use_auth_token=HF_TOKEN,
94
+ )
95
+ except Exception as exc:
96
+ raise FileNotFoundError(
97
+ f"Unable to download {filename} from {MODEL_REPO}: {exc}"
98
+ ) from exc
99
+
100
+
101
+ def get_pytorch_model():
102
+ global _pytorch_model
103
+ if _pytorch_model is None:
104
+ if not PYTORCH_PATH.exists():
105
+ download_model_file("models/pytorch_model.pth", PYTORCH_PATH)
106
+ model = CNN(num_classes=len(CLASSES))
107
+ model.load_state_dict(torch.load(str(PYTORCH_PATH), map_location="cpu"))
108
+ model.eval()
109
+ _pytorch_model = model
110
+ return _pytorch_model
111
+
112
+
113
+ def get_tensorflow_model():
114
+ global _tensorflow_model
115
+ if _tensorflow_model is None:
116
+ if not TENSORFLOW_PATH.exists():
117
+ download_model_file("models/model_best.keras", TENSORFLOW_PATH)
118
+ import tensorflow as tf
119
+ _tensorflow_model = tf.keras.models.load_model(str(TENSORFLOW_PATH), compile=False)
120
+ return _tensorflow_model
121
+
122
+
123
+ def predict_pytorch(image: Image.Image):
124
+ model = get_pytorch_model()
125
+ tensor = pytorch_transform(image).unsqueeze(0)
126
+ with torch.no_grad():
127
+ outputs = model(tensor)
128
+ probs = torch.nn.functional.softmax(outputs, dim=1).squeeze().cpu().numpy()
129
+
130
+ sorted_indices = np.argsort(probs)[::-1]
131
+ confidence = float(probs[sorted_indices[0]])
132
+ all_probs = [[CLASSES[i], float(probs[i])] for i in sorted_indices]
133
+ predicted_class = "unknown" if confidence < CONFIDENCE_THRESHOLD else CLASSES[sorted_indices[0]]
134
+ return predicted_class, confidence, all_probs
135
+
136
+
137
+ def predict_tensorflow(image: Image.Image):
138
+ model = get_tensorflow_model()
139
+ img = image.resize((130, 130))
140
+ arr = np.array(img, dtype=np.float32) / 255.0
141
+ arr = np.expand_dims(arr, 0)
142
+ preds = model.predict(arr, verbose=0)[0]
143
+ sorted_indices = np.argsort(preds)[::-1]
144
+ confidence = float(preds[sorted_indices[0]])
145
+ all_probs = [[CLASSES[i], float(preds[i])] for i in sorted_indices]
146
+ predicted_class = "unknown" if confidence < CONFIDENCE_THRESHOLD else CLASSES[sorted_indices[0]]
147
+ return predicted_class, confidence, all_probs
148
+
149
+
150
+ def classify(image: Image.Image, model_choice: str):
151
+ if model_choice == "pytorch":
152
+ return predict_pytorch(image)
153
+ return predict_tensorflow(image)
154
+
155
+
156
+ @app.get("/")
157
+ def read_index():
158
+ index_path = BASE_DIR / "index.html"
159
+ if not index_path.exists():
160
+ raise HTTPException(status_code=404, detail="index.html not found")
161
+ return FileResponse(index_path, media_type="text/html")
162
+
163
+
164
+ @app.get("/health")
165
+ def health_check():
166
+ return JSONResponse({"status": "ok"})
167
+
168
+
169
+ @app.post("/predict")
170
+ def predict(image: UploadFile = File(...), model_choice: str = Form("pytorch")):
171
+ if image.content_type.split('/')[0] != 'image':
172
+ raise HTTPException(status_code=400, detail="Le fichier doit être une image.")
173
+
174
+ image_data = image.file.read()
175
+ try:
176
+ img = Image.open(io.BytesIO(image_data)).convert("RGB")
177
+ except Exception as exc:
178
+ raise HTTPException(status_code=400, detail=f"Impossible de lire l'image: {exc}")
179
+
180
+ predicted_class, confidence, all_probs = classify(img, model_choice)
181
+ return {
182
+ "predicted_class": predicted_class,
183
+ "confidence": f"{confidence * 100:.2f}%",
184
+ "probabilities": all_probs,
185
+ }
186
+
187
+
188
+ if __name__ == "__main__":
189
+ uvicorn.run(app, host="0.0.0.0", port=7860)
index.html ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Intel Image Classifier</title>
7
+ <style>
8
+ body { font-family: Arial, sans-serif; background: #f4f7fb; color: #1f2937; margin: 0; padding: 0; }
9
+ .container { max-width: 700px; margin: 3rem auto; padding: 2rem; background: white; border-radius: 16px; box-shadow: 0 20px 50px rgba(15,23,42,0.08); }
10
+ h1 { margin-top: 0; font-size: 2rem; color: #111827; }
11
+ p { line-height: 1.6; color: #374151; }
12
+ .form-group { margin-bottom: 1.25rem; }
13
+ label { display: block; margin-bottom: 0.5rem; font-weight: 600; }
14
+ input[type="file"], select { width: 100%; padding: 0.8rem 1rem; border-radius: 0.75rem; border: 1px solid #d1d5db; background: #f9fafb; }
15
+ button { border: none; background: #2563eb; color: white; padding: 0.9rem 1.4rem; border-radius: 0.9rem; font-weight: 700; cursor: pointer; transition: background 0.2s ease; }
16
+ button:hover { background: #1d4ed8; }
17
+ .result { margin-top: 1.5rem; padding: 1.2rem; border-radius: 1rem; background: #eff6ff; border: 1px solid #bfdbfe; }
18
+ .result strong { display: inline-block; width: 170px; }
19
+ .probabilities { margin-top: 1rem; width: 100%; border-collapse: collapse; }
20
+ .probabilities th, .probabilities td { padding: 0.75rem 0.9rem; border-bottom: 1px solid #e5e7eb; text-align: left; }
21
+ .spinner { display: none; margin-top: 1rem; color: #2563eb; }
22
+ </style>
23
+ </head>
24
+ <body>
25
+ <div class="container">
26
+ <h1>Intel Image Classifier</h1>
27
+ <p>Déposez une image et choisissez un modèle pour obtenir une prédiction en temps réel.</p>
28
+
29
+ <form id="predict-form">
30
+ <div class="form-group">
31
+ <label for="image">Image</label>
32
+ <input type="file" id="image" name="image" accept="image/*" required />
33
+ </div>
34
+ <div class="form-group">
35
+ <label for="model-choice">Modèle</label>
36
+ <select id="model-choice" name="model_choice">
37
+ <option value="pytorch">PyTorch</option>
38
+ <option value="tensorflow">TensorFlow</option>
39
+ </select>
40
+ </div>
41
+ <button type="submit">Classer l'image</button>
42
+ <p class="spinner" id="spinner">Analyse en cours…</p>
43
+ </form>
44
+
45
+ <div class="result" id="result" style="display:none;">
46
+ <p><strong>Classe prédite :</strong> <span id="predicted-class"></span></p>
47
+ <p><strong>Confiance :</strong> <span id="confidence"></span></p>
48
+ <div id="probabilities-container"></div>
49
+ </div>
50
+ </div>
51
+
52
+ <script>
53
+ const form = document.getElementById('predict-form');
54
+ const spinner = document.getElementById('spinner');
55
+ const resultBox = document.getElementById('result');
56
+ const predictedClass = document.getElementById('predicted-class');
57
+ const confidence = document.getElementById('confidence');
58
+ const probabilitiesContainer = document.getElementById('probabilities-container');
59
+
60
+ form.addEventListener('submit', async (event) => {
61
+ event.preventDefault();
62
+ const fileInput = document.getElementById('image');
63
+ const modelChoice = document.getElementById('model-choice').value;
64
+ const file = fileInput.files[0];
65
+
66
+ if (!file) {
67
+ alert('Veuillez sélectionner une image avant de soumettre.');
68
+ return;
69
+ }
70
+
71
+ const formData = new FormData();
72
+ formData.append('image', file);
73
+ formData.append('model_choice', modelChoice);
74
+
75
+ spinner.style.display = 'block';
76
+ resultBox.style.display = 'none';
77
+ probabilitiesContainer.innerHTML = '';
78
+
79
+ try {
80
+ const response = await fetch('/predict', {
81
+ method: 'POST',
82
+ body: formData,
83
+ });
84
+
85
+ if (!response.ok) {
86
+ const errorText = await response.text();
87
+ throw new Error(errorText || 'Erreur serveur');
88
+ }
89
+
90
+ const data = await response.json();
91
+ predictedClass.textContent = data.predicted_class;
92
+ confidence.textContent = data.confidence;
93
+
94
+ const table = document.createElement('table');
95
+ table.className = 'probabilities';
96
+ table.innerHTML = '<thead><tr><th>Classe</th><th>Probabilité</th></tr></thead>';
97
+ const tbody = document.createElement('tbody');
98
+ data.probabilities.forEach(item => {
99
+ const row = document.createElement('tr');
100
+ row.innerHTML = `<td>${item[0]}</td><td>${(item[1] * 100).toFixed(2)}%</td>`;
101
+ tbody.appendChild(row);
102
+ });
103
+ table.appendChild(tbody);
104
+ probabilitiesContainer.appendChild(table);
105
+
106
+ resultBox.style.display = 'block';
107
+ } catch (error) {
108
+ alert('Erreur lors de la requête : ' + error.message);
109
+ } finally {
110
+ spinner.style.display = 'none';
111
+ }
112
+ });
113
+ </script>
114
+ </body>
115
+ </html>
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ python-multipart
4
+ huggingface_hub
5
+ torch>=2.2.0
6
+ torchvision>=0.17.0
7
+ tensorflow>=2.16.0
8
+ numpy>=1.26.0
9
+ pillow