File size: 9,940 Bytes
481cfe6
de01232
481cfe6
2171485
481cfe6
 
 
 
 
 
 
 
 
 
 
9b43085
c2fae2a
de01232
 
481cfe6
 
 
 
 
9b43085
481cfe6
 
9b43085
481cfe6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9b43085
481cfe6
 
 
 
 
 
 
 
9b43085
481cfe6
 
9b43085
481cfe6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9b43085
481cfe6
 
c2fae2a
481cfe6
 
9b43085
481cfe6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9b43085
481cfe6
 
c2fae2a
481cfe6
 
de01232
481cfe6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c2fae2a
481cfe6
 
 
c2fae2a
481cfe6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2171485
481cfe6
 
 
2171485
481cfe6
de01232
2171485
 
481cfe6
 
 
 
de01232
481cfe6
 
 
 
 
 
c2fae2a
481cfe6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c2fae2a
481cfe6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
de01232
 
481cfe6
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
from flask import Flask, render_template, request, jsonify, send_file
import os
import json
import uuid
import threading
import time
from datetime import datetime
import base64
import mimetypes
from google import genai
from google.genai import types
from reportlab.lib.pagesizes import A4
from reportlab.platypus import SimpleDocTemplate, Image
from reportlab.lib.utils import ImageReader
from PIL import Image as PILImage
import io

app = Flask(__name__)

# Configuration
UPLOAD_FOLDER = 'generated_pages'
PDF_FOLDER = 'generated_pdfs'
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(PDF_FOLDER, exist_ok=True)

# Stockage des tâches en cours
active_tasks = {}

class MangaGenerator:
    def __init__(self, api_key):
        self.client = genai.Client(api_key=api_key)
        self.model = "gemini-2.5-flash-image-preview"
    
    def save_binary_file(self, file_name, data):
        """Sauvegarde un fichier binaire"""
        try:
            with open(file_name, "wb") as f:
                f.write(data)
            return True
        except Exception as e:
            print(f"Erreur lors de la sauvegarde: {e}")
            return False
    
    def generate_page(self, prompt, page_number, task_id):
        """Génère une page de manga avec le prompt donné"""
        try:
            contents = [
                types.Content(
                    role="user",
                    parts=[
                        types.Part.from_text(text=prompt),
                    ],
                ),
            ]
            
            generate_content_config = types.GenerateContentConfig(
                response_modalities=["IMAGE", "TEXT"],
            )
            
            generated_files = []
            
            for chunk in self.client.models.generate_content_stream(
                model=self.model,
                contents=contents,
                config=generate_content_config,
            ):
                if (
                    chunk.candidates is None
                    or chunk.candidates[0].content is None
                    or chunk.candidates[0].content.parts is None
                ):
                    continue
                
                if (chunk.candidates[0].content.parts[0].inline_data and 
                    chunk.candidates[0].content.parts[0].inline_data.data):
                    
                    file_name = f"{UPLOAD_FOLDER}/page_{page_number}_{task_id}"
                    inline_data = chunk.candidates[0].content.parts[0].inline_data
                    data_buffer = inline_data.data
                    file_extension = mimetypes.guess_extension(inline_data.mime_type) or '.png'
                    
                    full_path = f"{file_name}{file_extension}"
                    if self.save_binary_file(full_path, data_buffer):
                        generated_files.append(full_path)
                else:
                    if hasattr(chunk, 'text') and chunk.text:
                        print(f"Texte généré pour page {page_number}: {chunk.text}")
            
            return generated_files
            
        except Exception as e:
            print(f"Erreur lors de la génération de la page {page_number}: {e}")
            return []

def create_pdf(image_paths, output_path):
    """Crée un PDF à partir des images générées"""
    try:
        doc = SimpleDocTemplate(output_path, pagesize=A4)
        story = []
        
        for img_path in sorted(image_paths):
            if os.path.exists(img_path):
                # Redimensionner l'image pour s'adapter à la page A4
                with PILImage.open(img_path) as img:
                    img_buffer = io.BytesIO()
                    img.save(img_buffer, format='PNG')
                    img_buffer.seek(0)
                    
                    # Calculer les dimensions pour s'adapter à A4
                    page_width, page_height = A4
                    img_width, img_height = img.size
                    
                    # Maintenir le ratio d'aspect
                    ratio = min(page_width/img_width, page_height/img_height) * 0.9
                    new_width = img_width * ratio
                    new_height = img_height * ratio
                    
                    story.append(Image(ImageReader(img_buffer), 
                                     width=new_width, height=new_height))
        
        doc.build(story)
        return True
        
    except Exception as e:
        print(f"Erreur lors de la création du PDF: {e}")
        return False

def generate_manga_task(manga_data, task_id):
    """Tâche de génération de manga qui s'exécute en arrière-plan"""
    try:
        api_key = os.environ.get("GEMINI_API_KEY")
        if not api_key:
            active_tasks[task_id]['status'] = 'error'
            active_tasks[task_id]['error'] = 'API Key Gemini non trouvée'
            return
        
        generator = MangaGenerator(api_key)
        active_tasks[task_id]['status'] = 'generating'
        active_tasks[task_id]['total_pages'] = len(manga_data)
        active_tasks[task_id]['current_page'] = 0
        
        generated_files = []
        
        # Trier les parties par ordre numérique
        sorted_parts = sorted(manga_data.items(), key=lambda x: int(x[0].split('-')[1]))
        
        for i, (part_key, prompt) in enumerate(sorted_parts, 1):
            active_tasks[task_id]['current_page'] = i
            active_tasks[task_id]['current_part'] = part_key
            
            print(f"Génération de la page {i}/{len(sorted_parts)} - {part_key}")
            
            page_files = generator.generate_page(prompt, i, task_id)
            generated_files.extend(page_files)
            
            # Attendre un peu entre les générations pour éviter les limites de taux
            time.sleep(2)
        
        # Créer le PDF
        active_tasks[task_id]['status'] = 'creating_pdf'
        pdf_path = f"{PDF_FOLDER}/manga_{task_id}.pdf"
        
        if create_pdf(generated_files, pdf_path):
            active_tasks[task_id]['status'] = 'completed'
            active_tasks[task_id]['pdf_path'] = pdf_path
            active_tasks[task_id]['completed_at'] = datetime.now().isoformat()
        else:
            active_tasks[task_id]['status'] = 'error'
            active_tasks[task_id]['error'] = 'Erreur lors de la création du PDF'
            
    except Exception as e:
        active_tasks[task_id]['status'] = 'error'
        active_tasks[task_id]['error'] = str(e)
        print(f"Erreur dans la tâche {task_id}: {e}")

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/generate', methods=['POST'])
def generate():
    try:
        data = request.get_json()
        
        if not data:
            return jsonify({'error': 'Aucune donnée JSON fournie'}), 400
        
        # Valider que les données contiennent des parties
        manga_parts = {k: v for k, v in data.items() if k.startswith('partie-')}
        
        if not manga_parts:
            return jsonify({'error': 'Aucune partie trouvée dans les données'}), 400
        
        # Créer une nouvelle tâche
        task_id = str(uuid.uuid4())
        active_tasks[task_id] = {
            'status': 'queued',
            'created_at': datetime.now().isoformat(),
            'manga_data': manga_parts
        }
        
        # Lancer la génération en arrière-plan
        thread = threading.Thread(target=generate_manga_task, args=(manga_parts, task_id))
        thread.daemon = True
        thread.start()
        
        return jsonify({
            'task_id': task_id,
            'status': 'queued',
            'message': 'Génération démarrée'
        })
        
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/status/<task_id>')
def get_status(task_id):
    if task_id not in active_tasks:
        return jsonify({'error': 'Tâche non trouvée'}), 404
    
    task = active_tasks[task_id].copy()
    # Ne pas renvoyer les données du manga dans le status pour économiser la bande passante
    if 'manga_data' in task:
        del task['manga_data']
    
    return jsonify(task)

@app.route('/download/<task_id>')
def download_pdf(task_id):
    if task_id not in active_tasks:
        return jsonify({'error': 'Tâche non trouvée'}), 404
    
    task = active_tasks[task_id]
    if task['status'] != 'completed' or 'pdf_path' not in task:
        return jsonify({'error': 'PDF non disponible'}), 400
    
    pdf_path = task['pdf_path']
    if not os.path.exists(pdf_path):
        return jsonify({'error': 'Fichier PDF non trouvé'}), 404
    
    return send_file(pdf_path, as_attachment=True, 
                     download_name=f'manga_{task_id}.pdf')

@app.route('/tasks')
def list_tasks():
    # Nettoyer les tâches anciennes (plus de 24h)
    current_time = datetime.now()
    to_remove = []
    
    for task_id, task in active_tasks.items():
        created_at = datetime.fromisoformat(task['created_at'])
        if (current_time - created_at).total_seconds() > 86400:  # 24 heures
            to_remove.append(task_id)
    
    for task_id in to_remove:
        del active_tasks[task_id]
    
    # Retourner la liste des tâches sans les données manga
    tasks_summary = {}
    for task_id, task in active_tasks.items():
        task_copy = task.copy()
        if 'manga_data' in task_copy:
            del task_copy['manga_data']
        tasks_summary[task_id] = task_copy
    
    return jsonify(tasks_summary)

if __name__ == '__main__':
    # Vérifier que l'API key est configurée
    if not os.environ.get("GEMINI_API_KEY"):
        print("⚠️  ATTENTION: La variable d'environnement GEMINI_API_KEY n'est pas définie!")
        print("   Définissez-la avec: export GEMINI_API_KEY=votre_clé_api")
    
    app.run(debug=True, host='0.0.0.0', port=5000)