Update app.py
Browse files
app.py
CHANGED
@@ -1,62 +1,56 @@
|
|
1 |
-
|
|
|
|
|
|
|
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 |
-
|
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 |
-
|
23 |
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
24 |
-
os.makedirs(
|
25 |
|
26 |
-
#
|
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
|
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 |
-
"""
|
46 |
try:
|
47 |
-
contents = [
|
48 |
-
|
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
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
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 |
-
|
96 |
-
|
|
|
|
|
|
|
97 |
try:
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
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
|
126 |
return False
|
127 |
|
|
|
|
|
|
|
128 |
def generate_manga_task(manga_data, task_id):
|
129 |
-
"""
|
|
|
|
|
130 |
try:
|
131 |
api_key = os.environ.get("GEMINI_API_KEY")
|
132 |
if not api_key:
|
133 |
-
active_tasks[task_id]
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
143 |
|
144 |
-
# Trier les parties
|
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]
|
149 |
-
|
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 |
-
#
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
|
|
167 |
else:
|
168 |
-
active_tasks[task_id]
|
169 |
-
|
|
|
|
|
170 |
|
171 |
except Exception as e:
|
172 |
-
active_tasks[task_id]
|
173 |
-
|
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
|
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 |
-
|
223 |
-
|
224 |
-
|
|
|
225 |
|
226 |
return jsonify(task)
|
227 |
|
228 |
@app.route('/download/<task_id>')
|
229 |
-
def
|
|
|
|
|
|
|
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
|
235 |
-
return jsonify({'error': '
|
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 |
-
|
242 |
-
|
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 |
-
|
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 |
-
#
|
270 |
if not os.environ.get("GEMINI_API_KEY"):
|
271 |
-
print("⚠️ ATTENTION: La variable d'environnement
|
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
|
|
|
|
|
|