#!/usr/bin/env python3 """ # ══════════════════════════════════════════════════════════════ # ☢︎ RADIOTERAPIA.AI — POP de elite # app.py — Hugging Face Spaces # por: Braga, HF. # ══════════════════════════════════════════════════════════════ """ import json, os, tempfile, io from datetime import datetime try: import graphviz as gv HAS_GRAPHVIZ = True except ImportError: HAS_GRAPHVIZ = False from docx import Document from docx.shared import Pt, Cm, Emu, RGBColor, Inches from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.enum.table import WD_TABLE_ALIGNMENT from docx.oxml.ns import qn, nsdecls from docx.oxml import parse_xml # ============================================================ # SISTEMA DE CORES DINÂMICO # ============================================================ def hex_to_rgb(h): h = h.lstrip("#") return tuple(int(h[i:i+2], 16) for i in (0, 2, 4)) def rgb_to_hex(r, g, b): return f"{min(255,max(0,int(r))):02X}{min(255,max(0,int(g))):02X}{min(255,max(0,int(b))):02X}" def lighten(hex_color, pct): """Clareia uma cor por pct% misturando com branco.""" r, g, b = hex_to_rgb(hex_color) return rgb_to_hex(r + (255-r)*pct, g + (255-g)*pct, b + (255-b)*pct) def darken(hex_color, pct): r, g, b = hex_to_rgb(hex_color) return rgb_to_hex(r*(1-pct), g*(1-pct), b*(1-pct)) def build_palette(primary_hex): """Gera paleta completa a partir de uma cor primária.""" p = primary_hex.lstrip("#").upper() return { "primary": p, # H1, bullets, table headers "primary_dark": darken(p, 0.15), # Texto sobre fundo claro "secondary": lighten(p, 0.45), # H2, SmartArt — +10% mais claro que v4.0 "tertiary": lighten(p, 0.70), # Footer header, zebra — +10% mais claro "quaternary": lighten(p, 0.85), # Zebra alternada mais clara "text_on_primary": "FFFFFF", # Texto sobre cor primária "text_on_secondary": darken(p, 0.30), # Texto sobre cor secundária "text_body": "1A1A1A", "header_text": "000000", # PRETO automático para cabeçalho "border": darken(p, 0.0), "border_light": lighten(p, 0.20), "risk_red": "8B1A1A", "barrier_green": "1B5E20", "gray_border": "7F8C9A", "annex_border": "000000", } def parse_color_input(val): """Parseia qualquer formato de cor do Gradio ColorPicker → hex 6 chars.""" if not val: return DEFAULT_PRIMARY val = str(val).strip() # Formato rgb(R, G, B) ou rgba(R, G, B, A) — COM FLOATS if val.startswith("rgb"): import re # Capturar números com decimais: 248.402, 79.543, etc. nums = re.findall(r'[\d]+\.?[\d]*', val) if len(nums) >= 3: r = min(255, max(0, int(float(nums[0])))) g = min(255, max(0, int(float(nums[1])))) b = min(255, max(0, int(float(nums[2])))) return rgb_to_hex(r, g, b) return DEFAULT_PRIMARY # Formato hex c = val.lstrip("#") c = ''.join(ch for ch in c if ch in '0123456789abcdefABCDEF') if len(c) >= 6: return c[:6].upper() if len(c) == 3: return (c[0]*2 + c[1]*2 + c[2]*2).upper() return DEFAULT_PRIMARY DEFAULT_PRIMARY = "283264" FONTE = "Calibri" TAM_CORPO = Pt(11) TAM_H1 = Pt(12) TAM_H2 = Pt(11) TAM_SMALL = Pt(9) TAM_HEADER = Pt(9) TAM_TINY = Pt(8) # Página A4 MARGEM_SUP = Cm(6.0) # 6cm — espaço uniforme header↔corpo em todas as páginas MARGEM_INF = Cm(3.0) MARGEM_ESQ = Cm(2.5) MARGEM_DIR = Cm(2.0) LARGURA_DXA = int((21 - 2.5 - 2.0) * 567) # Indentação hierárquica IND_H2 = 0.5 IND_BODY = 0.5 IND_ITEM = 1.0 IND_SUBITEM = 1.5 # ============================================================ # UTILITÁRIOS DOCX # ============================================================ def set_shading(cell, color): cell._tc.get_or_add_tcPr().append( parse_xml(f'')) def set_borders(cell, top=None, bottom=None, left=None, right=None): tcPr = cell._tc.get_or_add_tcPr() borders = parse_xml(f'') for side, val in [("top",top),("bottom",bottom),("left",left),("right",right)]: if val: c, s = val if isinstance(val, tuple) else (val, "4") borders.append(parse_xml( f'')) tcPr.append(borders) def set_valign(cell, v="center"): cell._tc.get_or_add_tcPr().append(parse_xml(f'')) def set_width(cell, w): cell._tc.get_or_add_tcPr().append( parse_xml(f'')) def set_margins(cell, t=0, b=0, l=80, r=80): cell._tc.get_or_add_tcPr().append(parse_xml( f'' f'' f'')) def fmt(p, text, bold=False, italic=False, size=None, color=None, font=FONTE, caps=False): run = p.add_run(text) run.bold = bold; run.italic = italic if size: run.font.size = size if color: run.font.color.rgb = RGBColor.from_string(color) run.font.name = font if caps: run.font.all_caps = True return run def _add_highlight_to_run(run, highlight_color="cyan"): """Aplica highlight (fundo colorido) a um run via XML. Aceita nomes Word (cyan, yellow...) ou hex 6-char para shading.""" rPr = run._r.get_or_add_rPr() if len(highlight_color) == 6 and all(c in '0123456789ABCDEFabcdef' for c in highlight_color): # Hex color → usar shading (aceita qualquer cor) rPr.append(parse_xml(f'')) else: # Nome Word predefinido (cyan, yellow, etc) rPr.append(parse_xml(f'')) def fmt_with_meta_badges(p, text, size=None, color="1A1A1A", font=FONTE, pal=None): """Renderiza texto com badges visuais para Metas OMS (Meta 1..Meta 6). Metas ficam em bold + highlight com cor terciária da paleta.""" import re pattern = re.compile(r'(Meta\s+[1-6])', re.IGNORECASE) parts = pattern.split(text) badge_color = pal["tertiary"] if pal else "cyan" badge_text_color = pal["primary_dark"] if pal else "1A3A6E" for part in parts: if pattern.match(part): run = p.add_run(f" {part} ") run.bold = True run.font.name = font if size: run.font.size = size run.font.color.rgb = RGBColor.from_string(badge_text_color) _add_highlight_to_run(run, badge_color) else: if part: run = p.add_run(part) run.font.name = font if size: run.font.size = size if color: run.font.color.rgb = RGBColor.from_string(color) def spacing(p, before=0, after=0, line=1.15): pf = p.paragraph_format pf.space_before = Pt(before); pf.space_after = Pt(after); pf.line_spacing = line def p_shading(p, color): p._p.get_or_add_pPr().append( parse_xml(f'')) def repeat_header(row): row._tr.get_or_add_trPr().append(parse_xml(f'')) def page_break(doc): p = doc.add_paragraph() p.add_run()._r.append(parse_xml(f'')) # ============================================================ # HEADER + FOOTER # ============================================================ def build_header(section, meta, pal, logo_bytes=None): header = section.header header.is_linked_to_previous = False for p in header.paragraphs: p.clear() tbl = header.add_table(rows=4, cols=3, width=Cm(16.5)) tbl.alignment = WD_TABLE_ALIGNMENT.CENTER cw = [1700, 5800, 2900] brd = (pal["gray_border"], "4") for row in tbl.rows: for i, cell in enumerate(row.cells): set_width(cell, cw[i]); set_valign(cell) set_margins(cell, 30, 30, 80, 80) set_borders(cell, brd, brd, brd, brd) # Logo logo_cell = tbl.cell(0,0).merge(tbl.cell(3,0)) logo_cell.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER if logo_bytes: from docx.shared import Inches as In run = logo_cell.paragraphs[0].add_run() run.add_picture(io.BytesIO(logo_bytes), height=Cm(1.8)) else: fmt(logo_cell.paragraphs[0], "[LOGO]", bold=True, size=Pt(10), color="888888") # Título — PRETO tc = tbl.cell(0,1).merge(tbl.cell(1,1)) tc.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(tc.paragraphs[0], "PROCEDIMENTO OPERACIONAL PADRÃO", bold=True, size=Pt(13), color="000000") # Nome do processo — PRETO pc = tbl.cell(2,1).merge(tbl.cell(3,1)) pc.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(pc.paragraphs[0], meta.get("titulo_processo","").upper(), bold=True, size=Pt(12), color="000000") # Metadados — PRETO for i, (lbl, val) in enumerate([ ("Código: ", meta.get("codigo","POP-XXX-001")), ("Elaborado em: ", meta.get("data_elaboracao","")), ("Revisado em: ", meta.get("data_revisao","—")), ("Válido até: ", meta.get("validade","")), ]): c = tbl.cell(i, 2) c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(c.paragraphs[0], lbl, bold=True, size=TAM_HEADER, color="000000") fmt(c.paragraphs[0], val or "—", size=TAM_HEADER, color="000000") def build_footer(section, meta, pal): footer = section.footer footer.is_linked_to_previous = False for p in footer.paragraphs: p.clear() footer.add_paragraph() # separator tbl = footer.add_table(rows=2, cols=3, width=Cm(16.5)) tbl.alignment = WD_TABLE_ALIGNMENT.CENTER cw = [3500, 3500, 3400] brd = (pal["gray_border"], "4") for row in tbl.rows: for i, cell in enumerate(row.cells): set_width(cell, cw[i]); set_valign(cell) set_margins(cell, 30, 30, 60, 60) set_borders(cell, brd, brd, brd, brd) for i, h in enumerate(["Elaborado por:", "Validado por:", "Aprovado por:"]): c = tbl.cell(0, i) set_shading(c, pal["tertiary"]) # Cor terciária no header do rodapé c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(c.paragraphs[0], h, bold=True, size=TAM_SMALL, color=pal["primary_dark"]) for i, key in enumerate(["elaborado_por", "validado_por", "aprovado_por"]): c = tbl.cell(1, i) person = meta.get(key, {}) nome = person.get("nome","") if isinstance(person, dict) else "" cargo = person.get("cargo","") if isinstance(person, dict) else "" c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER if nome: fmt(c.paragraphs[0], nome, bold=True, size=TAM_SMALL, color=pal["text_body"]) p2 = c.add_paragraph(); p2.alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(p2, cargo, size=TAM_TINY, color="666666") else: fmt(c.paragraphs[0], "________________", size=TAM_SMALL, color="AAAAAA") # Página X de Y pp = footer.add_paragraph() pp.alignment = WD_ALIGN_PARAGRAPH.RIGHT spacing(pp, 2, 0) fmt(pp, "Página ", size=TAM_TINY, color="888888") # Campo PAGE (número atual) for x in [f'', f' PAGE ', f'']: pp.add_run()._r.append(parse_xml(x)) r = pp.add_run("1"); r.font.size = TAM_TINY; r.font.color.rgb = RGBColor.from_string("888888") pp.add_run()._r.append(parse_xml(f'')) # " de " + NUMPAGES (total) fmt(pp, " de ", size=TAM_TINY, color="888888") for x in [f'', f' NUMPAGES ', f'']: pp.add_run()._r.append(parse_xml(x)) r2 = pp.add_run("1"); r2.font.size = TAM_TINY; r2.font.color.rgb = RGBColor.from_string("888888") pp.add_run()._r.append(parse_xml(f'')) fmt(pp, f" | {meta.get('codigo','POP-XXX-001')} | v{meta.get('versao','01')}", size=TAM_TINY, color="888888") # ============================================================ # ELEMENTOS DE CONTEÚDO # ============================================================ def add_h1(doc, num, titulo, pal): p = doc.add_paragraph(); spacing(p, 16, 8) p_shading(p, pal["primary"]) pf = p.paragraph_format; pf.left_indent = Cm(0.3); pf.right_indent = Cm(0.3) fmt(p, f" {num}. {titulo.upper()}", bold=True, size=TAM_H1, color=pal["text_on_primary"]) def add_h2(doc, num, titulo, pal): p = doc.add_paragraph(); spacing(p, 12, 6) p_shading(p, pal["secondary"]) pf = p.paragraph_format; pf.left_indent = Cm(IND_H2 + 0.3); pf.right_indent = Cm(0.3) fmt(p, f" {num}. {titulo.upper()}", bold=True, size=TAM_H2, color=pal["text_on_secondary"]) def add_body(doc, text, indent=IND_BODY): p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY spacing(p, 2, 5, 1.15); p.paragraph_format.left_indent = Cm(indent) fmt(p, text, size=TAM_CORPO, color="1A1A1A") def add_bullet(doc, text, bold_prefix=None, indent=IND_ITEM, pal=None): c = pal["primary"] if pal else DEFAULT_PRIMARY p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY spacing(p, 1, 3, 1.15) p.paragraph_format.left_indent = Cm(indent); p.paragraph_format.first_line_indent = Cm(-0.4) fmt(p, "● ", size=Pt(9), color=c, bold=True) if bold_prefix: fmt(p, bold_prefix, bold=True, size=TAM_CORPO, color=c) fmt(p, text, size=TAM_CORPO, color="1A1A1A") else: fmt(p, text, size=TAM_CORPO, color="1A1A1A") def add_num(doc, n, text, indent=IND_SUBITEM, pal=None, critico=False): c = pal["primary"] if pal else DEFAULT_PRIMARY p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY spacing(p, 1, 4, 1.15) p.paragraph_format.left_indent = Cm(indent); p.paragraph_format.first_line_indent = Cm(-0.5) if critico: fmt(p, f"\u26a0 ", bold=True, size=TAM_CORPO, color="C05000") fmt(p, f"{n}. [PASSO CR\u00cdTICO] ", bold=True, size=TAM_CORPO, color="C05000") else: fmt(p, f"{n}. ", bold=True, size=TAM_CORPO, color=c) # Texto com badges de Meta OMS fmt_with_meta_badges(p, text, size=TAM_CORPO, pal=pal) def add_def_item(doc, termo, defn, pal): p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY spacing(p, 2, 5, 1.15) p.paragraph_format.left_indent = Cm(IND_ITEM); p.paragraph_format.first_line_indent = Cm(-0.4) fmt(p, "● ", size=Pt(9), color=pal["primary"], bold=True) fmt(p, f"{termo}: ", bold=True, size=TAM_CORPO, color=pal["primary"]) fmt(p, defn, size=TAM_CORPO, color="1A1A1A") def add_risk(doc, risco, barreira, pal): p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY spacing(p, 4, 3, 1.15) p.paragraph_format.left_indent = Cm(IND_ITEM); p.paragraph_format.first_line_indent = Cm(-0.5) fmt(p, "⚠ ", size=TAM_CORPO, color=pal["risk_red"], bold=True) fmt(p, "Risco: ", bold=True, size=TAM_CORPO, color=pal["risk_red"]) fmt(p, risco, size=TAM_CORPO, color="1A1A1A") p2 = doc.add_paragraph(); p2.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY spacing(p2, 0, 8, 1.15); p2.paragraph_format.left_indent = Cm(IND_SUBITEM) fmt(p2, "↳ Barreira de Segurança: ", bold=True, size=TAM_CORPO, color=pal["barrier_green"]) fmt(p2, barreira, size=TAM_CORPO, color="1A1A1A") # ============================================================ # TABELAS # ============================================================ def add_table(doc, headers, rows, col_widths=None, pal=None): nc = len(headers) if not col_widths: col_widths = [LARGURA_DXA // nc] * nc tbl = doc.add_table(rows=1+len(rows), cols=nc) tbl.alignment = WD_TABLE_ALIGNMENT.CENTER brd = (pal["border_light"], "4") if pal else ("3B6AA0", "4") brd2 = (pal["gray_border"], "2") if pal else ("7F8C9A", "2") for i, h in enumerate(headers): c = tbl.cell(0, i); set_shading(c, pal["primary"] if pal else DEFAULT_PRIMARY) set_width(c, col_widths[i]); set_valign(c) set_margins(c, 50, 50, 80, 80); set_borders(c, brd, brd, brd, brd) c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(c.paragraphs[0], h, bold=True, size=TAM_SMALL, color="FFFFFF") repeat_header(tbl.rows[0]) for ri, rd in enumerate(rows): for ci, val in enumerate(rd): c = tbl.cell(ri+1, ci); set_width(c, col_widths[ci]); set_valign(c) set_margins(c, 40, 40, 80, 80); set_borders(c, brd2, brd2, brd2, brd2) a = WD_ALIGN_PARAGRAPH.LEFT if ci == 0 and nc > 2 else WD_ALIGN_PARAGRAPH.CENTER c.paragraphs[0].alignment = a fmt(c.paragraphs[0], str(val), size=TAM_SMALL, color="1A1A1A") if ri % 2 == 0: for ci in range(nc): set_shading(tbl.cell(ri+1, ci), pal["tertiary"] if pal else "E8EFF7") doc.add_paragraph() # ============================================================ # SMARTART # ============================================================ def add_process(doc, titulo, etapas, pal): if not etapas: return p = doc.add_paragraph(); spacing(p, 8, 6) p.paragraph_format.left_indent = Cm(IND_BODY) fmt(p, f"▸ {titulo}", bold=True, size=TAM_CORPO, color=pal["primary"]) n = len(etapas); total = n*2-1; aw = 400 bw = (LARGURA_DXA - aw*(n-1)) // n tbl = doc.add_table(rows=1, cols=total) tbl.alignment = WD_TABLE_ALIGNMENT.CENTER colors = [pal["primary"], pal["primary"], pal["primary"], pal["primary"]] for ci in range(total): c = tbl.cell(0, ci) if ci % 2 == 0: ei = ci // 2 set_width(c, bw); set_shading(c, pal["primary"] if ei % 2 == 0 else lighten(pal["primary"], 0.15)) bg = pal["primary"] if ei % 2 == 0 else lighten(pal["primary"], 0.15) set_borders(c, (bg,"6"), (bg,"6"), (bg,"6"), (bg,"6")) set_valign(c); set_margins(c, 60, 60, 80, 80) c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(c.paragraphs[0], f"Etapa {ei+1}", bold=True, size=TAM_TINY, color="FFFFFF") p2 = c.add_paragraph(); p2.alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(p2, etapas[ei], size=TAM_SMALL, color="FFFFFF") else: set_width(c, aw) set_borders(c,("FFFFFF","0"),("FFFFFF","0"),("FFFFFF","0"),("FFFFFF","0")) set_valign(c); c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(c.paragraphs[0], "→", bold=True, size=Pt(16), color=pal["primary"]) doc.add_paragraph() def add_checklist(doc, titulo, itens, pal): if not itens: return p = doc.add_paragraph(); spacing(p, 8, 6) p.paragraph_format.left_indent = Cm(IND_BODY) fmt(p, f"▸ {titulo}", bold=True, size=TAM_CORPO, color=pal["primary"]) tbl = doc.add_table(rows=len(itens), cols=2) tbl.alignment = WD_TABLE_ALIGNMENT.CENTER chw = 600; tw = LARGURA_DXA - chw for ri, item in enumerate(itens): cc = tbl.cell(ri, 0); set_width(cc, chw); set_valign(cc) set_margins(cc, 40, 40, 40, 40); set_shading(cc, pal["primary"]) set_borders(cc,("FFFFFF","2"),("FFFFFF","2"),(pal["primary"],"4"),("FFFFFF","2")) cc.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(cc.paragraphs[0], "✓", bold=True, size=Pt(14), color="FFFFFF") ct = tbl.cell(ri, 1); set_width(ct, tw); set_valign(ct) set_margins(ct, 50, 50, 120, 80) bg = pal["quaternary"] if ri % 2 == 0 else "FFFFFF" set_shading(ct, bg) set_borders(ct,(pal["gray_border"],"2"),(pal["gray_border"],"2"),("FFFFFF","0"),(pal["gray_border"],"2")) ct.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.LEFT fmt(ct.paragraphs[0], item, size=TAM_CORPO, color="1A1A1A") doc.add_paragraph() def add_cycle(doc, titulo, etapas, pal): if not etapas: return p = doc.add_paragraph(); spacing(p, 8, 6) p.paragraph_format.left_indent = Cm(IND_BODY) fmt(p, f"▸ {titulo}", bold=True, size=TAM_CORPO, color=pal["primary"]) n = len(etapas); cpr = min(n, 4); rows_n = (n+cpr-1)//cpr bw = LARGURA_DXA // cpr tbl = doc.add_table(rows=rows_n, cols=cpr) tbl.alignment = WD_TABLE_ALIGNMENT.CENTER for idx, et in enumerate(etapas): r, c_idx = idx//cpr, idx%cpr c = tbl.cell(r, c_idx); set_width(c, bw) bg = pal["primary"] if idx % 2 == 0 else lighten(pal["primary"], 0.15) set_shading(c, bg) set_borders(c,("FFFFFF","4"),("FFFFFF","4"),("FFFFFF","4"),("FFFFFF","4")) set_valign(c); set_margins(c, 60, 60, 80, 80) c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER arrow = " →" if idx < n-1 else " ⟳" fmt(c.paragraphs[0], f"{idx+1}{arrow}", bold=True, size=TAM_TINY, color="FFFFFF") p2 = c.add_paragraph(); p2.alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(p2, et, size=TAM_SMALL, color="FFFFFF") for idx in range(n, rows_n*cpr): c = tbl.cell(idx//cpr, idx%cpr) set_borders(c,("FFFFFF","0"),("FFFFFF","0"),("FFFFFF","0"),("FFFFFF","0")) doc.add_paragraph() # ============================================================ # ANEXOS COM BORDA — 1 PÁGINA POR ANEXO, BORDA FULL-HEIGHT # ============================================================ # Altura da caixa bordada em DXA (~14.5cm para caber com H1+descrição na mesma página) ANNEX_BOX_HEIGHT_DXA = 8200 def _set_row_height(row, height_dxa): """Define altura mínima de uma linha de tabela.""" trPr = row._tr.get_or_add_trPr() trPr.append(parse_xml( f'')) def _fill_cell_blank_lines(cell, n=8): """Adiciona linhas em branco ao final de uma célula para preenchimento vertical.""" for _ in range(n): p = cell.add_paragraph() p.paragraph_format.space_before = Pt(0) p.paragraph_format.space_after = Pt(0) def start_annex_border(doc, pal): """Cria container bordado com altura mínima full-page.""" tbl = doc.add_table(rows=1, cols=1) tbl.alignment = WD_TABLE_ALIGNMENT.CENTER c = tbl.cell(0, 0) brd = (pal["annex_border"], "6") set_borders(c, brd, brd, brd, brd) set_width(c, LARGURA_DXA) set_margins(c, 150, 150, 250, 250) _set_row_height(tbl.rows[0], ANNEX_BOX_HEIGHT_DXA) return c, tbl # ============================================================ # GERADOR DE DIAGRAMAS VIA GRAPHVIZ (imagens PNG) # ============================================================ def gerar_diagrama_png(tipo, conteudo, titulo, pal, context="annex"): """Gera PNG de diagrama profissional via Graphviz. Retorna path ou None. context: 'inline' (no corpo do procedimento) ou 'annex' (página de anexo).""" if not HAS_GRAPHVIZ: return None try: dot = gv.Digraph(format='png') dot.attr(dpi='120', bgcolor='transparent', margin='0.15', nodesep='0.4', ranksep='0.5') node_style = { 'style': 'filled,rounded', 'shape': 'box', 'fontname': 'Arial', 'fontsize': '11', 'fillcolor': f'#{pal["primary"]}', 'fontcolor': '#FFFFFF', 'color': f'#{pal["primary_dark"]}', 'penwidth': '1.2', 'height': '0.5', 'margin': '0.12,0.06' } node_light = {**node_style, 'fillcolor': f'#{pal["secondary"]}', 'fontcolor': f'#{pal["text_on_secondary"]}'} node_ter = {**node_style, 'fillcolor': f'#{pal["tertiary"]}', 'fontcolor': f'#{pal["primary_dark"]}'} edge_style = {'color': f'#{pal["primary"]}', 'penwidth': '1.0', 'arrowsize': '0.6'} node_decision = {**node_style, 'shape': 'diamond', 'height': '0.7', 'width': '1.2'} # Mapear formas forma_map = {'elipse': 'ellipse', 'losango': 'diamond', 'retangulo': 'box', 'ellipse': 'ellipse', 'diamond': 'diamond', 'box': 'box'} def _parse_etapa(et, idx=0, total=1): if isinstance(et, dict): txt = et.get('texto', et.get('titulo', str(et)))[:30] frm = forma_map.get(et.get('forma', 'retangulo'), 'box') return txt, frm txt = str(et)[:30] if idx == 0 or idx == total - 1: return txt, 'ellipse' if '?' in str(et): return txt, 'diamond' return txt, 'box' if tipo in ("processo", "fluxograma"): etapas_raw = conteudo if isinstance(conteudo, list) else [conteudo] has_decision = any( (isinstance(e, dict) and e.get('forma') in ('losango','diamond')) or (isinstance(e, str) and '?' in e) for e in etapas_raw ) if context == "inline" and not has_decision: # ── INLINE TIPO 1: Linear simples, max 5 etapas ── etapas = etapas_raw[:5] dot.attr(rankdir='LR', size='8,2!') for i, et in enumerate(etapas): txt, frm = _parse_etapa(et, i, len(etapas)) st = {**(node_style if i % 2 == 0 else node_light), 'shape': frm} dot.node(f'n{i}', txt, **st) if i > 0: dot.edge(f'n{i-1}', f'n{i}', **edge_style) elif context == "inline" and has_decision: # ── INLINE TIPO 2: 2 sequenciais + decisão + split ── # Layout: n0 → n1 (mesma linha) # ↓ # n2 (losango) # ↙ ↘ # n3 n4 etapas = etapas_raw[:5] dot.attr(rankdir='TB', size='7,4!') for i, et in enumerate(etapas): txt, frm = _parse_etapa(et, i, len(etapas)) if i == 2 or frm == 'diamond': st = {**node_decision} elif i < 2: st = {**node_style, 'shape': frm} else: st = {**node_light, 'shape': frm} dot.node(f'n{i}', txt, **st) # Edges: 0→1, 1→2, 2→3(left), 2→4(right) if len(etapas) >= 2: dot.edge('n0', 'n1', **edge_style) if len(etapas) >= 3: dot.edge('n1', 'n2', **edge_style) if len(etapas) >= 4: dot.edge('n2', 'n3', **edge_style, label=' N\u00e3o', fontsize='9', fontcolor=f'#{pal["primary"]}') if len(etapas) >= 5: dot.edge('n2', 'n4', **edge_style, label=' Sim', fontsize='9', fontcolor=f'#{pal["primary"]}') # Ranks dot.body.append(' { rank=same; n0 n1 }') if len(etapas) >= 4: rank_bottom = 'n3' if len(etapas) >= 5: rank_bottom += ' n4' dot.body.append(f' {{ rank=same; {rank_bottom} }}') else: # ── ANEXO: Layout 3 colunas com splits em decisões ── etapas = etapas_raw[:10] n = len(etapas) dot.attr(rankdir='TB', size='7,4!') # Criar todos os nós for i, et in enumerate(etapas): txt, frm = _parse_etapa(et, i, n) if frm == 'diamond': st = {**node_decision} elif i % 2 == 0: st = {**node_style, 'shape': frm} else: st = {**node_light, 'shape': frm} dot.node(f'n{i}', txt, **st) # Edges inteligentes: detectar diamonds e criar splits diamonds = [i for i in range(n) if _parse_etapa(etapas[i], i, n)[1] == 'diamond'] if diamonds: # Edges sequenciais até o primeiro diamond d1 = diamonds[0] for i in range(d1): dot.edge(f'n{i}', f'n{i+1}', **edge_style) # Primeiro split left_idx = d1 + 1 if d1 + 1 < n else None right_idx = d1 + 2 if d1 + 2 < n else None if left_idx is not None: dot.edge(f'n{d1}', f'n{left_idx}', **edge_style, label=' N\u00e3o', fontsize='9', fontcolor=f'#{pal["primary"]}') if right_idx is not None: dot.edge(f'n{d1}', f'n{right_idx}', **edge_style, label=' Sim', fontsize='9', fontcolor=f'#{pal["primary"]}') # Ranks: centro, split esquerdo/direito if left_idx and right_idx: dot.body.append(f' {{ rank=same; n{left_idx} n{right_idx} }}') # Continuação após split used = set(range(d1 + 1)) | {d1, left_idx, right_idx} remaining = [i for i in range(n) if i not in used and i is not None] # Left path if left_idx is not None and remaining: left_next = remaining[0] dot.edge(f'n{left_idx}', f'n{left_next}', **edge_style) remaining = remaining[1:] # Right path: sequencial prev_right = right_idx for ri in remaining: if prev_right is not None: frm_ri = _parse_etapa(etapas[ri], ri, n)[1] if frm_ri == 'diamond' and ri + 1 < n and ri + 2 < n: # Segundo split dot.edge(f'n{prev_right}', f'n{ri}', **edge_style) dot.edge(f'n{ri}', f'n{ri+1}', **edge_style, label=' N\u00e3o', fontsize='9', fontcolor=f'#{pal["primary"]}') dot.edge(f'n{ri}', f'n{ri+2}', **edge_style, label=' Sim', fontsize='9', fontcolor=f'#{pal["primary"]}') dot.body.append(f' {{ rank=same; n{ri+1} n{ri+2} }}') break else: dot.edge(f'n{prev_right}', f'n{ri}', **edge_style) prev_right = ri else: # Sem diamonds: zigzag simples per_row = 3 for i in range(n): if i > 0: dot.edge(f'n{i-1}', f'n{i}', **edge_style) for row_start in range(0, n, per_row): row_nodes = ' '.join(f'n{j}' for j in range(row_start, min(row_start + per_row, n))) dot.body.append(f' {{ rank=same; {row_nodes} }}') elif tipo == "hierarquia": dot.attr(rankdir='TB', size='8,4.5!') items = conteudo if isinstance(conteudo, list) else [conteudo] for i, item in enumerate(items): if isinstance(item, dict): lbl = item.get('titulo', '')[:30] dot.node(f'g{i}', lbl, **node_style) for j, sub in enumerate(item.get('subitens', [])): dot.node(f'g{i}s{j}', sub[:25], **node_light) dot.edge(f'g{i}', f'g{i}s{j}', **edge_style) else: dot.node(f'g{i}', str(item)[:30], **node_style) if i > 0: dot.edge(f'g{i-1}', f'g{i}', **edge_style) elif tipo == "piramide": dot.attr(rankdir='TB', size='7,5!') items = conteudo if isinstance(conteudo, list) else [conteudo] for i, item in enumerate(items): w = str(max(1.5, 3.5 - i * 0.4)) st = {**node_style, 'width': w, 'fixedsize': 'true', 'height': '0.45'} if i > len(items) // 2: st = {**node_ter, 'width': w, 'fixedsize': 'true', 'height': '0.45'} dot.node(f'p{i}', str(item)[:30], **st) if i > 0: dot.edge(f'p{i-1}', f'p{i}', **edge_style, style='invis') for i in range(len(items)): dot.body.append(f' {{ rank=same; p{i} }}') elif tipo == "ciclo": dot.attr(rankdir='LR', size='8,3!') # Horizontal e compacto etapas = conteudo if isinstance(conteudo, list) else [conteudo] n = len(etapas) for i, et in enumerate(etapas): st = node_style if i % 2 == 0 else node_light dot.node(f'c{i}', et[:30], **st) for i in range(n): dot.edge(f'c{i}', f'c{(i+1)%n}', **edge_style) else: return None out = os.path.join(tempfile.gettempdir(), f"diag_{tipo}_{id(conteudo)}") return dot.render(out, cleanup=True) except Exception: return None def _inserir_diagrama_na_celula(cell, img_path): """Insere imagem PNG do diagrama dentro de uma célula do docx. Limita largura a 15cm E altura a 10cm para caber na caixa do anexo.""" if not img_path or not os.path.exists(img_path): return False try: from PIL import Image as PILImage img = PILImage.open(img_path) w_px, h_px = img.size # Calcular dimensões respeitando limites max_w = Cm(15) max_h = Cm(10) ratio = w_px / h_px width = max_w height = int(width / ratio) if ratio > 0 else max_h if height > max_h: height = max_h width = int(height * ratio) p = cell.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER run = p.add_run() run.add_picture(img_path, width=width) return True except Exception: try: p = cell.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER run = p.add_run() run.add_picture(img_path, width=Cm(12)) return True except: return False def _inserir_diagrama_no_corpo(doc, img_path, legenda="", pal=None): """Insere imagem PNG de diagrama no corpo do documento (fora de tabela/célula). Centralizado, com legenda em itálico abaixo.""" if not img_path or not os.path.exists(img_path): return try: p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.space_before = Pt(8) p.paragraph_format.space_after = Pt(4) run = p.add_run() run.add_picture(img_path, width=Cm(14)) if legenda: pl = doc.add_paragraph() pl.alignment = WD_ALIGN_PARAGRAPH.CENTER pl.paragraph_format.space_before = Pt(2) pl.paragraph_format.space_after = Pt(8) fmt(pl, legenda, italic=True, size=Pt(9), color=pal["primary"] if pal else "333333") except Exception: pass def _render_passo(doc, num, item, pal): """Renderiza um item de procedimento: string, dict com texto/prioridade, ou dict com diagrama. Formatos aceitos: - "texto simples" - {"texto": "...", "prioridade": "critica"} - {"diagrama": {"tipo":..., "titulo":..., "conteudo":...}} - {"tipo":..., "titulo":..., "conteudo":...} """ if isinstance(item, str): add_num(doc, num, item, pal=pal) elif isinstance(item, dict): # Formato 1: {"texto": "...", "prioridade": "critica"} if "texto" in item and "diagrama" not in item and "tipo" not in item: texto = item.get("texto", "") critico = item.get("prioridade", "").lower() == "critica" add_num(doc, num, texto, pal=pal, critico=critico) return # Formato 2: diagrama (com ou sem wrapper "diagrama") diag = item.get("diagrama", None) if diag is None and "tipo" in item: diag = item if diag is None: # Dict não reconhecido — texto fallback add_num(doc, num, str(item), pal=pal) return tipo = diag.get("tipo", "processo") titulo = diag.get("titulo", "Diagrama") conteudo = diag.get("conteudo", []) img_path = gerar_diagrama_png(tipo, conteudo, titulo, pal, context="inline") if img_path: _inserir_diagrama_no_corpo(doc, img_path, titulo, pal) else: items_txt = ', '.join(str(c) for c in conteudo) if isinstance(conteudo, list) else str(conteudo) add_num(doc, num, f"[{titulo}]: {items_txt}", pal=pal) def render_annex(doc, anexo, num, pal): page_break(doc) titulo = anexo.get("titulo", anexo) if isinstance(anexo, dict) else str(anexo) add_h1(doc, f"ANEXO {num}", titulo.upper(), pal) if isinstance(anexo, str): add_body(doc, f"Conteúdo do {titulo} — a ser inserido pela instituição.") return if anexo.get("descricao"): add_body(doc, anexo["descricao"]) tipo = anexo.get("tipo", "texto") conteudo = anexo.get("conteudo", "") # Criar container bordado com altura full-page bc, border_tbl = start_annex_border(doc, pal) # Para tipos de diagrama: tentar Graphviz primeiro graphviz_ok = False if tipo in ("processo", "fluxograma", "hierarquia", "piramide", "ciclo"): img_path = gerar_diagrama_png(tipo, conteudo, titulo, pal) if img_path: _inserir_diagrama_na_celula(bc, img_path) graphviz_ok = True _fill_cell_blank_lines(bc, 6) # === CHECKLIST === if graphviz_ok: pass # Graphviz renderizou — pular SmartArt de tabela elif tipo == "checklist": itens = conteudo if isinstance(conteudo, list) else [conteudo] pt = bc.paragraphs[0]; pt.alignment = WD_ALIGN_PARAGRAPH.LEFT fmt(pt, f"▸ {titulo}", bold=True, size=TAM_CORPO, color=pal["primary"]) inner = bc.add_table(rows=len(itens), cols=2) chw = 500; tw = LARGURA_DXA - 900 for ri, item in enumerate(itens): cc = inner.cell(ri, 0); set_width(cc, chw); set_valign(cc) set_margins(cc, 30, 30, 30, 30); set_shading(cc, pal["primary"]) set_borders(cc,("FFFFFF","2"),("FFFFFF","2"),(pal["primary"],"4"),("FFFFFF","2")) cc.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(cc.paragraphs[0], "✓", bold=True, size=Pt(12), color="FFFFFF") ct = inner.cell(ri, 1); set_width(ct, tw); set_valign(ct) set_margins(ct, 40, 40, 100, 60) set_shading(ct, pal["quaternary"] if ri % 2 == 0 else "FFFFFF") set_borders(ct,(pal["gray_border"],"1"),(pal["gray_border"],"1"),("FFFFFF","0"),(pal["gray_border"],"1")) fmt(ct.paragraphs[0], item, size=TAM_SMALL, color="1A1A1A") _fill_cell_blank_lines(bc, max(1, 12 - len(itens))) # === ESCALA / TABELA === elif tipo in ("escala", "tabela", "referencia", "referência"): headers = anexo.get("colunas", []) rows_data = anexo.get("linhas", []) # Handle Gemini format: conteudo = [{colunas: [...], linhas: [...]}] if not headers and isinstance(conteudo, list) and len(conteudo) > 0 and isinstance(conteudo[0], dict): tbl_obj = conteudo[0] headers = tbl_obj.get("colunas", []) rows_data = tbl_obj.get("linhas", []) # Fallback: if conteudo is list of lists (actual row data) and no linhas at annexo level if not rows_data and isinstance(conteudo, list) and len(conteudo) > 0 and isinstance(conteudo[0], list): rows_data = conteudo if headers and rows_data: bc.paragraphs[0].text = "" nc = len(headers); cw_each = (LARGURA_DXA - 900) // nc inner = bc.add_table(rows=1+len(rows_data), cols=nc) for i, h in enumerate(headers): c = inner.cell(0, i); set_shading(c, pal["primary"]) set_width(c, cw_each); set_valign(c); set_margins(c, 40, 40, 60, 60) set_borders(c,(pal["primary"],"4"),(pal["primary"],"4"),(pal["primary"],"4"),(pal["primary"],"4")) c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(c.paragraphs[0], h, bold=True, size=TAM_SMALL, color="FFFFFF") for ri, rd in enumerate(rows_data): for ci, val in enumerate(rd): c = inner.cell(ri+1, ci); set_width(c, cw_each); set_valign(c) set_margins(c, 30, 30, 60, 60) set_borders(c,(pal["gray_border"],"1"),(pal["gray_border"],"1"), (pal["gray_border"],"1"),(pal["gray_border"],"1")) c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(c.paragraphs[0], str(val), size=TAM_SMALL, color="1A1A1A") if ri % 2 == 0: for ci in range(nc): set_shading(inner.cell(ri+1, ci), pal["tertiary"]) _fill_cell_blank_lines(bc, max(1, 10 - len(rows_data))) # === PROCESSO / FLUXOGRAMA === elif tipo in ("processo", "fluxograma"): etapas = conteudo if isinstance(conteudo, list) else [conteudo] etapas = etapas[:6] fmt(bc.paragraphs[0], f"▸ Fluxo do Processo", bold=True, size=TAM_CORPO, color=pal["primary"]) n = len(etapas); total = n*2-1; aw = 350 bw = (LARGURA_DXA - 900 - aw*(n-1)) // n inner = bc.add_table(rows=1, cols=total) for ci in range(total): c = inner.cell(0, ci) if ci % 2 == 0: ei = ci // 2; set_width(c, bw) bg = pal["primary"] if ei % 2 == 0 else lighten(pal["primary"], 0.15) set_shading(c, bg); set_borders(c,(bg,"4"),(bg,"4"),(bg,"4"),(bg,"4")) set_valign(c); set_margins(c, 50, 50, 60, 60) c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(c.paragraphs[0], f"Etapa {ei+1}", bold=True, size=TAM_TINY, color="FFFFFF") p2 = c.add_paragraph(); p2.alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(p2, etapas[ei], size=Pt(8), color="FFFFFF") else: set_width(c, aw) set_borders(c,("FFFFFF","0"),("FFFFFF","0"),("FFFFFF","0"),("FFFFFF","0")) set_valign(c); c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(c.paragraphs[0], "\u2192", bold=True, size=Pt(14), color=pal["primary"]) _fill_cell_blank_lines(bc, 15) # === HIERARQUIA (organograma / classificação em níveis) === elif tipo == "hierarquia": items = conteudo if isinstance(conteudo, list) else [conteudo] fmt(bc.paragraphs[0], f"\u25b8 {titulo}", bold=True, size=TAM_CORPO, color=pal["primary"]) for item in items: if isinstance(item, dict): # Nível 1: box colorido p1 = bc.add_paragraph() p1.paragraph_format.space_before = Pt(6) p1_shading = parse_xml(f'') p1._p.get_or_add_pPr().append(p1_shading) fmt(p1, f" {item.get('titulo', '')}", bold=True, size=TAM_CORPO, color="FFFFFF") # Nível 2: subitens indentados com barra lateral for sub in item.get("subitens", []): p2 = bc.add_paragraph() p2.paragraph_format.left_indent = Cm(1.0) p2.paragraph_format.space_before = Pt(2) p2_shading = parse_xml(f'') p2._p.get_or_add_pPr().append(p2_shading) fmt(p2, f" \u25b9 {sub}", size=TAM_SMALL, color=pal["primary_dark"]) elif isinstance(item, str): p1 = bc.add_paragraph() p1_shading = parse_xml(f'') p1._p.get_or_add_pPr().append(p1_shading) fmt(p1, f" {item}", bold=True, size=TAM_SMALL, color=pal["primary_dark"]) _fill_cell_blank_lines(bc, max(1, 8 - len(items))) # === PIRÂMIDE (de cima para baixo, estreito→largo) === elif tipo == "piramide": items = conteudo if isinstance(conteudo, list) else [conteudo] fmt(bc.paragraphs[0], f"\u25b8 {titulo}", bold=True, size=TAM_CORPO, color=pal["primary"]) n = len(items) max_w = LARGURA_DXA - 900 for i, item in enumerate(items): # Cada nível fica mais largo (proporção crescente) level_w = int(max_w * (0.4 + 0.6 * i / max(n-1, 1))) margin_l = (max_w - level_w) // 2 inner = bc.add_table(rows=1, cols=1) inner.alignment = WD_TABLE_ALIGNMENT.CENTER c = inner.cell(0, 0) set_width(c, level_w) # Gradiente de cores: topo=primário, base=terciário frac = i / max(n-1, 1) r1, g1, b1 = hex_to_rgb(pal["primary"]) r2, g2, b2 = hex_to_rgb(pal["tertiary"]) bg = rgb_to_hex(r1+(r2-r1)*frac, g1+(g2-g1)*frac, b1+(b2-b1)*frac) set_shading(c, bg) brd = (bg, "4") set_borders(c, brd, brd, brd, brd) set_valign(c); set_margins(c, 40, 40, 80, 80) c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER txt_color = "FFFFFF" if frac < 0.5 else pal["primary_dark"] fmt(c.paragraphs[0], str(item), bold=True, size=TAM_SMALL, color=txt_color) _fill_cell_blank_lines(bc, max(1, 10 - n)) # === CICLO (PDCA, melhoria contínua) === elif tipo == "ciclo": etapas = conteudo if isinstance(conteudo, list) else [conteudo] fmt(bc.paragraphs[0], f"\u25b8 {titulo}", bold=True, size=TAM_CORPO, color=pal["primary"]) n = len(etapas) # Layout em grid 2x2 ou linear if n == 4: inner = bc.add_table(rows=2, cols=2) inner.alignment = WD_TABLE_ALIGNMENT.CENTER positions = [(0,0),(0,1),(1,1),(1,0)] # Sentido horário colors = [pal["primary"], lighten(pal["primary"],0.15), lighten(pal["primary"],0.30), lighten(pal["primary"],0.45)] bw = (LARGURA_DXA - 900) // 2 for idx, (r,c_idx) in enumerate(positions): c = inner.cell(r, c_idx); set_width(c, bw); set_valign(c) set_shading(c, colors[idx]) brd = (colors[idx], "4") set_borders(c, brd, brd, brd, brd) set_margins(c, 50, 50, 80, 80) c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(c.paragraphs[0], etapas[idx], bold=True, size=TAM_SMALL, color="FFFFFF") # Setas no centro p_arrow = bc.add_paragraph() p_arrow.alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(p_arrow, "\u21bb Ciclo Cont\u00ednuo", bold=True, size=TAM_SMALL, color=pal["primary"]) else: cpr = min(n, 4); rows_n = (n+cpr-1)//cpr bw = (LARGURA_DXA - 900) // cpr inner = bc.add_table(rows=rows_n, cols=cpr) inner.alignment = WD_TABLE_ALIGNMENT.CENTER for idx, et in enumerate(etapas): r, ci2 = divmod(idx, cpr) c = inner.cell(r, ci2); set_width(c, bw); set_valign(c) bg = pal["primary"] if idx % 2 == 0 else lighten(pal["primary"], 0.20) set_shading(c, bg) brd = (bg, "4") set_borders(c, brd, brd, brd, brd) set_margins(c, 40, 40, 60, 60) c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(c.paragraphs[0], et, bold=True, size=TAM_TINY, color="FFFFFF") _fill_cell_blank_lines(bc, 10) # === MISTO === elif tipo == "misto": bc.paragraphs[0].text = "" elementos = conteudo if isinstance(conteudo, list) else [conteudo] for elem in elementos: if isinstance(elem, str): pe = bc.add_paragraph(); fmt(pe, elem, size=TAM_CORPO, color="1A1A1A") elif isinstance(elem, dict): st = elem.get("tipo", "texto") if st == "texto": pe = bc.add_paragraph(); fmt(pe, elem.get("conteudo",""), size=TAM_CORPO, color="1A1A1A") elif st == "checklist": pe = bc.add_paragraph() fmt(pe, f"\n\u25b8 {elem.get('titulo','')}", bold=True, size=TAM_CORPO, color=pal["primary"]) for it in elem.get("itens", []): pi = bc.add_paragraph(); pi.paragraph_format.left_indent = Cm(0.5) fmt(pi, "\u2713 ", bold=True, size=TAM_SMALL, color=pal["primary"]) fmt(pi, it, size=TAM_SMALL, color="1A1A1A") elif st == "processo": pe = bc.add_paragraph() fmt(pe, f"\n\u25b8 {elem.get('titulo','')}", bold=True, size=TAM_CORPO, color=pal["primary"]) for i, et in enumerate(elem.get("etapas",[]), 1): pi = bc.add_paragraph(); pi.paragraph_format.left_indent = Cm(0.5) fmt(pi, f"{i}. ", bold=True, size=TAM_SMALL, color=pal["primary"]) fmt(pi, et, size=TAM_SMALL, color="1A1A1A") elif st == "tabela": pe = bc.add_paragraph() fmt(pe, f"\n\u25b8 {elem.get('titulo','')}", bold=True, size=TAM_CORPO, color=pal["primary"]) hdrs = elem.get("colunas",[]); rws = elem.get("linhas",[]) if hdrs and rws: nc2 = len(hdrs); cw2 = (LARGURA_DXA - 900) // nc2 it2 = bc.add_table(rows=1+len(rws), cols=nc2) for i, h in enumerate(hdrs): cc2 = it2.cell(0,i); set_shading(cc2, pal["primary"]) set_width(cc2,cw2); set_valign(cc2); set_margins(cc2,30,30,50,50) set_borders(cc2,(pal["primary"],"3"),(pal["primary"],"3"), (pal["primary"],"3"),(pal["primary"],"3")) cc2.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(cc2.paragraphs[0], h, bold=True, size=TAM_TINY, color="FFFFFF") for ri2, rd2 in enumerate(rws): for ci2, v2 in enumerate(rd2): cc2 = it2.cell(ri2+1,ci2); set_width(cc2,cw2); set_valign(cc2) set_margins(cc2,25,25,50,50) set_borders(cc2,(pal["gray_border"],"1"),(pal["gray_border"],"1"), (pal["gray_border"],"1"),(pal["gray_border"],"1")) cc2.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(cc2.paragraphs[0], str(v2), size=TAM_TINY, color="1A1A1A") if ri2 % 2 == 0: for ci2 in range(nc2): set_shading(it2.cell(ri2+1,ci2), pal["tertiary"]) _fill_cell_blank_lines(bc, 4) # === GLOSSÁRIO / SÍMBOLOS === elif tipo in ("glossario", "glossário", "simbolos", "símbolos", "glossario_visual"): bc.paragraphs[0].text = "" if isinstance(conteudo, list): if len(conteudo) > 0 and isinstance(conteudo[0], dict): # Formato tabela: [{colunas:..., linhas:...}] ou [{simbolo:..., nome:...}] tbl_obj = conteudo[0] if "colunas" in tbl_obj: headers = tbl_obj.get("colunas", []) rows_data = tbl_obj.get("linhas", []) if headers and rows_data: nc = len(headers); cw_each = (LARGURA_DXA - 900) // nc inner = bc.add_table(rows=1+len(rows_data), cols=nc) for i, h in enumerate(headers): c = inner.cell(0, i); set_shading(c, pal["primary"]) set_width(c, cw_each); set_valign(c); set_margins(c, 40, 40, 60, 60) set_borders(c,(pal["primary"],"4"),(pal["primary"],"4"),(pal["primary"],"4"),(pal["primary"],"4")) c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(c.paragraphs[0], h, bold=True, size=TAM_SMALL, color="FFFFFF") for ri, rd in enumerate(rows_data): for ci, val in enumerate(rd): c = inner.cell(ri+1, ci); set_width(c, cw_each); set_valign(c) set_margins(c, 30, 30, 60, 60) set_borders(c,(pal["gray_border"],"1"),(pal["gray_border"],"1"), (pal["gray_border"],"1"),(pal["gray_border"],"1")) c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER fmt(c.paragraphs[0], str(val), size=TAM_SMALL, color="1A1A1A") if ri % 2 == 0: for ci in range(nc): set_shading(inner.cell(ri+1, ci), pal["tertiary"]) else: # Formato dict simples: [{simbolo: "⚠", nome: "Atenção"}, ...] for item in conteudo: pi = bc.add_paragraph() pi.paragraph_format.left_indent = Cm(0.5) simb = item.get("simbolo", item.get("icone", "●")) nome = item.get("nome", item.get("descricao", str(item))) fmt(pi, f"{simb} {nome}", size=TAM_CORPO, color="1A1A1A") else: # Formato lista simples de strings: "⚠ Atenção" (símbolo + 2 espaços + nome) for item in conteudo: pi = bc.add_paragraph() pi.paragraph_format.left_indent = Cm(0.5) fmt(pi, str(item), size=TAM_CORPO, color="1A1A1A") _fill_cell_blank_lines(bc, max(1, 10 - len(conteudo) if isinstance(conteudo, list) else 10)) # === TEXTO / FALLBACK === else: if isinstance(conteudo, str): fmt(bc.paragraphs[0], conteudo, size=TAM_CORPO, color="1A1A1A") elif isinstance(conteudo, list): bc.paragraphs[0].text = "" for item in conteudo: pi = bc.add_paragraph(); fmt(pi, f"\u25cf {item}", size=TAM_CORPO, color="1A1A1A") _fill_cell_blank_lines(bc, 12) # ============================================================ # GERADOR PRINCIPAL # ============================================================ def gerar_pop_docx(json_str, primary_color=None, logo_bytes=None, palette_overrides=None): data = json.loads(json_str) meta = data.get("metadata", {}) secoes = data.get("secoes", {}) pal = build_palette(parse_color_input(primary_color)) # Aplicar overrides individuais de cor if palette_overrides: for k, v in palette_overrides.items(): if v and v.strip(): pal[k] = parse_color_input(v) doc = Document() style = doc.styles["Normal"] style.font.name = FONTE; style.font.size = TAM_CORPO style.font.color.rgb = RGBColor.from_string("1A1A1A") style.paragraph_format.space_after = Pt(4); style.paragraph_format.line_spacing = 1.15 sec = doc.sections[0] sec.page_width = Cm(21); sec.page_height = Cm(29.7) sec.top_margin = MARGEM_SUP; sec.bottom_margin = MARGEM_INF sec.left_margin = MARGEM_ESQ; sec.right_margin = MARGEM_DIR build_header(sec, meta, pal, logo_bytes) build_footer(sec, meta, pal) if doc.paragraphs and doc.paragraphs[0].text == "": doc.paragraphs[0]._element.getparent().remove(doc.paragraphs[0]._element) # Seções sn = 1 add_h1(doc, sn, "OBJETIVO / FINALIDADE", pal) # Remover space_before do primeiro H1 (margem superior já cuida do espaço) doc.paragraphs[0].paragraph_format.space_before = Pt(0) add_body(doc, secoes.get("objetivo", "[Objetivo não fornecido]")) sn += 1; add_h1(doc, sn, "CAMPO DE APLICAÇÃO / ÁREA", pal) add_body(doc, secoes.get("campo_aplicacao", "[Campo não fornecido]")) sn += 1; add_h1(doc, sn, "CONCEITOS E DEFINIÇÕES", pal) for it in secoes.get("conceitos", []): if isinstance(it, dict): add_def_item(doc, it.get("termo",""), it.get("definicao",""), pal) else: add_bullet(doc, str(it), pal=pal) sn += 1; add_h1(doc, sn, "RESPONSABILIDADES E COMPETÊNCIAS", pal) for it in secoes.get("responsabilidades", []): if isinstance(it, dict): add_bullet(doc, it.get("acoes",""), bold_prefix=f"{it.get('papel','')}: ", pal=pal) else: add_bullet(doc, str(it), pal=pal) sn += 1; add_h1(doc, sn, "RECURSOS NECESSÁRIOS (MATERIAIS E EQUIPAMENTOS)", pal) for it in secoes.get("recursos", []): add_bullet(doc, str(it), pal=pal) sn += 1; add_h1(doc, sn, "DESCRIÇÃO DO PROCEDIMENTO (PASSO A PASSO)", pal) proc = secoes.get("procedimento", {}) sub = 1 add_h2(doc, f"{sn}.{sub}", "Ações Iniciais e Preparo", pal) for i, p in enumerate(proc.get("acoes_iniciais",[]), 1): _render_passo(doc, i, p, pal) sub += 1; add_h2(doc, f"{sn}.{sub}", "Execução Técnica", pal) for i, p in enumerate(proc.get("execucao_tecnica",[]), 1): _render_passo(doc, i, p, pal) sub += 1; add_h2(doc, f"{sn}.{sub}", "Ações Finais e Organização", pal) for i, p in enumerate(proc.get("acoes_finais",[]), 1): _render_passo(doc, i, p, pal) sn += 1; add_h1(doc, sn, "GERENCIAMENTO DE RISCO E PONTOS CRÍTICOS", pal) riscos = secoes.get("riscos", {}) add_h2(doc, f"{sn}.1", "Riscos Assistenciais", pal) # Ordenar riscos: Crítico primeiro, Moderado depois, demais por último riscos_list = riscos.get("assistenciais", []) def _risk_sort_key(it): r = it.get("risco","").lower() if isinstance(it, dict) else str(it).lower() if "crít" in r or "crit" in r: return 0 if "moder" in r: return 1 return 2 riscos_list = sorted(riscos_list, key=_risk_sort_key) for it in riscos_list: if isinstance(it, dict): add_risk(doc, it.get("risco",""), it.get("barreira",""), pal) else: add_bullet(doc, str(it), pal=pal) add_h2(doc, f"{sn}.2", "Plano de Contingência", pal) # Caixa de destaque com borda lateral vermelha cont_raw = riscos.get("contingencia","[Contingência não fornecida]") # Normalizar: str → lista de 1 item if isinstance(cont_raw, str): cont_items = [cont_raw] elif isinstance(cont_raw, list): cont_items = cont_raw else: cont_items = [str(cont_raw)] cont_tbl = doc.add_table(rows=1, cols=1) cont_tbl.alignment = WD_TABLE_ALIGNMENT.CENTER cc = cont_tbl.cell(0, 0) set_width(cc, LARGURA_DXA - 200) set_margins(cc, 80, 80, 200, 200) set_borders(cc, (pal["risk_red"],"12"), (pal["gray_border"],"1"), (pal["gray_border"],"1"), (pal["gray_border"],"1")) set_shading(cc, "FFF3E0") # Título pt = cc.paragraphs[0] pt.paragraph_format.space_after = Pt(8) fmt(pt, "\u26a0 ATEN\u00c7\u00c3O \u2014 Plano de Conting\u00eancia", bold=True, size=TAM_CORPO, color=pal["risk_red"]) # Itens numerados: conceito bold + ":" + ações com seta for idx, item_c in enumerate(cont_items, 1): pi = cc.add_paragraph() pi.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY pi.paragraph_format.line_spacing = 1.15 pi.paragraph_format.left_indent = Cm(0.5) pi.paragraph_format.space_before = Pt(3) pi.paragraph_format.space_after = Pt(3) # Número bold + cor risk_red fmt(pi, f"{idx}. ", bold=True, size=TAM_SMALL, color=pal["risk_red"]) # Limpar texto: remover prefixos "→ " ou "1. " que o Gemini pode ter adicionado txt = str(item_c).strip() import re txt = re.sub(r'^\d+\.\s*', '', txt) # Remove "1. " prefix txt = re.sub(r'^[\u2192→]\s*', '', txt) # Remove "→ " prefix # Separar conceito (antes do primeiro ":") do resto if ':' in txt: conceito, resto = txt.split(':', 1) # Strip markdown bold markers ** conceito = conceito.replace('**', '').strip() fmt(pi, conceito + ":", bold=True, size=TAM_SMALL, color="1A1A1A") # Formatar setas com espaçamento resto = resto.strip() # Normalizar setas: garantir " → " com espaços resto = re.sub(r'\s*[\u2192→]\s*', ' \u2192 ', resto) fmt_with_meta_badges(pi, " " + resto, size=TAM_SMALL, pal=pal) else: fmt_with_meta_badges(pi, txt, size=TAM_SMALL, pal=pal) sn += 1; add_h1(doc, sn, "REGISTROS DA QUALIDADE (EVIDÊNCIAS)", pal) add_body(doc, "A execução deste procedimento e eventuais intercorrências devem ser " "obrigatoriamente documentadas nos seguintes registros para rastreabilidade e auditoria:") for it in secoes.get("registros",[]): add_bullet(doc, str(it), pal=pal) sn += 1; add_h1(doc, sn, "INDICADORES DE MONITORAMENTO", pal) add_body(doc, "A eficácia da adesão a este POP será medida através dos seguintes indicadores institucionais:") inds = secoes.get("indicadores", []) if inds and isinstance(inds[0], dict) and "meta" in inds[0]: add_table(doc, ["Indicador","Meta","Periodicidade"], [[i.get("nome",""),i.get("meta",""),i.get("periodicidade","")] for i in inds], [5157,2400,1800], pal) else: for it in inds: add_bullet(doc, str(it), pal=pal) sn += 1; add_h1(doc, sn, "REFERÊNCIAS BIBLIOGRÁFICAS / NORMATIVAS", pal) for i, ref in enumerate(secoes.get("referencias",[]), 1): add_num(doc, i, str(ref), indent=IND_ITEM, pal=pal) sn += 1; add_h1(doc, sn, "ANEXOS / FLUXOGRAMAS", pal) anexos = secoes.get("anexos", []) if anexos: for idx, it in enumerate(anexos, 1): nome = it.get("titulo", it) if isinstance(it, dict) else str(it) add_bullet(doc, f"Anexo {idx}: {nome}", pal=pal) else: add_body(doc, "Não há anexos vinculados a este POP nesta versão.") sn += 1; add_h1(doc, sn, "HISTÓRICO DE REVISÕES", pal) revs = secoes.get("historico_revisoes", []) if revs: add_table(doc, ["Versão","Data","Descrição da Alteração","Responsável"], [[r.get("versao",""),r.get("data",""),r.get("descricao",""),r.get("responsavel","")] for r in revs], [1000,1200,5557,1600], pal) # Anexos completos if anexos: for idx, anx in enumerate(anexos, 1): render_annex(doc, anx, idx, pal) titulo_safe = meta.get("titulo_processo","POP").replace(" ","_").replace("/","-") codigo = meta.get("codigo","POP-XXX-001") fp = os.path.join(tempfile.gettempdir(), f"{codigo}_{titulo_safe}.docx") doc.save(fp) return fp # ============================================================ # VALIDADOR # ============================================================ def validar(json_str): try: data = json.loads(json_str) except json.JSONDecodeError as e: return False, f"❌ JSON inválido: {e}", None erros, avisos = [], [] if "metadata" not in data: erros.append("'metadata' ausente") else: for c in ["titulo_processo","codigo","versao","setor"]: if not data["metadata"].get(c): erros.append(f"metadata.{c} vazio") if "secoes" not in data: erros.append("'secoes' ausente") else: for s in ["objetivo","campo_aplicacao","conceitos","responsabilidades","recursos", "procedimento","riscos","registros","indicadores","referencias","historico_revisoes"]: if s not in data["secoes"]: erros.append(f"Seção '{s}' ausente") if erros: return False, "❌ ERROS:\n"+"\n".join(f" • {e}" for e in erros), None msg = "✅ JSON válido!" if avisos: msg += "\n⚠️ "+"\n".join(avisos) return True, msg, data # ============================================================ # GRADIO FRONTEND # ============================================================ DEFAULT_UI_COLOR = "283264" def processar(json_text, json_file, c_pri, c_sec, c_ter, c_zeb, logo_file): json_str = "" # Prioridade 1: arquivo anexo if json_file is not None: try: fpath = json_file.name if hasattr(json_file, 'name') else json_file with open(fpath, "r", encoding="utf-8") as f: json_str = f.read() except Exception as e: return None, f"\u274c {e}", None # Prioridade 2: texto colado if not json_str.strip(): json_str = json_text or "" if not json_str.strip(): return None, "\u26a0\ufe0f Insira o c\u00f3digo.", None jc = json_str.strip() # Remover backticks de codeblock markdown (```json ... ```) if jc.startswith("```"): jc = jc.split("\n",1)[1] if "\n" in jc else jc[3:] jc = jc.strip() if jc.startswith("json"): jc = jc[4:].strip() if jc.endswith("```"): jc = jc[:-3].strip() ok, msg, data = validar(jc) if not ok: return None, msg, None pri = parse_color_input(c_pri) if c_pri else DEFAULT_UI_COLOR pal = build_palette(pri) if c_sec and c_sec.strip(): pal["secondary"] = parse_color_input(c_sec) if c_ter and c_ter.strip(): pal["tertiary"] = parse_color_input(c_ter) if c_zeb and c_zeb.strip(): pal["quaternary"] = parse_color_input(c_zeb) logo_bytes = None if logo_file is not None: try: lp = logo_file if isinstance(logo_file, str) else (logo_file.name if hasattr(logo_file,'name') else None) if lp and os.path.exists(lp): with open(lp, "rb") as f: logo_bytes = f.read() except: pass try: overrides = {} if c_sec and c_sec.strip(): overrides["secondary"] = c_sec if c_ter and c_ter.strip(): overrides["tertiary"] = c_ter if c_zeb and c_zeb.strip(): overrides["quaternary"] = c_zeb fp = gerar_pop_docx(jc, pri, logo_bytes, palette_overrides=overrides) meta = data.get("metadata",{}) info = "\u2705 POP gerado com sucesso!" return fp, info, meta except Exception as e: return None, f"\u274c {str(e)}", None def criar_interface(): import gradio as gr TITLE_COLOR = "#9ab4d2" init_pal = build_palette(DEFAULT_UI_COLOR) APP_CSS = f""" .gradio-container {{ max-width: 1200px !important; }} #pop-hex-pri, #pop-hex-sec, #pop-hex-ter, #pop-hex-zeb {{ height: 0 !important; overflow: hidden !important; margin: 0 !important; padding: 0 !important; }} #color-state-row {{ height: 0 !important; overflow: hidden !important; margin: 0 !important; padding: 0 !important; gap: 0 !important; }} #json-paste textarea {{ min-height: 120px !important; height: 120px !important; overflow-y: auto !important; }} .upload-container span {{ font-size: 10px !important; }} .upload-container {{ min-height: 130px !important; }} h1, h3 {{ color: {TITLE_COLOR} !important; }} .left-col {{ flex-shrink: 0 !important; }} .step3-row {{ flex-wrap: nowrap !important; }} .step3-row > div {{ min-width: 0 !important; }} .landing-title {{ text-align: center; margin-top: 60px; margin-bottom: 0; }} .landing-title h1 {{ font-size: 2.2em !important; margin-bottom: 0 !important; padding-bottom: 0 !important; }} .landing-sub {{ text-align: center; color: #9ab4d2 !important; font-size: 0.95em; margin-top: 8px; position: relative; z-index: 10; }} .landing-steps {{ margin-top: 40px; }} .step-card {{ text-align: center; padding: 10px; }} .step-card button {{ min-height: 80px !important; white-space: normal !important; line-height: 1.4 !important; padding: 16px 20px !important; background: #283264 !important; border-color: #283264 !important; color: #fff !important; }} .step-card button:hover {{ background: #3a4a8a !important; border-color: #3a4a8a !important; }} .btn-voltar {{ position: absolute; right: 16px; top: 8px; z-index: 100; }} """ with gr.Blocks(title="\u2622\ufe0e RADIOTERAPIA.AI - POP de elite") as demo: # ══════════════════════════════════════ # LANDING PAGE # ══════════════════════════════════════ with gr.Column(visible=True) as landing_page: gr.HTML("""
\u2014 POP de Elite \u2014
por: Braga, HF.
""") gr.HTML("
") with gr.Row(elem_classes="landing-steps"): with gr.Column(scale=1, elem_classes="step-card"): btn_passo1 = gr.Button( "\U0001f4ac Passo 1: Clique aqui primeiro, passe sua demanda e receba o conte\u00fado do POP (em c\u00f3digo) com o nosso Agente Conselheiro da Qualidade.", variant="primary", size="lg") with gr.Column(scale=1, elem_classes="step-card"): btn_passo2 = gr.Button( "\U0001f4c4 Passo 2: Depois de receber o c\u00f3digo, clique aqui para montar o POP e receber o documento final", variant="primary", size="lg") # ══════════════════════════════════════ # APP PRINCIPAL (começa oculto) # ══════════════════════════════════════ with gr.Column(visible=False) as main_app: with gr.Row(): gr.HTML("""

\u2014 POP de Elite \u2014
por: Braga, HF.
""") btn_voltar = gr.Button("\u2190 Voltar", size="sm", variant="secondary", elem_classes="btn-voltar") gr.Markdown("---") with gr.Row(): # ══════════════ COLUNA ESQUERDA ══════════════ with gr.Column(scale=3, min_width=580, elem_classes="left-col"): # ── ETAPA 1: LOGO (compacto) ── gr.Markdown("### \U0001f5bc\ufe0e Etapa 2.1 \u2014 Logotipo institucional *(opcional)*") logo_input = gr.Image( type="filepath", sources=["upload"], height=130, show_label=False ) gr.Markdown("---") # ── ETAPA 2: CORES (4 boxes clicáveis) ── gr.Markdown("### \U0001f3a8\ufe0e Etapa 2.2 \u2014 Paleta de cores") with gr.Row(elem_id="color-state-row"): color_pri = gr.Textbox(value=f"#{init_pal['primary']}", elem_id="pop-hex-pri", show_label=False, container=False) color_sec = gr.Textbox(value="", elem_id="pop-hex-sec", show_label=False, container=False) color_ter = gr.Textbox(value="", elem_id="pop-hex-ter", show_label=False, container=False) color_zeb = gr.Textbox(value="", elem_id="pop-hex-zeb", show_label=False, container=False) gr.HTML(f"""
Prim\u00e1ria
#{init_pal['primary']}
Secund\u00e1ria
#{init_pal['secondary']}
Terci\u00e1ria
#{init_pal['tertiary']}
Zebra
#{init_pal['quaternary']}
""") btn_reset_colors = gr.Button("\u21ba restaurar cores padr\u00e3o", size="sm", variant="secondary") gr.Markdown("---") # ── ETAPA 3: JSON (lado a lado) ── gr.Markdown("### \U0001f4dd\ufe0e Etapa 2.3 \u2014 Insira o c\u00f3digo recebido *(upload do arquivo OU cole o c\u00f3digo)*") with gr.Row(equal_height=True, elem_classes="step3-row"): with gr.Column(scale=3): json_file_input = gr.File( file_types=[".json", ".txt"], type="filepath", height=130, show_label=False ) with gr.Column(scale=0, min_width=30): gr.HTML('
OU
') with gr.Column(scale=3): json_text_input = gr.Textbox( placeholder='Cole o c\u00f3digo aqui', lines=6, max_lines=6, show_label=False, elem_id="json-paste" ) # Quando arquivo é carregado, desabilita textbox (mas preserva o valor existente) def toggle_textbox(file, current_text): if file is not None: return gr.Textbox(value=current_text or "", interactive=False, placeholder='Arquivo carregado \u2014 usando arquivo anexo') return gr.Textbox(value=current_text or "", interactive=True, placeholder='Cole o c\u00f3digo aqui') json_file_input.change( fn=toggle_textbox, inputs=[json_file_input, json_text_input], outputs=[json_text_input] ) btn_gerar = gr.Button("\U0001f680 GERAR POP (.docx)", variant="primary", size="lg") btn_novo = gr.Button("\U0001f504 Novo POP \u2014 limpar c\u00f3digo", variant="secondary", size="sm") # ══════════════ COLUNA DIREITA ══════════════ with gr.Column(scale=2, min_width=280): result_info = gr.Markdown("") btn_download = gr.DownloadButton( "\U0001f4e5 Baixar POP (.docx)", visible=False, variant="primary", size="lg" ) preview_gallery = gr.Gallery( label="\u25c4 \u25ba Preview do documento", columns=1, rows=1, height=320, object_fit="contain", visible=False ) # ═══════════ EVENTOS ═══════════ def generate_preview_images(docx_path): """Converte .docx → PDF → imagens para preview.""" import subprocess, glob try: # Converter docx → pdf via LibreOffice tmp_dir = tempfile.mkdtemp() subprocess.run( ["libreoffice", "--headless", "--convert-to", "pdf", "--outdir", tmp_dir, docx_path], capture_output=True, timeout=30 ) pdf_files = glob.glob(os.path.join(tmp_dir, "*.pdf")) if not pdf_files: return [] pdf_path = pdf_files[0] # Converter pdf → imagens via pdftoppm subprocess.run( ["pdftoppm", "-jpeg", "-r", "150", pdf_path, os.path.join(tmp_dir, "page")], capture_output=True, timeout=30 ) imgs = sorted(glob.glob(os.path.join(tmp_dir, "page-*.jpg"))) return imgs if imgs else [] except Exception: return [] def on_generate(json_text, json_file, cpri, csec, cter, czeb, logo): fp, info, meta = processar(json_text, json_file, cpri, csec, cter, czeb, logo) if fp: fname = os.path.basename(fp) msg = f"\u2705 POP gerado com sucesso!\n\n`{fname}`" imgs = generate_preview_images(fp) if imgs: return (msg, gr.DownloadButton(value=fp, visible=True), gr.Gallery(value=imgs, visible=True), gr.Button("\U0001f680 GERAR POP (.docx)", interactive=True)) return (msg, gr.DownloadButton(value=fp, visible=True), gr.Gallery(visible=False), gr.Button("\U0001f680 GERAR POP (.docx)", interactive=True)) return (info or "\u26a0\ufe0f Erro", gr.DownloadButton(visible=False), gr.Gallery(visible=False), gr.Button("\U0001f680 GERAR POP (.docx)", interactive=True)) def on_start_generate(): return ("\u23f3 *Redigindo o POP, aguarde...*", gr.DownloadButton(visible=False), gr.Gallery(visible=False), gr.Button("\u23f3 Redigindo...", variant="secondary", interactive=False)) btn_gerar.click( fn=on_start_generate, outputs=[result_info, btn_download, preview_gallery, btn_gerar] ).then( fn=on_generate, inputs=[json_text_input, json_file_input, color_pri, color_sec, color_ter, color_zeb, logo_input], outputs=[result_info, btn_download, preview_gallery, btn_gerar] ) def on_novo(): return "", None, "", gr.DownloadButton(visible=False), gr.Gallery(visible=False) btn_novo.click( fn=on_novo, outputs=[json_text_input, json_file_input, result_info, btn_download, preview_gallery] ) # Reset cores para padrão def on_reset_colors(): p = build_palette(DEFAULT_UI_COLOR) return f"#{p['primary']}", "", "", "" btn_reset_colors.click( fn=on_reset_colors, outputs=[color_pri, color_sec, color_ter, color_zeb], js=f"""() => {{ var p = '{init_pal["primary"]}', s = '{init_pal["secondary"]}', t = '{init_pal["tertiary"]}', z = '{init_pal["quaternary"]}'; var d = '{init_pal["primary_dark"]}', ts = '{init_pal["text_on_secondary"]}'; document.getElementById('cbox-pri').style.background='#'+p; document.getElementById('cbox-pri').querySelector('span').innerHTML='Prim\\u00e1ria
#'+p; document.getElementById('cpick-pri').value='#'+p; document.getElementById('cbox-sec').style.background='#'+s; document.getElementById('cbox-sec').querySelector('span').innerHTML='Secund\\u00e1ria
#'+s; document.getElementById('cpick-sec').value='#'+s; document.getElementById('cbox-ter').style.background='#'+t; document.getElementById('cbox-ter').querySelector('span').innerHTML='Terci\\u00e1ria
#'+t; document.getElementById('cpick-ter').value='#'+t; document.getElementById('cbox-zeb').style.background='#'+z; document.getElementById('cbox-zeb').querySelector('span').innerHTML='Zebra
#'+z; document.getElementById('cpick-zeb').value='#'+z; }}""" ) # ═══════════ NAVEGA\u00c7\u00c3O LANDING \u2194 APP ═══════════ # Passo 1: abre URL do Gemini Gem em nova aba btn_passo1.click( fn=None, inputs=None, outputs=None, js="() => { window.open('https://gemini.google.com/gem/c86826a9110d', '_blank'); }" ) # Passo 2: oculta landing, mostra app def show_app(): return gr.Column(visible=False), gr.Column(visible=True) btn_passo2.click(fn=show_app, outputs=[landing_page, main_app]) # Voltar: oculta app, mostra landing def show_landing(): return gr.Column(visible=True), gr.Column(visible=False) btn_voltar.click(fn=show_landing, outputs=[landing_page, main_app]) return demo, APP_CSS # ══════════════════════════════════════════════════════════════ # INICIAR APLICAÇÃO # ══════════════════════════════════════════════════════════════ demo, app_css = criar_interface() demo.launch(debug=False, show_error=True, css=app_css, theme=__import__('gradio').themes.Soft(primary_hue="blue"))