Ludovic commited on
Commit
04969c3
·
1 Parent(s): 4474779
.DS_Store CHANGED
Binary files a/.DS_Store and b/.DS_Store differ
 
app/main.py CHANGED
@@ -2,16 +2,18 @@ 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
@@ -22,16 +24,13 @@ 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:
@@ -43,7 +42,7 @@ async def get_current_username(credentials: HTTPBasicCredentials = Depends(secur
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(
@@ -58,7 +57,7 @@ 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)
@@ -68,7 +67,7 @@ os.makedirs(OUTPUT_CAPTION_DIR, exist_ok=True)
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()
@@ -77,7 +76,7 @@ templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates"))
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)):
@@ -91,89 +90,100 @@ async def upload_images_for_captioning(
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
 
2
  import shutil
3
  import uuid
4
  import secrets # Pour la comparaison sécurisée des identifiants
5
+ import zipfile # Ajouté pour créer des archives ZIP
6
+ import io # Ajouté pour manipuler des flux de bytes en mémoire
7
 
8
  from fastapi import Depends, FastAPI, File, UploadFile, Request, HTTPException, status
9
+ from fastapi.responses import HTMLResponse, StreamingResponse # JSONResponse n'est plus utilisé pour le retour principal de upload
10
  from fastapi.staticfiles import StaticFiles
11
  from fastapi.templating import Jinja2Templates
12
  from fastapi.security import HTTPBasic, HTTPBasicCredentials
13
  from passlib.context import CryptContext # Pour hacher les mots de passe
14
  from typing import List
15
 
16
+ from . import processing # Contient la logique du modèle [cite: 48]
17
  from . import utils
18
 
19
  # Configuration de l'application FastAPI
 
24
  pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
25
 
26
  # Lire les identifiants depuis les variables d'environnement (pour Hugging Face Spaces Secrets)
27
+ APP_USERNAME_DEFAULT = "admin" # [cite: 51]
28
+ APP_PASSWORD_DEFAULT = "changezceci" # [cite: 51]
 
 
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
+ CORRECT_PASSWORD_HASH = pwd_context.hash(APP_PASSWORD_RAW) if APP_PASSWORD_RAW else None # [cite: 50]
 
34
 
35
  def verify_password(plain_password: str, hashed_password: str) -> bool:
36
  if hashed_password is None:
 
42
  correct_username_bytes = APP_USERNAME.encode("utf8")
43
 
44
  is_correct_username = secrets.compare_digest(current_username_bytes, correct_username_bytes)
45
+ is_correct_password = verify_password(credentials.password, CORRECT_PASSWORD_HASH) # [cite: 52]
46
 
47
  if not (is_correct_username and is_correct_password):
48
  raise HTTPException(
 
57
  PROJECT_ROOT = os.path.dirname(BASE_DIR) # Répertoire AUTOCAPTION
58
 
59
  UPLOAD_DIR = os.path.join(PROJECT_ROOT, "uploads")
60
+ OUTPUT_IMAGE_DIR = os.path.join(PROJECT_ROOT, "outputs", "images") # [cite: 53]
61
  OUTPUT_CAPTION_DIR = os.path.join(PROJECT_ROOT, "outputs", "captions")
62
 
63
  os.makedirs(UPLOAD_DIR, exist_ok=True)
 
67
  app.mount("/static", StaticFiles(directory=os.path.join(BASE_DIR, "static")), name="static")
68
  templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates"))
69
 
70
+ # Optionnel: Pré-chargement du modèle
71
  # print("Tentative de pré-chargement du modèle actif au démarrage de l'application...")
72
  # try:
73
  # processing.load_active_model()
 
76
  # else:
77
  # print(f"AVERTISSEMENT: Le modèle actif ({processing.ACTIVE_MODEL}) n'a pas pu être pré-chargé.")
78
  # except Exception as e:
79
+ # print(f"AVERTISSEMENT: Erreur lors du pré-chargement du modèle actif ({processing.ACTIVE_MODEL}): {e}") [cite: 54]
80
 
81
  @app.get("/", response_class=HTMLResponse)
82
  async def get_root(request: Request, current_user: str = Depends(get_current_username)):
 
90
  if not files:
91
  raise HTTPException(status_code=400, detail="Aucun fichier n'a été téléversé.")
92
 
 
 
93
  if not processing.is_active_model_loaded():
94
+ print(f"Le modèle actif ({processing.ACTIVE_MODEL}) n'est pas chargé. Tentative de chargement maintenant...") # [cite: 55]
95
  try:
96
  processing.load_active_model()
97
  if not processing.is_active_model_loaded():
98
  raise HTTPException(status_code=503, detail=f"Le modèle IA ({processing.ACTIVE_MODEL}) n'a pas pu être chargé (vérification post-tentative).")
99
  print(f"Modèle actif ({processing.ACTIVE_MODEL}) chargé avec succès à la demande.")
100
  except Exception as e:
101
+ error_detail_str = str(e) if str(e) else f"Le modèle IA ({processing.ACTIVE_MODEL}) n'a pas pu être chargé." # [cite: 56]
102
  raise HTTPException(status_code=503, detail=f"Erreur serveur critique : {error_detail_str}")
103
 
104
+ # Créer un buffer en mémoire pour le fichier ZIP
105
+ zip_buffer = io.BytesIO()
106
+
107
+ # Compteur pour s'assurer qu'au moins un fichier est traité pour le ZIP
108
+ files_added_to_zip = 0
109
+
110
+ with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
111
+ for file in files:
112
+ temp_file_path = None
113
+ try:
114
+ if not file.content_type or not file.content_type.startswith("image/"):
115
+ print(f"Fichier ignoré (type non supporté: {file.content_type}): {file.filename}") # [cite: 57]
116
+ continue
117
+
118
+ unique_base_name = utils.generate_simple_unique_name()
119
+ original_extension = os.path.splitext(file.filename)[1].lower()
120
+
121
+ if not original_extension and file.content_type: # Déduction d'extension
122
+ ext_map = {"image/jpeg": ".jpg", "image/png": ".png", "image/gif": ".gif", "image/webp": ".webp"} # [cite: 58]
123
+ original_extension = ext_map.get(file.content_type, ".img")
124
+ elif not original_extension:
125
+ original_extension = ".jpg"
126
+
127
+ # unique_image_name = f"{unique_base_name}{original_extension}" # Non utilisé directement pour le nom dans le zip
128
+ unique_caption_name = f"{unique_base_name}.txt"
129
+
130
+ temp_upload_filename = f"temp_{uuid.uuid4().hex}{original_extension}" # [cite: 59]
131
+ temp_file_path = os.path.join(UPLOAD_DIR, temp_upload_filename)
132
+
133
+ with open(temp_file_path, "wb") as buffer:
134
+ shutil.copyfileobj(file.file, buffer)
135
+
136
+ image_description = "Description non générée par défaut." # [cite: 60]
137
+ if processing.is_active_model_loaded():
138
+ print(f"Génération de description pour {temp_file_path} avec le modèle {processing.ACTIVE_MODEL}")
139
+ image_description = processing.generate_active_description(temp_file_path)
140
+ else:
141
+ print(f"ERREUR: Tentative de génération alors que le modèle {processing.ACTIVE_MODEL} n'est pas chargé.")
142
+ image_description = f"ERREUR CRITIQUE: Le modèle IA ({processing.ACTIVE_MODEL}) n'est pas disponible." # [cite: 61]
143
+
144
+ # Sauvegarde de l'image traitée (optionnel de l'inclure dans le zip)
145
+ # output_image_path = os.path.join(OUTPUT_IMAGE_DIR, unique_image_name)
146
+ # shutil.copy(temp_file_path, output_image_path)
147
+
148
+ # Écrire la description dans le fichier ZIP
149
+ # Utiliser le nom original du fichier image + .txt pour le nom dans le ZIP pour clarté
150
+ caption_filename_in_zip = f"{os.path.splitext(file.filename)[0]}_caption.txt"
151
+ zf.writestr(caption_filename_in_zip, image_description)
152
+ files_added_to_zip += 1
153
+
154
+ # Sauvegarde du fichier de caption localement (si toujours nécessaire)
155
+ output_caption_path = os.path.join(OUTPUT_CAPTION_DIR, unique_caption_name)
156
+ with open(output_caption_path, "w", encoding="utf-8") as caption_file:
157
+ caption_file.write(image_description) # [cite: 62]
158
+
159
+ except HTTPException:
160
+ raise
161
+ except Exception as e:
162
+ print(f"Erreur inattendue lors du traitement du fichier {file.filename}: {e}")
163
+ processing.traceback.print_exc() # [cite: 64]
164
+ finally:
165
+ if hasattr(file, 'file') and file.file and not file.file.closed:
166
+ file.file.close()
167
+ if temp_file_path and os.path.exists(temp_file_path):
168
+ try:
169
+ os.remove(temp_file_path) # [cite: 65]
170
+ except Exception as e_remove:
171
+ print(f"Erreur lors de la suppression du fichier temporaire {temp_file_path}: {e_remove}")
172
+
173
+ if files_added_to_zip == 0:
174
+ raise HTTPException(status_code=400, detail="Aucun fichier image valide n'a été traité pour générer des descriptions.")
175
+
176
+ # Rembobiner le buffer
177
+ zip_buffer.seek(0)
178
+
179
+ # Nom du fichier ZIP pour le téléchargement
180
+ zip_filename = f"captions_output_{utils.generate_simple_unique_name()}.zip"
181
+
182
+ return StreamingResponse(
183
+ zip_buffer,
184
+ media_type="application/zip",
185
+ headers={"Content-Disposition": f"attachment; filename={zip_filename}"}
186
  )
187
 
188
  if __name__ == "__main__":
189
+ print("Pour lancer l'application, utilisez la commande : uvicorn app.main:app --host 0.0.0.0 --port 8000")
 
 
app/static/css/style.css CHANGED
@@ -1,171 +1,225 @@
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
  }
 
1
+ /* ... (styles existants de :root à footer) ... */
2
  :root {
3
+ --font-primary: 'Montserrat', sans-serif; /* [cite: 97] */
4
+ --font-secondary: 'Cormorant Garamond', serif; /* [cite: 97] */
5
+ --color-text: #333333; /* [cite: 98] */
6
+ --color-primary: #0A0A0A; /* [cite: 99] */
7
+ --color-secondary: #B08D57; /* [cite: 100] */
8
+ --color-background: #FDFDFD; /* [cite: 101] */
9
  --color-light-gray: #e9e9e9;
10
  --color-border: #d1d1d1;
11
  }
12
 
13
  body {
14
+ font-family: var(--font-secondary); /* [cite: 102] */
15
+ line-height: 1.7; /* [cite: 102] */
16
+ margin: 0; /* [cite: 102] */
17
+ padding: 0; /* [cite: 102] */
18
+ background-color: var(--color-background); /* [cite: 102] */
19
+ color: var(--color-text); /* [cite: 102] */
20
+ font-weight: 400; /* [cite: 103] */
21
  }
22
 
23
  h1, h2, h3, h4, h5, h6 {
24
+ font-family: var(--font-primary); /* [cite: 104] */
25
+ font-weight: 500; /* [cite: 105] */
26
+ color: var(--color-primary); /* [cite: 105] */
27
  }
28
 
29
  header {
30
+ background: var(--color-primary); /* [cite: 106] */
31
+ color: var(--color-background); /* [cite: 106] */
32
+ padding: 2rem 1rem; /* [cite: 106] */
33
+ text-align: center; /* [cite: 106] */
34
+ border-bottom: 3px solid var(--color-secondary); /* [cite: 107] */
35
  }
36
 
37
  header h1 {
38
+ font-weight: 300; /* [cite: 108] */
39
+ letter-spacing: 2px; /* [cite: 108] */
40
+ font-size: 2.5em; /* [cite: 108] */
41
+ color: var(--color-background); /* [cite: 109] */
42
  }
43
 
44
  main {
45
+ padding: 30px; /* [cite: 110] */
46
+ max-width: 900px; /* [cite: 110] */
47
+ margin: 30px auto; /* [cite: 110] */
48
+ background: #ffffff; /* [cite: 111] */
49
+ box-shadow: 0 5px 25px rgba(0,0,0,0.05); /* [cite: 112] */
50
+ border-radius: 8px; /* [cite: 112] */
51
  }
52
 
53
  section {
54
+ margin-bottom: 30px; /* [cite: 113] */
55
+ padding: 25px; /* [cite: 113] */
56
+ border: 1px solid var(--color-light-gray); /* [cite: 113] */
57
+ border-radius: 5px; /* [cite: 113] */
58
  }
59
 
60
  h2 {
61
+ font-size: 1.8em; /* [cite: 114] */
62
+ font-weight: 400; /* [cite: 114] */
63
+ margin-bottom: 20px; /* [cite: 114] */
64
+ padding-bottom: 10px; /* [cite: 114] */
65
+ border-bottom: 1px solid var(--color-light-gray); /* [cite: 114] */
66
+ color: var(--color-primary); /* [cite: 114] */
67
  }
68
 
69
  input[type="file"] {
70
+ font-family: var(--font-primary); /* [cite: 115] */
71
+ padding: 10px; /* [cite: 115] */
72
+ border: 1px solid var(--color-border); /* [cite: 115] */
73
+ border-radius: 4px; /* [cite: 115] */
74
+ background-color: #fff; /* [cite: 115] */
75
+ margin-bottom: 20px; /* [cite: 115] */
76
+ display: block; /* [cite: 116] */
77
+ width: calc(100% - 22px); /* [cite: 117] */
78
  }
79
 
80
  button[type="submit"] {
81
+ font-family: var(--font-primary); /* [cite: 118] */
82
+ background: var(--color-secondary); /* [cite: 118] */
83
+ color: white; /* [cite: 118] */
84
+ border: none; /* [cite: 118] */
85
+ padding: 12px 25px; /* [cite: 118] */
86
+ cursor: pointer; /* [cite: 118] */
87
+ border-radius: 4px; /* [cite: 118] */
88
+ text-transform: uppercase; /* [cite: 119] */
89
+ font-weight: 500; /* [cite: 119] */
90
+ letter-spacing: 1px; /* [cite: 119] */
91
+ transition: background-color 0.3s ease; /* [cite: 120] */
92
  }
93
 
94
  button[type="submit"]:hover {
95
+ background: #9c7b4d; /* [cite: 120] */
96
  }
97
 
98
  #message-area {
99
+ margin-bottom: 15px; /* [cite: 121] */
100
+ padding: 15px; /* [cite: 121] */
101
+ border-radius: 4px; /* [cite: 121] */
102
+ font-family: var(--font-primary); /* [cite: 121] */
103
+ font-size: 0.95em; /* [cite: 121] */
104
  }
105
 
106
  #message-area.success {
107
+ background-color: #e6f4ea; /* [cite: 122] */
108
+ color: #3d8b50; /* [cite: 122] */
109
+ border: 1px solid #c3e0c9; /* [cite: 122] */
110
  }
111
 
112
  #message-area.error {
113
+ background-color: #f8d7da; /* [cite: 123] */
114
+ color: #721c24; /* [cite: 123] */
115
+ border: 1px solid #f5c6cb; /* [cite: 123] */
116
  }
117
 
118
+ /* L'ancienne #file-list n'est plus directement utilisée pour les résultats */
119
  #file-list {
120
+ padding-left: 0; /* [cite: 124] */
121
  }
122
  #file-list li {
123
+ list-style: none; /* [cite: 125] */
124
+ padding: 10px; /* [cite: 125] */
125
+ border-bottom: 1px var(--color-light-gray) dashed; /* [cite: 125] */
126
+ font-size: 0.9em; /* [cite: 125] */
127
+ font-family: var(--font-secondary); /* [cite: 125] */
128
  }
129
  #file-list li:last-child {
130
+ border-bottom: none; /* [cite: 126] */
131
  }
132
 
133
  footer {
134
+ text-align: center; /* [cite: 127] */
135
+ padding: 20px; /* [cite: 127] */
136
+ background: var(--color-primary); /* [cite: 127] */
137
+ color: var(--color-light-gray); /* [cite: 127] */
138
+ margin-top: 40px; /* [cite: 127] */
139
+ font-family: var(--font-primary); /* [cite: 127] */
140
+ font-size: 0.9em; /* [cite: 127] */
141
+ font-weight: 300; /* [cite: 127] */
142
+ }
143
+
144
+ /* Styles pour la barre de progression */
145
+ #progress-container {
146
+ margin-top: 20px;
147
  font-family: var(--font-primary);
 
 
148
  }
149
 
150
+ #progress-text {
151
+ text-align: center;
152
+ margin-bottom: 10px;
153
+ color: var(--color-text);
154
+ }
155
+
156
+ .progress-bar-wrapper {
157
+ width: 100%;
158
+ background-color: var(--color-light-gray);
159
+ border-radius: 5px;
160
+ padding: 3px;
161
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
162
+ }
163
+
164
+ .progress-bar-fill {
165
+ height: 20px;
166
+ background-color: var(--color-secondary);
167
+ border-radius: 3px;
168
+ width: 0%; /* Initialement à 0% */
169
+ transition: width 0.3s ease-out;
170
+ text-align: center;
171
+ color: white;
172
+ font-size: 0.8em;
173
+ line-height: 20px; /* Centrer le texte verticalement */
174
+ animation: pulse-indeterminate 2s infinite ease-in-out; /* Animation indéterminée */
175
+ }
176
+
177
+ .progress-bar-fill.determinate {
178
+ animation: none; /* Stopper l'animation quand la progression est déterminée */
179
+ }
180
+
181
+
182
+ @keyframes pulse-indeterminate {
183
+ 0% {
184
+ width: 0%;
185
+ opacity: 0.7;
186
+ }
187
+ 50% {
188
+ width: 100%;
189
+ opacity: 1;
190
+ }
191
+ 100% {
192
+ width: 0%;
193
+ opacity: 0.7;
194
+ }
195
+ }
196
+
197
+ /* Styles pour le Spinner / Chargeur (non utilisé mais conservé au cas où) */
198
  #progress-indicator {
199
+ /* text-align: center; */ /* [cite: 128] */
200
  }
201
 
202
  .loader-container {
203
+ display: flex; /* [cite: 129] */
204
+ flex-direction: column; /* [cite: 129] */
205
+ justify-content: center; /* [cite: 129] */
206
+ align-items: center; /* [cite: 129] */
207
+ padding: 20px; /* [cite: 129] */
208
+ min-height: 100px; /* [cite: 129] */
209
+ font-family: var(--font-primary); /* [cite: 129] */
210
  }
211
 
212
  .spinner {
213
+ border: 6px solid var(--color-light-gray); /* [cite: 130] */
214
+ border-top: 6px solid var(--color-secondary); /* [cite: 131] */
215
+ border-radius: 50%; /* [cite: 131] */
216
+ width: 50px; /* [cite: 132] */
217
+ height: 50px; /* [cite: 132] */
218
+ animation: spin 1s linear infinite; /* [cite: 132] */
219
+ margin-bottom: 15px; /* [cite: 132] */
220
  }
221
 
222
  @keyframes spin {
223
+ 0% { transform: rotate(0deg); } /* [cite: 133] */
224
+ 100% { transform: rotate(360deg); } /* [cite: 133] */
225
  }
app/static/js/script.js CHANGED
@@ -2,71 +2,90 @@ 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
  });
 
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'); // N'est plus utilisé pour afficher les résultats
6
+
7
+ const progressContainer = document.getElementById('progress-container');
8
+ const progressBarFill = document.getElementById('progress-bar');
9
+ const progressTextElement = document.getElementById('progress-text');
10
 
11
  uploadForm.addEventListener('submit', async (event) => {
12
  event.preventDefault();
13
  messageArea.textContent = '';
14
+ messageArea.className = ''; // Effacer les classes précédentes [cite: 134]
15
+ // fileList.innerHTML = ''; // Plus nécessaire [cite: 134]
16
 
17
+ const files = imageFilesInput.files;
18
+ const numFiles = files.length;
19
 
20
  if (numFiles === 0) {
21
  messageArea.textContent = 'Veuillez sélectionner au moins une image.';
22
+ messageArea.classList.add('error'); // [cite: 135]
23
  return;
24
  }
25
 
26
+ progressTextElement.textContent = `Préparation de ${numFiles} image(s)...`;
27
+ progressBarFill.style.width = '0%';
28
+ progressBarFill.classList.remove('determinate'); // Activer l'animation indéterminée
29
+ progressBarFill.textContent = '';
30
+ progressContainer.style.display = 'block';
 
 
31
 
32
  const formData = new FormData();
33
+ for (const file of files) { // [cite: 138]
34
+ formData.append('files', file); // [cite: 139]
35
  }
36
 
37
  try {
38
+ progressTextElement.textContent = `Traitement de ${numFiles} image(s) en cours, veuillez patienter...`;
39
+
40
  const response = await fetch('/api/upload-images/', {
41
  method: 'POST',
42
  body: formData,
43
  });
44
 
 
 
 
 
45
  if (response.ok) {
46
+ // Le serveur renvoie un fichier ZIP
47
+ const blob = await response.blob();
48
+ const contentDisposition = response.headers.get('content-disposition');
49
+ let filename = "captions.zip"; // Nom par défaut
50
+ if (contentDisposition) {
51
+ const filenameMatch = contentDisposition.match(/filename="?(.+)"?/i);
52
+ if (filenameMatch && filenameMatch.length === 2)
53
+ filename = filenameMatch[1];
 
 
 
54
  }
55
+
56
+ const link = document.createElement('a');
57
+ link.href = URL.createObjectURL(blob);
58
+ link.download = filename;
59
+ document.body.appendChild(link);
60
+ link.click();
61
+ document.body.removeChild(link);
62
+ URL.revokeObjectURL(link.href); // Libérer l'objet URL
63
+
64
+ messageArea.textContent = `${numFiles} image(s) traitée(s). Le fichier ZIP '${filename}' est en cours de téléchargement.`;
65
+ messageArea.classList.add('success');
66
+
67
+ progressBarFill.style.width = '100%';
68
+ progressBarFill.classList.add('determinate'); // Stopper l'animation
69
+ progressBarFill.textContent = 'Terminé !';
70
+ progressTextElement.textContent = "Traitement terminé avec succès !";
71
+
72
+
73
  } else {
74
+ // Gérer les erreurs HTTP (ex: 400, 401, 500, 503)
75
+ const errorResult = await response.json().catch(() => null); // Essayer de parser JSON, sinon null
76
+ if (errorResult && errorResult.detail) {
77
+ messageArea.textContent = `Erreur: ${errorResult.detail}`;
78
+ } else {
79
+ messageArea.textContent = `Une erreur est survenue (code: ${response.status}). Veuillez réessayer.`;
80
+ }
81
  messageArea.classList.add('error');
82
+ progressContainer.style.display = 'none'; // Cacher la barre en cas d'erreur
83
  }
84
  } catch (error) {
85
+ console.error('Upload error:', error); // [cite: 149]
86
+ messageArea.textContent = 'Erreur de connexion ou le serveur ne répond pas. Vérifiez la console du navigateur et du serveur.'; // [cite: 148]
87
+ messageArea.classList.add('error'); // [cite: 148]
88
+ progressContainer.style.display = 'none'; // Cacher la barre
89
  }
90
  });
91
  });
app/templates/index.html CHANGED
@@ -3,7 +3,8 @@
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') }}">
@@ -14,28 +15,22 @@
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>
 
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>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
  <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">
10
  <link rel="stylesheet" href="{{ url_for('static', path='/css/style.css') }}">
 
15
  </header>
16
  <main>
17
  <section id="upload-section">
18
+ <h2>Téléverser des Images</h2> <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-container" style="display:none;"> <p id="progress-text">Traitement des images en cours...</p>
23
+ <div class="progress-bar-wrapper">
24
+ <div id="progress-bar" class="progress-bar-fill"></div>
 
25
  </div>
26
  </div>
27
  </section>
28
  <section id="results-section">
29
+ <h2>Résultats</h2> <div id="message-area"></div>
30
+ </section>
 
 
 
31
  </main>
32
  <footer>
33
+ <p>&copy; 2025 Votre Application AutoCaption</p> </footer>
 
34
  <script src="{{ url_for('static', path='/js/script.js') }}"></script>
35
  </body>
36
  </html>
generer_doc.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ # --- Fonctions Auxiliaires ---
4
+
5
+ def _ecrire_structure_partie(f_out, chemin_a_parcourir, chemin_base_relativisation, exclusions_dossiers, exclusions_fichiers, exclusions_extensions):
6
+ """
7
+ Écrit la structure des dossiers/fichiers pour un chemin donné.
8
+ L'indentation est relative à chemin_a_parcourir.
9
+ Les chemins affichés sont relatifs à chemin_base_relativisation.
10
+ """
11
+ for root, dirs, files in os.walk(chemin_a_parcourir, topdown=True):
12
+ # Exclure les dossiers spécifiés
13
+ dirs[:] = [d for d in dirs if d not in exclusions_dossiers]
14
+ dirs.sort() # Trier les dossiers pour un ordre cohérent
15
+
16
+ # Calcul de l'indentation
17
+ if root == chemin_a_parcourir:
18
+ level = 0
19
+ # Nom à afficher pour le dossier racine de cette section
20
+ current_dir_display_name = os.path.relpath(root, chemin_base_relativisation)
21
+ if current_dir_display_name == ".": # chemin_a_parcourir == chemin_base_relativisation
22
+ current_dir_display_name = os.path.basename(root)
23
+ else:
24
+ # Calculer le niveau de profondeur par rapport à chemin_a_parcourir
25
+ relative_to_scan_root = os.path.relpath(root, chemin_a_parcourir)
26
+ level = relative_to_scan_root.count(os.sep) + 1 if relative_to_scan_root != "." else 1
27
+ current_dir_display_name = os.path.basename(root)
28
+
29
+ indent = ' ' * 4 * level
30
+ f_out.write(f"{indent}{current_dir_display_name}/\n")
31
+
32
+ sub_indent = ' ' * 4 * (level + 1)
33
+ for file_name in sorted(files): # Trier les fichiers
34
+ if file_name not in exclusions_fichiers and not any(file_name.endswith(ext) for ext in exclusions_extensions):
35
+ f_out.write(f"{sub_indent}{file_name}\n")
36
+
37
+ def _ecrire_contenu_partie(f_out, chemin_a_parcourir, chemin_base_relativisation, exclusions_dossiers, exclusions_fichiers, exclusions_extensions):
38
+ """
39
+ Écrit le contenu des fichiers pour un chemin donné.
40
+ Les chemins des fichiers sont relatifs à chemin_base_relativisation.
41
+ """
42
+ for root, dirs, files in os.walk(chemin_a_parcourir, topdown=True):
43
+ dirs[:] = [d for d in dirs if d not in exclusions_dossiers]
44
+
45
+ for file_name in sorted(files): # Trier les fichiers
46
+ if file_name not in exclusions_fichiers and not any(file_name.endswith(ext) for ext in exclusions_extensions):
47
+ chemin_fichier_absolu = os.path.join(root, file_name)
48
+ chemin_fichier_relatif_display = os.path.relpath(chemin_fichier_absolu, chemin_base_relativisation)
49
+
50
+ # Construction du chemin affiché pour qu'il commence par / s'il n'est pas à la racine immédiate.
51
+ # os.path.relpath peut retourner "fichier.txt" ou "dossier/fichier.txt"
52
+ if chemin_fichier_relatif_display == "." or chemin_fichier_relatif_display == os.path.basename(chemin_fichier_relatif_display):
53
+ # Fichier à la racine de chemin_base_relativisation
54
+ display_path_final = os.path.join(os.path.sep, os.path.basename(chemin_fichier_absolu))
55
+ else:
56
+ # Fichier dans un sous-dossier de chemin_base_relativisation
57
+ display_path_final = os.path.join(os.path.sep, chemin_fichier_relatif_display)
58
+
59
+ # Nettoyage pour éviter les "./" au début après la jointure
60
+ if display_path_final.startswith(os.path.sep + '.' + os.path.sep):
61
+ display_path_final = display_path_final.replace(os.path.sep + '.' + os.path.sep, os.path.sep, 1)
62
+ elif display_path_final == os.path.sep + '.': # cas où chemin_fichier_relatif_display était '.'
63
+ display_path_final = os.path.join(os.path.sep, os.path.basename(chemin_fichier_absolu))
64
+
65
+
66
+ f_out.write("-" * 30 + "\n")
67
+ f_out.write(f"Fichier : {display_path_final}\n")
68
+ f_out.write("-" * 30 + "\n")
69
+ try:
70
+ with open(chemin_fichier_absolu, 'r', encoding='utf-8', errors='ignore') as f_in:
71
+ contenu = f_in.read()
72
+ f_out.write(contenu)
73
+ f_out.write("\n\n")
74
+ except Exception as e:
75
+ f_out.write(f"Erreur lors de la lecture du fichier {file_name}: {e}\n\n")
76
+
77
+ # --- Fonctions de Génération de Document ---
78
+
79
+ def generer_document_unique(nom_section_document, chemin_a_scanner, chemin_base_relativisation, fichier_sortie, exclusions_dossiers, exclusions_fichiers, exclusions_extensions):
80
+ """
81
+ Génère un document (structure et contenu) pour un seul chemin racine.
82
+ """
83
+ if not os.path.isdir(chemin_a_scanner):
84
+ print(f"AVERTISSEMENT : Le dossier '{chemin_a_scanner}' pour la section '{nom_section_document}' est introuvable. Le fichier '{fichier_sortie}' ne sera pas généré.")
85
+ return
86
+
87
+ with open(fichier_sortie, 'w', encoding='utf-8') as f_out:
88
+ f_out.write(f"Structure pour : {nom_section_document}\n")
89
+ f_out.write(f"(Chemin de base pour l'analyse : {chemin_a_scanner})\n")
90
+ f_out.write(f"(Chemins affichés relatifs à : {chemin_base_relativisation})\n")
91
+ f_out.write("=" * 50 + "\n\n")
92
+
93
+ _ecrire_structure_partie(f_out, chemin_a_scanner, chemin_base_relativisation, exclusions_dossiers, exclusions_fichiers, exclusions_extensions)
94
+
95
+ f_out.write("\n\n" + "=" * 50 + "\n")
96
+ f_out.write(f"Contenu des fichiers pour : {nom_section_document}\n")
97
+ f_out.write("=" * 50 + "\n\n")
98
+
99
+ _ecrire_contenu_partie(f_out, chemin_a_scanner, chemin_base_relativisation, exclusions_dossiers, exclusions_fichiers, exclusions_extensions)
100
+
101
+ print(f"Le fichier '{fichier_sortie}' ({nom_section_document}) a été généré avec succès !")
102
+
103
+ def generer_document_combine(titre_global_document, liste_dossiers_sources_avec_noms, chemin_base_relativisation_global, fichier_sortie, exclusions_dossiers, exclusions_fichiers, exclusions_extensions):
104
+ """
105
+ Génère un document combinant la structure et le contenu de plusieurs dossiers sources.
106
+ liste_dossiers_sources_avec_noms: une liste de tuples [("Nom Section", "/chemin/vers/dossier"), ...]
107
+ Les chemins dans le document sont relatifs à chemin_base_relativisation_global.
108
+ """
109
+ with open(fichier_sortie, 'w', encoding='utf-8') as f_out:
110
+ f_out.write(f"{titre_global_document}\n")
111
+ f_out.write(f"(Chemins affichés relatifs à : {chemin_base_relativisation_global})\n")
112
+ f_out.write("=" * 50 + "\n\n")
113
+
114
+ f_out.write("Structure des dossiers :\n")
115
+ f_out.write("-" * 50 + "\n\n")
116
+ for nom_section, chemin_a_scanner in liste_dossiers_sources_avec_noms:
117
+ if not os.path.isdir(chemin_a_scanner):
118
+ f_out.write(f"--- Dossier '{nom_section}' ({chemin_a_scanner}) introuvable. Section de structure ignorée. ---\n\n")
119
+ continue
120
+ f_out.write(f"--- Structure pour : {nom_section} (depuis {chemin_a_scanner}) ---\n")
121
+ _ecrire_structure_partie(f_out, chemin_a_scanner, chemin_base_relativisation_global, exclusions_dossiers, exclusions_fichiers, exclusions_extensions)
122
+ f_out.write(f"--- Fin Structure pour : {nom_section} ---\n\n")
123
+
124
+ f_out.write("\n\n" + "=" * 50 + "\n")
125
+ f_out.write("Contenu des fichiers :\n")
126
+ f_out.write("=" * 50 + "\n\n")
127
+ for nom_section, chemin_a_scanner in liste_dossiers_sources_avec_noms:
128
+ if not os.path.isdir(chemin_a_scanner):
129
+ f_out.write(f"--- Dossier '{nom_section}' ({chemin_a_scanner}) introuvable. Section de contenu ignorée. ---\n\n")
130
+ continue
131
+ f_out.write(f"--- Contenu pour : {nom_section} (depuis {chemin_a_scanner}) ---\n")
132
+ _ecrire_contenu_partie(f_out, chemin_a_scanner, chemin_base_relativisation_global, exclusions_dossiers, exclusions_fichiers, exclusions_extensions)
133
+ f_out.write(f"--- Fin Contenu pour : {nom_section} ---\n\n")
134
+
135
+ print(f"Le fichier combiné '{fichier_sortie}' ({titre_global_document}) a été généré avec succès !")
136
+
137
+
138
+ if __name__ == "__main__":
139
+ # --- Configuration ---
140
+ home_dir = os.path.expanduser("~")
141
+
142
+ # Nom du dossier de votre projet cible
143
+ nom_dossier_projet = "autocaption-hf-space"
144
+
145
+ # Chemin de base où se trouve votre dossier de projet (Documents sur Mac)
146
+ # Si "autocaption-hf-space" est directement dans "Documents"
147
+ chemin_base_parent_projet = os.path.join(home_dir, "Documents")
148
+
149
+ # Chemin complet vers le dossier du projet cible
150
+ chemin_projet_cible = os.path.join(chemin_base_parent_projet, nom_dossier_projet)
151
+
152
+ # Chemins spécifiques pour d'éventuels sous-dossiers src et backend (si votre projet les utilise)
153
+ # Si votre projet "autocaption-hf-space" n'a pas ces sous-dossiers distincts,
154
+ # les rapports pour "SRC" et "BACKEND" ne seront simplement pas générés (ou seront vides).
155
+ chemin_src = os.path.join(chemin_projet_cible, "src")
156
+ chemin_backend = os.path.join(chemin_projet_cible, "backend")
157
+
158
+ # Dossiers à exclure (vous pouvez personnaliser cette liste)
159
+ dossiers_a_exclure = [
160
+ "node_modules", "venv", ".venv", "__pycache__", ".git", ".vscode",
161
+ ".idea", "dist", "build", "target", "docs", "coverage", "logs",
162
+ "temp", "tmp", "env",
163
+ # Ajoutez "cesium" ou tout autre dossier spécifique à "autocaption-hf-space" que vous souhaitez exclure
164
+ # "nom_dossier_specifique_a_exclure_dans_autocaption"
165
+ ]
166
+
167
+ # Fichiers spécifiques à exclure (vous pouvez personnaliser cette liste)
168
+ fichiers_a_exclure = [
169
+ "package-lock.json", "yarn.lock", "composer.lock", ".env",
170
+ ".DS_Store", "Thumbs.db", "npm-debug.log", "yarn-debug.log",
171
+ "yarn-error.log"
172
+ ]
173
+
174
+ # Extensions de fichiers à exclure (vous pouvez personnaliser cette liste)
175
+ extensions_a_exclure = [
176
+ ".pyc", ".pyo", ".o", ".so", ".dll", ".exe", ".jar", ".war", ".ear",
177
+ ".class", ".zip", ".tar", ".gz", ".rar", ".7z", ".pdf", ".doc",
178
+ ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".odt", ".ods", ".odp",
179
+ ".img", ".iso", ".dmg", ".mp3", ".mp4", ".avi", ".mov", ".wav",
180
+ ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".lock",
181
+ # ".xml" # Décommentez si vous souhaitez exclure les fichiers XML, sinon laissez commenté
182
+ ]
183
+ # --- Fin de la configuration ---
184
+
185
+ if not os.path.isdir(chemin_projet_cible):
186
+ print(f"ERREUR : Le dossier du projet '{chemin_projet_cible}' n'a pas été trouvé.")
187
+ print("Veuillez vérifier le chemin et la configuration 'nom_dossier_projet' et 'chemin_base_parent_projet' dans le script.")
188
+ else:
189
+ # Les fichiers de sortie seront sauvegardés dans le dossier "Documents" (le parent de votre projet)
190
+ dossier_de_sortie = chemin_base_parent_projet # Ou os.path.dirname(chemin_projet_cible)
191
+
192
+ # 1. Document pour le dossier SRC uniquement (si ce sous-dossier existe dans autocaption-hf-space)
193
+ nom_fichier_src = f"structure_et_contenu_{nom_dossier_projet}_SRC.txt"
194
+ fichier_sortie_src = os.path.join(dossier_de_sortie, nom_fichier_src)
195
+ if os.path.isdir(chemin_src):
196
+ generer_document_unique(
197
+ nom_section_document=f"Dossier SRC de {nom_dossier_projet}",
198
+ chemin_a_scanner=chemin_src,
199
+ chemin_base_relativisation=chemin_src, # Chemins relatifs à la racine de SRC
200
+ fichier_sortie=fichier_sortie_src,
201
+ exclusions_dossiers=dossiers_a_exclure,
202
+ exclusions_fichiers=fichiers_a_exclure,
203
+ exclusions_extensions=extensions_a_exclure
204
+ )
205
+ else:
206
+ print(f"INFO : Sous-dossier SRC '{chemin_src}' non trouvé dans '{nom_dossier_projet}'. Le fichier '{nom_fichier_src}' ne sera pas généré.")
207
+
208
+ # 2. Document pour le dossier BACKEND uniquement (si ce sous-dossier existe dans autocaption-hf-space)
209
+ nom_fichier_backend = f"structure_et_contenu_{nom_dossier_projet}_BACKEND.txt"
210
+ fichier_sortie_backend = os.path.join(dossier_de_sortie, nom_fichier_backend)
211
+ if os.path.isdir(chemin_backend):
212
+ generer_document_unique(
213
+ nom_section_document=f"Dossier BACKEND de {nom_dossier_projet}",
214
+ chemin_a_scanner=chemin_backend,
215
+ chemin_base_relativisation=chemin_backend, # Chemins relatifs à la racine de BACKEND
216
+ fichier_sortie=fichier_sortie_backend,
217
+ exclusions_dossiers=dossiers_a_exclure,
218
+ exclusions_fichiers=fichiers_a_exclure,
219
+ exclusions_extensions=extensions_a_exclure
220
+ )
221
+ else:
222
+ print(f"INFO : Sous-dossier BACKEND '{chemin_backend}' non trouvé dans '{nom_dossier_projet}'. Le fichier '{nom_fichier_backend}' ne sera pas généré.")
223
+
224
+ # 3. Document combinant SRC et Backend (si ces sous-dossiers existent)
225
+ nom_fichier_combine = f"structure_et_contenu_{nom_dossier_projet}_SRC_ET_BACKEND.txt"
226
+ fichier_sortie_combine = os.path.join(dossier_de_sortie, nom_fichier_combine)
227
+ dossiers_a_combiner = []
228
+ if os.path.isdir(chemin_src):
229
+ dossiers_a_combiner.append((f"SRC ({nom_dossier_projet})", chemin_src))
230
+ else:
231
+ print(f"INFO : Sous-dossier SRC '{chemin_src}' non trouvé. Ne sera pas inclus dans le fichier combiné '{nom_fichier_combine}'.")
232
+ if os.path.isdir(chemin_backend):
233
+ dossiers_a_combiner.append((f"BACKEND ({nom_dossier_projet})", chemin_backend))
234
+ else:
235
+ print(f"INFO : Sous-dossier BACKEND '{chemin_backend}' non trouvé. Ne sera pas inclus dans le fichier combiné '{nom_fichier_combine}'.")
236
+
237
+ if dossiers_a_combiner:
238
+ generer_document_combine(
239
+ titre_global_document=f"Combinaison des dossiers SRC et BACKEND de {nom_dossier_projet}",
240
+ liste_dossiers_sources_avec_noms=dossiers_a_combiner,
241
+ chemin_base_relativisation_global=chemin_projet_cible, # Chemins relatifs à la racine du projet
242
+ fichier_sortie=fichier_sortie_combine,
243
+ exclusions_dossiers=dossiers_a_exclure,
244
+ exclusions_fichiers=fichiers_a_exclure,
245
+ exclusions_extensions=extensions_a_exclure
246
+ )
247
+ elif not os.path.isdir(chemin_src) and not os.path.isdir(chemin_backend) and (os.path.exists(fichier_sortie_combine) or (not os.path.exists(fichier_sortie_combine) and (os.path.isdir(chemin_src) or os.path.isdir(chemin_backend)))): # Condition pour éviter message si aucun des deux n'est attendu
248
+ print(f"INFO : Ni le sous-dossier SRC ni le sous-dossier BACKEND n'ont été trouvés pour {nom_dossier_projet}. Le fichier combiné '{nom_fichier_combine}' ne sera pas généré.")
249
+
250
+
251
+ # 4. Document pour le projet complet "autocaption-hf-space"
252
+ nom_fichier_complet = f"structure_et_contenu_{nom_dossier_projet}_COMPLET.txt"
253
+ fichier_sortie_complet = os.path.join(dossier_de_sortie, nom_fichier_complet)
254
+ generer_document_unique(
255
+ nom_section_document=f"Projet Complet : {nom_dossier_projet}",
256
+ chemin_a_scanner=chemin_projet_cible,
257
+ chemin_base_relativisation=chemin_projet_cible, # Chemins relatifs à la racine du projet
258
+ fichier_sortie=fichier_sortie_complet,
259
+ exclusions_dossiers=dossiers_a_exclure,
260
+ exclusions_fichiers=fichiers_a_exclure,
261
+ exclusions_extensions=extensions_a_exclure
262
+ )