Ludovic commited on
Commit
5e23602
·
1 Parent(s): 83b2bcc

Déploiement propre initial sans fichiers volumineux

Browse files
# 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>&copy; 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)