Skip to content

Commit 63f2263

Browse files
committed
Gradio + pytest
1 parent d7f0d75 commit 63f2263

6 files changed

Lines changed: 201 additions & 72 deletions

File tree

app/__init__.py

Whitespace-only changes.
173 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.

static/index.html

Lines changed: 142 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,153 @@
1-
<!doctype html>
1+
<!DOCTYPE html>
22
<html lang="fr">
3-
43
<head>
5-
<meta charset="utf-8" />
6-
<meta name="viewport" content="width=device-width, initial-scale=1" />
7-
<title>Test Sentiment API</title>
8-
<!-- Tailwind CSS CDN -->
9-
<script src="https://cdn.tailwindcss.com"></script>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Sentiment Analysis API - Test Interface</title>
7+
<style>
8+
* { margin: 0; padding: 0; box-sizing: border-box; }
9+
body {
10+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
11+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
12+
min-height: 100vh;
13+
padding: 20px;
14+
}
15+
.container {
16+
max-width: 700px;
17+
margin: 0 auto;
18+
background: white;
19+
border-radius: 15px;
20+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
21+
overflow: hidden;
22+
}
23+
.header {
24+
background: linear-gradient(135deg, #4CAF50, #45a049);
25+
color: white;
26+
padding: 30px;
27+
text-align: center;
28+
}
29+
.header h1 { font-size: 2em; margin-bottom: 10px; }
30+
.header p { font-size: 1.1em; opacity: 0.9; }
31+
.content { padding: 30px; }
32+
.form-group { margin-bottom: 20px; }
33+
label { display: block; font-weight: 600; margin-bottom: 8px; color: #333; }
34+
textarea {
35+
width: 100%; padding: 15px; border: 2px solid #e1e1e1; border-radius: 8px;
36+
font-size: 16px; font-family: inherit; resize: vertical; min-height: 120px;
37+
transition: border-color 0.3s ease;
38+
}
39+
textarea:focus {
40+
outline: none; border-color: #4CAF50;
41+
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
42+
}
43+
.button-group { display: flex; gap: 15px; margin-bottom: 25px; }
44+
button {
45+
flex: 1; padding: 15px 25px; font-size: 16px; font-weight: 600; border: none;
46+
border-radius: 8px; cursor: pointer; transition: all 0.3s ease;
47+
}
48+
.btn-primary { background: linear-gradient(135deg, #4CAF50, #45a049); color: white; }
49+
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4); }
50+
.btn-secondary { background: #f8f9fa; color: #333; border: 2px solid #e1e1e1; }
51+
.btn-secondary:hover { background: #e9ecef; border-color: #adb5bd; }
52+
.result { margin-top: 25px; padding: 20px; border-radius: 10px; display: none; }
53+
.result.success { background: linear-gradient(135deg, #d4edda, #c3e6cb); border-left: 5px solid #28a745; }
54+
.result.error { background: linear-gradient(135deg, #f8d7da, #f1aeb5); border-left: 5px solid #dc3545; }
55+
.result h3 { margin-bottom: 15px; color: #333; }
56+
.sentiment-display { display: flex; align-items: center; gap: 15px; margin: 20px 0; }
57+
.sentiment-badge { padding: 8px 16px; border-radius: 25px; font-weight: 600; font-size: 14px; }
58+
.sentiment-positive { background: #28a745; color: white; }
59+
.sentiment-negative { background: #dc3545; color: white; }
60+
.confidence-bar { flex: 1; background: #e9ecef; border-radius: 10px; height: 20px; overflow: hidden; }
61+
.confidence-fill { height: 100%; transition: width 0.5s ease; display: flex; align-items: center; justify-content: center; color: white; font-weight: 600; font-size: 12px; }
62+
.confidence-positive { background: linear-gradient(90deg, #28a745, #20c997); }
63+
.confidence-negative { background: linear-gradient(90deg, #dc3545, #e74c3c); }
64+
.json-display { background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; padding: 20px; margin-top: 20px; font-family: 'Courier New', monospace; font-size: 14px; max-height: 300px; overflow-y: auto; }
65+
</style>
1066
</head>
67+
<body>
68+
<div class="container">
69+
<div class="header">
70+
<h1>🎯 Sentiment Analysis API</h1>
71+
<p>Testez l'API et analysez vos textes</p>
72+
</div>
1173

12-
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
13-
<div class="max-w-2xl w-full bg-white shadow-md rounded-2xl p-8">
14-
<h1 class="text-2xl font-bold mb-4">Test de l'API pour l'analyse des sentiments</h1>
15-
<p class="text-gray-700 mb-6">
16-
Tapez un texte et cliquez sur <b>Analyser</b>.
17-
<!-- L'API appelle <code class="bg-gray-100 px-1 py-0.5 rounded">POST /predict</code> et affiche le JSON de réponse. -->
18-
</p>
74+
<div class="content">
75+
<div class="form-group">
76+
<label for="textInput">Texte à analyser :</label>
77+
<textarea id="textInput" placeholder="I love this product!"></textarea>
78+
</div>
79+
<div class="button-group">
80+
<button id="btn" class="btn-primary">🔍 Analyser</button>
81+
<button type="button" onclick="clearResults()" class="btn-secondary">🗑️ Effacer</button>
82+
</div>
1983

20-
<div class="mb-4">
21-
<label for="text" class="block font-medium text-gray-800 mb-2">Votre texte</label>
22-
<textarea id="text" rows="4" placeholder="I love this product!"
23-
class="w-full border border-gray-300 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
84+
<div id="result" class="result">
85+
<h3>Résultat de l'analyse</h3>
86+
<div id="resultContent"></div>
87+
<div class="json-display">
88+
<pre id="jsonOutput"></pre>
89+
</div>
90+
</div>
91+
</div>
2492
</div>
2593

26-
<button id="btn" class="bg-blue-600 text-white px-5 py-2 rounded-lg hover:bg-blue-700 transition">
27-
Analyser
28-
</button>
94+
<script>
95+
const btn = document.getElementById('btn');
96+
const textInput = document.getElementById('textInput');
97+
const result = document.getElementById('result');
98+
const resultContent = document.getElementById('resultContent');
99+
const jsonOutput = document.getElementById('jsonOutput');
29100

30-
<div class="mt-6">
31-
<h5 class="text-lg font-semibold">Résultat</h5>
32-
<pre id="result"
33-
class="mt-2 border border-gray-300 rounded-lg bg-gray-50 p-4 text-sm text-gray-800 whitespace-pre-wrap">
34-
Résultat…
35-
</pre>
36-
</div>
37-
</div>
101+
btn.onclick = async () => {
102+
const text = textInput.value.trim();
103+
if (!text) { alert('Veuillez saisir un texte.'); return; }
38104

39-
<script>
40-
const btn = document.getElementById('btn');
41-
const result = document.getElementById('result');
105+
result.style.display = 'none';
106+
resultContent.innerHTML = 'Analyse en cours...';
42107

43-
btn.onclick = async () => {
44-
const text = document.getElementById('text').value.trim();
45-
if (!text) { result.textContent = "Veuillez saisir un texte."; return; }
46-
result.textContent = "Analyse en cours...";
47-
try {
48-
const resp = await fetch("/predict", {
49-
method: "POST",
50-
headers: { "Content-Type": "application/json" },
51-
body: JSON.stringify({ text })
52-
});
53-
const data = await resp.json();
54-
result.textContent = JSON.stringify(data, null, 2);
55-
} catch (e) {
56-
result.textContent = "Erreur: " + e.message;
57-
}
58-
};
59-
</script>
60-
</body>
108+
try {
109+
const resp = await fetch('/predict', {
110+
method: 'POST',
111+
headers: { 'Content-Type': 'application/json' },
112+
body: JSON.stringify({ text })
113+
});
114+
const data = await resp.json();
115+
if (resp.ok) {
116+
const sentimentClass = data.label === 'positive' ? 'positive' : 'negative';
117+
const confidencePercent = Math.round(data.confidence * 100);
118+
resultContent.innerHTML = `
119+
<div class="sentiment-display">
120+
<span class="sentiment-badge sentiment-${sentimentClass}">
121+
${data.label === 'positive' ? '😊 POSITIF' : '😞 NÉGATIF'}
122+
</span>
123+
<div class="confidence-bar">
124+
<div class="confidence-fill confidence-${sentimentClass}" style="width: ${confidencePercent}%">
125+
${confidencePercent}%
126+
</div>
127+
</div>
128+
</div>
129+
<p><strong>Confiance :</strong> ${data.confidence}</p>
130+
<p><strong>Texte analysé :</strong> "${data.text}"</p>
131+
`;
132+
jsonOutput.textContent = JSON.stringify(data, null, 2);
133+
result.className = 'result success';
134+
} else {
135+
result.className = 'result error';
136+
resultContent.innerHTML = `<p>❌ Erreur : ${data.detail || 'Analyse impossible'}</p>`;
137+
}
138+
result.style.display = 'block';
139+
} catch (e) {
140+
result.className = 'result error';
141+
result.style.display = 'block';
142+
resultContent.innerHTML = `<p>❌ Erreur de connexion à l'API</p>`;
143+
}
144+
};
61145

62-
</html>
146+
function clearResults() {
147+
textInput.value = '';
148+
result.style.display = 'none';
149+
textInput.focus();
150+
}
151+
</script>
152+
</body>
153+
</html>
8.04 KB
Binary file not shown.

tests/test_api.py

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,63 @@
1+
# tests/test_api.py
2+
import sys
3+
import os
4+
import time
5+
import pytest
6+
7+
# ---- Ajouter le dossier racine dans sys.path ----
8+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
9+
110
from fastapi.testclient import TestClient
11+
from app.main import app
212

313
client = TestClient(app)
414

5-
def test_health():
6-
r = client.get("/health")
7-
assert r.status_code == 200
8-
data = r.json()
9-
assert data["status"] == "ok"
10-
assert "model" in data
11-
12-
def test_predict_success():
13-
r = client.post("/predict", json={"text": "I love this!"})
14-
assert r.status_code == 200
15-
data = r.json()
16-
assert data["label"] in ("positive", "negative")
17-
assert 0.0 <= data["confidence"] <= 1.0
18-
assert "model" in data
19-
20-
def test_predict_error_empty():
21-
r = client.post("/predict", json={"text": ""})
22-
# Pydantic validation error -> 422
23-
assert r.status_code == 422
24-
body = r.json()
25-
assert "text must be a non-empty string" in str(body)
15+
16+
# ---------------- Tests Unitaires ----------------
17+
18+
def test_health_endpoint():
19+
"""Test endpoint /health"""
20+
response = client.get("/health")
21+
assert response.status_code == 200
22+
assert "status" in response.json()
23+
assert response.json()["status"] == "ok"
24+
25+
26+
def test_root_endpoint():
27+
"""Test endpoint /"""
28+
response = client.get("/")
29+
assert response.status_code in [200, 404] # 404 si index.html absent
30+
31+
content_type = response.headers.get("content-type", "")
32+
if "application/json" in content_type:
33+
data = response.json()
34+
assert "model" in data
35+
elif "text/html" in content_type:
36+
assert "<!doctype html>" in response.text.lower() # vérifier qu'on a bien du HTML
37+
else:
38+
pytest.fail(f"Unexpected content-type: {content_type}")
39+
40+
41+
42+
def test_predict_positive():
43+
"""Test prédiction sentiment positif"""
44+
payload = {"text": "I love this project!"}
45+
response = client.post("/predict", json=payload)
46+
assert response.status_code == 200
47+
data = response.json()
48+
assert data["label"] in ["positive", "negative"]
49+
assert isinstance(data["confidence"], float)
50+
assert data["model"] != ""
51+
52+
53+
# ---------------- Benchmark simple ----------------
54+
55+
# def test_predict_benchmark():
56+
# """Benchmark prédiction sur un texte moyen"""
57+
# payload = {"text": "This is a test sentence for benchmarking the sentiment API."}
58+
# start_time = time.time()
59+
# response = client.post("/predict", json=payload)
60+
# duration_ms = (time.time() - start_time) * 1000
61+
# assert response.status_code == 200
62+
# print(f"Benchmark: prediction took {duration_ms:.2f} ms")
63+
# assert duration_ms < 2000 # par exemple < 2s pour CPU

0 commit comments

Comments
 (0)