Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| from pptx import Presentation | |
| from pptx.util import Inches, Pt | |
| from pptx.enum.text import MSO_ANCHOR, MSO_AUTO_SIZE, PP_ALIGN | |
| import requests | |
| from PIL import Image | |
| from io import BytesIO | |
| import io | |
| import os | |
| import json | |
| import re | |
| import google.generativeai as genai | |
| import pdfplumber | |
| # --- CONFIGURACIÓN --- | |
| GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") | |
| if not GEMINI_API_KEY: | |
| st.error("GEMINI_API_KEY no encontrada. Por favor configúrala en tus variables de entorno.") | |
| st.stop() | |
| genai.configure(api_key=GEMINI_API_KEY) | |
| PIXABAY_API_KEY = st.secrets.get("pixabay") | |
| if not PIXABAY_API_KEY: | |
| st.error("Clave API de Pixabay no encontrada en los secretos de Streamlit.") | |
| st.stop() | |
| # --- MODELO CACHEADO --- | |
| def get_gemini_model(): | |
| return genai.GenerativeModel("gemini-2.5-flash-lite") | |
| # --- FUNCIONES AUXILIARES --- | |
| def extract_text_from_pdf(uploaded_file): | |
| if uploaded_file is None: | |
| return None | |
| try: | |
| with pdfplumber.open(uploaded_file) as pdf: | |
| full_text = [page.extract_text() for page in pdf.pages if page.extract_text()] | |
| return "\n".join(full_text) | |
| except Exception as e: | |
| st.error(f"Error al procesar el PDF: {e}") | |
| return None | |
| def extract_and_clean_json(text_response): | |
| match = re.search(r"```json\s*(\{.*?\})\s*```", text_response, re.DOTALL) | |
| if match: | |
| json_str = match.group(1) | |
| else: | |
| json_match = re.search(r'\{.*\}', text_response, re.DOTALL) | |
| if json_match: | |
| json_str = json_match.group(0) | |
| else: | |
| return None | |
| json_str = re.sub(r'[\n\t\r]', '', json_str) | |
| json_str = re.sub(r',\s*([}\]])', r'\1', json_str) | |
| return json_str | |
| def parse_gemini_response_for_slides(response_text, expected_slides_key="slides", expected_num_slides=None, is_single_slide=False): | |
| json_str = extract_and_clean_json(response_text) | |
| if not json_str: | |
| raise ValueError("No se pudo extraer una cadena JSON válida de la respuesta.") | |
| try: | |
| data = json.loads(json_str) | |
| except json.JSONDecodeError as e: | |
| raise ValueError(f"Error al decodificar JSON: {e}. JSON intentado: {json_str[:500]}...") | |
| if is_single_slide: | |
| if not (isinstance(data, dict) and 'title' in data and 'content' in data): | |
| raise ValueError("La estructura del JSON para la diapositiva de título es incorrecta. Se esperaba 'title' y 'content'.") | |
| if not (isinstance(data['title'], str) and isinstance(data['content'], str)): | |
| raise ValueError("El título y el contenido de la diapositiva de título deben ser cadenas.") | |
| return [data] | |
| if not isinstance(data, dict) or expected_slides_key not in data: | |
| raise ValueError(f"La estructura del JSON es incorrecta. Falta la clave '{expected_slides_key}'.") | |
| slides = data[expected_slides_key] | |
| if not isinstance(slides, list): | |
| raise ValueError(f"La clave '{expected_slides_key}' debe contener una lista de diapositivas.") | |
| if expected_num_slides is not None and len(slides) != expected_num_slides: | |
| raise ValueError(f"Se esperaban {expected_num_slides} diapositivas, pero se recibieron {len(slides)}.") | |
| for i, slide in enumerate(slides): | |
| if not (isinstance(slide, dict) and 'title' in slide and 'content' in slide): | |
| raise ValueError(f"La diapositiva {i+1} tiene una estructura incorrecta. Debe ser un diccionario con 'title' y 'content'.") | |
| if not (isinstance(slide['title'], str) and isinstance(slide['content'], str)): | |
| raise ValueError(f"El título y el contenido de la diapositiva {i+1} deben ser cadenas de texto.") | |
| return slides | |
| def generate_creative_title_slide(topic, client, max_retries=3): | |
| prompt = f"""Genera un título MUY llamativo y creativo para una presentación sobre "{topic}". | |
| Además, proporciona una breve frase o subtítulo de impacto para acompañar el título. | |
| Es CRUCIAL que tu respuesta sea ÚNICAMENTE un JSON válido con la siguiente estructura exacta, sin texto adicional antes o después: | |
| {{ | |
| "title": "Título Creativo y Llamativo Generado", | |
| "content": "Breve subtítulo o frase de impacto." | |
| }} | |
| No incluyas ningún otro texto, explicación o saludo. Solo el JSON. | |
| El título debe despertar curiosidad e interés. El contenido debe ser muy conciso. | |
| """ | |
| last_error = None | |
| last_response_text = "" | |
| for attempt in range(max_retries): | |
| try: | |
| response_obj = client.generate_content(prompt) | |
| if not response_obj.parts: raise ValueError("Respuesta del modelo vacía (título creativo).") | |
| last_response_text = response_obj.text | |
| title_slide_data = parse_gemini_response_for_slides(last_response_text, is_single_slide=True) | |
| return title_slide_data | |
| except (ValueError, json.JSONDecodeError) as e: | |
| last_error = e | |
| st.warning(f"Intento {attempt + 1}/{max_retries} (título creativo) fallido: {e}") | |
| if attempt == max_retries - 1: | |
| st.error(f"Error final al generar título creativo: {last_error}") | |
| st.text_area("Última respuesta (título creativo):", last_response_text, height=100) | |
| return None | |
| except Exception as e: | |
| last_error = e; st.warning(f"Error API Gemini (título creativo) intento {attempt + 1}: {e}") | |
| if attempt == max_retries - 1: st.error(f"Error API Gemini (título creativo): {last_error}"); return None | |
| return None | |
| def generate_presentation_content(topic, client, max_retries=3): | |
| prompt = f"""Genera una presentación de PowerPoint sobre el tema: "{topic}". | |
| Debes crear exactamente 9 diapositivas de contenido principal. Cada diapositiva debe tener un título y contenido. | |
| Es CRUCIAL que tu respuesta sea ÚNICAMENTE un JSON válido con la siguiente estructura exacta, sin texto adicional antes o después: | |
| {{ | |
| "slides": [ | |
| {{"title": "Título Diapositiva 1", "content": "Contenido Diapositiva 1"}}, | |
| // ...hasta... | |
| {{"title": "Título Diapositiva 9", "content": "Contenido Diapositiva 9"}} | |
| ] | |
| }} | |
| No incluyas ningún otro texto. Solo el JSON. Los títulos deben ser informativos y seguir una progresión lógica. | |
| El contenido debe ser conciso. Usa comillas dobles para claves y strings. | |
| """ | |
| last_error = None; last_response_text = "" | |
| for attempt in range(max_retries): | |
| try: | |
| response_obj = client.generate_content(prompt) | |
| if not response_obj.parts: raise ValueError("Respuesta del modelo vacía (contenido principal).") | |
| last_response_text = response_obj.text | |
| return parse_gemini_response_for_slides(last_response_text, expected_num_slides=9) | |
| except (ValueError, json.JSONDecodeError) as e: | |
| last_error = e; st.warning(f"Intento {attempt + 1}/{max_retries} (contenido principal) fallido: {e}") | |
| if attempt == max_retries - 1: | |
| st.error(f"Error final (contenido principal): {last_error}") | |
| st.text_area("Última resp. (contenido principal):", last_response_text, height=150); return None | |
| except Exception as e: | |
| last_error = e; st.warning(f"Error API Gemini (contenido principal) intento {attempt + 1}: {e}") | |
| if attempt == max_retries - 1: st.error(f"Error API Gemini (contenido principal): {last_error}"); return None | |
| return None | |
| def generate_conclusion_slides_from_text(text_to_summarize, num_conclusion_slides, topic, client, max_retries=3): | |
| """Genera diapositivas de 'conclusión' (contenido relevante del PDF) basadas en el texto y el tema general.""" | |
| if not text_to_summarize: return [] | |
| max_len = 30000 | |
| if len(text_to_summarize) > max_len: | |
| st.warning(f"Texto del PDF ({len(text_to_summarize)} chars) truncado a {max_len} para las diapositivas adicionales.") | |
| text_to_summarize = text_to_summarize[:max_len] | |
| # ***** INICIO DE LA MODIFICACIÓN DEL PROMPT ***** | |
| prompt = f"""Dado el siguiente texto (extraído de un documento relacionado con el tema general "{topic}"): | |
| --- TEXTO DEL DOCUMENTO --- | |
| {text_to_summarize} | |
| --- FIN DEL TEXTO --- | |
| Genera {num_conclusion_slides} diapositiva(s) de contenido relevante para una presentación sobre "{topic}", basándote en el texto anterior. | |
| Estas diapositivas deben sintetizar las ideas clave del texto proporcionado y presentarlas de forma que complementen el tema "{topic}". | |
| 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. | |
| Es CRUCIAL que tu respuesta sea ÚNICAMENTE un JSON válido con la siguiente estructura exacta, sin texto adicional antes o después: | |
| {{ | |
| "slides": [ | |
| {{"title": "Título Descriptivo Diapositiva 1 (basado en contenido)", "content": "Contenido de la diapositiva 1..."}}, | |
| // ... (hasta num_conclusion_slides) | |
| {{"title": "Título Descriptivo Diapositiva {num_conclusion_slides} (basado en contenido)", "content": "Contenido de la diapositiva {num_conclusion_slides}..."}} | |
| ] | |
| }} | |
| Debe haber exactamente {num_conclusion_slides} objetos en la lista "slides". Contenido conciso. | |
| Usa comillas dobles para claves y strings. | |
| """ | |
| # ***** FIN DE LA MODIFICACIÓN DEL PROMPT ***** | |
| last_error = None; last_response_text = "" | |
| for attempt in range(max_retries): | |
| try: | |
| response_obj = client.generate_content(prompt) | |
| if not response_obj.parts: raise ValueError("Respuesta del modelo vacía (diapositivas adicionales PDF).") | |
| last_response_text = response_obj.text | |
| return parse_gemini_response_for_slides(last_response_text, expected_num_slides=num_conclusion_slides) | |
| except (ValueError, json.JSONDecodeError) as e: | |
| last_error = e; st.warning(f"Intento {attempt + 1}/{max_retries} (diapositivas adicionales PDF) fallido: {e}") | |
| if attempt == max_retries - 1: | |
| st.error(f"Error final (diapositivas adicionales PDF): {last_error}") | |
| st.text_area("Última resp. (diapositivas adicionales PDF):", last_response_text, height=150); return [] | |
| except Exception as e: | |
| last_error = e; st.warning(f"Error API Gemini (diapositivas adicionales PDF) intento {attempt + 1}: {e}") | |
| if attempt == max_retries - 1: st.error(f"Error API Gemini (diapositivas adicionales PDF): {last_error}"); return [] | |
| return [] | |
| def buscar_imagen_pixabay(query): | |
| if not PIXABAY_API_KEY: return None | |
| url = f"https://pixabay.com/api/?key={PIXABAY_API_KEY}&q={requests.utils.quote(query)}&image_type=photo&per_page=3&safesearch=true" | |
| try: | |
| response = requests.get(url, timeout=10); response.raise_for_status() | |
| data = response.json() | |
| if data.get('hits'): | |
| image_info = data['hits'][0] | |
| image_url = image_info.get('largeImageURL', image_info.get('webformatURL')) | |
| if not image_url: return None | |
| image_response = requests.get(image_url, timeout=10); image_response.raise_for_status() | |
| img_bytes = BytesIO(image_response.content) | |
| try: | |
| img = Image.open(img_bytes); img.verify(); img_bytes.seek(0) | |
| return Image.open(img_bytes) | |
| except Exception as pil_e: print(f"Error PIL ({query}): {pil_e}"); return None | |
| except requests.exceptions.RequestException as e: print(f"Error Pixabay req ({query}): {e}") | |
| except Exception as e: print(f"Error Pixabay gral ({query}): {e}") | |
| return None | |
| def add_slide_content_and_image(slide, slide_data, prs_slide_width, prs_slide_height, add_image=True, is_title_slide=False): | |
| if slide.shapes.title: | |
| slide.shapes.title.text = slide_data['title'] | |
| content_placeholder = None | |
| if is_title_slide and slide.placeholders and slide.placeholders[0] == slide.shapes.title and len(slide.placeholders) > 1: | |
| content_placeholder = slide.placeholders[1] | |
| elif not is_title_slide and slide.placeholders and len(slide.placeholders) > 1 and slide.placeholders[1]: | |
| content_placeholder = slide.placeholders[1] | |
| margin_emu = Inches(0.3) | |
| common_top_emu = Inches(0.5) if is_title_slide and slide.shapes.title else Inches(1.6) | |
| if is_title_slide and content_placeholder: | |
| common_top_emu = content_placeholder.top | |
| content_bottom_emu = prs_slide_height - margin_emu | |
| text_area_width_float = prs_slide_width - (2 * margin_emu) | |
| text_area_left_emu = margin_emu | |
| img_area_width_float = 0 | |
| if add_image and not is_title_slide: | |
| img_area_width_float = prs_slide_width * 0.40 | |
| text_area_width_float = prs_slide_width - img_area_width_float - (3 * margin_emu) | |
| common_height_emu = content_bottom_emu - common_top_emu | |
| if is_title_slide and content_placeholder: | |
| common_height_emu = content_placeholder.height | |
| if content_placeholder: | |
| content_placeholder.left = int(text_area_left_emu) | |
| if not is_title_slide: | |
| content_placeholder.top = int(common_top_emu) | |
| content_placeholder.width = int(text_area_width_float) | |
| content_placeholder.height = int(common_height_emu) | |
| tf = content_placeholder.text_frame; tf.clear() | |
| p = tf.add_paragraph(); p.text = slide_data['content'] | |
| tf.word_wrap = True | |
| tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE if not is_title_slide else MSO_AUTO_SIZE.NONE | |
| if is_title_slide: | |
| p.alignment = PP_ALIGN.CENTER | |
| tf.vertical_anchor = MSO_ANCHOR.MIDDLE | |
| else: | |
| txBox_top = common_top_emu | |
| if is_title_slide and slide.shapes.title: | |
| txBox_top = slide.shapes.title.top + slide.shapes.title.height + Inches(0.2) | |
| txBox = slide.shapes.add_textbox( | |
| int(text_area_left_emu), int(txBox_top), | |
| int(text_area_width_float), int(common_height_emu / (1.5 if is_title_slide else 1) ) | |
| ) | |
| tf = txBox.text_frame; tf.text = slide_data['content'] | |
| tf.word_wrap = True | |
| tf.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT if is_title_slide else MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE | |
| if is_title_slide: | |
| p = tf.paragraphs[0]; p.alignment = PP_ALIGN.CENTER | |
| tf.vertical_anchor = MSO_ANCHOR.TOP | |
| if add_image and not is_title_slide: | |
| try: | |
| first_sentence = slide_data['content'].split('.')[0] if '.' in slide_data['content'] else slide_data['content'][:70] | |
| image_query = f"{slide_data['title']} {first_sentence}" | |
| pil_image = buscar_imagen_pixabay(image_query) | |
| if pil_image: | |
| img_byte_arr = io.BytesIO(); img_format = pil_image.format if pil_image.format in ['JPEG', 'PNG', 'GIF'] else 'PNG' | |
| if img_format == 'JPEG' and pil_image.mode == 'RGBA': pil_image = pil_image.convert('RGB') | |
| pil_image.save(img_byte_arr, format=img_format); img_byte_arr.seek(0) | |
| img_native_width_px, img_native_height_px = pil_image.size | |
| available_width_for_img_emu = int(img_area_width_float) | |
| available_height_for_img_emu = common_height_emu | |
| native_width_inches = img_native_width_px / 96.0; native_height_inches = img_native_height_px / 96.0 | |
| available_width_inches = available_width_for_img_emu / 914400.0 | |
| available_height_inches = available_height_for_img_emu / 914400.0 | |
| ratio_w = available_width_inches / native_width_inches if native_width_inches > 0 else 1.0 | |
| ratio_h = available_height_inches / native_height_inches if native_height_inches > 0 else 1.0 | |
| scale_ratio = min(ratio_w, ratio_h) | |
| pic_width_emu = Inches(native_width_inches * scale_ratio) | |
| pic_height_emu = Inches(native_height_inches * scale_ratio) | |
| img_left_float = prs_slide_width - margin_emu - pic_width_emu | |
| if img_left_float < text_area_left_emu + text_area_width_float + Inches(0.1): | |
| img_left_float = text_area_left_emu + text_area_width_float + Inches(0.1) | |
| img_top_float = float(common_top_emu) + (float(common_height_emu) - pic_height_emu) / 2.0 | |
| slide.shapes.add_picture( | |
| img_byte_arr, int(img_left_float), int(img_top_float), | |
| width=pic_width_emu, height=pic_height_emu | |
| ) | |
| except Exception as e: | |
| print(f"No se pudo procesar/insertar imagen ('{slide_data['title']}'): {e}") | |
| def create_powerpoint(title_slide_content, main_slides_content, template_path, conclusion_slides_content=None): | |
| prs = Presentation(template_path) | |
| slide_width_emu = prs.slide_width | |
| slide_height_emu = prs.slide_height | |
| if title_slide_content: | |
| title_layout_index = 0 | |
| if len(prs.slide_layouts) > 5: | |
| title_layout_index = 5 | |
| elif not prs.slide_layouts: | |
| try: | |
| prs.slide_master.slide_layouts.add_slide_layout() | |
| except Exception: pass # Si falla, los siguientes chequeos deberían manejarlo | |
| title_layout_index = 0 | |
| if title_layout_index >= len(prs.slide_layouts) : | |
| title_layout_index = 0 if prs.slide_layouts else -1 # -1 si no hay layouts en absoluto | |
| if title_layout_index == -1 or not prs.slide_layouts: # No hay layouts o el índice es inválido | |
| # Como último recurso, intenta usar el layout 1 si existe, o crea uno si es posible | |
| # Esto es para plantillas extremadamente vacías o corruptas | |
| if len(prs.slide_layouts) > 1: | |
| title_layout = prs.slide_layouts[1] | |
| else: # Intenta añadir uno y usarlo, o fallará si es imposible | |
| try: | |
| title_layout = prs.slide_master.slide_layouts.add_slide_layout() | |
| if not title_layout: # Si add_slide_layout retorna None o falla silenciosamente | |
| raise Exception("No se pudo obtener o crear un slide layout.") | |
| except Exception as e_layout: | |
| st.error(f"Error crítico: No se pudieron obtener layouts de la plantilla: {e_layout}") | |
| raise # Re-lanza la excepción para detener la creación de PPT | |
| else: | |
| title_layout = prs.slide_layouts[title_layout_index] | |
| slide = prs.slides.add_slide(title_layout) | |
| add_slide_content_and_image(slide, title_slide_content[0], slide_width_emu, slide_height_emu, add_image=False, is_title_slide=True) | |
| for slide_data in main_slides_content: | |
| slide_layout = prs.slide_layouts[1] | |
| slide = prs.slides.add_slide(slide_layout) | |
| add_slide_content_and_image(slide, slide_data, slide_width_emu, slide_height_emu, add_image=True) | |
| if conclusion_slides_content: | |
| for slide_data in conclusion_slides_content: | |
| slide_layout = prs.slide_layouts[1] | |
| slide = prs.slides.add_slide(slide_layout) | |
| add_slide_content_and_image(slide, slide_data, slide_width_emu, slide_height_emu, add_image=False) | |
| final_slide_layout = prs.slide_layouts[1] | |
| final_slide = prs.slides.add_slide(final_slide_layout) | |
| if final_slide.shapes.title: final_slide.shapes.title.text = "¡Gracias por su atención!" | |
| if len(final_slide.placeholders) > 1 and final_slide.placeholders[1]: | |
| try: content_ph = final_slide.placeholders[1]; sp = content_ph.element; sp.getparent().remove(sp) | |
| except Exception as e: print(f"No se pudo quitar placeholder de diap. final: {e}") | |
| pptx_buffer = io.BytesIO(); prs.save(pptx_buffer); pptx_buffer.seek(0) | |
| return pptx_buffer | |
| # --- INTERFAZ DE STREAMLIT (main) --- | |
| def main(): | |
| st.set_page_config(page_title="PowerPoint Mágico", layout="wide", initial_sidebar_state="expanded") | |
| st.title("✨ PowerPoint Mágico con el Búho 🦉") | |
| st.markdown("Genera presentaciones impactantes con IA, incluyendo contenido adicional de PDF.") | |
| if not GEMINI_API_KEY or not PIXABAY_API_KEY: return | |
| client = get_gemini_model() | |
| st.sidebar.header("Configuración de la Presentación") | |
| topic = st.sidebar.text_input("📝 Tema principal de la presentación:", placeholder="Ej: El futuro de la IA") | |
| plantillas_dir = "PLANTILLAS" | |
| if not os.path.exists(plantillas_dir): | |
| try: os.makedirs(plantillas_dir); st.sidebar.info(f"Directorio '{plantillas_dir}' creado.") | |
| except OSError as e: st.sidebar.error(f"No se pudo crear dir '{plantillas_dir}': {e}"); st.stop() | |
| available_templates = [] | |
| try: | |
| if os.path.isdir(plantillas_dir): | |
| available_templates = [f for f in os.listdir(plantillas_dir) if f.endswith(".pptx") and os.path.isfile(os.path.join(plantillas_dir, f))] | |
| except Exception as e: st.sidebar.error(f"Error al acceder dir plantillas: {e}") | |
| if not available_templates: | |
| st.sidebar.warning(f"No hay plantillas .pptx en '{plantillas_dir}'.") | |
| default_prs = Presentation() | |
| if not default_prs.slide_layouts: | |
| try: default_prs.slide_master.slide_layouts.add_slide_layout() | |
| except Exception: pass | |
| if default_prs.slide_layouts: | |
| try: default_prs.slides.add_slide(default_prs.slide_layouts[0]) | |
| except IndexError: | |
| if default_prs.slide_layouts: default_prs.slides.add_slide(default_prs.slide_layouts[0]) | |
| default_template_path = os.path.join(plantillas_dir, "default.pptx") | |
| try: default_prs.save(default_template_path); st.sidebar.info(f"Plantilla 'default.pptx' creada."); available_templates.append("default.pptx") | |
| except Exception as e: st.sidebar.error(f"No se pudo crear plantilla por defecto: {e}") | |
| template_options = {os.path.splitext(t)[0]: os.path.join(plantillas_dir, t) for t in available_templates} | |
| if not template_options: st.sidebar.error("CRÍTICO: No hay plantillas. Cree 'PLANTILLAS' y añada un .pptx."); st.stop() | |
| selected_template_name = st.sidebar.selectbox("🎨 Seleccione una plantilla:", list(template_options.keys())) | |
| st.sidebar.subheader("Contenido Adicional desde PDF (Opcional)") | |
| uploaded_pdf = st.sidebar.file_uploader("📄 Cargue un PDF para generar diapositivas adicionales:", type="pdf") | |
| num_additional_slides = 0 # Renombrado de num_conclusion_slides | |
| if uploaded_pdf: | |
| num_additional_slides = st.sidebar.number_input("Nº de diapositivas adicionales desde el PDF:", min_value=1, max_value=3, value=1, step=1) | |
| if st.button("🚀 Generar Presentación", type="primary", use_container_width=True): | |
| if not topic: st.warning("⚠️ Ingrese un tema."); return | |
| if not selected_template_name: st.warning("⚠️ Seleccione una plantilla."); return | |
| template_path = template_options[selected_template_name] | |
| if not os.path.exists(template_path): st.error(f"No se encontró plantilla: {template_path}"); return | |
| title_slide_content = None | |
| main_slides_content = [] | |
| additional_slides_content = [] # Renombrado de conclusion_slides_content | |
| overall_progress = st.progress(0) | |
| status_text = st.empty() | |
| status_text.info("🎨 Creando un título impactante...") | |
| title_slide_content = generate_creative_title_slide(topic, client) | |
| if not title_slide_content: | |
| status_text.warning("⚠️ No se pudo generar el título creativo, continuando sin él.") | |
| else: | |
| status_text.success("👍 Título creativo generado.") | |
| overall_progress.progress(10) | |
| status_text.info("🧠 Generando contenido principal con Gemini...") | |
| main_slides_content = generate_presentation_content(topic, client) | |
| if not main_slides_content: | |
| status_text.error("❌ Falló la generación del contenido principal."); overall_progress.progress(100); return | |
| status_text.success("👍 Contenido principal generado."); overall_progress.progress(50) | |
| if uploaded_pdf and num_additional_slides > 0: | |
| status_text.info(f"📄 Procesando PDF '{uploaded_pdf.name}' para contenido adicional...") | |
| pdf_text = extract_text_from_pdf(uploaded_pdf); overall_progress.progress(60) | |
| if pdf_text: | |
| status_text.info("✍️ Generando diapositivas adicionales desde el PDF con Gemini...") | |
| # Usar la función renombrada o la original con el prompt modificado | |
| additional_slides_content = generate_conclusion_slides_from_text(pdf_text, num_additional_slides, topic, client) | |
| if additional_slides_content: status_text.success(f"👍 Contenido adicional del PDF generado ({len(additional_slides_content)} diap.).") | |
| else: status_text.warning("⚠️ No se pudo generar contenido adicional del PDF.") | |
| else: status_text.warning("⚠️ No se pudo extraer texto del PDF.") | |
| overall_progress.progress(80) | |
| status_text.info("🛠️ Creando archivo PowerPoint...") | |
| try: | |
| pptx_buffer = create_powerpoint(title_slide_content, main_slides_content, template_path, additional_slides_content) | |
| overall_progress.progress(100) | |
| status_text.success("🎉 ¡Presentación generada con éxito!") | |
| clean_topic = re.sub(r'[^\w\s-]', '', topic).strip().replace(' ', '_') | |
| file_name = f"{clean_topic}_{selected_template_name}_presentacion.pptx" | |
| st.download_button( | |
| label="📥 Descargar Presentación", data=pptx_buffer, file_name=file_name, | |
| mime="application/vnd.openxmlformats-officedocument.presentationml.presentation", | |
| use_container_width=True | |
| ) | |
| except Exception as e: | |
| overall_progress.progress(100); status_text.error(f"❌ Error al crear PowerPoint: {str(e)}"); st.exception(e) | |
| if __name__ == "__main__": | |
| main() |