JairoCesar commited on
Commit
819cf24
·
verified ·
1 Parent(s): 1af37fb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +47 -43
app.py CHANGED
@@ -1,7 +1,7 @@
1
  import streamlit as st
2
  from pptx import Presentation
3
  from pptx.util import Inches, Pt
4
- from pptx.enum.text import MSO_ANCHOR, MSO_AUTO_SIZE
5
  import requests
6
  from PIL import Image
7
  from io import BytesIO
@@ -65,12 +65,12 @@ def parse_gemini_response_for_slides(response_text, expected_slides_key="slides"
65
  except json.JSONDecodeError as e:
66
  raise ValueError(f"Error al decodificar JSON: {e}. JSON intentado: {json_str[:500]}...")
67
 
68
- if is_single_slide: # Para el título creativo
69
  if not (isinstance(data, dict) and 'title' in data and 'content' in data):
70
  raise ValueError("La estructura del JSON para la diapositiva de título es incorrecta. Se esperaba 'title' y 'content'.")
71
  if not (isinstance(data['title'], str) and isinstance(data['content'], str)):
72
  raise ValueError("El título y el contenido de la diapositiva de título deben ser cadenas.")
73
- return [data] # Devolver como lista de una diapositiva
74
 
75
  if not isinstance(data, dict) or expected_slides_key not in data:
76
  raise ValueError(f"La estructura del JSON es incorrecta. Falta la clave '{expected_slides_key}'.")
@@ -90,7 +90,6 @@ def parse_gemini_response_for_slides(response_text, expected_slides_key="slides"
90
  return slides
91
 
92
  def generate_creative_title_slide(topic, client, max_retries=3):
93
- """Genera una única diapositiva de título creativa."""
94
  prompt = f"""Genera un título MUY llamativo y creativo para una presentación sobre "{topic}".
95
  Además, proporciona una breve frase o subtítulo de impacto para acompañar el título.
96
  Es CRUCIAL que tu respuesta sea ÚNICAMENTE un JSON válido con la siguiente estructura exacta, sin texto adicional antes o después:
@@ -109,7 +108,7 @@ def generate_creative_title_slide(topic, client, max_retries=3):
109
  if not response_obj.parts: raise ValueError("Respuesta del modelo vacía (título creativo).")
110
  last_response_text = response_obj.text
111
  title_slide_data = parse_gemini_response_for_slides(last_response_text, is_single_slide=True)
112
- return title_slide_data # Es una lista con un solo diccionario de slide
113
  except (ValueError, json.JSONDecodeError) as e:
114
  last_error = e
115
  st.warning(f"Intento {attempt + 1}/{max_retries} (título creativo) fallido: {e}")
@@ -155,7 +154,6 @@ def generate_presentation_content(topic, client, max_retries=3):
155
  return None
156
 
157
  def generate_conclusion_slides_from_text(text_to_summarize, num_conclusion_slides, topic, client, max_retries=3):
158
- """Genera diapositivas de conclusión basadas en el texto y el tema general."""
159
  if not text_to_summarize: return []
160
  max_len = 30000
161
  if len(text_to_summarize) > max_len:
@@ -225,16 +223,14 @@ def add_slide_content_and_image(slide, slide_data, prs_slide_width, prs_slide_he
225
  slide.shapes.title.text = slide_data['title']
226
 
227
  content_placeholder = None
228
- # Para la diapositiva de título, el placeholder de contenido puede ser el primero si el layout es de título solo
229
  if is_title_slide and slide.placeholders and slide.placeholders[0] == slide.shapes.title and len(slide.placeholders) > 1:
230
- content_placeholder = slide.placeholders[1] # Subtitle placeholder
231
  elif not is_title_slide and slide.placeholders and len(slide.placeholders) > 1 and slide.placeholders[1]:
232
- content_placeholder = slide.placeholders[1] # Body placeholder
233
 
234
  margin_emu = Inches(0.3)
235
- # Si es diapositiva de título, el contenido (subtítulo) puede estar más arriba
236
  common_top_emu = Inches(0.5) if is_title_slide and slide.shapes.title else Inches(1.6)
237
- if is_title_slide and content_placeholder: # Ajustar top del subtitulo
238
  common_top_emu = content_placeholder.top
239
 
240
  content_bottom_emu = prs_slide_height - margin_emu
@@ -243,18 +239,17 @@ def add_slide_content_and_image(slide, slide_data, prs_slide_width, prs_slide_he
243
  text_area_left_emu = margin_emu
244
 
245
  img_area_width_float = 0
246
- if add_image and not is_title_slide: # No añadir imagen a la diapositiva de título principal
247
  img_area_width_float = prs_slide_width * 0.40
248
- text_area_width_float = prs_slide_width - img_area_width_float - (3 * margin_emu) # +margen entre texto e img
249
-
250
 
251
  common_height_emu = content_bottom_emu - common_top_emu
252
- if is_title_slide and content_placeholder: # Ajustar altura del subtitulo
253
  common_height_emu = content_placeholder.height
254
 
255
  if content_placeholder:
256
  content_placeholder.left = int(text_area_left_emu)
257
- if not is_title_slide: # No sobreescribir el top del subtítulo si ya está bien
258
  content_placeholder.top = int(common_top_emu)
259
  content_placeholder.width = int(text_area_width_float)
260
  content_placeholder.height = int(common_height_emu)
@@ -263,23 +258,23 @@ def add_slide_content_and_image(slide, slide_data, prs_slide_width, prs_slide_he
263
  p = tf.add_paragraph(); p.text = slide_data['content']
264
  tf.word_wrap = True
265
  tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE if not is_title_slide else MSO_AUTO_SIZE.NONE
266
- if is_title_slide: # Centrar subtítulo
267
- p.alignment = PP_ALIGN.CENTER # Necesitarías: from pptx.enum.text import PP_ALIGN
268
  tf.vertical_anchor = MSO_ANCHOR.MIDDLE
269
  else:
270
  txBox_top = common_top_emu
271
- if is_title_slide and slide.shapes.title: # Colocar subtítulo debajo del título principal
272
  txBox_top = slide.shapes.title.top + slide.shapes.title.height + Inches(0.2)
273
 
274
  txBox = slide.shapes.add_textbox(
275
  int(text_area_left_emu), int(txBox_top),
276
- int(text_area_width_float), int(common_height_emu / (1.5 if is_title_slide else 1) ) # Menos altura para subtitulo
277
  )
278
  tf = txBox.text_frame; tf.text = slide_data['content']
279
  tf.word_wrap = True
280
  tf.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT if is_title_slide else MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
281
- if is_title_slide: # Centrar subtítulo
282
- p = tf.paragraphs[0]; p.alignment = PP_ALIGN.CENTER # Necesitarías: from pptx.enum.text import PP_ALIGN
283
  tf.vertical_anchor = MSO_ANCHOR.TOP
284
 
285
 
@@ -327,29 +322,34 @@ def create_powerpoint(title_slide_content, main_slides_content, template_path, c
327
  prs = Presentation(template_path)
328
  slide_width_emu = prs.slide_width
329
  slide_height_emu = prs.slide_height
330
- from pptx.enum.text import PP_ALIGN # Importar aquí para evitar error si no se usa add_slide_content_and_image
331
 
332
- # 0. Diapositiva de Título Creativa (usar layout de título, ej. 0 o 5)
333
- if title_slide_content: # es una lista con 1 slide_data
334
- # Intentar usar un layout de solo título si está disponible, sino el de título y contenido.
335
- title_layout = prs.slide_layouts[0] if prs.slide_layouts else prs.slide_layouts[5] if len(prs.slide_layouts) > 5 else prs.slide_layouts[1]
 
 
 
 
 
 
 
 
 
336
  slide = prs.slides.add_slide(title_layout)
337
  add_slide_content_and_image(slide, title_slide_content[0], slide_width_emu, slide_height_emu, add_image=False, is_title_slide=True)
338
 
339
- # 1. Diapositivas principales (con imágenes)
340
  for slide_data in main_slides_content:
341
- slide_layout = prs.slide_layouts[1] # Título y Contenido
342
  slide = prs.slides.add_slide(slide_layout)
343
  add_slide_content_and_image(slide, slide_data, slide_width_emu, slide_height_emu, add_image=True)
344
 
345
- # 2. Diapositivas de Conclusión (sin imágenes)
346
  if conclusion_slides_content:
347
  for slide_data in conclusion_slides_content:
348
  slide_layout = prs.slide_layouts[1]
349
  slide = prs.slides.add_slide(slide_layout)
350
  add_slide_content_and_image(slide, slide_data, slide_width_emu, slide_height_emu, add_image=False)
351
 
352
- # 3. Diapositiva final de agradecimiento
353
  final_slide_layout = prs.slide_layouts[1]
354
  final_slide = prs.slides.add_slide(final_slide_layout)
355
  if final_slide.shapes.title: final_slide.shapes.title.text = "¡Gracias por su atención!"
@@ -374,7 +374,6 @@ def main():
374
  topic = st.sidebar.text_input("📝 Tema principal de la presentación:", placeholder="Ej: El futuro de la IA")
375
 
376
  plantillas_dir = "PLANTILLAS"
377
- # ... (resto de la lógica de plantillas sin cambios, solo movida a sidebar si no estaba ya) ...
378
  if not os.path.exists(plantillas_dir):
379
  try: os.makedirs(plantillas_dir); st.sidebar.info(f"Directorio '{plantillas_dir}' creado.")
380
  except OSError as e: st.sidebar.error(f"No se pudo crear dir '{plantillas_dir}': {e}"); st.stop()
@@ -386,13 +385,26 @@ def main():
386
  if not available_templates:
387
  st.sidebar.warning(f"No hay plantillas .pptx en '{plantillas_dir}'.")
388
  default_prs = Presentation()
389
- if not default_prs.slide_layouts: default_prs.slide_master.slide_layouts.add_slide_layout()
390
- if default_prs.slide_layouts: default_prs.slides.add_slide(default_prs.slide_layouts[0])
 
 
 
 
 
 
 
 
 
 
 
 
391
  default_template_path = os.path.join(plantillas_dir, "default.pptx")
392
  try: default_prs.save(default_template_path); st.sidebar.info(f"Plantilla 'default.pptx' creada."); available_templates.append("default.pptx")
393
  except Exception as e: st.sidebar.error(f"No se pudo crear plantilla por defecto: {e}")
 
394
  template_options = {os.path.splitext(t)[0]: os.path.join(plantillas_dir, t) for t in available_templates}
395
- if not template_options: st.sidebar.error("CRÍTICO: No hay plantillas."); st.stop()
396
  selected_template_name = st.sidebar.selectbox("🎨 Seleccione una plantilla:", list(template_options.keys()))
397
 
398
 
@@ -416,25 +428,20 @@ def main():
416
  overall_progress = st.progress(0)
417
  status_text = st.empty()
418
 
419
- # 0. Generar título creativo
420
  status_text.info("🎨 Creando un título impactante...")
421
  title_slide_content = generate_creative_title_slide(topic, client)
422
  if not title_slide_content:
423
  status_text.warning("⚠️ No se pudo generar el título creativo, continuando sin él.")
424
- # No detenemos la ejecución, solo no habrá diapositiva de título.
425
  else:
426
  status_text.success("👍 Título creativo generado.")
427
  overall_progress.progress(10)
428
 
429
-
430
- # 1. Generar contenido principal
431
  status_text.info("🧠 Generando contenido principal con Gemini...")
432
  main_slides_content = generate_presentation_content(topic, client)
433
  if not main_slides_content:
434
  status_text.error("❌ Falló la generación del contenido principal."); overall_progress.progress(100); return
435
  status_text.success("👍 Contenido principal generado."); overall_progress.progress(50)
436
 
437
- # 2. Procesar PDF para conclusiones
438
  if uploaded_pdf and num_conclusion_slides > 0:
439
  status_text.info(f"📄 Procesando PDF '{uploaded_pdf.name}' para conclusiones...")
440
  pdf_text = extract_text_from_pdf(uploaded_pdf); overall_progress.progress(60)
@@ -446,11 +453,8 @@ def main():
446
  else: status_text.warning("⚠️ No se pudo extraer texto del PDF.")
447
  overall_progress.progress(80)
448
 
449
- # 3. Crear archivo PowerPoint
450
  status_text.info("🛠️ Creando archivo PowerPoint...")
451
  try:
452
- # Necesitamos PP_ALIGN para centrar el subtítulo de la diapositiva de título
453
- from pptx.enum.text import PP_ALIGN
454
  pptx_buffer = create_powerpoint(title_slide_content, main_slides_content, template_path, conclusion_slides_content)
455
  overall_progress.progress(100)
456
  status_text.success("🎉 ¡Presentación generada con éxito!")
 
1
  import streamlit as st
2
  from pptx import Presentation
3
  from pptx.util import Inches, Pt
4
+ from pptx.enum.text import MSO_ANCHOR, MSO_AUTO_SIZE, PP_ALIGN # IMPORTACIÓN CORREGIDA AQUÍ
5
  import requests
6
  from PIL import Image
7
  from io import BytesIO
 
65
  except json.JSONDecodeError as e:
66
  raise ValueError(f"Error al decodificar JSON: {e}. JSON intentado: {json_str[:500]}...")
67
 
68
+ if is_single_slide:
69
  if not (isinstance(data, dict) and 'title' in data and 'content' in data):
70
  raise ValueError("La estructura del JSON para la diapositiva de título es incorrecta. Se esperaba 'title' y 'content'.")
71
  if not (isinstance(data['title'], str) and isinstance(data['content'], str)):
72
  raise ValueError("El título y el contenido de la diapositiva de título deben ser cadenas.")
73
+ return [data]
74
 
75
  if not isinstance(data, dict) or expected_slides_key not in data:
76
  raise ValueError(f"La estructura del JSON es incorrecta. Falta la clave '{expected_slides_key}'.")
 
90
  return slides
91
 
92
  def generate_creative_title_slide(topic, client, max_retries=3):
 
93
  prompt = f"""Genera un título MUY llamativo y creativo para una presentación sobre "{topic}".
94
  Además, proporciona una breve frase o subtítulo de impacto para acompañar el título.
95
  Es CRUCIAL que tu respuesta sea ÚNICAMENTE un JSON válido con la siguiente estructura exacta, sin texto adicional antes o después:
 
108
  if not response_obj.parts: raise ValueError("Respuesta del modelo vacía (título creativo).")
109
  last_response_text = response_obj.text
110
  title_slide_data = parse_gemini_response_for_slides(last_response_text, is_single_slide=True)
111
+ return title_slide_data
112
  except (ValueError, json.JSONDecodeError) as e:
113
  last_error = e
114
  st.warning(f"Intento {attempt + 1}/{max_retries} (título creativo) fallido: {e}")
 
154
  return None
155
 
156
  def generate_conclusion_slides_from_text(text_to_summarize, num_conclusion_slides, topic, client, max_retries=3):
 
157
  if not text_to_summarize: return []
158
  max_len = 30000
159
  if len(text_to_summarize) > max_len:
 
223
  slide.shapes.title.text = slide_data['title']
224
 
225
  content_placeholder = None
 
226
  if is_title_slide and slide.placeholders and slide.placeholders[0] == slide.shapes.title and len(slide.placeholders) > 1:
227
+ content_placeholder = slide.placeholders[1]
228
  elif not is_title_slide and slide.placeholders and len(slide.placeholders) > 1 and slide.placeholders[1]:
229
+ content_placeholder = slide.placeholders[1]
230
 
231
  margin_emu = Inches(0.3)
 
232
  common_top_emu = Inches(0.5) if is_title_slide and slide.shapes.title else Inches(1.6)
233
+ if is_title_slide and content_placeholder:
234
  common_top_emu = content_placeholder.top
235
 
236
  content_bottom_emu = prs_slide_height - margin_emu
 
239
  text_area_left_emu = margin_emu
240
 
241
  img_area_width_float = 0
242
+ if add_image and not is_title_slide:
243
  img_area_width_float = prs_slide_width * 0.40
244
+ text_area_width_float = prs_slide_width - img_area_width_float - (3 * margin_emu)
 
245
 
246
  common_height_emu = content_bottom_emu - common_top_emu
247
+ if is_title_slide and content_placeholder:
248
  common_height_emu = content_placeholder.height
249
 
250
  if content_placeholder:
251
  content_placeholder.left = int(text_area_left_emu)
252
+ if not is_title_slide:
253
  content_placeholder.top = int(common_top_emu)
254
  content_placeholder.width = int(text_area_width_float)
255
  content_placeholder.height = int(common_height_emu)
 
258
  p = tf.add_paragraph(); p.text = slide_data['content']
259
  tf.word_wrap = True
260
  tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE if not is_title_slide else MSO_AUTO_SIZE.NONE
261
+ if is_title_slide:
262
+ p.alignment = PP_ALIGN.CENTER
263
  tf.vertical_anchor = MSO_ANCHOR.MIDDLE
264
  else:
265
  txBox_top = common_top_emu
266
+ if is_title_slide and slide.shapes.title:
267
  txBox_top = slide.shapes.title.top + slide.shapes.title.height + Inches(0.2)
268
 
269
  txBox = slide.shapes.add_textbox(
270
  int(text_area_left_emu), int(txBox_top),
271
+ int(text_area_width_float), int(common_height_emu / (1.5 if is_title_slide else 1) )
272
  )
273
  tf = txBox.text_frame; tf.text = slide_data['content']
274
  tf.word_wrap = True
275
  tf.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT if is_title_slide else MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
276
+ if is_title_slide:
277
+ p = tf.paragraphs[0]; p.alignment = PP_ALIGN.CENTER
278
  tf.vertical_anchor = MSO_ANCHOR.TOP
279
 
280
 
 
322
  prs = Presentation(template_path)
323
  slide_width_emu = prs.slide_width
324
  slide_height_emu = prs.slide_height
 
325
 
326
+ if title_slide_content:
327
+ title_layout_index = 0
328
+ if len(prs.slide_layouts) > 5:
329
+ title_layout_index = 5
330
+ elif not prs.slide_layouts:
331
+ prs.slide_master.slide_layouts.add_slide_layout()
332
+ title_layout_index = 0
333
+
334
+ if title_layout_index >= len(prs.slide_layouts) :
335
+ title_layout_index = 0
336
+
337
+ title_layout = prs.slide_layouts[title_layout_index] if prs.slide_layouts else prs.slide_layouts[0] # Fallback final
338
+
339
  slide = prs.slides.add_slide(title_layout)
340
  add_slide_content_and_image(slide, title_slide_content[0], slide_width_emu, slide_height_emu, add_image=False, is_title_slide=True)
341
 
 
342
  for slide_data in main_slides_content:
343
+ slide_layout = prs.slide_layouts[1]
344
  slide = prs.slides.add_slide(slide_layout)
345
  add_slide_content_and_image(slide, slide_data, slide_width_emu, slide_height_emu, add_image=True)
346
 
 
347
  if conclusion_slides_content:
348
  for slide_data in conclusion_slides_content:
349
  slide_layout = prs.slide_layouts[1]
350
  slide = prs.slides.add_slide(slide_layout)
351
  add_slide_content_and_image(slide, slide_data, slide_width_emu, slide_height_emu, add_image=False)
352
 
 
353
  final_slide_layout = prs.slide_layouts[1]
354
  final_slide = prs.slides.add_slide(final_slide_layout)
355
  if final_slide.shapes.title: final_slide.shapes.title.text = "¡Gracias por su atención!"
 
374
  topic = st.sidebar.text_input("📝 Tema principal de la presentación:", placeholder="Ej: El futuro de la IA")
375
 
376
  plantillas_dir = "PLANTILLAS"
 
377
  if not os.path.exists(plantillas_dir):
378
  try: os.makedirs(plantillas_dir); st.sidebar.info(f"Directorio '{plantillas_dir}' creado.")
379
  except OSError as e: st.sidebar.error(f"No se pudo crear dir '{plantillas_dir}': {e}"); st.stop()
 
385
  if not available_templates:
386
  st.sidebar.warning(f"No hay plantillas .pptx en '{plantillas_dir}'.")
387
  default_prs = Presentation()
388
+ # Asegurar que hay al menos un layout usable
389
+ if not default_prs.slide_layouts:
390
+ try:
391
+ default_prs.slide_master.slide_layouts.add_slide_layout() # Intenta añadir un layout maestro
392
+ except Exception: # Si falla, al menos intenta con los layouts por defecto
393
+ pass
394
+ if default_prs.slide_layouts:
395
+ try:
396
+ default_prs.slides.add_slide(default_prs.slide_layouts[0])
397
+ except IndexError: # Si el layout 0 no existe tras añadir
398
+ if default_prs.slide_layouts: # Intenta con el primero que encuentre
399
+ default_prs.slides.add_slide(default_prs.slide_layouts[0])
400
+
401
+
402
  default_template_path = os.path.join(plantillas_dir, "default.pptx")
403
  try: default_prs.save(default_template_path); st.sidebar.info(f"Plantilla 'default.pptx' creada."); available_templates.append("default.pptx")
404
  except Exception as e: st.sidebar.error(f"No se pudo crear plantilla por defecto: {e}")
405
+
406
  template_options = {os.path.splitext(t)[0]: os.path.join(plantillas_dir, t) for t in available_templates}
407
+ if not template_options: st.sidebar.error("CRÍTICO: No hay plantillas. Cree 'PLANTILLAS' y añada un .pptx."); st.stop()
408
  selected_template_name = st.sidebar.selectbox("🎨 Seleccione una plantilla:", list(template_options.keys()))
409
 
410
 
 
428
  overall_progress = st.progress(0)
429
  status_text = st.empty()
430
 
 
431
  status_text.info("🎨 Creando un título impactante...")
432
  title_slide_content = generate_creative_title_slide(topic, client)
433
  if not title_slide_content:
434
  status_text.warning("⚠️ No se pudo generar el título creativo, continuando sin él.")
 
435
  else:
436
  status_text.success("👍 Título creativo generado.")
437
  overall_progress.progress(10)
438
 
 
 
439
  status_text.info("🧠 Generando contenido principal con Gemini...")
440
  main_slides_content = generate_presentation_content(topic, client)
441
  if not main_slides_content:
442
  status_text.error("❌ Falló la generación del contenido principal."); overall_progress.progress(100); return
443
  status_text.success("👍 Contenido principal generado."); overall_progress.progress(50)
444
 
 
445
  if uploaded_pdf and num_conclusion_slides > 0:
446
  status_text.info(f"📄 Procesando PDF '{uploaded_pdf.name}' para conclusiones...")
447
  pdf_text = extract_text_from_pdf(uploaded_pdf); overall_progress.progress(60)
 
453
  else: status_text.warning("⚠️ No se pudo extraer texto del PDF.")
454
  overall_progress.progress(80)
455
 
 
456
  status_text.info("🛠️ Creando archivo PowerPoint...")
457
  try:
 
 
458
  pptx_buffer = create_powerpoint(title_slide_content, main_slides_content, template_path, conclusion_slides_content)
459
  overall_progress.progress(100)
460
  status_text.success("🎉 ¡Presentación generada con éxito!")