Docfile commited on
Commit
74a519a
·
verified ·
1 Parent(s): 1a8ed7d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +119 -158
app.py CHANGED
@@ -1,62 +1,56 @@
1
- from flask import Flask, render_template, request, jsonify, send_file
 
 
 
2
  import os
3
  import json
4
  import uuid
5
  import threading
6
  import time
7
  from datetime import datetime
8
- import base64
9
  import mimetypes
10
  from google import genai
11
  from google.genai import types
12
- from reportlab.lib.pagesizes import A4
13
- from reportlab.platypus import SimpleDocTemplate, Image
14
- from reportlab.lib.utils import ImageReader
15
- from PIL import Image as PILImage
16
- import io
17
 
 
 
 
18
  app = Flask(__name__)
19
 
20
- # Configuration
21
  UPLOAD_FOLDER = 'generated_pages'
22
- PDF_FOLDER = 'generated_pdfs'
23
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
24
- os.makedirs(PDF_FOLDER, exist_ok=True)
25
 
26
- # Stockage des tâches en cours
27
  active_tasks = {}
28
 
 
 
 
29
  class MangaGenerator:
 
30
  def __init__(self, api_key):
31
  self.client = genai.Client(api_key=api_key)
32
  self.model = "gemini-2.5-flash-image-preview"
33
 
34
  def save_binary_file(self, file_name, data):
35
- """Sauvegarde un fichier binaire"""
36
  try:
37
  with open(file_name, "wb") as f:
38
  f.write(data)
39
  return True
40
  except Exception as e:
41
- print(f"Erreur lors de la sauvegarde: {e}")
42
  return False
43
 
44
  def generate_page(self, prompt, page_number, task_id):
45
- """Génère une page de manga avec le prompt donné"""
46
  try:
47
- contents = [
48
- types.Content(
49
- role="user",
50
- parts=[
51
- types.Part.from_text(text=prompt),
52
- ],
53
- ),
54
- ]
55
-
56
- generate_content_config = types.GenerateContentConfig(
57
- response_modalities=["IMAGE", "TEXT"],
58
- )
59
-
60
  generated_files = []
61
 
62
  for chunk in self.client.models.generate_content_stream(
@@ -64,27 +58,16 @@ class MangaGenerator:
64
  contents=contents,
65
  config=generate_content_config,
66
  ):
67
- if (
68
- chunk.candidates is None
69
- or chunk.candidates[0].content is None
70
- or chunk.candidates[0].content.parts is None
71
- ):
72
- continue
73
-
74
- if (chunk.candidates[0].content.parts[0].inline_data and
75
- chunk.candidates[0].content.parts[0].inline_data.data):
76
-
77
- file_name = f"{UPLOAD_FOLDER}/page_{page_number}_{task_id}"
78
- inline_data = chunk.candidates[0].content.parts[0].inline_data
79
- data_buffer = inline_data.data
80
- file_extension = mimetypes.guess_extension(inline_data.mime_type) or '.png'
81
-
82
- full_path = f"{file_name}{file_extension}"
83
- if self.save_binary_file(full_path, data_buffer):
84
- generated_files.append(full_path)
85
- else:
86
- if hasattr(chunk, 'text') and chunk.text:
87
- print(f"Texte généré pour page {page_number}: {chunk.text}")
88
 
89
  return generated_files
90
 
@@ -92,183 +75,161 @@ class MangaGenerator:
92
  print(f"Erreur lors de la génération de la page {page_number}: {e}")
93
  return []
94
 
95
- def create_pdf(image_paths, output_path):
96
- """Crée un PDF à partir des images générées"""
 
 
 
97
  try:
98
- doc = SimpleDocTemplate(output_path, pagesize=A4)
99
- story = []
100
-
101
- for img_path in sorted(image_paths):
102
- if os.path.exists(img_path):
103
- # Redimensionner l'image pour s'adapter à la page A4
104
- with PILImage.open(img_path) as img:
105
- img_buffer = io.BytesIO()
106
- img.save(img_buffer, format='PNG')
107
- img_buffer.seek(0)
108
-
109
- # Calculer les dimensions pour s'adapter à A4
110
- page_width, page_height = A4
111
- img_width, img_height = img.size
112
-
113
- # Maintenir le ratio d'aspect
114
- ratio = min(page_width/img_width, page_height/img_height) * 0.9
115
- new_width = img_width * ratio
116
- new_height = img_height * ratio
117
-
118
- story.append(Image(ImageReader(img_buffer),
119
- width=new_width, height=new_height))
120
-
121
- doc.build(story)
122
  return True
123
-
124
  except Exception as e:
125
- print(f"Erreur lors de la création du PDF: {e}")
126
  return False
127
 
 
 
 
128
  def generate_manga_task(manga_data, task_id):
129
- """Tâche de génération de manga qui s'exécute en arrière-plan"""
 
 
130
  try:
131
  api_key = os.environ.get("GEMINI_API_KEY")
132
  if not api_key:
133
- active_tasks[task_id]['status'] = 'error'
134
- active_tasks[task_id]['error'] = 'API Key Gemini non trouvée'
135
  return
136
 
137
  generator = MangaGenerator(api_key)
138
- active_tasks[task_id]['status'] = 'generating'
139
- active_tasks[task_id]['total_pages'] = len(manga_data)
140
- active_tasks[task_id]['current_page'] = 0
141
 
142
- generated_files = []
 
 
 
 
 
 
143
 
144
- # Trier les parties par ordre numérique
145
  sorted_parts = sorted(manga_data.items(), key=lambda x: int(x[0].split('-')[1]))
146
 
 
147
  for i, (part_key, prompt) in enumerate(sorted_parts, 1):
148
- active_tasks[task_id]['current_page'] = i
149
- active_tasks[task_id]['current_part'] = part_key
150
-
151
- print(f"Génération de la page {i}/{len(sorted_parts)} - {part_key}")
152
 
153
  page_files = generator.generate_page(prompt, i, task_id)
154
- generated_files.extend(page_files)
155
 
156
- # Attendre un peu entre les générations pour éviter les limites de taux
157
- time.sleep(2)
158
-
159
- # Créer le PDF
160
- active_tasks[task_id]['status'] = 'creating_pdf'
161
- pdf_path = f"{PDF_FOLDER}/manga_{task_id}.pdf"
162
-
163
- if create_pdf(generated_files, pdf_path):
164
- active_tasks[task_id]['status'] = 'completed'
165
- active_tasks[task_id]['pdf_path'] = pdf_path
166
- active_tasks[task_id]['completed_at'] = datetime.now().isoformat()
 
167
  else:
168
- active_tasks[task_id]['status'] = 'error'
169
- active_tasks[task_id]['error'] = 'Erreur lors de la création du PDF'
 
 
170
 
171
  except Exception as e:
172
- active_tasks[task_id]['status'] = 'error'
173
- active_tasks[task_id]['error'] = str(e)
174
- print(f"Erreur dans la tâche {task_id}: {e}")
175
 
 
 
 
176
  @app.route('/')
177
  def index():
 
178
  return render_template('index.html')
179
 
 
 
 
 
 
 
 
180
  @app.route('/generate', methods=['POST'])
181
  def generate():
 
 
 
182
  try:
183
  data = request.get_json()
184
-
185
  if not data:
186
  return jsonify({'error': 'Aucune donnée JSON fournie'}), 400
187
 
188
- # Valider que les données contiennent des parties
189
  manga_parts = {k: v for k, v in data.items() if k.startswith('partie-')}
190
-
191
  if not manga_parts:
192
- return jsonify({'error': 'Aucune partie trouvée dans les données'}), 400
193
 
194
- # Créer une nouvelle tâche
195
  task_id = str(uuid.uuid4())
196
  active_tasks[task_id] = {
197
  'status': 'queued',
198
- 'created_at': datetime.now().isoformat(),
199
- 'manga_data': manga_parts
200
  }
201
 
202
- # Lancer la génération en arrière-plan
203
  thread = threading.Thread(target=generate_manga_task, args=(manga_parts, task_id))
204
  thread.daemon = True
205
  thread.start()
206
 
207
- return jsonify({
208
- 'task_id': task_id,
209
- 'status': 'queued',
210
- 'message': 'Génération démarrée'
211
- })
212
 
213
  except Exception as e:
214
  return jsonify({'error': str(e)}), 500
215
 
216
  @app.route('/status/<task_id>')
217
  def get_status(task_id):
 
 
 
218
  if task_id not in active_tasks:
219
  return jsonify({'error': 'Tâche non trouvée'}), 404
220
 
221
  task = active_tasks[task_id].copy()
222
- # Ne pas renvoyer les données du manga dans le status pour économiser la bande passante
223
- if 'manga_data' in task:
224
- del task['manga_data']
 
225
 
226
  return jsonify(task)
227
 
228
  @app.route('/download/<task_id>')
229
- def download_pdf(task_id):
 
 
 
230
  if task_id not in active_tasks:
231
  return jsonify({'error': 'Tâche non trouvée'}), 404
232
 
233
  task = active_tasks[task_id]
234
- if task['status'] != 'completed' or 'pdf_path' not in task:
235
- return jsonify({'error': 'PDF non disponible'}), 400
236
-
237
- pdf_path = task['pdf_path']
238
- if not os.path.exists(pdf_path):
239
- return jsonify({'error': 'Fichier PDF non trouvé'}), 404
240
 
241
- return send_file(pdf_path, as_attachment=True,
242
- download_name=f'manga_{task_id}.pdf')
243
-
244
- @app.route('/tasks')
245
- def list_tasks():
246
- # Nettoyer les tâches anciennes (plus de 24h)
247
- current_time = datetime.now()
248
- to_remove = []
249
-
250
- for task_id, task in active_tasks.items():
251
- created_at = datetime.fromisoformat(task['created_at'])
252
- if (current_time - created_at).total_seconds() > 86400: # 24 heures
253
- to_remove.append(task_id)
254
-
255
- for task_id in to_remove:
256
- del active_tasks[task_id]
257
 
258
- # Retourner la liste des tâches sans les données manga
259
- tasks_summary = {}
260
- for task_id, task in active_tasks.items():
261
- task_copy = task.copy()
262
- if 'manga_data' in task_copy:
263
- del task_copy['manga_data']
264
- tasks_summary[task_id] = task_copy
265
-
266
- return jsonify(tasks_summary)
267
 
 
 
 
268
  if __name__ == '__main__':
269
- # Vérifier que l'API key est configurée
270
  if not os.environ.get("GEMINI_API_KEY"):
271
- print("⚠️ ATTENTION: La variable d'environnement GEMINI_API_KEY n'est pas définie!")
272
- print(" Définissez-la avec: export GEMINI_API_KEY=votre_clé_api")
273
-
274
- app.run(debug=True, host='0.0.0.0', port=5000)
 
1
+ # -----------------------------------------------------------------------------
2
+ # Imports
3
+ # -----------------------------------------------------------------------------
4
+ from flask import Flask, render_template, request, jsonify, send_file, send_from_directory
5
  import os
6
  import json
7
  import uuid
8
  import threading
9
  import time
10
  from datetime import datetime
 
11
  import mimetypes
12
  from google import genai
13
  from google.genai import types
14
+ import zipfile # Bibliothèque pour créer des archives ZIP
 
 
 
 
15
 
16
+ # -----------------------------------------------------------------------------
17
+ # Initialisation de l'application Flask et Configuration
18
+ # -----------------------------------------------------------------------------
19
  app = Flask(__name__)
20
 
21
+ # Configuration des dossiers de sortie
22
  UPLOAD_FOLDER = 'generated_pages'
23
+ ZIP_FOLDER = 'generated_zips'
24
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
25
+ os.makedirs(ZIP_FOLDER, exist_ok=True)
26
 
27
+ # Dictionnaire pour stocker l'état des tâches en cours
28
  active_tasks = {}
29
 
30
+ # -----------------------------------------------------------------------------
31
+ # Classe pour interagir avec l'API Gemini
32
+ # -----------------------------------------------------------------------------
33
  class MangaGenerator:
34
+ """Gère la communication avec l'API Google Gemini pour générer des images."""
35
  def __init__(self, api_key):
36
  self.client = genai.Client(api_key=api_key)
37
  self.model = "gemini-2.5-flash-image-preview"
38
 
39
  def save_binary_file(self, file_name, data):
40
+ """Sauvegarde les données binaires (image) dans un fichier."""
41
  try:
42
  with open(file_name, "wb") as f:
43
  f.write(data)
44
  return True
45
  except Exception as e:
46
+ print(f"Erreur lors de la sauvegarde du fichier {file_name}: {e}")
47
  return False
48
 
49
  def generate_page(self, prompt, page_number, task_id):
50
+ """Envoie un prompt à Gemini et sauvegarde l'image générée."""
51
  try:
52
+ contents = [types.Content(role="user", parts=[types.Part.from_text(text=prompt)])]
53
+ generate_content_config = types.GenerateContentConfig(response_modalities=["IMAGE", "TEXT"])
 
 
 
 
 
 
 
 
 
 
 
54
  generated_files = []
55
 
56
  for chunk in self.client.models.generate_content_stream(
 
58
  contents=contents,
59
  config=generate_content_config,
60
  ):
61
+ if (chunk.candidates and chunk.candidates[0].content and chunk.candidates[0].content.parts):
62
+ part = chunk.candidates[0].content.parts[0]
63
+ if part.inline_data and part.inline_data.data:
64
+ file_name_base = f"{UPLOAD_FOLDER}/page_{page_number}_{task_id}"
65
+ data_buffer = part.inline_data.data
66
+ file_extension = mimetypes.guess_extension(part.inline_data.mime_type) or '.png'
67
+
68
+ full_path = f"{file_name_base}{file_extension}"
69
+ if self.save_binary_file(full_path, data_buffer):
70
+ generated_files.append(full_path)
 
 
 
 
 
 
 
 
 
 
 
71
 
72
  return generated_files
73
 
 
75
  print(f"Erreur lors de la génération de la page {page_number}: {e}")
76
  return []
77
 
78
+ # -----------------------------------------------------------------------------
79
+ # Fonctions Utilitaires
80
+ # -----------------------------------------------------------------------------
81
+ def create_zip(image_paths, output_path):
82
+ """Crée une archive ZIP à partir d'une liste de chemins d'images."""
83
  try:
84
+ with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
85
+ for img_path in sorted(image_paths):
86
+ if os.path.exists(img_path):
87
+ # On ajoute l'image à l'archive avec son nom de base (sans le chemin du dossier)
88
+ zipf.write(img_path, os.path.basename(img_path))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  return True
 
90
  except Exception as e:
91
+ print(f"Erreur lors de la création de l'archive ZIP: {e}")
92
  return False
93
 
94
+ # -----------------------------------------------------------------------------
95
+ # Tâche de fond pour la génération
96
+ # -----------------------------------------------------------------------------
97
  def generate_manga_task(manga_data, task_id):
98
+ """
99
+ Tâche exécutée en arrière-plan (thread) pour générer toutes les pages du manga.
100
+ """
101
  try:
102
  api_key = os.environ.get("GEMINI_API_KEY")
103
  if not api_key:
104
+ active_tasks[task_id].update({'status': 'error', 'error': 'API Key Gemini non trouvée'})
 
105
  return
106
 
107
  generator = MangaGenerator(api_key)
 
 
 
108
 
109
+ # Initialisation de la tâche
110
+ active_tasks[task_id].update({
111
+ 'status': 'generating',
112
+ 'total_pages': len(manga_data),
113
+ 'current_page': 0,
114
+ 'generated_files': [] # Important pour le suivi progressif
115
+ })
116
 
117
+ # Trier les parties pour les générer dans l'ordre (partie-1, partie-2, etc.)
118
  sorted_parts = sorted(manga_data.items(), key=lambda x: int(x[0].split('-')[1]))
119
 
120
+ # Boucle de génération pour chaque page
121
  for i, (part_key, prompt) in enumerate(sorted_parts, 1):
122
+ active_tasks[task_id].update({'current_page': i, 'current_part': part_key})
123
+ print(f"Génération de la page {i}/{len(sorted_parts)} pour la tâche {task_id}")
 
 
124
 
125
  page_files = generator.generate_page(prompt, i, task_id)
126
+ active_tasks[task_id]['generated_files'].extend(page_files)
127
 
128
+ time.sleep(1) # Petite pause pour éviter de surcharger l'API
129
+
130
+ # Création de l'archive ZIP
131
+ active_tasks[task_id]['status'] = 'creating_zip'
132
+ zip_path = f"{ZIP_FOLDER}/manga_{task_id}.zip"
133
+
134
+ if create_zip(active_tasks[task_id]['generated_files'], zip_path):
135
+ active_tasks[task_id].update({
136
+ 'status': 'completed',
137
+ 'zip_path': zip_path,
138
+ 'completed_at': datetime.now().isoformat()
139
+ })
140
  else:
141
+ active_tasks[task_id].update({
142
+ 'status': 'error',
143
+ 'error': "Erreur lors de la création de l'archive ZIP"
144
+ })
145
 
146
  except Exception as e:
147
+ active_tasks[task_id].update({'status': 'error', 'error': str(e)})
148
+ print(f"Erreur majeure dans la tâche {task_id}: {e}")
 
149
 
150
+ # -----------------------------------------------------------------------------
151
+ # Routes de l'API Flask
152
+ # -----------------------------------------------------------------------------
153
  @app.route('/')
154
  def index():
155
+ """Sert la page principale de l'application."""
156
  return render_template('index.html')
157
 
158
+ @app.route('/generated_pages/<filename>')
159
+ def serve_generated_image(filename):
160
+ """
161
+ Sert les fichiers images individuels pour l'affichage progressif sur la page web.
162
+ """
163
+ return send_from_directory(UPLOAD_FOLDER, filename)
164
+
165
  @app.route('/generate', methods=['POST'])
166
  def generate():
167
+ """
168
+ Démarre une nouvelle tâche de génération de manga.
169
+ """
170
  try:
171
  data = request.get_json()
 
172
  if not data:
173
  return jsonify({'error': 'Aucune donnée JSON fournie'}), 400
174
 
 
175
  manga_parts = {k: v for k, v in data.items() if k.startswith('partie-')}
 
176
  if not manga_parts:
177
+ return jsonify({'error': 'Aucune "partie-X" trouvée dans les données'}), 400
178
 
 
179
  task_id = str(uuid.uuid4())
180
  active_tasks[task_id] = {
181
  'status': 'queued',
182
+ 'created_at': datetime.now().isoformat()
 
183
  }
184
 
185
+ # Lancer la génération dans un thread pour ne pas bloquer la requête
186
  thread = threading.Thread(target=generate_manga_task, args=(manga_parts, task_id))
187
  thread.daemon = True
188
  thread.start()
189
 
190
+ return jsonify({'task_id': task_id, 'status': 'queued'})
 
 
 
 
191
 
192
  except Exception as e:
193
  return jsonify({'error': str(e)}), 500
194
 
195
  @app.route('/status/<task_id>')
196
  def get_status(task_id):
197
+ """
198
+ Retourne le statut actuel d'une tâche de génération.
199
+ """
200
  if task_id not in active_tasks:
201
  return jsonify({'error': 'Tâche non trouvée'}), 404
202
 
203
  task = active_tasks[task_id].copy()
204
+
205
+ # Transformer les chemins de fichiers locaux en URLs accessibles par le navigateur
206
+ if 'generated_files' in task:
207
+ task['image_urls'] = [f"/generated_pages/{os.path.basename(p)}" for p in task['generated_files']]
208
 
209
  return jsonify(task)
210
 
211
  @app.route('/download/<task_id>')
212
+ def download_zip(task_id):
213
+ """
214
+ Permet de télécharger l'archive ZIP finale.
215
+ """
216
  if task_id not in active_tasks:
217
  return jsonify({'error': 'Tâche non trouvée'}), 404
218
 
219
  task = active_tasks[task_id]
220
+ if task.get('status') != 'completed' or 'zip_path' not in task:
221
+ return jsonify({'error': 'Archive ZIP non disponible ou non finalisée'}), 400
 
 
 
 
222
 
223
+ zip_path = task['zip_path']
224
+ if not os.path.exists(zip_path):
225
+ return jsonify({'error': 'Fichier ZIP non trouvé sur le serveur'}), 404
 
 
 
 
 
 
 
 
 
 
 
 
 
226
 
227
+ return send_file(zip_path, as_attachment=True, download_name=f'manga_{task_id}.zip')
 
 
 
 
 
 
 
 
228
 
229
+ # -----------------------------------------------------------------------------
230
+ # Démarrage de l'application
231
+ # -----------------------------------------------------------------------------
232
  if __name__ == '__main__':
233
+ # Vérification de la présence de la clé API au démarrage
234
  if not os.environ.get("GEMINI_API_KEY"):
235
+ print("\n⚠️ ATTENTION: La variable d'environnement GEMINI_API_KE