Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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, PP_ALIGN
|
| 5 |
import requests
|
| 6 |
from PIL import Image
|
| 7 |
from io import BytesIO
|
|
@@ -154,47 +154,51 @@ def generate_presentation_content(topic, client, max_retries=3):
|
|
| 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:
|
| 160 |
-
st.warning(f"Texto del PDF ({len(text_to_summarize)} chars) truncado a {max_len} para
|
| 161 |
text_to_summarize = text_to_summarize[:max_len]
|
| 162 |
|
|
|
|
| 163 |
prompt = f"""Dado el siguiente texto (extraído de un documento relacionado con el tema general "{topic}"):
|
| 164 |
--- TEXTO DEL DOCUMENTO ---
|
| 165 |
{text_to_summarize}
|
| 166 |
--- FIN DEL TEXTO ---
|
| 167 |
|
| 168 |
-
Genera {num_conclusion_slides} diapositiva(s) de
|
| 169 |
-
Estas diapositivas deben sintetizar las ideas clave del texto proporcionado y
|
| 170 |
-
Los títulos de estas diapositivas deben
|
| 171 |
|
| 172 |
Es CRUCIAL que tu respuesta sea ÚNICAMENTE un JSON válido con la siguiente estructura exacta, sin texto adicional antes o después:
|
| 173 |
{{
|
| 174 |
"slides": [
|
| 175 |
-
{{"title": "Título
|
| 176 |
// ... (hasta num_conclusion_slides)
|
| 177 |
-
{{"title": "Título
|
| 178 |
]
|
| 179 |
}}
|
| 180 |
Debe haber exactamente {num_conclusion_slides} objetos en la lista "slides". Contenido conciso.
|
| 181 |
Usa comillas dobles para claves y strings.
|
| 182 |
"""
|
|
|
|
|
|
|
| 183 |
last_error = None; last_response_text = ""
|
| 184 |
for attempt in range(max_retries):
|
| 185 |
try:
|
| 186 |
response_obj = client.generate_content(prompt)
|
| 187 |
-
if not response_obj.parts: raise ValueError("Respuesta del modelo vacía (
|
| 188 |
last_response_text = response_obj.text
|
| 189 |
return parse_gemini_response_for_slides(last_response_text, expected_num_slides=num_conclusion_slides)
|
| 190 |
except (ValueError, json.JSONDecodeError) as e:
|
| 191 |
-
last_error = e; st.warning(f"Intento {attempt + 1}/{max_retries} (
|
| 192 |
if attempt == max_retries - 1:
|
| 193 |
-
st.error(f"Error final (
|
| 194 |
-
st.text_area("Última resp. (
|
| 195 |
except Exception as e:
|
| 196 |
-
last_error = e; st.warning(f"Error API Gemini (
|
| 197 |
-
if attempt == max_retries - 1: st.error(f"Error API Gemini (
|
| 198 |
return []
|
| 199 |
|
| 200 |
|
|
@@ -328,13 +332,29 @@ def create_powerpoint(title_slide_content, main_slides_content, template_path, c
|
|
| 328 |
if len(prs.slide_layouts) > 5:
|
| 329 |
title_layout_index = 5
|
| 330 |
elif not prs.slide_layouts:
|
| 331 |
-
|
|
|
|
|
|
|
| 332 |
title_layout_index = 0
|
| 333 |
|
| 334 |
if title_layout_index >= len(prs.slide_layouts) :
|
| 335 |
-
title_layout_index = 0
|
| 336 |
|
| 337 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
|
@@ -365,7 +385,7 @@ def create_powerpoint(title_slide_content, main_slides_content, template_path, c
|
|
| 365 |
def main():
|
| 366 |
st.set_page_config(page_title="PowerPoint Mágico", layout="wide", initial_sidebar_state="expanded")
|
| 367 |
st.title("✨ PowerPoint Mágico con el Búho 🦉")
|
| 368 |
-
st.markdown("Genera presentaciones impactantes con IA, incluyendo
|
| 369 |
|
| 370 |
if not GEMINI_API_KEY or not PIXABAY_API_KEY: return
|
| 371 |
client = get_gemini_model()
|
|
@@ -385,20 +405,13 @@ def main():
|
|
| 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 |
-
|
| 392 |
-
except Exception: # Si falla, al menos intenta con los layouts por defecto
|
| 393 |
-
pass
|
| 394 |
if default_prs.slide_layouts:
|
| 395 |
-
try:
|
| 396 |
-
|
| 397 |
-
|
| 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}")
|
|
@@ -408,11 +421,11 @@ def main():
|
|
| 408 |
selected_template_name = st.sidebar.selectbox("🎨 Seleccione una plantilla:", list(template_options.keys()))
|
| 409 |
|
| 410 |
|
| 411 |
-
st.sidebar.subheader("
|
| 412 |
-
uploaded_pdf = st.sidebar.file_uploader("📄 Cargue un PDF para generar
|
| 413 |
-
|
| 414 |
if uploaded_pdf:
|
| 415 |
-
|
| 416 |
|
| 417 |
if st.button("🚀 Generar Presentación", type="primary", use_container_width=True):
|
| 418 |
if not topic: st.warning("⚠️ Ingrese un tema."); return
|
|
@@ -423,7 +436,7 @@ def main():
|
|
| 423 |
|
| 424 |
title_slide_content = None
|
| 425 |
main_slides_content = []
|
| 426 |
-
|
| 427 |
|
| 428 |
overall_progress = st.progress(0)
|
| 429 |
status_text = st.empty()
|
|
@@ -442,20 +455,21 @@ def main():
|
|
| 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
|
| 446 |
-
status_text.info(f"📄 Procesando PDF '{uploaded_pdf.name}' para
|
| 447 |
pdf_text = extract_text_from_pdf(uploaded_pdf); overall_progress.progress(60)
|
| 448 |
if pdf_text:
|
| 449 |
-
status_text.info("✍️ Generando diapositivas
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
|
|
|
| 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,
|
| 459 |
overall_progress.progress(100)
|
| 460 |
status_text.success("🎉 ¡Presentación generada con éxito!")
|
| 461 |
clean_topic = re.sub(r'[^\w\s-]', '', topic).strip().replace(' ', '_')
|
|
|
|
| 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
|
| 5 |
import requests
|
| 6 |
from PIL import Image
|
| 7 |
from io import BytesIO
|
|
|
|
| 154 |
return None
|
| 155 |
|
| 156 |
def generate_conclusion_slides_from_text(text_to_summarize, num_conclusion_slides, topic, client, max_retries=3):
|
| 157 |
+
"""Genera diapositivas de 'conclusión' (contenido relevante del PDF) basadas en el texto y el tema general."""
|
| 158 |
if not text_to_summarize: return []
|
| 159 |
max_len = 30000
|
| 160 |
if len(text_to_summarize) > max_len:
|
| 161 |
+
st.warning(f"Texto del PDF ({len(text_to_summarize)} chars) truncado a {max_len} para las diapositivas adicionales.")
|
| 162 |
text_to_summarize = text_to_summarize[:max_len]
|
| 163 |
|
| 164 |
+
# ***** INICIO DE LA MODIFICACIÓN DEL PROMPT *****
|
| 165 |
prompt = f"""Dado el siguiente texto (extraído de un documento relacionado con el tema general "{topic}"):
|
| 166 |
--- TEXTO DEL DOCUMENTO ---
|
| 167 |
{text_to_summarize}
|
| 168 |
--- FIN DEL TEXTO ---
|
| 169 |
|
| 170 |
+
Genera {num_conclusion_slides} diapositiva(s) de contenido relevante para una presentación sobre "{topic}", basándote en el texto anterior.
|
| 171 |
+
Estas diapositivas deben sintetizar las ideas clave del texto proporcionado y presentarlas de forma que complementen el tema "{topic}".
|
| 172 |
+
Los títulos de estas diapositivas deben ser descriptivos del contenido de cada una y mantener coherencia con el tema general "{topic}". NO uses la palabra "PDF", "Resumen" ni frases explícitas como "Conclusiones", "Reflexiones Finales", etc., en los títulos. Simplemente titula la diapositiva según su contenido principal extraído del documento.
|
| 173 |
|
| 174 |
Es CRUCIAL que tu respuesta sea ÚNICAMENTE un JSON válido con la siguiente estructura exacta, sin texto adicional antes o después:
|
| 175 |
{{
|
| 176 |
"slides": [
|
| 177 |
+
{{"title": "Título Descriptivo Diapositiva 1 (basado en contenido)", "content": "Contenido de la diapositiva 1..."}},
|
| 178 |
// ... (hasta num_conclusion_slides)
|
| 179 |
+
{{"title": "Título Descriptivo Diapositiva {num_conclusion_slides} (basado en contenido)", "content": "Contenido de la diapositiva {num_conclusion_slides}..."}}
|
| 180 |
]
|
| 181 |
}}
|
| 182 |
Debe haber exactamente {num_conclusion_slides} objetos en la lista "slides". Contenido conciso.
|
| 183 |
Usa comillas dobles para claves y strings.
|
| 184 |
"""
|
| 185 |
+
# ***** FIN DE LA MODIFICACIÓN DEL PROMPT *****
|
| 186 |
+
|
| 187 |
last_error = None; last_response_text = ""
|
| 188 |
for attempt in range(max_retries):
|
| 189 |
try:
|
| 190 |
response_obj = client.generate_content(prompt)
|
| 191 |
+
if not response_obj.parts: raise ValueError("Respuesta del modelo vacía (diapositivas adicionales PDF).")
|
| 192 |
last_response_text = response_obj.text
|
| 193 |
return parse_gemini_response_for_slides(last_response_text, expected_num_slides=num_conclusion_slides)
|
| 194 |
except (ValueError, json.JSONDecodeError) as e:
|
| 195 |
+
last_error = e; st.warning(f"Intento {attempt + 1}/{max_retries} (diapositivas adicionales PDF) fallido: {e}")
|
| 196 |
if attempt == max_retries - 1:
|
| 197 |
+
st.error(f"Error final (diapositivas adicionales PDF): {last_error}")
|
| 198 |
+
st.text_area("Última resp. (diapositivas adicionales PDF):", last_response_text, height=150); return []
|
| 199 |
except Exception as e:
|
| 200 |
+
last_error = e; st.warning(f"Error API Gemini (diapositivas adicionales PDF) intento {attempt + 1}: {e}")
|
| 201 |
+
if attempt == max_retries - 1: st.error(f"Error API Gemini (diapositivas adicionales PDF): {last_error}"); return []
|
| 202 |
return []
|
| 203 |
|
| 204 |
|
|
|
|
| 332 |
if len(prs.slide_layouts) > 5:
|
| 333 |
title_layout_index = 5
|
| 334 |
elif not prs.slide_layouts:
|
| 335 |
+
try:
|
| 336 |
+
prs.slide_master.slide_layouts.add_slide_layout()
|
| 337 |
+
except Exception: pass # Si falla, los siguientes chequeos deberían manejarlo
|
| 338 |
title_layout_index = 0
|
| 339 |
|
| 340 |
if title_layout_index >= len(prs.slide_layouts) :
|
| 341 |
+
title_layout_index = 0 if prs.slide_layouts else -1 # -1 si no hay layouts en absoluto
|
| 342 |
|
| 343 |
+
if title_layout_index == -1 or not prs.slide_layouts: # No hay layouts o el índice es inválido
|
| 344 |
+
# Como último recurso, intenta usar el layout 1 si existe, o crea uno si es posible
|
| 345 |
+
# Esto es para plantillas extremadamente vacías o corruptas
|
| 346 |
+
if len(prs.slide_layouts) > 1:
|
| 347 |
+
title_layout = prs.slide_layouts[1]
|
| 348 |
+
else: # Intenta añadir uno y usarlo, o fallará si es imposible
|
| 349 |
+
try:
|
| 350 |
+
title_layout = prs.slide_master.slide_layouts.add_slide_layout()
|
| 351 |
+
if not title_layout: # Si add_slide_layout retorna None o falla silenciosamente
|
| 352 |
+
raise Exception("No se pudo obtener o crear un slide layout.")
|
| 353 |
+
except Exception as e_layout:
|
| 354 |
+
st.error(f"Error crítico: No se pudieron obtener layouts de la plantilla: {e_layout}")
|
| 355 |
+
raise # Re-lanza la excepción para detener la creación de PPT
|
| 356 |
+
else:
|
| 357 |
+
title_layout = prs.slide_layouts[title_layout_index]
|
| 358 |
|
| 359 |
slide = prs.slides.add_slide(title_layout)
|
| 360 |
add_slide_content_and_image(slide, title_slide_content[0], slide_width_emu, slide_height_emu, add_image=False, is_title_slide=True)
|
|
|
|
| 385 |
def main():
|
| 386 |
st.set_page_config(page_title="PowerPoint Mágico", layout="wide", initial_sidebar_state="expanded")
|
| 387 |
st.title("✨ PowerPoint Mágico con el Búho 🦉")
|
| 388 |
+
st.markdown("Genera presentaciones impactantes con IA, incluyendo contenido adicional de PDF.")
|
| 389 |
|
| 390 |
if not GEMINI_API_KEY or not PIXABAY_API_KEY: return
|
| 391 |
client = get_gemini_model()
|
|
|
|
| 405 |
if not available_templates:
|
| 406 |
st.sidebar.warning(f"No hay plantillas .pptx en '{plantillas_dir}'.")
|
| 407 |
default_prs = Presentation()
|
|
|
|
| 408 |
if not default_prs.slide_layouts:
|
| 409 |
+
try: default_prs.slide_master.slide_layouts.add_slide_layout()
|
| 410 |
+
except Exception: pass
|
|
|
|
|
|
|
| 411 |
if default_prs.slide_layouts:
|
| 412 |
+
try: default_prs.slides.add_slide(default_prs.slide_layouts[0])
|
| 413 |
+
except IndexError:
|
| 414 |
+
if default_prs.slide_layouts: default_prs.slides.add_slide(default_prs.slide_layouts[0])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
default_template_path = os.path.join(plantillas_dir, "default.pptx")
|
| 416 |
try: default_prs.save(default_template_path); st.sidebar.info(f"Plantilla 'default.pptx' creada."); available_templates.append("default.pptx")
|
| 417 |
except Exception as e: st.sidebar.error(f"No se pudo crear plantilla por defecto: {e}")
|
|
|
|
| 421 |
selected_template_name = st.sidebar.selectbox("🎨 Seleccione una plantilla:", list(template_options.keys()))
|
| 422 |
|
| 423 |
|
| 424 |
+
st.sidebar.subheader("Contenido Adicional desde PDF (Opcional)")
|
| 425 |
+
uploaded_pdf = st.sidebar.file_uploader("📄 Cargue un PDF para generar diapositivas adicionales:", type="pdf")
|
| 426 |
+
num_additional_slides = 0 # Renombrado de num_conclusion_slides
|
| 427 |
if uploaded_pdf:
|
| 428 |
+
num_additional_slides = st.sidebar.number_input("Nº de diapositivas adicionales desde el PDF:", min_value=1, max_value=3, value=1, step=1)
|
| 429 |
|
| 430 |
if st.button("🚀 Generar Presentación", type="primary", use_container_width=True):
|
| 431 |
if not topic: st.warning("⚠️ Ingrese un tema."); return
|
|
|
|
| 436 |
|
| 437 |
title_slide_content = None
|
| 438 |
main_slides_content = []
|
| 439 |
+
additional_slides_content = [] # Renombrado de conclusion_slides_content
|
| 440 |
|
| 441 |
overall_progress = st.progress(0)
|
| 442 |
status_text = st.empty()
|
|
|
|
| 455 |
status_text.error("❌ Falló la generación del contenido principal."); overall_progress.progress(100); return
|
| 456 |
status_text.success("👍 Contenido principal generado."); overall_progress.progress(50)
|
| 457 |
|
| 458 |
+
if uploaded_pdf and num_additional_slides > 0:
|
| 459 |
+
status_text.info(f"📄 Procesando PDF '{uploaded_pdf.name}' para contenido adicional...")
|
| 460 |
pdf_text = extract_text_from_pdf(uploaded_pdf); overall_progress.progress(60)
|
| 461 |
if pdf_text:
|
| 462 |
+
status_text.info("✍️ Generando diapositivas adicionales desde el PDF con Gemini...")
|
| 463 |
+
# Usar la función renombrada o la original con el prompt modificado
|
| 464 |
+
additional_slides_content = generate_conclusion_slides_from_text(pdf_text, num_additional_slides, topic, client)
|
| 465 |
+
if additional_slides_content: status_text.success(f"👍 Contenido adicional del PDF generado ({len(additional_slides_content)} diap.).")
|
| 466 |
+
else: status_text.warning("⚠️ No se pudo generar contenido adicional del PDF.")
|
| 467 |
else: status_text.warning("⚠️ No se pudo extraer texto del PDF.")
|
| 468 |
overall_progress.progress(80)
|
| 469 |
|
| 470 |
status_text.info("🛠️ Creando archivo PowerPoint...")
|
| 471 |
try:
|
| 472 |
+
pptx_buffer = create_powerpoint(title_slide_content, main_slides_content, template_path, additional_slides_content)
|
| 473 |
overall_progress.progress(100)
|
| 474 |
status_text.success("🎉 ¡Presentación generada con éxito!")
|
| 475 |
clean_topic = re.sub(r'[^\w\s-]', '', topic).strip().replace(' ', '_')
|