cyberai-1 commited on
Commit
ab51159
·
1 Parent(s): 4622aca

Add model weights

Browse files
Files changed (9) hide show
  1. .gitattributes +1 -0
  2. Dockerfile +16 -0
  3. Procfile +1 -0
  4. README.md +65 -7
  5. app.py +120 -0
  6. parfait_model.keras +3 -0
  7. parfait_model.pth +3 -0
  8. requirements.txt +7 -0
  9. templates/index.html +543 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.keras filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
+ # you will also find guides on how best to write your Dockerfile
3
+
4
+ FROM python:3.9
5
+
6
+ RUN useradd -m -u 1000 user
7
+ USER user
8
+ ENV PATH="/home/user/.local/bin:$PATH"
9
+
10
+ WORKDIR /app
11
+
12
+ COPY --chown=user ./requirements.txt requirements.txt
13
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
14
+
15
+ COPY --chown=user . /app
16
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
Procfile ADDED
@@ -0,0 +1 @@
 
 
1
+ web: gunicorn app:app --bind 0.0.0.0:$PORT --workers 1 --timeout 120
README.md CHANGED
@@ -1,10 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: Cv Project 1
3
- emoji: 😻
4
- colorFrom: yellow
5
- colorTo: blue
6
- sdk: docker
7
- pinned: false
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
1
+ # Intel Scene Classifier — Déploiement
2
+
3
+ ## Structure du projet
4
+ ```
5
+ webapp/
6
+ ├── app.py ← Backend Flask
7
+ ├── templates/
8
+ │ └── index.html ← Interface web
9
+ ├── parfait_model.pth ← ⚠️ À placer ici (téléchargé depuis Kaggle)
10
+ ├── parfait_model.keras ← ⚠️ À placer ici (téléchargé depuis Kaggle)
11
+ ├── requirements.txt
12
+ ├── Procfile
13
+ └── README.md
14
+ ```
15
+
16
+ > ⚠️ **Important** : Placez `parfait_model.pth` et `parfait_model.keras`
17
+ > à la racine du projet avant de déployer.
18
+
19
+ ---
20
+
21
+ ## Option A — PythonAnywhere (recommandé, gratuit)
22
+
23
+ 1. Créez un compte sur https://www.pythonanywhere.com
24
+ 2. Onglet **Files** → uploadez tous vos fichiers (y compris les modèles .pth et .keras)
25
+ 3. Onglet **Consoles** → ouvrir un Bash :
26
+ ```bash
27
+ pip install -r requirements.txt
28
+ ```
29
+ 4. Onglet **Web** → Add a new web app → Manual configuration → Python 3.10
30
+ 5. Dans **WSGI configuration file**, remplacez le contenu par :
31
+ ```python
32
+ import sys
33
+ sys.path.insert(0, '/home/VOTRE_USERNAME')
34
+ from app import app as application
35
+ ```
36
+ 6. **Reload** → votre app est en ligne !
37
+
38
+ ---
39
+
40
+ ## Option B — Railway (gratuit, très simple)
41
+
42
+ 1. Créez un compte sur https://railway.app
43
+ 2. **New Project** → Deploy from GitHub (poussez votre code sur GitHub d'abord)
44
+ 3. Railway détecte automatiquement le `Procfile`
45
+ 4. Ajoutez vos fichiers modèles via **Volume** ou committez-les dans le repo
46
+ 5. Deploy → URL générée automatiquement
47
+
48
  ---
49
+
50
+ ## Option C — Render (gratuit)
51
+
52
+ 1. https://render.com → New Web Service
53
+ 2. Connectez votre repo GitHub
54
+ 3. Build Command : `pip install -r requirements.txt`
55
+ 4. Start Command : `gunicorn app:app --bind 0.0.0.0:$PORT --workers 1 --timeout 120`
56
+ 5. Uploadez les modèles dans le repo ou via un bucket S3
57
+
58
  ---
59
 
60
+ ## Lancer en local
61
+
62
+ ```bash
63
+ pip install -r requirements.txt
64
+
65
+ # Placez parfait_model.pth et parfait_model.keras ici, puis :
66
+ python app.py
67
+ # → http://localhost:5000
68
+ ```
app.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Intel Scene Classifier — Flask App
3
+ """
4
+
5
+ import io
6
+ import os
7
+ import numpy as np
8
+ from flask import Flask, jsonify, render_template, request
9
+ from PIL import Image
10
+
11
+ app = Flask(__name__)
12
+
13
+ CLASSES = ["buildings", "forest", "glacier", "mountain", "sea", "street"]
14
+ IMG_SIZE = 150
15
+
16
+ _pytorch_model = None
17
+ _tf_model = None
18
+
19
+
20
+ # ── Loaders ────────────────────────────────────────────────────────────────────
21
+ def load_pytorch():
22
+ global _pytorch_model
23
+ if _pytorch_model is not None:
24
+ return _pytorch_model
25
+
26
+ import torch
27
+ import torch.nn as nn
28
+ import torch.nn.functional as F
29
+ from torchvision import transforms
30
+
31
+ class CNN_Torch(nn.Module):
32
+ def __init__(self, num_classes=6):
33
+ super().__init__()
34
+ self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
35
+ self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
36
+ self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
37
+ self.conv3_drop = nn.Dropout2d(p=0.25)
38
+ self.pool = nn.MaxPool2d(2, 2)
39
+ self.fc1 = nn.Linear(128 * 18 * 18, 256)
40
+ self.fc2 = nn.Linear(256, num_classes)
41
+
42
+ def forward(self, x):
43
+ x = self.pool(F.relu(self.conv1(x)))
44
+ x = self.pool(F.relu(self.conv2(x)))
45
+ x = self.pool(F.relu(self.conv3_drop(self.conv3(x))))
46
+ x = x.view(-1, 128 * 18 * 18)
47
+ x = F.relu(self.fc1(x))
48
+ x = F.dropout(x, training=self.training)
49
+ x = self.fc2(x)
50
+ return F.log_softmax(x, dim=1)
51
+
52
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
53
+ model = CNN_Torch(6).to(device)
54
+ model.load_state_dict(torch.load("parfait_model.pth", map_location=device))
55
+ model.eval()
56
+
57
+ tf = transforms.Compose([
58
+ transforms.Resize((IMG_SIZE, IMG_SIZE)),
59
+ transforms.ToTensor(),
60
+ transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
61
+ ])
62
+ _pytorch_model = (model, device, tf)
63
+ return _pytorch_model
64
+
65
+
66
+ def load_tensorflow():
67
+ global _tf_model
68
+ if _tf_model is None:
69
+ import tensorflow as tf
70
+ _tf_model = tf.keras.models.load_model("parfait_model.keras")
71
+ return _tf_model
72
+
73
+
74
+ # ── Routes ─────────────────────────────────────────────────────────────────────
75
+ @app.route("/")
76
+ def index():
77
+ return render_template("index.html")
78
+
79
+
80
+ @app.route("/predict", methods=["POST"])
81
+ def predict():
82
+ if "image" not in request.files:
83
+ return jsonify({"error": "Aucune image fournie"}), 400
84
+
85
+ framework = request.form.get("model", "pytorch")
86
+
87
+ try:
88
+ pil_img = Image.open(io.BytesIO(request.files["image"].read())).convert("RGB")
89
+ except Exception:
90
+ return jsonify({"error": "Fichier image invalide"}), 400
91
+
92
+ try:
93
+ if framework == "pytorch":
94
+ import torch
95
+ model, device, tf = load_pytorch()
96
+ tensor = tf(pil_img).unsqueeze(0).to(device)
97
+ with torch.no_grad():
98
+ out = model(tensor)
99
+ probs = torch.exp(out).cpu().numpy()[0]
100
+ else:
101
+ model = load_tensorflow()
102
+ arr = np.array(pil_img.resize((IMG_SIZE, IMG_SIZE)), dtype=np.float32)
103
+ probs = model.predict(np.expand_dims(arr, 0), verbose=0)[0]
104
+
105
+ pred_idx = int(np.argmax(probs))
106
+ return jsonify({
107
+ "class": CLASSES[pred_idx],
108
+ "confidence": float(probs[pred_idx]),
109
+ "probabilities": {c: float(p) for c, p in zip(CLASSES, probs)},
110
+ })
111
+
112
+ except FileNotFoundError as e:
113
+ return jsonify({"error": f"Modèle introuvable : {e}. Placez les fichiers .pth et .keras à la racine."}), 500
114
+ except Exception as e:
115
+ return jsonify({"error": str(e)}), 500
116
+
117
+
118
+ if __name__ == "__main__":
119
+ port = int(os.environ.get("PORT", 5000))
120
+ app.run(host="0.0.0.0", port=port, debug=False)
parfait_model.keras ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a477cdf46a8abe2d6c9aede0dee169e4d3b0f5706f57e8e40a8db3ed1ac0c133
3
+ size 1619355
parfait_model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:681ed1e9855e437851f8028b9dd54e6a6c1101f34f9904a3dbb19c3a4e0e57a9
3
+ size 523282
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ flask>=3.0.0
2
+ pillow>=10.0.0
3
+ numpy>=1.24.0
4
+ torch>=2.0.0
5
+ torchvision>=0.15.0
6
+ tensorflow>=2.13.0
7
+ gunicorn>=21.0.0
templates/index.html ADDED
@@ -0,0 +1,543 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Scene Classifier · Parfait</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=Instrument+Sans:wght@300;400;500&family=Roboto+Mono:wght@400;500&display=swap" rel="stylesheet">
9
+ <style>
10
+ /* ── Tokens ── */
11
+ :root {
12
+ --ink: #0d0d0d;
13
+ --paper: #f5f2ec;
14
+ --paper2: #edeae2;
15
+ --rule: #d4cfc4;
16
+ --accent: #c84b2f;
17
+ --accent2: #2563eb;
18
+ --muted: #8a8070;
19
+ --white: #ffffff;
20
+ --ff-head: 'Syne', sans-serif;
21
+ --ff-body: 'Instrument Sans', sans-serif;
22
+ --ff-mono: 'Roboto Mono', monospace;
23
+ --r: 8px;
24
+ }
25
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
26
+ html { font-size: 16px; }
27
+
28
+ body {
29
+ background: var(--paper);
30
+ color: var(--ink);
31
+ font-family: var(--ff-body);
32
+ min-height: 100vh;
33
+ display: grid;
34
+ grid-template-rows: auto 1fr auto;
35
+ }
36
+
37
+ /* ── Header ── */
38
+ header {
39
+ border-bottom: 2px solid var(--ink);
40
+ padding: 1.1rem 2.5rem;
41
+ display: flex;
42
+ align-items: baseline;
43
+ gap: 1.2rem;
44
+ }
45
+ .site-name {
46
+ font-family: var(--ff-head);
47
+ font-size: 1.4rem;
48
+ font-weight: 800;
49
+ letter-spacing: -.02em;
50
+ }
51
+ .site-name span { color: var(--accent); }
52
+ .site-sub {
53
+ font-family: var(--ff-mono);
54
+ font-size: .65rem;
55
+ color: var(--muted);
56
+ letter-spacing: .1em;
57
+ text-transform: uppercase;
58
+ }
59
+ .badge {
60
+ margin-left: auto;
61
+ font-family: var(--ff-mono);
62
+ font-size: .6rem;
63
+ letter-spacing: .1em;
64
+ padding: .3rem .75rem;
65
+ border: 1.5px solid var(--ink);
66
+ border-radius: 99px;
67
+ text-transform: uppercase;
68
+ }
69
+
70
+ /* ── Main layout ── */
71
+ main {
72
+ display: grid;
73
+ grid-template-columns: 1fr 1fr;
74
+ max-width: 1100px;
75
+ margin: 0 auto;
76
+ width: 100%;
77
+ gap: 0;
78
+ padding: 3rem 2rem;
79
+ }
80
+
81
+ /* ── Left panel ── */
82
+ .left-panel {
83
+ padding-right: 3rem;
84
+ border-right: 1.5px solid var(--rule);
85
+ display: flex;
86
+ flex-direction: column;
87
+ gap: 2rem;
88
+ }
89
+
90
+ .panel-label {
91
+ font-family: var(--ff-mono);
92
+ font-size: .6rem;
93
+ letter-spacing: .15em;
94
+ color: var(--muted);
95
+ text-transform: uppercase;
96
+ margin-bottom: .5rem;
97
+ }
98
+
99
+ /* Model selector */
100
+ .model-tabs {
101
+ display: flex;
102
+ gap: .5rem;
103
+ }
104
+ .tab-btn {
105
+ flex: 1;
106
+ padding: .9rem 1rem;
107
+ border: 1.5px solid var(--rule);
108
+ border-radius: var(--r);
109
+ background: var(--white);
110
+ font-family: var(--ff-head);
111
+ font-size: .9rem;
112
+ font-weight: 600;
113
+ cursor: pointer;
114
+ transition: all .2s;
115
+ display: flex;
116
+ flex-direction: column;
117
+ gap: .2rem;
118
+ text-align: left;
119
+ }
120
+ .tab-btn .tab-icon { font-size: 1.3rem; }
121
+ .tab-btn .tab-fw { font-size: .65rem; font-family: var(--ff-mono); color: var(--muted); font-weight: 400; }
122
+ .tab-btn:hover { border-color: var(--ink); }
123
+ .tab-btn.active { border-color: var(--ink); background: var(--ink); color: var(--white); }
124
+ .tab-btn.active .tab-fw { color: var(--rule); }
125
+
126
+ /* Drop zone */
127
+ .drop-zone {
128
+ border: 2px dashed var(--rule);
129
+ border-radius: var(--r);
130
+ min-height: 240px;
131
+ display: flex;
132
+ flex-direction: column;
133
+ align-items: center;
134
+ justify-content: center;
135
+ gap: 1rem;
136
+ cursor: pointer;
137
+ transition: border-color .2s, background .2s;
138
+ position: relative;
139
+ overflow: hidden;
140
+ background: var(--white);
141
+ }
142
+ .drop-zone:hover, .drop-zone.drag { border-color: var(--accent); background: #fdf8f6; }
143
+ .drop-zone img {
144
+ position: absolute; inset: 0;
145
+ width: 100%; height: 100%;
146
+ object-fit: cover;
147
+ border-radius: calc(var(--r) - 2px);
148
+ display: none;
149
+ }
150
+ .drop-zone.has-img img { display: block; }
151
+ .drop-zone.has-img .dz-ph { display: none; }
152
+ .dz-ph {
153
+ display: flex; flex-direction: column; align-items: center; gap: .75rem;
154
+ color: var(--muted); pointer-events: none;
155
+ }
156
+ .dz-icon {
157
+ width: 52px; height: 52px; border: 1.5px solid var(--rule);
158
+ border-radius: 50%; display: grid; place-items: center; font-size: 1.3rem;
159
+ }
160
+ .dz-ph p { font-size: .85rem; }
161
+ .dz-ph em { font-size: .75rem; font-style: normal; font-family: var(--ff-mono); }
162
+ #file-input { display: none; }
163
+
164
+ /* Analyse button */
165
+ #classify-btn {
166
+ width: 100%;
167
+ padding: 1.1rem;
168
+ background: var(--accent);
169
+ color: var(--white);
170
+ border: none;
171
+ border-radius: var(--r);
172
+ font-family: var(--ff-head);
173
+ font-size: 1.1rem;
174
+ font-weight: 700;
175
+ letter-spacing: .03em;
176
+ cursor: pointer;
177
+ transition: opacity .2s, transform .15s;
178
+ position: relative;
179
+ overflow: hidden;
180
+ }
181
+ #classify-btn::after {
182
+ content: '';
183
+ position: absolute; inset: 0;
184
+ background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,.12) 50%, transparent 100%);
185
+ transform: translateX(-100%);
186
+ }
187
+ #classify-btn.loading::after {
188
+ animation: shimmer 1.2s infinite;
189
+ }
190
+ @keyframes shimmer { to { transform: translateX(100%); } }
191
+ #classify-btn:disabled { opacity: .4; cursor: default; }
192
+ #classify-btn:not(:disabled):hover { opacity: .88; transform: translateY(-1px); }
193
+
194
+ /* ── Right panel ── */
195
+ .right-panel {
196
+ padding-left: 3rem;
197
+ display: flex;
198
+ flex-direction: column;
199
+ gap: 2.5rem;
200
+ }
201
+
202
+ /* Prediction hero */
203
+ .pred-hero { display: flex; flex-direction: column; gap: .4rem; }
204
+ .pred-waiting {
205
+ display: flex; flex-direction: column;
206
+ justify-content: center; align-items: flex-start;
207
+ min-height: 120px; gap: .5rem;
208
+ }
209
+ .pred-waiting p { color: var(--muted); font-size: .9rem; line-height: 1.6; }
210
+ .pred-waiting strong { color: var(--ink); }
211
+
212
+ .pred-result { display: none; flex-direction: column; gap: .3rem; }
213
+ .pred-result.show { display: flex; animation: fadeUp .35s ease; }
214
+ @keyframes fadeUp { from{opacity:0;transform:translateY(10px)} to{opacity:1;transform:none} }
215
+
216
+ .pred-cat {
217
+ font-family: var(--ff-mono);
218
+ font-size: .6rem; letter-spacing: .15em;
219
+ color: var(--muted); text-transform: uppercase;
220
+ }
221
+ .pred-class {
222
+ font-family: var(--ff-head);
223
+ font-size: 3.5rem;
224
+ font-weight: 800;
225
+ letter-spacing: -.03em;
226
+ line-height: 1;
227
+ color: var(--accent);
228
+ }
229
+ .pred-conf {
230
+ font-family: var(--ff-mono);
231
+ font-size: .8rem;
232
+ color: var(--muted);
233
+ }
234
+ .pred-conf strong { color: var(--ink); font-size: 1rem; }
235
+
236
+ /* Conf bar */
237
+ .conf-track {
238
+ height: 4px; background: var(--rule);
239
+ border-radius: 99px; overflow: hidden; margin-top: .5rem;
240
+ }
241
+ .conf-fill {
242
+ height: 100%;
243
+ background: var(--accent);
244
+ border-radius: 99px;
245
+ width: 0%;
246
+ transition: width .8s cubic-bezier(.4,0,.2,1);
247
+ }
248
+
249
+ /* Divider */
250
+ .divider {
251
+ height: 1px; background: var(--rule);
252
+ }
253
+
254
+ /* Prob bars */
255
+ .prob-section { display: flex; flex-direction: column; gap: 1rem; }
256
+ .prob-row { display: flex; flex-direction: column; gap: .3rem; }
257
+ .prob-meta {
258
+ display: flex; justify-content: space-between; align-items: baseline;
259
+ }
260
+ .prob-name {
261
+ font-family: var(--ff-body);
262
+ font-size: .82rem;
263
+ font-weight: 500;
264
+ display: flex; align-items: center; gap: .4rem;
265
+ }
266
+ .prob-name .emoji { font-size: .9rem; }
267
+ .prob-pct {
268
+ font-family: var(--ff-mono);
269
+ font-size: .72rem;
270
+ color: var(--muted);
271
+ }
272
+ .prob-track {
273
+ height: 6px; background: var(--paper2);
274
+ border-radius: 99px; overflow: hidden;
275
+ }
276
+ .prob-fill {
277
+ height: 100%;
278
+ background: var(--rule);
279
+ border-radius: 99px;
280
+ width: 0%;
281
+ transition: width .7s cubic-bezier(.4,0,.2,1);
282
+ }
283
+ .prob-row.top .prob-name { color: var(--accent); }
284
+ .prob-row.top .prob-pct { color: var(--accent); }
285
+ .prob-row.top .prob-fill { background: var(--accent); }
286
+
287
+ /* Error */
288
+ .pred-error {
289
+ display: none; padding: 1rem;
290
+ background: #fef2f2; border: 1px solid #fecaca;
291
+ border-radius: var(--r); color: #dc2626;
292
+ font-size: .85rem; line-height: 1.5;
293
+ }
294
+ .pred-error.show { display: block; animation: fadeUp .3s ease; }
295
+
296
+ /* ── Footer ── */
297
+ footer {
298
+ border-top: 1.5px solid var(--rule);
299
+ padding: 1rem 2.5rem;
300
+ display: flex;
301
+ align-items: center;
302
+ justify-content: space-between;
303
+ }
304
+ footer p { font-size: .72rem; color: var(--muted); font-family: var(--ff-mono); }
305
+ .classes-pills { display: flex; gap: .4rem; flex-wrap: wrap; }
306
+ .cpill {
307
+ font-family: var(--ff-mono); font-size: .6rem; letter-spacing: .05em;
308
+ padding: .2rem .6rem; border: 1px solid var(--rule); border-radius: 99px;
309
+ color: var(--muted);
310
+ }
311
+
312
+ /* ── Responsive ── */
313
+ @media (max-width: 720px) {
314
+ main { grid-template-columns: 1fr; padding: 1.5rem 1rem; }
315
+ .left-panel { padding-right: 0; border-right: none; border-bottom: 1.5px solid var(--rule); padding-bottom: 2rem; }
316
+ .right-panel { padding-left: 0; }
317
+ header { padding: 1rem; flex-wrap: wrap; }
318
+ }
319
+ </style>
320
+ </head>
321
+ <body>
322
+
323
+ <header>
324
+ <div class="site-name">SCENE<span>.</span>AI</div>
325
+ <div class="site-sub">Intel Image Classifier · CNN</div>
326
+ <div class="badge">by Parfait</div>
327
+ </header>
328
+
329
+ <main>
330
+ <!-- LEFT : inputs -->
331
+ <div class="left-panel">
332
+
333
+ <div>
334
+ <div class="panel-label">01 — Choisir le modèle</div>
335
+ <div class="model-tabs">
336
+ <button class="tab-btn active" data-fw="pytorch" onclick="selectModel(this)">
337
+ <span class="tab-icon">⚡</span>
338
+ PyTorch
339
+ <span class="tab-fw">CNN_Torch · .pth</span>
340
+ </button>
341
+ <button class="tab-btn" data-fw="tensorflow" onclick="selectModel(this)">
342
+ <span class="tab-icon">🧠</span>
343
+ TensorFlow
344
+ <span class="tab-fw">CNN_TF · .keras</span>
345
+ </button>
346
+ </div>
347
+ </div>
348
+
349
+ <div>
350
+ <div class="panel-label">02 — Uploader une image</div>
351
+ <div class="drop-zone" id="drop-zone" onclick="document.getElementById('file-input').click()">
352
+ <img id="preview" src="" alt="preview"/>
353
+ <div class="dz-ph">
354
+ <div class="dz-icon">↑</div>
355
+ <p>Glisser-déposer ou cliquer</p>
356
+ <em>JPG, PNG, WEBP</em>
357
+ </div>
358
+ </div>
359
+ <input type="file" id="file-input" accept="image/*"/>
360
+ </div>
361
+
362
+ <div>
363
+ <div class="panel-label">03 — Analyser</div>
364
+ <button id="classify-btn" disabled onclick="classify()">
365
+ Analyser l'image →
366
+ </button>
367
+ </div>
368
+
369
+ </div>
370
+
371
+ <!-- RIGHT : results -->
372
+ <div class="right-panel">
373
+
374
+ <div class="pred-hero">
375
+ <div class="panel-label">Résultat</div>
376
+
377
+ <div class="pred-waiting" id="pred-waiting">
378
+ <p>Uploadez une image et cliquez sur <strong>Analyser</strong> pour voir la prédiction du modèle CNN sur les 6 catégories Intel.</p>
379
+ </div>
380
+
381
+ <div class="pred-result" id="pred-result">
382
+ <div class="pred-cat">Classe prédite</div>
383
+ <div class="pred-class" id="pred-class-name">—</div>
384
+ <div class="pred-conf">Confiance : <strong id="pred-conf-val">—</strong></div>
385
+ <div class="conf-track"><div class="conf-fill" id="conf-fill"></div></div>
386
+ </div>
387
+
388
+ <div class="pred-error" id="pred-error"></div>
389
+ </div>
390
+
391
+ <div class="divider"></div>
392
+
393
+ <div class="prob-section" id="prob-section">
394
+ <div class="panel-label">Scores par catégorie</div>
395
+ <div id="prob-bars">
396
+ <!-- generated by JS -->
397
+ <div style="color:var(--muted);font-size:.85rem;">Les scores apparaîtront ici après l'analyse.</div>
398
+ </div>
399
+ </div>
400
+
401
+ </div>
402
+ </main>
403
+
404
+ <footer>
405
+ <p>Intel Image Classification · 6 classes · CNN PyTorch & TensorFlow</p>
406
+ <div class="classes-pills">
407
+ <span class="cpill">🏙 buildings</span>
408
+ <span class="cpill">🌲 forest</span>
409
+ <span class="cpill">🧊 glacier</span>
410
+ <span class="cpill">⛰ mountain</span>
411
+ <span class="cpill">🌊 sea</span>
412
+ <span class="cpill">🛣 street</span>
413
+ </div>
414
+ </footer>
415
+
416
+ <script>
417
+ const EMOJIS = {buildings:"🏙",forest:"🌲",glacier:"🧊",mountain:"⛰",sea:"🌊",street:"🛣"};
418
+ let selectedFile = null;
419
+ let selectedFw = "pytorch";
420
+
421
+ /* ── Model tabs ── */
422
+ function selectModel(btn) {
423
+ document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
424
+ btn.classList.add("active");
425
+ selectedFw = btn.dataset.fw;
426
+ }
427
+
428
+ /* ── File input ── */
429
+ const fileInput = document.getElementById("file-input");
430
+ const dropZone = document.getElementById("drop-zone");
431
+ const preview = document.getElementById("preview");
432
+ const btn = document.getElementById("classify-btn");
433
+
434
+ fileInput.addEventListener("change", () => loadFile(fileInput.files[0]));
435
+
436
+ dropZone.addEventListener("dragover", e => { e.preventDefault(); dropZone.classList.add("drag"); });
437
+ dropZone.addEventListener("dragleave", () => dropZone.classList.remove("drag"));
438
+ dropZone.addEventListener("drop", e => {
439
+ e.preventDefault(); dropZone.classList.remove("drag");
440
+ const f = e.dataTransfer.files[0];
441
+ if (f && f.type.startsWith("image/")) loadFile(f);
442
+ });
443
+
444
+ function loadFile(file) {
445
+ if (!file) return;
446
+ selectedFile = file;
447
+ btn.disabled = false;
448
+ const reader = new FileReader();
449
+ reader.onload = e => {
450
+ preview.src = e.target.result;
451
+ dropZone.classList.add("has-img");
452
+ };
453
+ reader.readAsDataURL(file);
454
+ resetResult();
455
+ }
456
+
457
+ /* ── Classify ── */
458
+ async function classify() {
459
+ if (!selectedFile) return;
460
+
461
+ btn.disabled = true;
462
+ btn.textContent = "Analyse en cours…";
463
+ btn.classList.add("loading");
464
+
465
+ const form = new FormData();
466
+ form.append("image", selectedFile);
467
+ form.append("model", selectedFw);
468
+
469
+ try {
470
+ const res = await fetch("/predict", { method: "POST", body: form });
471
+ const data = await res.json();
472
+ if (data.error) throw new Error(data.error);
473
+ showResult(data);
474
+ } catch(err) {
475
+ showError(err.message);
476
+ } finally {
477
+ btn.disabled = false;
478
+ btn.textContent = "Analyser l'image →";
479
+ btn.classList.remove("loading");
480
+ }
481
+ }
482
+
483
+ /* ── Display result ── */
484
+ function showResult(data) {
485
+ document.getElementById("pred-waiting").style.display = "none";
486
+ document.getElementById("pred-error").classList.remove("show");
487
+
488
+ const pct = Math.round(data.confidence * 100);
489
+ document.getElementById("pred-class-name").textContent =
490
+ (EMOJIS[data.class] || "") + " " + data.class.charAt(0).toUpperCase() + data.class.slice(1);
491
+ document.getElementById("pred-conf-val").textContent = pct + "%";
492
+
493
+ const result = document.getElementById("pred-result");
494
+ result.classList.add("show");
495
+
496
+ setTimeout(() => {
497
+ document.getElementById("conf-fill").style.width = pct + "%";
498
+ }, 60);
499
+
500
+ // probability bars
501
+ const container = document.getElementById("prob-bars");
502
+ container.innerHTML = "";
503
+ const sorted = Object.entries(data.probabilities).sort((a,b) => b[1]-a[1]);
504
+ sorted.forEach(([cls, prob], i) => {
505
+ const p = Math.round(prob * 100);
506
+ const top = cls === data.class;
507
+ const row = document.createElement("div");
508
+ row.className = "prob-row" + (top ? " top" : "");
509
+ row.innerHTML = `
510
+ <div class="prob-meta">
511
+ <span class="prob-name"><span class="emoji">${EMOJIS[cls]}</span>${cls}</span>
512
+ <span class="prob-pct" id="pct-${cls}">0%</span>
513
+ </div>
514
+ <div class="prob-track">
515
+ <div class="prob-fill" id="bar-${cls}"></div>
516
+ </div>`;
517
+ container.appendChild(row);
518
+ setTimeout(() => {
519
+ document.getElementById("bar-" + cls).style.width = p + "%";
520
+ document.getElementById("pct-" + cls).textContent = p + "%";
521
+ }, 80 + i * 40);
522
+ });
523
+ }
524
+
525
+ function showError(msg) {
526
+ document.getElementById("pred-waiting").style.display = "none";
527
+ document.getElementById("pred-result").classList.remove("show");
528
+ const err = document.getElementById("pred-error");
529
+ err.textContent = "Erreur : " + msg;
530
+ err.classList.add("show");
531
+ }
532
+
533
+ function resetResult() {
534
+ document.getElementById("pred-waiting").style.display = "flex";
535
+ document.getElementById("pred-result").classList.remove("show");
536
+ document.getElementById("pred-error").classList.remove("show");
537
+ document.getElementById("conf-fill").style.width = "0%";
538
+ document.getElementById("prob-bars").innerHTML =
539
+ "<div style='color:var(--muted);font-size:.85rem;'>Les scores apparaîtront ici après l'analyse.</div>";
540
+ }
541
+ </script>
542
+ </body>
543
+ </html>