Spaces:
Sleeping
Sleeping
Ludovic commited on
Commit ·
5e23602
1
Parent(s): 83b2bcc
Déploiement propre initial sans fichiers volumineux
Browse files- # +0 -0
- .DS_Store +0 -0
- Dockerfile +27 -0
- IA +0 -0
- Pour +0 -0
- README.md +0 -11
- app/.DS_Store +0 -0
- app/__init__.py +0 -0
- app/main.py +179 -0
- app/models.py +0 -0
- app/processing.py +195 -0
- app/static/css/style.css +171 -0
- app/static/js/script.js +72 -0
- app/templates/index.html +41 -0
- app/utils.py +26 -0
- configuration +0 -0
- du +0 -0
- environment.yml +23 -0
- la +0 -0
- modèle +0 -0
- outputs/.DS_Store +0 -0
- outputs/captions/.DS_Store +0 -0
- outputs/images/.DS_Store +0 -0
- requirements.txt +20 -0
#
ADDED
|
File without changes
|
.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
Dockerfile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Utiliser une image Python officielle comme base
|
| 2 |
+
FROM python:3.9-slim
|
| 3 |
+
|
| 4 |
+
# Définir le répertoire de travail dans le conteneur
|
| 5 |
+
WORKDIR /code
|
| 6 |
+
|
| 7 |
+
# Copier le fichier des dépendances
|
| 8 |
+
COPY ./requirements.txt /code/requirements.txt
|
| 9 |
+
|
| 10 |
+
# Mettre à jour pip et installer les dépendances
|
| 11 |
+
# --no-cache-dir réduit la taille de l'image
|
| 12 |
+
# --default-timeout augmente le délai pour les gros téléchargements comme torch
|
| 13 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 14 |
+
pip install --no-cache-dir --default-timeout=300 -r /code/requirements.txt
|
| 15 |
+
|
| 16 |
+
# Copier le reste du code de l'application
|
| 17 |
+
# On copie le dossier 'app' qui contient main.py, processing.py, etc.
|
| 18 |
+
# et les dossiers static/templates
|
| 19 |
+
COPY ./app /code/app
|
| 20 |
+
|
| 21 |
+
# Le port sur lequel Uvicorn écoutera.
|
| 22 |
+
# Hugging Face Spaces injecte la variable $PORT (souvent 7860 par défaut).
|
| 23 |
+
# EXPOSE ${PORT:-7860} # Pas strictement nécessaire car Spaces gère le mapping
|
| 24 |
+
|
| 25 |
+
# Commande pour lancer l'application
|
| 26 |
+
# Uvicorn écoutera sur toutes les interfaces (0.0.0.0) sur le port fourni par Spaces.
|
| 27 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "${PORT:-7860}"]
|
IA
ADDED
|
File without changes
|
Pour
ADDED
|
File without changes
|
README.md
CHANGED
|
@@ -1,11 +0,0 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Autocaption App
|
| 3 |
-
emoji: 🚀
|
| 4 |
-
colorFrom: indigo
|
| 5 |
-
colorTo: indigo
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
short_description: Création de description d'images
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
app/__init__.py
ADDED
|
File without changes
|
app/main.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import shutil
|
| 3 |
+
import uuid
|
| 4 |
+
import secrets # Pour la comparaison sécurisée des identifiants
|
| 5 |
+
|
| 6 |
+
from fastapi import Depends, FastAPI, File, UploadFile, Request, HTTPException, status
|
| 7 |
+
from fastapi.responses import HTMLResponse, JSONResponse
|
| 8 |
+
from fastapi.staticfiles import StaticFiles
|
| 9 |
+
from fastapi.templating import Jinja2Templates
|
| 10 |
+
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
| 11 |
+
from passlib.context import CryptContext # Pour hacher les mots de passe
|
| 12 |
+
from typing import List
|
| 13 |
+
|
| 14 |
+
from . import processing # Contient la logique du modèle
|
| 15 |
+
from . import utils
|
| 16 |
+
|
| 17 |
+
# Configuration de l'application FastAPI
|
| 18 |
+
app = FastAPI(title="AutoCaption IA - Studio Luxe")
|
| 19 |
+
|
| 20 |
+
# Configuration pour l'authentification Basic Auth
|
| 21 |
+
security = HTTPBasic()
|
| 22 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 23 |
+
|
| 24 |
+
# Lire les identifiants depuis les variables d'environnement (pour Hugging Face Spaces Secrets)
|
| 25 |
+
# Fournir des valeurs par défaut UNIQUEMENT pour le test local si les variables ne sont pas définies.
|
| 26 |
+
# EN PRODUCTION SUR SPACES, CES VALEURS SERONT IGNORÉES AU PROFIT DES SECRETS DU SPACE.
|
| 27 |
+
APP_USERNAME_DEFAULT = "admin"
|
| 28 |
+
APP_PASSWORD_DEFAULT = "changezceci" # Changez ce mot de passe par défaut si vous testez localement
|
| 29 |
+
|
| 30 |
+
APP_USERNAME = os.environ.get("APP_USERNAME", APP_USERNAME_DEFAULT)
|
| 31 |
+
APP_PASSWORD_RAW = os.environ.get("APP_PASSWORD", APP_PASSWORD_DEFAULT)
|
| 32 |
+
|
| 33 |
+
# Hasher le mot de passe une seule fois au démarrage si disponible
|
| 34 |
+
CORRECT_PASSWORD_HASH = pwd_context.hash(APP_PASSWORD_RAW) if APP_PASSWORD_RAW else None
|
| 35 |
+
|
| 36 |
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
| 37 |
+
if hashed_password is None:
|
| 38 |
+
return False
|
| 39 |
+
return pwd_context.verify(plain_password, hashed_password)
|
| 40 |
+
|
| 41 |
+
async def get_current_username(credentials: HTTPBasicCredentials = Depends(security)):
|
| 42 |
+
current_username_bytes = credentials.username.encode("utf8")
|
| 43 |
+
correct_username_bytes = APP_USERNAME.encode("utf8")
|
| 44 |
+
|
| 45 |
+
is_correct_username = secrets.compare_digest(current_username_bytes, correct_username_bytes)
|
| 46 |
+
is_correct_password = verify_password(credentials.password, CORRECT_PASSWORD_HASH)
|
| 47 |
+
|
| 48 |
+
if not (is_correct_username and is_correct_password):
|
| 49 |
+
raise HTTPException(
|
| 50 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 51 |
+
detail="Nom d'utilisateur ou mot de passe incorrect",
|
| 52 |
+
headers={"WWW-Authenticate": "Basic"},
|
| 53 |
+
)
|
| 54 |
+
return credentials.username
|
| 55 |
+
|
| 56 |
+
# Configuration des chemins
|
| 57 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) # Répertoire /app
|
| 58 |
+
PROJECT_ROOT = os.path.dirname(BASE_DIR) # Répertoire AUTOCAPTION
|
| 59 |
+
|
| 60 |
+
UPLOAD_DIR = os.path.join(PROJECT_ROOT, "uploads")
|
| 61 |
+
OUTPUT_IMAGE_DIR = os.path.join(PROJECT_ROOT, "outputs", "images")
|
| 62 |
+
OUTPUT_CAPTION_DIR = os.path.join(PROJECT_ROOT, "outputs", "captions")
|
| 63 |
+
|
| 64 |
+
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
| 65 |
+
os.makedirs(OUTPUT_IMAGE_DIR, exist_ok=True)
|
| 66 |
+
os.makedirs(OUTPUT_CAPTION_DIR, exist_ok=True)
|
| 67 |
+
|
| 68 |
+
app.mount("/static", StaticFiles(directory=os.path.join(BASE_DIR, "static")), name="static")
|
| 69 |
+
templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates"))
|
| 70 |
+
|
| 71 |
+
# Pré-chargement optionnel du modèle (commenté par défaut pour Spaces pour un démarrage plus rapide du conteneur)
|
| 72 |
+
# print("Tentative de pré-chargement du modèle actif au démarrage de l'application...")
|
| 73 |
+
# try:
|
| 74 |
+
# processing.load_active_model()
|
| 75 |
+
# if processing.is_active_model_loaded():
|
| 76 |
+
# print(f"Modèle actif ({processing.ACTIVE_MODEL}) pré-chargé avec succès.")
|
| 77 |
+
# else:
|
| 78 |
+
# print(f"AVERTISSEMENT: Le modèle actif ({processing.ACTIVE_MODEL}) n'a pas pu être pré-chargé.")
|
| 79 |
+
# except Exception as e:
|
| 80 |
+
# print(f"AVERTISSEMENT: Erreur lors du pré-chargement du modèle actif ({processing.ACTIVE_MODEL}): {e}")
|
| 81 |
+
|
| 82 |
+
@app.get("/", response_class=HTMLResponse)
|
| 83 |
+
async def get_root(request: Request, current_user: str = Depends(get_current_username)):
|
| 84 |
+
return templates.TemplateResponse("index.html", {"request": request, "title": "Générateur de Descriptions IA"})
|
| 85 |
+
|
| 86 |
+
@app.post("/api/upload-images/")
|
| 87 |
+
async def upload_images_for_captioning(
|
| 88 |
+
files: List[UploadFile] = File(...),
|
| 89 |
+
current_user: str = Depends(get_current_username) # Authentification
|
| 90 |
+
):
|
| 91 |
+
if not files:
|
| 92 |
+
raise HTTPException(status_code=400, detail="Aucun fichier n'a été téléversé.")
|
| 93 |
+
|
| 94 |
+
processed_files_info = []
|
| 95 |
+
|
| 96 |
+
if not processing.is_active_model_loaded():
|
| 97 |
+
print(f"Le modèle actif ({processing.ACTIVE_MODEL}) n'est pas chargé. Tentative de chargement maintenant...")
|
| 98 |
+
try:
|
| 99 |
+
processing.load_active_model()
|
| 100 |
+
if not processing.is_active_model_loaded():
|
| 101 |
+
raise HTTPException(status_code=503, detail=f"Le modèle IA ({processing.ACTIVE_MODEL}) n'a pas pu être chargé (vérification post-tentative).")
|
| 102 |
+
print(f"Modèle actif ({processing.ACTIVE_MODEL}) chargé avec succès à la demande.")
|
| 103 |
+
except Exception as e:
|
| 104 |
+
error_detail_str = str(e) if str(e) else f"Le modèle IA ({processing.ACTIVE_MODEL}) n'a pas pu être chargé."
|
| 105 |
+
raise HTTPException(status_code=503, detail=f"Erreur serveur critique : {error_detail_str}")
|
| 106 |
+
|
| 107 |
+
for file in files:
|
| 108 |
+
temp_file_path = None
|
| 109 |
+
try:
|
| 110 |
+
if not file.content_type or not file.content_type.startswith("image/"):
|
| 111 |
+
print(f"Fichier ignoré (type non supporté: {file.content_type}): {file.filename}")
|
| 112 |
+
continue
|
| 113 |
+
|
| 114 |
+
unique_base_name = utils.generate_simple_unique_name()
|
| 115 |
+
original_extension = os.path.splitext(file.filename)[1].lower()
|
| 116 |
+
|
| 117 |
+
if not original_extension and file.content_type: # Déduction d'extension
|
| 118 |
+
ext_map = {"image/jpeg": ".jpg", "image/png": ".png", "image/gif": ".gif", "image/webp": ".webp"}
|
| 119 |
+
original_extension = ext_map.get(file.content_type, ".img")
|
| 120 |
+
elif not original_extension:
|
| 121 |
+
original_extension = ".jpg"
|
| 122 |
+
|
| 123 |
+
unique_image_name = f"{unique_base_name}{original_extension}"
|
| 124 |
+
unique_caption_name = f"{unique_base_name}.txt"
|
| 125 |
+
|
| 126 |
+
temp_upload_filename = f"temp_{uuid.uuid4().hex}{original_extension}" # Nom temporaire unique
|
| 127 |
+
temp_file_path = os.path.join(UPLOAD_DIR, temp_upload_filename)
|
| 128 |
+
|
| 129 |
+
with open(temp_file_path, "wb") as buffer:
|
| 130 |
+
shutil.copyfileobj(file.file, buffer)
|
| 131 |
+
|
| 132 |
+
image_description = "Description non générée par défaut."
|
| 133 |
+
if processing.is_active_model_loaded():
|
| 134 |
+
print(f"Génération de description pour {temp_file_path} avec le modèle {processing.ACTIVE_MODEL}")
|
| 135 |
+
image_description = processing.generate_active_description(temp_file_path)
|
| 136 |
+
else:
|
| 137 |
+
print(f"ERREUR: Tentative de génération alors que le modèle {processing.ACTIVE_MODEL} n'est pas chargé.")
|
| 138 |
+
image_description = f"ERREUR CRITIQUE: Le modèle IA ({processing.ACTIVE_MODEL}) n'est pas disponible."
|
| 139 |
+
|
| 140 |
+
output_image_path = os.path.join(OUTPUT_IMAGE_DIR, unique_image_name)
|
| 141 |
+
shutil.copy(temp_file_path, output_image_path)
|
| 142 |
+
|
| 143 |
+
output_caption_path = os.path.join(OUTPUT_CAPTION_DIR, unique_caption_name)
|
| 144 |
+
with open(output_caption_path, "w", encoding="utf-8") as caption_file:
|
| 145 |
+
caption_file.write(image_description)
|
| 146 |
+
|
| 147 |
+
processed_files_info.append({
|
| 148 |
+
"original_name": file.filename,
|
| 149 |
+
"image_name": unique_image_name,
|
| 150 |
+
"caption_name": unique_caption_name,
|
| 151 |
+
"description_preview": (image_description[:100] + "..." if image_description and len(image_description) > 100 else image_description) or "Vide"
|
| 152 |
+
})
|
| 153 |
+
|
| 154 |
+
except HTTPException: # Laisser remonter les erreurs HTTP (ex: 503 du chargement modèle)
|
| 155 |
+
raise
|
| 156 |
+
except Exception as e:
|
| 157 |
+
print(f"Erreur inattendue lors du traitement du fichier {file.filename}: {e}")
|
| 158 |
+
processing.traceback.print_exc() # Afficher la trace complète pour les erreurs inattendues
|
| 159 |
+
finally:
|
| 160 |
+
if hasattr(file, 'file') and file.file and not file.file.closed:
|
| 161 |
+
file.file.close()
|
| 162 |
+
if temp_file_path and os.path.exists(temp_file_path):
|
| 163 |
+
try:
|
| 164 |
+
os.remove(temp_file_path)
|
| 165 |
+
except Exception as e_remove:
|
| 166 |
+
print(f"Erreur lors de la suppression du fichier temporaire {temp_file_path}: {e_remove}")
|
| 167 |
+
|
| 168 |
+
return JSONResponse(
|
| 169 |
+
content={
|
| 170 |
+
"message": f"{len(processed_files_info)} image(s) traitée(s) avec succès sur {len(files)} fichier(s) reçu(s).",
|
| 171 |
+
"processed_files": processed_files_info
|
| 172 |
+
},
|
| 173 |
+
status_code=200
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
if __name__ == "__main__":
|
| 177 |
+
print("Pour lancer l'application, utilisez la commande : uvicorn app.main:app --host 0.0.0.0 --port 8000")
|
| 178 |
+
# import uvicorn
|
| 179 |
+
# uvicorn.run(app, host="127.0.0.1", port=8000) # Pour test local direct
|
app/models.py
ADDED
|
File without changes
|
app/processing.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from PIL import Image, ImageDraw # ImageDraw pour la section de test
|
| 3 |
+
import os
|
| 4 |
+
import traceback
|
| 5 |
+
|
| 6 |
+
# Imports spécifiques pour LLaVA
|
| 7 |
+
from transformers import LlavaNextProcessor, LlavaNextForConditionalGeneration
|
| 8 |
+
|
| 9 |
+
# --- Configuration du modèle LLaVA-NeXT ---
|
| 10 |
+
LLAVA_MODEL_NAME = "llava-hf/llava-v1.6-mistral-7b-hf"
|
| 11 |
+
# Hash de commit de la branche 'main' de LLaVA au moment des tests.
|
| 12 |
+
# Vérifiez le plus récent sur https://huggingface.co/llava-hf/llava-v1.6-mistral-7b-hf/commits/main
|
| 13 |
+
LLAVA_REVISION = "082142fd2997099498027732cf8e945044bf48c3"
|
| 14 |
+
|
| 15 |
+
llava_processor = None
|
| 16 |
+
llava_model = None
|
| 17 |
+
llava_model_loaded = False
|
| 18 |
+
|
| 19 |
+
# Détection du device (CPU, CUDA, ou MPS pour Mac Apple Silicon)
|
| 20 |
+
if torch.cuda.is_available():
|
| 21 |
+
device = "cuda"
|
| 22 |
+
elif torch.backends.mps.is_available() and torch.backends.mps.is_built():
|
| 23 |
+
device = "mps"
|
| 24 |
+
else:
|
| 25 |
+
device = "cpu"
|
| 26 |
+
print(f"Utilisation du device : {device} pour les modèles d'IA.")
|
| 27 |
+
|
| 28 |
+
def load_llava_model():
|
| 29 |
+
global llava_processor, llava_model, llava_model_loaded, device
|
| 30 |
+
if llava_model_loaded:
|
| 31 |
+
print(f"Modèle LLaVA ({LLAVA_MODEL_NAME} rev {LLAVA_REVISION}) déjà chargé.")
|
| 32 |
+
return
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
print(f"Chargement du processor pour LLaVA ({LLAVA_MODEL_NAME} rev {LLAVA_REVISION})...")
|
| 36 |
+
llava_processor = LlavaNextProcessor.from_pretrained(
|
| 37 |
+
LLAVA_MODEL_NAME,
|
| 38 |
+
revision=LLAVA_REVISION # Épinglage de la révision
|
| 39 |
+
)
|
| 40 |
+
print("Processor LLaVA chargé.")
|
| 41 |
+
|
| 42 |
+
print(f"Chargement du modèle LLaVA ({LLAVA_MODEL_NAME} rev {LLAVA_REVISION}) sur '{device}'...")
|
| 43 |
+
model_args = {
|
| 44 |
+
"revision": LLAVA_REVISION, # Épinglage de la révision
|
| 45 |
+
"low_cpu_mem_usage": True,
|
| 46 |
+
}
|
| 47 |
+
if device == "cpu":
|
| 48 |
+
# Pas de torch_dtype pour CPU, utilise float32 par défaut pour plus de stabilité
|
| 49 |
+
print(f"Configuration de LLaVA pour CPU (float32 par défaut).")
|
| 50 |
+
elif device == "cuda":
|
| 51 |
+
model_args["torch_dtype"] = torch.float16 # ou torch.bfloat16 si GPU récent (Ampere+)
|
| 52 |
+
print(f"Configuration de LLaVA pour CUDA ({model_args['torch_dtype']}).")
|
| 53 |
+
elif device == "mps":
|
| 54 |
+
# Pour MPS, float16 est souvent utilisé, mais float32 est plus sûr pour commencer.
|
| 55 |
+
# Laisser float32 par défaut (pas de torch_dtype) est une option.
|
| 56 |
+
# Ou essayez float16 :
|
| 57 |
+
model_args["torch_dtype"] = torch.float16
|
| 58 |
+
print(f"Configuration de LLaVA pour MPS ({model_args['torch_dtype']}).")
|
| 59 |
+
|
| 60 |
+
llava_model = LlavaNextForConditionalGeneration.from_pretrained(
|
| 61 |
+
LLAVA_MODEL_NAME,
|
| 62 |
+
**model_args
|
| 63 |
+
).to(device).eval()
|
| 64 |
+
|
| 65 |
+
llava_model_loaded = True
|
| 66 |
+
print(f"Modèle LLaVA ({LLAVA_MODEL_NAME} rev {LLAVA_REVISION}) chargé avec succès sur '{device}'.")
|
| 67 |
+
|
| 68 |
+
except Exception as e:
|
| 69 |
+
print(f"Erreur critique lors du chargement du modèle LLaVA ({LLAVA_MODEL_NAME} rev {LLAVA_REVISION}): {e}")
|
| 70 |
+
traceback.print_exc()
|
| 71 |
+
llava_model_loaded = False
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def generate_description_llava(image_path: str) -> str:
|
| 75 |
+
global llava_processor, llava_model, llava_model_loaded, device
|
| 76 |
+
|
| 77 |
+
if not llava_model_loaded:
|
| 78 |
+
print("Modèle LLaVA non chargé. Tentative de chargement...")
|
| 79 |
+
load_llava_model()
|
| 80 |
+
if not llava_model_loaded:
|
| 81 |
+
return "Erreur: Le modèle LLaVA n'a pas pu être chargé."
|
| 82 |
+
|
| 83 |
+
if not os.path.exists(image_path):
|
| 84 |
+
return f"Erreur: Le fichier image {image_path} n'existe pas."
|
| 85 |
+
|
| 86 |
+
try:
|
| 87 |
+
image = Image.open(image_path).convert("RGB")
|
| 88 |
+
|
| 89 |
+
# Choix du prompt (anglais par défaut, comme demandé)
|
| 90 |
+
user_prompt = "Describe this image in English with precision and detail."
|
| 91 |
+
# user_prompt = "Décris cette image en français avec précision et de manière détaillée." # Si vous voulez du français
|
| 92 |
+
|
| 93 |
+
prompt_text = f"<s>[INST] <image>\n{user_prompt} [/INST]"
|
| 94 |
+
|
| 95 |
+
print(f"Préparation des entrées pour LLaVA avec le prompt: {user_prompt}")
|
| 96 |
+
inputs_on_cpu = llava_processor(text=prompt_text, images=image, return_tensors="pt")
|
| 97 |
+
|
| 98 |
+
inputs = {}
|
| 99 |
+
for key, value in inputs_on_cpu.items():
|
| 100 |
+
if torch.is_tensor(value):
|
| 101 |
+
inputs[key] = value.to(device)
|
| 102 |
+
else:
|
| 103 |
+
inputs[key] = value
|
| 104 |
+
|
| 105 |
+
if (device == "cuda" or device == "mps") and hasattr(llava_model, 'dtype') and \
|
| 106 |
+
(llava_model.dtype == torch.float16 or llava_model.dtype == torch.bfloat16):
|
| 107 |
+
for k_tensor, v_tensor in inputs.items():
|
| 108 |
+
if torch.is_tensor(v_tensor) and torch.is_floating_point(v_tensor):
|
| 109 |
+
inputs[k_tensor] = v_tensor.to(llava_model.dtype)
|
| 110 |
+
|
| 111 |
+
input_dtypes_log = {k: v.dtype for k,v in inputs.items() if torch.is_tensor(v)}
|
| 112 |
+
print(f"Génération de la description LLaVA pour {image_path} (device: {device}, input dtypes: {input_dtypes_log})...")
|
| 113 |
+
|
| 114 |
+
generation_kwargs = {
|
| 115 |
+
"max_new_tokens": 768,
|
| 116 |
+
"num_beams": 3,
|
| 117 |
+
"early_stopping": True
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
generated_ids = llava_model.generate(**inputs, **generation_kwargs)
|
| 121 |
+
|
| 122 |
+
input_token_len = inputs.get('input_ids', torch.tensor([])).shape[-1]
|
| 123 |
+
generated_ids_only = generated_ids[0, input_token_len:]
|
| 124 |
+
|
| 125 |
+
cleaned_text = llava_processor.decode(generated_ids_only, skip_special_tokens=True).strip()
|
| 126 |
+
|
| 127 |
+
# Nettoyage supplémentaire si nécessaire (ex: enlever des marqueurs résiduels)
|
| 128 |
+
inst_marker_space = " [/INST]" # Avec espace avant, comme souvent produit
|
| 129 |
+
inst_marker_no_space = "[/INST]"
|
| 130 |
+
if cleaned_text.startswith(inst_marker_space):
|
| 131 |
+
cleaned_text = cleaned_text[len(inst_marker_space):].strip()
|
| 132 |
+
elif cleaned_text.startswith(inst_marker_no_space):
|
| 133 |
+
cleaned_text = cleaned_text[len(inst_marker_no_space):].strip()
|
| 134 |
+
|
| 135 |
+
print(f"Description (nettoyée) de LLaVA: {cleaned_text}")
|
| 136 |
+
return cleaned_text if cleaned_text and cleaned_text.strip() else "Aucune description textuelle distincte n'a été générée par LLaVA."
|
| 137 |
+
|
| 138 |
+
except Exception as e:
|
| 139 |
+
print(f"Erreur détaillée lors de la génération de la description avec LLaVA:")
|
| 140 |
+
traceback.print_exc()
|
| 141 |
+
if torch.cuda.is_available() or device == "mps": # Vider le cache si GPU/MPS
|
| 142 |
+
if device == "cuda": torch.cuda.empty_cache()
|
| 143 |
+
# if device == "mps": torch.mps.empty_cache() # Si disponible et nécessaire
|
| 144 |
+
return f"Erreur lors de la génération de la description avec LLaVA: {type(e).__name__} - {str(e)}"
|
| 145 |
+
|
| 146 |
+
ACTIVE_MODEL = "llava"
|
| 147 |
+
|
| 148 |
+
def load_active_model():
|
| 149 |
+
print(f"Tentative de chargement du modèle actif: {ACTIVE_MODEL}")
|
| 150 |
+
if ACTIVE_MODEL == "llava":
|
| 151 |
+
load_llava_model()
|
| 152 |
+
else:
|
| 153 |
+
print(f"Modèle actif inconnu: {ACTIVE_MODEL}. Aucun modèle ne sera chargé.")
|
| 154 |
+
|
| 155 |
+
def generate_active_description(image_path: str) -> str:
|
| 156 |
+
if ACTIVE_MODEL == "llava":
|
| 157 |
+
return generate_description_llava(image_path)
|
| 158 |
+
else:
|
| 159 |
+
error_msg = f"Erreur: Modèle actif inconnu ({ACTIVE_MODEL}). Impossible de générer une description."
|
| 160 |
+
print(error_msg)
|
| 161 |
+
return error_msg
|
| 162 |
+
|
| 163 |
+
def is_active_model_loaded() -> bool:
|
| 164 |
+
if ACTIVE_MODEL == "llava":
|
| 165 |
+
return llava_model_loaded
|
| 166 |
+
return False
|
| 167 |
+
|
| 168 |
+
if __name__ == '__main__':
|
| 169 |
+
print("Début du test de processing.py...")
|
| 170 |
+
dummy_image_name = "dummy_test_image.png"
|
| 171 |
+
if not os.path.exists(dummy_image_name):
|
| 172 |
+
try:
|
| 173 |
+
img = Image.new('RGB', (200, 150), color = 'skyblue')
|
| 174 |
+
draw = ImageDraw.Draw(img)
|
| 175 |
+
draw.text((10, 10), "Test Image", fill='black')
|
| 176 |
+
img.save(dummy_image_name)
|
| 177 |
+
print(f"Image de test '{dummy_image_name}' créée.")
|
| 178 |
+
except Exception as e_img:
|
| 179 |
+
print(f"Impossible de créer l'image de test : {e_img}")
|
| 180 |
+
|
| 181 |
+
if os.path.exists(dummy_image_name):
|
| 182 |
+
print(f"Utilisation du modèle actif : {ACTIVE_MODEL}")
|
| 183 |
+
print("Chargement du modèle actif (peut prendre du temps)...")
|
| 184 |
+
load_active_model()
|
| 185 |
+
if is_active_model_loaded():
|
| 186 |
+
print(f"\nGénération de la description pour l'image de test '{dummy_image_name}'...")
|
| 187 |
+
description = generate_active_description(dummy_image_name)
|
| 188 |
+
print(f"\n--- Description Générée ---")
|
| 189 |
+
print(description)
|
| 190 |
+
print(f"--------------------------")
|
| 191 |
+
else:
|
| 192 |
+
print("Le modèle actif n'a pas pu être chargé. Test de description annulé.")
|
| 193 |
+
else:
|
| 194 |
+
print(f"Image de test '{dummy_image_name}' non trouvée. Test de description annulé.")
|
| 195 |
+
print("Fin du test de processing.py.")
|
app/static/css/style.css
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Réinitialisation de base et variables de couleur */
|
| 2 |
+
:root {
|
| 3 |
+
--font-primary: 'Montserrat', sans-serif;
|
| 4 |
+
--font-secondary: 'Cormorant Garamond', serif;
|
| 5 |
+
--color-text: #333333; /* Un gris foncé doux */
|
| 6 |
+
--color-primary: #0A0A0A; /* Presque noir pour l'élégance */
|
| 7 |
+
--color-secondary: #B08D57; /* Un doré/bronze doux */
|
| 8 |
+
--color-background: #FDFDFD; /* Blanc cassé très clair */
|
| 9 |
+
--color-light-gray: #e9e9e9;
|
| 10 |
+
--color-border: #d1d1d1;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
body {
|
| 14 |
+
font-family: var(--font-secondary); /* Police principale pour le corps */
|
| 15 |
+
line-height: 1.7;
|
| 16 |
+
margin: 0;
|
| 17 |
+
padding: 0;
|
| 18 |
+
background-color: var(--color-background);
|
| 19 |
+
color: var(--color-text);
|
| 20 |
+
font-weight: 400; /* Poids standard pour Cormorant Garamond */
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
h1, h2, h3, h4, h5, h6 {
|
| 24 |
+
font-family: var(--font-primary); /* Police pour les titres */
|
| 25 |
+
font-weight: 500; /* Un peu plus affirmé */
|
| 26 |
+
color: var(--color-primary);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
header {
|
| 30 |
+
background: var(--color-primary);
|
| 31 |
+
color: var(--color-background);
|
| 32 |
+
padding: 2rem 1rem; /* Plus d'espace */
|
| 33 |
+
text-align: center;
|
| 34 |
+
border-bottom: 3px solid var(--color-secondary);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
header h1 {
|
| 38 |
+
font-weight: 300; /* Léger pour le titre principal */
|
| 39 |
+
letter-spacing: 2px; /* Espacement des lettres */
|
| 40 |
+
font-size: 2.5em;
|
| 41 |
+
color: var(--color-background); /* S'assurer que le h1 hérite bien */
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
main {
|
| 45 |
+
padding: 30px;
|
| 46 |
+
max-width: 900px; /* Un peu plus large */
|
| 47 |
+
margin: 30px auto;
|
| 48 |
+
background: #ffffff; /* Blanc pur pour le contenu principal */
|
| 49 |
+
box-shadow: 0 5px 25px rgba(0,0,0,0.05); /* Ombre subtile */
|
| 50 |
+
border-radius: 8px;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
section {
|
| 54 |
+
margin-bottom: 30px;
|
| 55 |
+
padding: 25px;
|
| 56 |
+
border: 1px solid var(--color-light-gray);
|
| 57 |
+
border-radius: 5px;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
h2 {
|
| 61 |
+
font-size: 1.8em;
|
| 62 |
+
font-weight: 400;
|
| 63 |
+
margin-bottom: 20px;
|
| 64 |
+
padding-bottom: 10px;
|
| 65 |
+
border-bottom: 1px solid var(--color-light-gray);
|
| 66 |
+
color: var(--color-primary);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
input[type="file"] {
|
| 70 |
+
font-family: var(--font-primary);
|
| 71 |
+
padding: 10px;
|
| 72 |
+
border: 1px solid var(--color-border);
|
| 73 |
+
border-radius: 4px;
|
| 74 |
+
background-color: #fff;
|
| 75 |
+
margin-bottom: 20px; /* Plus d'espace */
|
| 76 |
+
display: block; /* Pour prendre toute la largeur */
|
| 77 |
+
width: calc(100% - 22px); /* Ajuster pour le padding et la bordure */
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
button[type="submit"] {
|
| 81 |
+
font-family: var(--font-primary);
|
| 82 |
+
background: var(--color-secondary);
|
| 83 |
+
color: white;
|
| 84 |
+
border: none;
|
| 85 |
+
padding: 12px 25px;
|
| 86 |
+
cursor: pointer;
|
| 87 |
+
border-radius: 4px;
|
| 88 |
+
text-transform: uppercase; /* Lettres capitales pour les boutons */
|
| 89 |
+
font-weight: 500;
|
| 90 |
+
letter-spacing: 1px;
|
| 91 |
+
transition: background-color 0.3s ease;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
button[type="submit"]:hover {
|
| 95 |
+
background: #9c7b4d; /* Un doré un peu plus foncé au survol */
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
#message-area {
|
| 99 |
+
margin-bottom: 15px;
|
| 100 |
+
padding: 15px;
|
| 101 |
+
border-radius: 4px;
|
| 102 |
+
font-family: var(--font-primary);
|
| 103 |
+
font-size: 0.95em;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
#message-area.success {
|
| 107 |
+
background-color: #e6f4ea; /* Vert pastel */
|
| 108 |
+
color: #3d8b50;
|
| 109 |
+
border: 1px solid #c3e0c9;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
#message-area.error {
|
| 113 |
+
background-color: #f8d7da; /* Rouge pastel */
|
| 114 |
+
color: #721c24;
|
| 115 |
+
border: 1px solid #f5c6cb;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
#file-list {
|
| 119 |
+
padding-left: 0; /* Pas de puces par défaut */
|
| 120 |
+
}
|
| 121 |
+
#file-list li {
|
| 122 |
+
list-style: none;
|
| 123 |
+
padding: 10px;
|
| 124 |
+
border-bottom: 1px var(--color-light-gray) dashed;
|
| 125 |
+
font-size: 0.9em;
|
| 126 |
+
font-family: var(--font-secondary); /* Police élégante pour les items */
|
| 127 |
+
}
|
| 128 |
+
#file-list li:last-child {
|
| 129 |
+
border-bottom: none;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
footer {
|
| 133 |
+
text-align: center;
|
| 134 |
+
padding: 20px;
|
| 135 |
+
background: var(--color-primary);
|
| 136 |
+
color: var(--color-light-gray);
|
| 137 |
+
margin-top: 40px;
|
| 138 |
+
font-family: var(--font-primary);
|
| 139 |
+
font-size: 0.9em;
|
| 140 |
+
font-weight: 300;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
/* Styles pour le Spinner / Chargeur */
|
| 144 |
+
#progress-indicator {
|
| 145 |
+
/* text-align: center; */
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.loader-container {
|
| 149 |
+
display: flex;
|
| 150 |
+
flex-direction: column;
|
| 151 |
+
justify-content: center;
|
| 152 |
+
align-items: center;
|
| 153 |
+
padding: 20px;
|
| 154 |
+
min-height: 100px;
|
| 155 |
+
font-family: var(--font-primary); /* Police pour le texte du loader */
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.spinner {
|
| 159 |
+
border: 6px solid var(--color-light-gray); /* Contour du spinner */
|
| 160 |
+
border-top: 6px solid var(--color-secondary); /* Couleur "active" du spinner (doré) */
|
| 161 |
+
border-radius: 50%;
|
| 162 |
+
width: 50px;
|
| 163 |
+
height: 50px;
|
| 164 |
+
animation: spin 1s linear infinite;
|
| 165 |
+
margin-bottom: 15px;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
@keyframes spin {
|
| 169 |
+
0% { transform: rotate(0deg); }
|
| 170 |
+
100% { transform: rotate(360deg); }
|
| 171 |
+
}
|
app/static/js/script.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 2 |
+
const uploadForm = document.getElementById('upload-form');
|
| 3 |
+
const imageFilesInput = document.getElementById('image-files');
|
| 4 |
+
const messageArea = document.getElementById('message-area');
|
| 5 |
+
const fileList = document.getElementById('file-list');
|
| 6 |
+
const progressIndicator = document.getElementById('progress-indicator');
|
| 7 |
+
// Récupérer l'élément <p> à l'intérieur de progressIndicator pour mettre à jour son texte
|
| 8 |
+
const progressText = progressIndicator.querySelector('p');
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
uploadForm.addEventListener('submit', async (event) => {
|
| 12 |
+
event.preventDefault();
|
| 13 |
+
messageArea.textContent = '';
|
| 14 |
+
messageArea.className = ''; // Effacer les classes précédentes
|
| 15 |
+
fileList.innerHTML = ''; // Effacer les résultats précédents
|
| 16 |
+
|
| 17 |
+
const numFiles = imageFilesInput.files.length;
|
| 18 |
+
|
| 19 |
+
if (numFiles === 0) {
|
| 20 |
+
messageArea.textContent = 'Veuillez sélectionner au moins une image.';
|
| 21 |
+
messageArea.classList.add('error');
|
| 22 |
+
return;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
if (progressText) { // Vérifier si l'élément p existe
|
| 26 |
+
progressText.textContent = `Traitement de ${numFiles} image(s) en cours, veuillez patienter...`;
|
| 27 |
+
} else { // Fallback si la structure HTML a changé et que p n'est plus là
|
| 28 |
+
progressIndicator.textContent = `Traitement de ${numFiles} image(s) en cours, veuillez patienter...`;
|
| 29 |
+
}
|
| 30 |
+
progressIndicator.style.display = 'block';
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
const formData = new FormData();
|
| 34 |
+
for (const file of imageFilesInput.files) {
|
| 35 |
+
formData.append('files', file);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
try {
|
| 39 |
+
const response = await fetch('/api/upload-images/', {
|
| 40 |
+
method: 'POST',
|
| 41 |
+
body: formData,
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
// Masquer l'indicateur de progression une fois la réponse reçue
|
| 45 |
+
progressIndicator.style.display = 'none';
|
| 46 |
+
const result = await response.json();
|
| 47 |
+
|
| 48 |
+
if (response.ok) {
|
| 49 |
+
messageArea.textContent = result.message || 'Traitement terminé avec succès !';
|
| 50 |
+
messageArea.classList.add('success');
|
| 51 |
+
if (result.processed_files && result.processed_files.length > 0) {
|
| 52 |
+
result.processed_files.forEach(file => {
|
| 53 |
+
const listItem = document.createElement('li');
|
| 54 |
+
listItem.textContent = `Image: ${file.image_name}, Description (début): ${file.description_preview || file.caption_name}`;
|
| 55 |
+
fileList.appendChild(listItem);
|
| 56 |
+
});
|
| 57 |
+
} else if (result.processed_files && result.processed_files.length === 0 && numFiles > 0) {
|
| 58 |
+
messageArea.textContent = result.message || "Aucun fichier n'a été traité avec succès parmi ceux envoyés.";
|
| 59 |
+
messageArea.classList.add('error'); // Ou une classe 'warning'
|
| 60 |
+
}
|
| 61 |
+
} else {
|
| 62 |
+
messageArea.textContent = result.detail || 'Une erreur est survenue lors du traitement.';
|
| 63 |
+
messageArea.classList.add('error');
|
| 64 |
+
}
|
| 65 |
+
} catch (error) {
|
| 66 |
+
progressIndicator.style.display = 'none';
|
| 67 |
+
messageArea.textContent = 'Erreur de connexion ou le serveur ne répond pas. Vérifiez la console du navigateur et du serveur.';
|
| 68 |
+
messageArea.classList.add('error');
|
| 69 |
+
console.error('Upload error:', error);
|
| 70 |
+
}
|
| 71 |
+
});
|
| 72 |
+
});
|
app/templates/index.html
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>AutoCaption IA - Studio Luxe</title> <link rel="preconnect" href="https://fonts.googleapis.com">
|
| 7 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@300;400;500;700&family=Montserrat:wght@300;400;500;700&display=swap" rel="stylesheet">
|
| 9 |
+
<link rel="stylesheet" href="{{ url_for('static', path='/css/style.css') }}">
|
| 10 |
+
</head>
|
| 11 |
+
<body>
|
| 12 |
+
<header>
|
| 13 |
+
<h1>Générateur de Descriptions d'Images</h1>
|
| 14 |
+
</header>
|
| 15 |
+
<main>
|
| 16 |
+
<section id="upload-section">
|
| 17 |
+
<h2>Téléverser des Images</h2>
|
| 18 |
+
<form id="upload-form" enctype="multipart/form-data">
|
| 19 |
+
<input type="file" id="image-files" name="files" multiple accept="image/*">
|
| 20 |
+
<button type="submit">Générer les Descriptions</button>
|
| 21 |
+
</form>
|
| 22 |
+
<div id="progress-indicator" style="display:none;">
|
| 23 |
+
<div class="loader-container">
|
| 24 |
+
<div class="spinner"></div>
|
| 25 |
+
<p>Traitement des images en cours, veuillez patienter...</p>
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
</section>
|
| 29 |
+
<section id="results-section">
|
| 30 |
+
<h2>Résultats</h2>
|
| 31 |
+
<div id="message-area"></div>
|
| 32 |
+
<ul id="file-list">
|
| 33 |
+
</ul>
|
| 34 |
+
</section>
|
| 35 |
+
</main>
|
| 36 |
+
<footer>
|
| 37 |
+
<p>© 2025 Votre Application AutoCaption</p>
|
| 38 |
+
</footer>
|
| 39 |
+
<script src="{{ url_for('static', path='/js/script.js') }}"></script>
|
| 40 |
+
</body>
|
| 41 |
+
</html>
|
app/utils.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
import time
|
| 3 |
+
|
| 4 |
+
# Compteur global pour les noms simples (si on préfère des numéros séquentiels par session)
|
| 5 |
+
# Pour une persistance entre redémarrages, il faudrait stocker ce compteur ailleurs (fichier, base de données)
|
| 6 |
+
# Pour la simplicité, on le réinitialise à chaque démarrage du serveur.
|
| 7 |
+
# Une meilleure approche pour des noms uniques simples pourrait être un timestamp précis.
|
| 8 |
+
_counter = 0
|
| 9 |
+
|
| 10 |
+
def generate_simple_unique_name() -> str:
|
| 11 |
+
"""Génère un nom de base unique simple basé sur un timestamp et un compteur."""
|
| 12 |
+
global _counter
|
| 13 |
+
_counter += 1
|
| 14 |
+
# timestamp_ms = int(time.time() * 1000)
|
| 15 |
+
# return f"img_{timestamp_ms}_{_counter:03d}"
|
| 16 |
+
return f"img_{uuid.uuid4().hex[:8]}" # Plus robuste pour l'unicité
|
| 17 |
+
|
| 18 |
+
def generate_uuid_name() -> str:
|
| 19 |
+
"""Génère un nom de base basé sur UUID (plus long mais garanti unique)."""
|
| 20 |
+
return str(uuid.uuid4())
|
| 21 |
+
|
| 22 |
+
if __name__ == '__main__':
|
| 23 |
+
# Test
|
| 24 |
+
print(generate_simple_unique_name())
|
| 25 |
+
print(generate_simple_unique_name())
|
| 26 |
+
print(generate_uuid_name())
|
configuration
ADDED
|
File without changes
|
du
ADDED
|
File without changes
|
environment.yml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# environment.yml
|
| 2 |
+
name: autocaption_env
|
| 3 |
+
channels:
|
| 4 |
+
- pytorch
|
| 5 |
+
- defaults
|
| 6 |
+
dependencies:
|
| 7 |
+
- python=3.9
|
| 8 |
+
- pytorch
|
| 9 |
+
- torchvision
|
| 10 |
+
- torchaudio
|
| 11 |
+
- cpuonly # Important si installé depuis le canal pytorch pour la version CPU
|
| 12 |
+
- fastapi
|
| 13 |
+
- uvicorn
|
| 14 |
+
- python-multipart
|
| 15 |
+
- jinja2
|
| 16 |
+
- pillow
|
| 17 |
+
- pip
|
| 18 |
+
- pip:
|
| 19 |
+
- transformers
|
| 20 |
+
- sentencepiece
|
| 21 |
+
- accelerate
|
| 22 |
+
- matplotlib
|
| 23 |
+
- tiktoken
|
la
ADDED
|
File without changes
|
modèle
ADDED
|
File without changes
|
outputs/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
outputs/captions/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
outputs/images/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
requirements.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn[standard]
|
| 3 |
+
python-multipart
|
| 4 |
+
jinja2
|
| 5 |
+
torch
|
| 6 |
+
torchvision
|
| 7 |
+
transformers
|
| 8 |
+
Pillow
|
| 9 |
+
accelerate
|
| 10 |
+
einops
|
| 11 |
+
# Dépendances potentiellement requises par les scripts distants de LLaVA
|
| 12 |
+
# Il est bon de les inclure si vous avez eu des erreurs sans elles précédemment.
|
| 13 |
+
# transformers_stream_generator # Si vous l'aviez ajouté pour une erreur précédente
|
| 14 |
+
matplotlib
|
| 15 |
+
tiktoken
|
| 16 |
+
# Pour l'authentification Basic Auth
|
| 17 |
+
python-jose[cryptography]
|
| 18 |
+
passlib[bcrypt]
|
| 19 |
+
|
| 20 |
+
# bitsandbytes # Optionnel pour la quantification (non utilisé pour l'instant)
|