Spaces:
Running
Running
| #!/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'<w:shd {nsdecls("w")} w:fill="{color}" w:val="clear"/>')) | |
| def set_borders(cell, top=None, bottom=None, left=None, right=None): | |
| tcPr = cell._tc.get_or_add_tcPr() | |
| borders = parse_xml(f'<w:tcBorders {nsdecls("w")}></w:tcBorders>') | |
| 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'<w:{side} {nsdecls("w")} w:val="single" w:sz="{s}" w:space="0" w:color="{c}"/>')) | |
| tcPr.append(borders) | |
| def set_valign(cell, v="center"): | |
| cell._tc.get_or_add_tcPr().append(parse_xml(f'<w:vAlign {nsdecls("w")} w:val="{v}"/>')) | |
| def set_width(cell, w): | |
| cell._tc.get_or_add_tcPr().append( | |
| parse_xml(f'<w:tcW {nsdecls("w")} w:w="{w}" w:type="dxa"/>')) | |
| def set_margins(cell, t=0, b=0, l=80, r=80): | |
| cell._tc.get_or_add_tcPr().append(parse_xml( | |
| f'<w:tcMar {nsdecls("w")}><w:top w:w="{t}" w:type="dxa"/>' | |
| f'<w:left w:w="{l}" w:type="dxa"/><w:bottom w:w="{b}" w:type="dxa"/>' | |
| f'<w:right w:w="{r}" w:type="dxa"/></w:tcMar>')) | |
| 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'<w:shd {nsdecls("w")} w:fill="{highlight_color}" w:val="clear"/>')) | |
| else: | |
| # Nome Word predefinido (cyan, yellow, etc) | |
| rPr.append(parse_xml(f'<w:highlight {nsdecls("w")} w:val="{highlight_color}"/>')) | |
| 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'<w:shd {nsdecls("w")} w:fill="{color}" w:val="clear"/>')) | |
| def repeat_header(row): | |
| row._tr.get_or_add_trPr().append(parse_xml(f'<w:tblHeader {nsdecls("w")}/>')) | |
| def page_break(doc): | |
| p = doc.add_paragraph() | |
| p.add_run()._r.append(parse_xml(f'<w:br {nsdecls("w")} w:type="page"/>')) | |
| # ============================================================ | |
| # 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'<w:fldChar {nsdecls("w")} w:fldCharType="begin"/>', | |
| f'<w:instrText {nsdecls("w")} xml:space="preserve"> PAGE </w:instrText>', | |
| f'<w:fldChar {nsdecls("w")} w:fldCharType="separate"/>']: | |
| 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'<w:fldChar {nsdecls("w")} w:fldCharType="end"/>')) | |
| # " de " + NUMPAGES (total) | |
| fmt(pp, " de ", size=TAM_TINY, color="888888") | |
| for x in [f'<w:fldChar {nsdecls("w")} w:fldCharType="begin"/>', | |
| f'<w:instrText {nsdecls("w")} xml:space="preserve"> NUMPAGES </w:instrText>', | |
| f'<w:fldChar {nsdecls("w")} w:fldCharType="separate"/>']: | |
| 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'<w:fldChar {nsdecls("w")} w:fldCharType="end"/>')) | |
| 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'<w:trHeight {nsdecls("w")} w:val="{height_dxa}" w:hRule="atLeast"/>')) | |
| 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'<w:shd {nsdecls("w")} w:fill="{pal["primary"]}" w:val="clear"/>') | |
| 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'<w:shd {nsdecls("w")} w:fill="{pal["tertiary"]}" w:val="clear"/>') | |
| 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'<w:shd {nsdecls("w")} w:fill="{pal["secondary"]}" w:val="clear"/>') | |
| 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(""" | |
| <div style="text-align:center; margin-top:50px;"> | |
| <img src="data:image/jpeg;base64,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0t/HPwPpUFjbfa7gL0HVj6Cl1C5FzcfLwijao9qS0VzOWsuUgoooqDQKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAuWMouYDbOepzGfQ1VljMMhVhgg4NIDtNXSP7Wgz/y8IOR/fFX8St1M/hd+hRqWyvZNPuVliba6nioyMGkqU2ndGh1Wkaq8M7X+nKpZlxd2T/cmXvx/hyO1bNu1vrVo91p5Zkj5mgf/W2319V/2h+OK4C1upLKdZImKOvQityw1NNRukngl/s7VEORIhwshr1MNi7rlf3f5f5bMxqUlLU3xS5qCHxHFLIIdUi/s27PSdFzBL7kD7v1XI9hV240+W3hWXAeB/uSxsHjf6MOK9CNpK8df09exyuDi9SHNJmjNGaQBmjOaM5pRQMBSigUZqywzRmjNT2+nS3MLS4WOBPvzSMEjT6seKaTeiAgqS6e30KzS61AsscnMMCf625+nov+0fwyaqS+JIYZTDpcX9pXfed1xBF7hT976tgexrFv9UTTrt7ieY6jqj8tIx3JEf64/T2rGpXhBXWvn0/4L8lp3ZrGHctavqzzzJqGpKoZRizsV4SFe3H+PJPWubvr6TUbpppW3O55PpTbq7kvZzJK5d26k1HXi167qPy/P1/rQ2SsFKBuOKSr1vGNOhE8gzIf9Wp/mawirilKyC4P9m2fkj/Wy8yf7I9Ko06SQyuWJySck02iTuEY2WoUUUUigooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAp0chicMpwR0NNooAvvGurRl0ws6jLL/AHvcVRIwaVJDE4ZSQR0Iq4JI9WGHxFcdm/hf61fxepnrD0KNFSXFs9rJtdSD/Oo6jY0TvqjRsfEcsEPkzqt1b/3JOo+h7Vp6Pfi0lMmk6jLp8r/ehlb5H9j2I9iDXN0V0QxM42vrb7/kxWO4fxRPEP8AiZaQr/8ATezby8+5HK/kBT4PEGjXh4v5rQntc27cfihb+QrjbPWLqw/1M8iD0zx+XSro8WSSj/SLe1uPdo/m/Ou2OPvv+K/VWZm6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/wARXLxrywSI/Mpx/E4UAZPOMV9Yal/wU2+EljpvnxaxqV5LjP2aHTJhJ06ZdVT/AMer5h/a2/4KMan8dNIn8PeGrS40Dw5cfLcvK4+2X6/3W28Ih7qCc92xxXn43IeEcsTxMsW8Q1tTi07vopOOy77foethsxzrF/u/Yqn3k09PRPr959o/soePbH4hfCRbzSIlt9Dtb64sNLiVcbLWB/Ki/EqoJz618N/8FP8A/k66+/7B1r/6Aa6/9i39vjwp+zt8Gv8AhHNe07xHdXa381yj2FvA8YjcLgEvKhzkN27jmvHf2x/jhpX7QvxtufEmi2+o21jNawwBL2NElDIuDwjMMfjXfxdxPgsw4Ww9CNVOteDlFdLJ3+SHlGVVcNmVSbi+TWz+aPvr9gA/8YieD/8ArjP/AOlEtfDn/BQ7/k7vxX9bb/0nir2X9mH/AIKLeCfgr8C9B8Mappfime/0uORZZLS2t2hYtK7jaWmU9GHUCvnL9qn4tad8cfjprXifSYb23sNS8nyo7tFSZdkSIdwVmHVT0JqOMs+y/FcL4PB4eqpVIez5ordWptP7nod2WYKrTxtSrONk7/meeUUUV+NH0YUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQB+3v/BmD/yOX7Qn/XloH/oeoV7R/wAHjX/JkHwv/wCx5H/pBdV+V/8AwRz/AOCx17/wSM1jx/d2fgC18df8J3DYQus2sNp/2P7K1wQQRDJv3eefTG3vnjsv+Cvf/BevUP8AgrH8FPDPg27+GVn4HTw3rf8AbK3cOutqBuD5EsPllDBHt/1mc5PTpzQV0Pz4ooooJCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Z" alt="radioterapia.ai" | |
| id="rt-landing-logo" style="height:130px; border-radius:12px; display:block; margin:0 auto;"> | |
| <div id="rt-pop-sub" style="margin-top:16px; font-size:1.3em; font-weight:600; letter-spacing:3px;">\u2014 POP de Elite \u2014</div> | |
| <div style="margin-top:8px; font-size:0.9em; opacity:0.55;">por: Braga, HF.</div> | |
| </div> | |
| <style> | |
| #rt-landing-logo { filter: invert(0.88) hue-rotate(180deg) brightness(1.5) contrast(0.85); } | |
| #rt-pop-sub { color: #283264; } | |
| .dark #rt-landing-logo { filter: none; } | |
| .dark #rt-pop-sub { color: #9ab4d2; } | |
| </style> | |
| """) | |
| gr.HTML("<div style='height:40px;'></div>") | |
| 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("""<div> | |
| <img src="data:image/jpeg;base64,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0t/HPwPpUFjbfa7gL0HVj6Cl1C5FzcfLwijao9qS0VzOWsuUgoooqDQKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAuWMouYDbOepzGfQ1VljMMhVhgg4NIDtNXSP7Wgz/y8IOR/fFX8St1M/hd+hRqWyvZNPuVliba6nioyMGkqU2ndGh1Wkaq8M7X+nKpZlxd2T/cmXvx/hyO1bNu1vrVo91p5Zkj5mgf/W2319V/2h+OK4C1upLKdZImKOvQityw1NNRukngl/s7VEORIhwshr1MNi7rlf3f5f5bMxqUlLU3xS5qCHxHFLIIdUi/s27PSdFzBL7kD7v1XI9hV240+W3hWXAeB/uSxsHjf6MOK9CNpK8df09exyuDi9SHNJmjNGaQBmjOaM5pRQMBSigUZqywzRmjNT2+nS3MLS4WOBPvzSMEjT6seKaTeiAgqS6e30KzS61AsscnMMCf625+nov+0fwyaqS+JIYZTDpcX9pXfed1xBF7hT976tgexrFv9UTTrt7ieY6jqj8tIx3JEf64/T2rGpXhBXWvn0/4L8lp3ZrGHctavqzzzJqGpKoZRizsV4SFe3H+PJPWubvr6TUbpppW3O55PpTbq7kvZzJK5d26k1HXi167qPy/P1/rQ2SsFKBuOKSr1vGNOhE8gzIf9Wp/mawirilKyC4P9m2fkj/Wy8yf7I9Ko06SQyuWJySck02iTuEY2WoUUUUigooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAp0chicMpwR0NNooAvvGurRl0ws6jLL/AHvcVRIwaVJDE4ZSQR0Iq4JI9WGHxFcdm/hf61fxepnrD0KNFSXFs9rJtdSD/Oo6jY0TvqjRsfEcsEPkzqt1b/3JOo+h7Vp6Pfi0lMmk6jLp8r/ehlb5H9j2I9iDXN0V0QxM42vrb7/kxWO4fxRPEP8AiZaQr/8ATezby8+5HK/kBT4PEGjXh4v5rQntc27cfihb+QrjbPWLqw/1M8iD0zx+XSro8WSSj/SLe1uPdo/m/Ou2OPvv+K/VWZm6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/wARXLxrywSI/Mpx/E4UAZPOMV9Yal/wU2+EljpvnxaxqV5LjP2aHTJhJ06ZdVT/AMer5h/a2/4KMan8dNIn8PeGrS40Dw5cfLcvK4+2X6/3W28Ih7qCc92xxXn43IeEcsTxMsW8Q1tTi07vopOOy77foethsxzrF/u/Yqn3k09PRPr959o/soePbH4hfCRbzSIlt9Dtb64sNLiVcbLWB/Ki/EqoJz618N/8FP8A/k66+/7B1r/6Aa6/9i39vjwp+zt8Gv8AhHNe07xHdXa381yj2FvA8YjcLgEvKhzkN27jmvHf2x/jhpX7QvxtufEmi2+o21jNawwBL2NElDIuDwjMMfjXfxdxPgsw4Ww9CNVOteDlFdLJ3+SHlGVVcNmVSbi+TWz+aPvr9gA/8YieD/8ArjP/AOlEtfDn/BQ7/k7vxX9bb/0nir2X9mH/AIKLeCfgr8C9B8Mappfime/0uORZZLS2t2hYtK7jaWmU9GHUCvnL9qn4tad8cfjprXifSYb23sNS8nyo7tFSZdkSIdwVmHVT0JqOMs+y/FcL4PB4eqpVIez5ordWptP7nod2WYKrTxtSrONk7/meeUUUV+NH0YUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQB+3v/BmD/yOX7Qn/XloH/oeoV7R/wAHjX/JkHwv/wCx5H/pBdV+V/8AwRz/AOCx17/wSM1jx/d2fgC18df8J3DYQus2sNp/2P7K1wQQRDJv3eefTG3vnjsv+Cvf/BevUP8AgrH8FPDPg27+GVn4HTw3rf8AbK3cOutqBuD5EsPllDBHt/1mc5PTpzQV0Pz4ooooJCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Z" alt="radioterapia.ai" | |
| id="rt-hdr-logo" style="height:40px; border-radius:6px;"> | |
| <br> | |
| <div id="rt-hdr-sub" style="font-size:0.9em; font-weight:600; letter-spacing:1px; margin-top:4px;">\u2014 POP de Elite \u2014</div> | |
| <div style="font-size:0.75em; opacity:0.5; margin-top:2px;">por: Braga, HF.</div> | |
| </div> | |
| <style> | |
| #rt-hdr-logo { filter: invert(0.88) hue-rotate(180deg) brightness(1.5) contrast(0.85); } | |
| #rt-hdr-sub { color: #283264; } | |
| .dark #rt-hdr-logo { filter: none; } | |
| .dark #rt-hdr-sub { color: #9ab4d2; } | |
| </style>""") | |
| 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""" | |
| <div style="display:flex; gap:6px; height:50px; position:relative;" id="color-boxes"> | |
| <input type="color" id="cpick-pri" value="#{init_pal['primary']}" | |
| style="position:absolute;opacity:0;width:0;height:0;" | |
| oninput=" | |
| var v=this.value, hex=v.replace('#',''); | |
| document.getElementById('cbox-pri').style.background=v; | |
| document.getElementById('cbox-pri').querySelector('span').innerHTML='Prim\\u00e1ria<br>'+v.toUpperCase(); | |
| var t=document.querySelector('#pop-hex-pri textarea')||document.querySelector('#pop-hex-pri input'); | |
| if(t){{t.value=v;t.dispatchEvent(new Event('input',{{bubbles:true}}));}} | |
| var r=parseInt(hex.substr(0,2),16),g=parseInt(hex.substr(2,2),16),b=parseInt(hex.substr(4,2),16); | |
| function mx(c,p){{return Math.min(255,Math.max(0,Math.round(c+(255-c)*p)));}} | |
| function th(r,g,b){{return '#'+[r,g,b].map(x=>x.toString(16).padStart(2,'0')).join('').toUpperCase();}} | |
| var cs=th(mx(r,.45),mx(g,.45),mx(b,.45)); | |
| var ct=th(mx(r,.70),mx(g,.70),mx(b,.70)); | |
| var cz=th(mx(r,.85),mx(g,.85),mx(b,.85)); | |
| [['sec',cs,'Secund\\u00e1ria'],['ter',ct,'Terci\\u00e1ria'],['zeb',cz,'Zebra']].forEach(function(x){{ | |
| document.getElementById('cbox-'+x[0]).style.background=x[1]; | |
| document.getElementById('cbox-'+x[0]).querySelector('span').innerHTML=x[2]+'<br>'+x[1]; | |
| document.getElementById('cpick-'+x[0]).value=x[1]; | |
| var t2=document.querySelector('#pop-hex-'+x[0]+' textarea')||document.querySelector('#pop-hex-'+x[0]+' input'); | |
| if(t2){{t2.value=x[1];t2.dispatchEvent(new Event('input',{{bubbles:true}}));}} | |
| }}); | |
| "> | |
| <div id="cbox-pri" onclick="document.getElementById('cpick-pri').click();" | |
| style="flex:1;background:#{init_pal['primary']};border-radius:6px;cursor:pointer; | |
| display:flex;align-items:center;justify-content:center;border:2px solid #444;"> | |
| <span style="color:#fff;font-size:9px;font-weight:bold;text-align:center;pointer-events:none;"> | |
| Prim\u00e1ria<br>#{init_pal['primary']}</span></div> | |
| <input type="color" id="cpick-sec" value="#{init_pal['secondary']}" | |
| style="position:absolute;opacity:0;width:0;height:0;" | |
| oninput="var v=this.value;document.getElementById('cbox-sec').style.background=v; | |
| document.getElementById('cbox-sec').querySelector('span').innerHTML='Secund\\u00e1ria<br>'+v.toUpperCase(); | |
| var t=document.querySelector('#pop-hex-sec textarea')||document.querySelector('#pop-hex-sec input'); | |
| if(t){{t.value=v;t.dispatchEvent(new Event('input',{{bubbles:true}}));}}"> | |
| <div id="cbox-sec" onclick="document.getElementById('cpick-sec').click();" | |
| style="flex:1;background:#{init_pal['secondary']};border-radius:6px;cursor:pointer; | |
| display:flex;align-items:center;justify-content:center;"> | |
| <span style="color:#{init_pal['text_on_secondary']};font-size:9px;font-weight:bold;text-align:center;pointer-events:none;"> | |
| Secund\u00e1ria<br>#{init_pal['secondary']}</span></div> | |
| <input type="color" id="cpick-ter" value="#{init_pal['tertiary']}" | |
| style="position:absolute;opacity:0;width:0;height:0;" | |
| oninput="var v=this.value;document.getElementById('cbox-ter').style.background=v; | |
| document.getElementById('cbox-ter').querySelector('span').innerHTML='Terci\\u00e1ria<br>'+v.toUpperCase(); | |
| var t=document.querySelector('#pop-hex-ter textarea')||document.querySelector('#pop-hex-ter input'); | |
| if(t){{t.value=v;t.dispatchEvent(new Event('input',{{bubbles:true}}));}}"> | |
| <div id="cbox-ter" onclick="document.getElementById('cpick-ter').click();" | |
| style="flex:1;background:#{init_pal['tertiary']};border-radius:6px;cursor:pointer; | |
| display:flex;align-items:center;justify-content:center;"> | |
| <span style="color:#{init_pal['primary_dark']};font-size:9px;font-weight:bold;text-align:center;pointer-events:none;"> | |
| Terci\u00e1ria<br>#{init_pal['tertiary']}</span></div> | |
| <input type="color" id="cpick-zeb" value="#{init_pal['quaternary']}" | |
| style="position:absolute;opacity:0;width:0;height:0;" | |
| oninput="var v=this.value;document.getElementById('cbox-zeb').style.background=v; | |
| document.getElementById('cbox-zeb').querySelector('span').innerHTML='Zebra<br>'+v.toUpperCase(); | |
| var t=document.querySelector('#pop-hex-zeb textarea')||document.querySelector('#pop-hex-zeb input'); | |
| if(t){{t.value=v;t.dispatchEvent(new Event('input',{{bubbles:true}}));}}"> | |
| <div id="cbox-zeb" onclick="document.getElementById('cpick-zeb').click();" | |
| style="flex:1;background:#{init_pal['quaternary']};border-radius:6px;cursor:pointer; | |
| display:flex;align-items:center;justify-content:center;"> | |
| <span style="color:#{init_pal['primary_dark']};font-size:9px;text-align:center;pointer-events:none;"> | |
| Zebra<br>#{init_pal['quaternary']}</span></div> | |
| </div> | |
| """) | |
| 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('<div style="display:flex;align-items:center;justify-content:center;height:100%;font-weight:700;color:#666;font-size:11px;">OU</div>') | |
| 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<br>#'+p; | |
| document.getElementById('cpick-pri').value='#'+p; | |
| document.getElementById('cbox-sec').style.background='#'+s; | |
| document.getElementById('cbox-sec').querySelector('span').innerHTML='Secund\\u00e1ria<br>#'+s; | |
| document.getElementById('cpick-sec').value='#'+s; | |
| document.getElementById('cbox-ter').style.background='#'+t; | |
| document.getElementById('cbox-ter').querySelector('span').innerHTML='Terci\\u00e1ria<br>#'+t; | |
| document.getElementById('cpick-ter').value='#'+t; | |
| document.getElementById('cbox-zeb').style.background='#'+z; | |
| document.getElementById('cbox-zeb').querySelector('span').innerHTML='Zebra<br>#'+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")) |