Files
serv_benchmark/docs/SESSION_2025-12-31_THUMBNAILS.md
Gilles Soulier c67befc549 addon
2026-01-05 16:08:01 +01:00

394 lines
11 KiB
Markdown
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Session 2025-12-31 : Génération automatique de miniatures
## 🎯 Objectif
Générer automatiquement des miniatures (thumbnails) lors de l'upload de photos pour optimiser le chargement des listes de périphériques.
## 📊 État actuel
### Avant
- ✅ Photos stockées et redimensionnées
- ❌ Pas de miniatures générées
- ❌ Liste charge les images pleines résolution (lent)
- ❌ Pas de champ `thumbnail_path` en base de données
### Architecture actuelle
```
Upload photo → process_image() → Stockage image redimensionnée → BDD (stored_path)
```
## ✅ Implémentation
### 1. Migration base de données (009)
**Fichier** : `backend/migrations/009_add_thumbnail_path.sql`
```sql
ALTER TABLE peripheral_photos ADD COLUMN thumbnail_path TEXT;
```
**Script application** : `backend/apply_migration_009.py`
```bash
# En local
cd backend
python3 apply_migration_009.py
# Dans Docker
docker exec linux_benchtools_backend python3 -c "
import sqlite3
conn = sqlite3.connect('/app/data/peripherals.db')
cursor = conn.cursor()
cursor.execute('ALTER TABLE peripheral_photos ADD COLUMN thumbnail_path TEXT')
conn.commit()
conn.close()
"
```
### 2. Modèle de données
**Fichier** : `backend/app/models/peripheral.py` (ligne 147)
```python
class PeripheralPhoto(BasePeripherals):
id = Column(Integer, primary_key=True)
peripheral_id = Column(Integer, nullable=False, index=True)
filename = Column(String(255), nullable=False)
stored_path = Column(String(500), nullable=False)
thumbnail_path = Column(String(500)) # ← NOUVEAU
mime_type = Column(String(100))
size_bytes = Column(Integer)
uploaded_at = Column(DateTime, server_default=func.now())
description = Column(Text)
is_primary = Column(Boolean, default=False)
```
### 3. Schéma API
**Fichier** : `backend/app/schemas/peripheral.py` (ligne 202)
```python
class PeripheralPhotoSchema(PeripheralPhotoBase):
id: int
peripheral_id: int
filename: str
stored_path: str
thumbnail_path: Optional[str] # ← NOUVEAU
mime_type: Optional[str]
size_bytes: Optional[int]
uploaded_at: datetime
```
### 4. Endpoint upload (génération)
**Fichier** : `backend/app/api/endpoints/peripherals.py` (lignes 292-320)
```python
# Process image (main + thumbnail)
try:
# Process main image with level configuration
processed_path, file_size, original_path = ImageProcessor.process_image_with_level(
image_path=temp_path,
output_dir=upload_dir,
compression_level="medium", # Niveau medium par défaut
save_original=True
)
mime_type = ImageProcessor.get_mime_type(processed_path)
# Generate thumbnail
thumbnail_path, thumbnail_size = ImageProcessor.create_thumbnail_with_level(
image_path=temp_path,
output_dir=upload_dir,
compression_level="medium"
)
# Create database entry
photo = PeripheralPhoto(
peripheral_id=peripheral_id,
filename=os.path.basename(processed_path),
stored_path=processed_path,
thumbnail_path=thumbnail_path, # ← NOUVEAU
mime_type=mime_type,
size_bytes=file_size,
description=description,
is_primary=is_primary
)
```
### 5. Endpoint GET (conversion chemins web)
**Fichier** : `backend/app/api/endpoints/peripherals.py` (ligne 358)
```python
photo_dict = {
"id": photo.id,
"peripheral_id": photo.peripheral_id,
"filename": photo.filename,
"stored_path": photo.stored_path.replace('/app/uploads/', '/uploads/')
if photo.stored_path.startswith('/app/uploads/')
else photo.stored_path,
"thumbnail_path": photo.thumbnail_path.replace('/app/uploads/', '/uploads/')
if photo.thumbnail_path and photo.thumbnail_path.startswith('/app/uploads/')
else photo.thumbnail_path, # ← NOUVEAU
"mime_type": photo.mime_type,
# ...
}
```
## 📁 Structure des fichiers
### Arborescence créée lors de l'upload
```
uploads/peripherals/photos/{peripheral_id}/
├── original/
│ └── photo_originale.webp (fichier source non modifié)
├── photo_redimensionnee.png (1920x1080 @ 85% qualité)
└── thumbnail/
└── thumb_photo_redimensionnee.png (300x300 @ 75% qualité)
```
### Chemins stockés en base de données
| Champ | Valeur (filesystem) | Valeur (API web) |
|-------|---------------------|------------------|
| `stored_path` | `/app/uploads/peripherals/photos/3/image.png` | `/uploads/peripherals/photos/3/image.png` |
| `thumbnail_path` | `/app/uploads/peripherals/photos/3/thumbnail/thumb_image.png` | `/uploads/peripherals/photos/3/thumbnail/thumb_image.png` |
## ⚙️ Configuration compression
**Fichier** : `config/image_compression.yaml`
### Niveau "medium" (par défaut)
```yaml
medium:
enabled: true
quality: 85 # Image principale
max_width: 1920
max_height: 1080
thumbnail_size: 300 # Miniature 300x300px
thumbnail_quality: 75 # Qualité miniature
description: "Qualité moyenne - Usage général"
```
### Tous les niveaux disponibles
| Niveau | Image principale | Thumbnail | Usage |
|--------|------------------|-----------|-------|
| **high** | 2560×1920 @ 92% | 400px @ 85% | Photos importantes |
| **medium** | 1920×1080 @ 85% | 300px @ 75% | Usage général ⭐ |
| **low** | 1280×720 @ 75% | 200px @ 65% | Économie d'espace |
| **minimal** | 800×600 @ 65% | 150px @ 55% | Aperçu seulement |
## 🔄 Flux complet
```
1. User upload photo.jpg
2. Backend reçoit le fichier
3. ImageProcessor.process_image_with_level()
│ ├─> Copie originale → original/photo.jpg
│ └─> Resize + compress → photo.png (1920x1080)
4. ImageProcessor.create_thumbnail_with_level()
└─> Resize + compress → thumbnail/thumb_photo.png (300x300)
5. Stockage en BDD
├─> stored_path: /app/uploads/.../photo.png
└─> thumbnail_path: /app/uploads/.../thumbnail/thumb_photo.png
6. API GET /peripherals/{id}/photos
├─> Conversion: /app/uploads/... → /uploads/...
└─> Retour JSON avec stored_path ET thumbnail_path
7. Frontend charge la miniature dans la liste
└─> <img src="/uploads/.../thumbnail/thumb_photo.png">
```
## 📊 Gain de performance
### Avant (sans thumbnails)
- Liste 10 périphériques avec photos
- Charge 10 images × ~500 KB = **~5 MB**
- Temps de chargement : **2-3 secondes** (connexion moyenne)
### Après (avec thumbnails)
- Liste 10 périphériques avec miniatures
- Charge 10 thumbnails × ~20 KB = **~200 KB**
- Temps de chargement : **<300ms** (connexion moyenne)
**Gain** : **~96% de données en moins** pour l'affichage de la liste
## 🧪 Test de validation
### 1. Upload une nouvelle photo
```bash
# Via API (avec curl)
curl -X POST "http://10.0.0.50:8007/api/peripherals/3/photos" \
-H "X-API-Token: YOUR_TOKEN" \
-F "file=@test_image.jpg" \
-F "is_primary=false"
```
### 2. Vérifier les fichiers générés
```bash
# Dans le conteneur backend
docker exec linux_benchtools_backend ls -lh /app/uploads/peripherals/photos/3/
# Devrait afficher :
# - original/test_image.jpg
# - test_image.png (image redimensionnée)
# - thumbnail/thumb_test_image.png (miniature)
```
### 3. Vérifier l'API
```bash
curl http://10.0.0.50:8007/api/peripherals/3/photos | python3 -m json.tool
```
**Résultat attendu** :
```json
[
{
"id": 5,
"peripheral_id": 3,
"filename": "test_image.png",
"stored_path": "/uploads/peripherals/photos/3/test_image.png",
"thumbnail_path": "/uploads/peripherals/photos/3/thumbnail/thumb_test_image.png",
"mime_type": "image/png",
"size_bytes": 450000,
"uploaded_at": "2025-12-31T10:00:00"
}
]
```
### 4. Vérifier nginx sert la miniature
```bash
curl -I http://10.0.0.50:8087/uploads/peripherals/photos/3/thumbnail/thumb_test_image.png
```
**Résultat attendu** :
```
HTTP/1.1 200 OK
Content-Type: image/png
```
## 🎨 Frontend (à implémenter)
### Liste des périphériques
**Utiliser `thumbnail_path` au lieu de `stored_path`** :
```javascript
// peripherals.js
function renderPeripheralCard(peripheral, photo) {
const imageUrl = photo.thumbnail_path || photo.stored_path || '/img/no-image.png';
return `
<div class="peripheral-card">
<img src="${escapeHtml(imageUrl)}"
alt="${escapeHtml(peripheral.nom)}"
loading="lazy">
...
</div>
`;
}
```
### Page détail du périphérique
**Utiliser `stored_path` (pleine résolution)** :
```javascript
// peripheral-detail.js
function displayPhotos(photos) {
grid.innerHTML = photos.map(photo => `
<div class="photo-item">
<img src="${photo.stored_path}"
alt="${escapeHtml(photo.description || 'Photo')}"
data-thumbnail="${photo.thumbnail_path}">
</div>
`).join('');
}
```
## 📝 Notes techniques
### Formats supportés
**Entrée** (config) :
- jpg, jpeg, png, webp
**Sortie** (config) :
- png (par défaut, configurable dans `image_compression.yaml`)
### Nommage des fichiers
**Préfixe miniature** : `thumb_` (configurable dans `image_compression.yaml`)
**Exemples** :
- Image : `photo_20251231_120000.png`
- Thumbnail : `thumb_photo_20251231_120000.png`
### Rétrocompatibilité
Les anciennes photos (sans `thumbnail_path`) :
- Continueront de fonctionner
- Frontend utilise fallback : `thumbnail_path || stored_path`
- Possibilité de régénérer les miniatures via script migration
## 🔧 Script de migration (optionnel)
Pour régénérer les miniatures des anciennes photos :
```python
# regenerate_thumbnails.py
from app.models.peripheral import PeripheralPhoto
from app.utils.image_processor import ImageProcessor
from app.db.session import get_peripherals_db
db = next(get_peripherals_db())
photos = db.query(PeripheralPhoto).filter(
PeripheralPhoto.thumbnail_path.is_(None)
).all()
for photo in photos:
if os.path.exists(photo.stored_path):
upload_dir = os.path.dirname(photo.stored_path)
thumbnail_path, _ = ImageProcessor.create_thumbnail_with_level(
image_path=photo.stored_path,
output_dir=upload_dir,
compression_level="medium"
)
photo.thumbnail_path = thumbnail_path
db.commit()
print(f"✅ Thumbnail generated for photo {photo.id}")
```
## 📋 Fichiers modifiés/créés
### Créés
-`backend/migrations/009_add_thumbnail_path.sql`
-`backend/apply_migration_009.py`
-`docs/SESSION_2025-12-31_THUMBNAILS.md` (ce fichier)
### Modifiés
-`backend/app/models/peripheral.py` - Ajout champ `thumbnail_path`
-`backend/app/schemas/peripheral.py` - Ajout champ dans schema
-`backend/app/api/endpoints/peripherals.py` - Génération thumbnail + conversion web paths
### Configuration existante
-`config/image_compression.yaml` - Configuration déjà en place
---
**Date** : 31 décembre 2025
**Statut** : ✅ Backend implémenté et testé
**Reste à faire** : Frontend (utiliser `thumbnail_path` dans les listes)