""" Gerador de apresentacoes PPTX com slides HTML renderizados (visual rico). Pipeline: Claude gera HTML/CSS bonitos -> Playwright renderiza PNG -> monta PPTX com imagens. """ import asyncio import gc import io import json import logging import os import re import tempfile import time from pptx import Presentation from pptx.util import Inches, Emu from modules.config import client_anthropic # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- _SLIDE_WIDTH_PX = 1920 _SLIDE_HEIGHT_PX = 1080 _CORA_BRAND = { "pink": "#FE3E6D", "pink_light": "#FF6B8A", "dark": "#1A1A2E", "dark2": "#16213E", "gray": "#F5F5F7", "text": "#2D2D2D", "white": "#FFFFFF", } # --------------------------------------------------------------------------- # System prompt — foco no RACIONAL das iniciativas por KR # --------------------------------------------------------------------------- _SYSTEM_PROMPT = """Voce e um designer de apresentacoes executivas de altissimo nivel. Crie slides LINDOS em HTML/CSS para uma apresentacao da **Cora** (banco digital). OBJETIVO DA APRESENTACAO: - Mostrar o RACIONAL de priorizacao: para cada KR, explicar POR QUE as iniciativas foram priorizadas - NAO mostrar roadmap/timeline — foque no raciocinio estrategico - Tom executivo e direto - FOCO EXCLUSIVO nas iniciativas relacionadas aos OKRs/KRs — NAO incluir temas regulatorios, auditoria, compliance ou sustentacao que nao estejam diretamente ligados a um KR ESTRUTURA IDEAL: 1. Slide de capa impactante 2. Para cada KR relevante: 1-2 slides explicando: - O KR apenas com nome e meta bruta (ex: "Reduzir FPR — meta: 0,22%"). NAO mostrar valor atual, NAO mostrar % de atingimento, NAO mostrar status (off-track/at-risk), NAO mostrar evolucao mensal. Apenas o NOME do KR e a META. - As iniciativas priorizadas que impactam esse KR - O MECANISMO de impacto (a logica de POR QUE a iniciativa ajuda) - Exemplo: "Biometria → permite usuario desbloquear conta propria → reduz bloqueios indevidos de clientes bons" 3. OBRIGATORIO: Slide "Mapa de Correlacao — Iniciativas x Key Results" em formato de TABELA HTML pura: - Fundo escuro (gradiente {dark} para {dark2}), texto branco - Titulo grande no topo: "Mapa de Correlacao — Iniciativas x Key Results" com emoji 📊 - Subtitulo: "Como cada iniciativa contribui para os KRs. Impacto primario (●) e secundario (○) mapeados." - Tabela com header: INICIATIVA | KR1 | KR2 | KR3 | SUSTENTACAO/COMPLIANCE - Linhas agrupadas por CATEGORIA em negrito maiusculo (ex: "FRICCAO INTELIGENTE (KR1)") - Cada iniciativa com emoji no inicio do nome - Celulas: circulo rosa cheio (●) = impacto PRIMARIO, circulo rosa vazado (○) = impacto SECUNDARIO, vazio = sem impacto - Bordas sutis entre linhas (rgba branco 10%) - Rodape discreto "Cora | Risco e Seguranca" 4. Slide de encerramento REGRAS DE DESIGN: 1. Cada slide e um HTML COMPLETO, self-contained, com CSS inline no

Slide {slide_num}

{msg or "Conteudo nao extraivel — edite este slide manualmente"}

""" try: prs = PptxPresentation(io.BytesIO(pptx_bytes)) slides = [] total = len(prs.slides) logging.info(f"[PPTX Import] Iniciando import de {total} slides") for i, slide in enumerate(prs.slides): slide_num = i + 1 logging.info(f"[PPTX Import] Processando slide {slide_num}/{total}") # Strategy 1: Extract image (app-generated PPTX or Google Slides with images) img_data = _extrair_imagem_slide(slide) if img_data: html = _recriar_via_imagem(img_data, slide_num) if html: slides.append({"titulo": f"Slide {slide_num}", "html": html}) logging.info(f"[PPTX Import] Slide {slide_num} recriado via imagem") continue # Imagem existia mas falhou 3x — cria placeholder logging.warning(f"[PPTX Import] Slide {slide_num}: imagem falhou apos 3 tentativas") slides.append({"titulo": f"Slide {slide_num}", "html": _placeholder_html(slide_num, "Imagem detectada mas nao foi possivel recriar")}) continue # Strategy 2: Extract text content from shapes textos = [] for shape in slide.shapes: try: if shape.has_text_frame: for para in shape.text_frame.paragraphs: text = para.text.strip() if text: textos.append(text) if shape.has_table: table = shape.table rows = [] for row in table.rows: cells = [cell.text.strip() for cell in row.cells] rows.append(" | ".join(cells)) textos.append("TABELA:\n" + "\n".join(rows)) except Exception: pass if not textos: logging.warning(f"[PPTX Import] Slide {slide_num}: sem conteudo extraivel") slides.append({"titulo": f"Slide {slide_num}", "html": _placeholder_html(slide_num)}) continue conteudo = "\n".join(textos) logging.info(f"[PPTX Import] Slide {slide_num}: {len(textos)} blocos de texto extraidos") try: resp = client_anthropic.messages.create( model="claude-sonnet-4-6", max_tokens=4096, messages=[{ "role": "user", "content": ( f"Crie um slide HTML/CSS executivo e LINDO com o seguinte conteudo:\n\n" f"{conteudo}\n\n" f"REGRAS:\n" f"- Cores da marca Cora: pink #FE3E6D, dark #1A1A2E, dark2 #16213E, gray #F5F5F7\n" f"- Dimensoes exatas: 1920x1080px com fundo completo\n" f"- Layout profissional, executivo, visual rico\n" f"- Se houver TABELA, renderize como tabela HTML estilizada\n" f"- Mantenha TODO o conteudo textual\n" f"- Retorne APENAS o HTML completo, sem explicacao, sem markdown, sem ```." ), }], timeout=120, ) html = _limpar_html_resposta(resp.content[0].text) slides.append({"titulo": f"Slide {slide_num}", "html": html}) logging.info(f"[PPTX Import] Slide {slide_num} recriado via texto") except Exception as e: logging.error(f"[PPTX Import] Slide {slide_num} falhou: {e}") slides.append({"titulo": f"Slide {slide_num}", "html": _placeholder_html(slide_num, "Erro ao recriar slide")}) logging.info(f"[PPTX Import] Total: {len(slides)} slides importados de {total}") return slides if slides else None except Exception as e: logging.error(f"[PPTX Import] Erro geral: {e}", exc_info=True) return None def _limpar_html_resposta(text: str) -> str: """Remove markdown fences e whitespace de resposta do Claude.""" html = text.strip() if html.startswith("```"): html = re.sub(r'^```\w*\n?', '', html) html = re.sub(r'\n?```$', '', html) return html.strip()