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

11 KiB
Executable File
Raw Permalink Blame History

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

ALTER TABLE peripheral_photos ADD COLUMN thumbnail_path TEXT;

Script application : backend/apply_migration_009.py

# 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)

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)

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)

# 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)

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)

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

# 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

# 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

curl http://10.0.0.50:8007/api/peripherals/3/photos | python3 -m json.tool

Résultat attendu :

[
  {
    "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

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 :

// 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) :

// 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 :

# 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)