Spaces:
Sleeping
Sleeping
| # ============================================================================ | |
| # DOCLING ENGINE — RADIOTERAPIA.AI | |
| # Hugging Face Spaces · Gradio · Motor de Transcrição de Documentos | |
| # ============================================================================ | |
| import os | |
| import time | |
| import base64 | |
| import hashlib | |
| import tempfile | |
| from pathlib import Path | |
| import gradio as gr | |
| try: | |
| from docling.document_converter import DocumentConverter | |
| DOCLING_READY = True | |
| except ImportError: | |
| DOCLING_READY = False | |
| # ============================================================================ | |
| # MOTOR DE PROCESSAMENTO | |
| # ============================================================================ | |
| class DoclingAppProcessor: | |
| """Motor Docling com deduplicação SHA-256 e conformidade LGPD.""" | |
| def __init__(self): | |
| self.processed_hashes = set() | |
| self._converter = None | |
| def converter(self): | |
| # Lazy-load: o DocumentConverter inicializa modelos pesados. | |
| # Carregamos só na primeira chamada de processamento. | |
| if self._converter is None and DOCLING_READY: | |
| self._converter = DocumentConverter() | |
| return self._converter | |
| def compute_hash(self, file_path): | |
| hasher = hashlib.sha256() | |
| with open(file_path, "rb") as f: | |
| for chunk in iter(lambda: f.read(4096), b""): | |
| hasher.update(chunk) | |
| return hasher.hexdigest() | |
| def calculate_confidence(self, text, ext): | |
| if not text or len(text.strip()) < 10: | |
| return 0.0 | |
| base = 95.0 if ext == ".pdf" else 75.0 | |
| words = len(text.split()) | |
| if words < 30: | |
| base *= 0.8 | |
| return min(100.0, base) | |
| def process_batch(self, files): | |
| if not DOCLING_READY: | |
| return "", "❌ Sistema indisponível. Verifique a instalação do Docling no ambiente." | |
| if not files: | |
| return "", "⚠️ Nenhum arquivo foi enviado para processamento." | |
| start_time = time.time() | |
| markdown_outputs = [] | |
| logs = [] | |
| for f in files: | |
| path = f.name if hasattr(f, "name") else f | |
| name = Path(path).name | |
| ext = Path(path).suffix.lower() | |
| if time.time() - start_time > 100: | |
| logs.append("⏱️ Limite de tempo de 100s atingido. Processamento interrompido.") | |
| break | |
| try: | |
| f_hash = self.compute_hash(path) | |
| except Exception: | |
| logs.append(f"❌ Falha ao ler {name} — Erro de acesso ao arquivo ⚠️") | |
| continue | |
| if f_hash in self.processed_hashes: | |
| logs.append(f"⚠️ {name} — Informação duplicada detectada (conteúdo idêntico ignorado).") | |
| continue | |
| try: | |
| result = self.converter.convert(path) | |
| md_content = result.document.export_to_markdown() | |
| if md_content.strip(): | |
| conf_score = self.calculate_confidence(md_content, ext) | |
| self.processed_hashes.add(f_hash) | |
| markdown_outputs.append(f"## 📄 {name}\n\n{md_content}\n\n---\n") | |
| logs.append(f"✅ {name} processado com sucesso 📄 | Confiança: {conf_score:.1f}%") | |
| else: | |
| logs.append(f"⚠️ Falha ao ler {name} — Conteúdo vazio ou ilegível ⚠️") | |
| except Exception: | |
| logs.append(f"❌ Falha ao ler {name} — Erro no processamento do arquivo ⚠️") | |
| finally: | |
| # Conformidade LGPD: exclusão imediata do arquivo temporário. | |
| try: | |
| if os.path.exists(path): | |
| os.remove(path) | |
| except Exception: | |
| pass | |
| elapsed_time = time.time() - start_time | |
| total_md = "\n".join(markdown_outputs) if markdown_outputs else "*Nenhum conteúdo inédito extraído.*" | |
| log_header = f"⏱️ Tempo de Execução: {elapsed_time:.2f}s | Arquivos Inéditos: {len(markdown_outputs)}\n" | |
| total_logs = ( | |
| log_header | |
| + "\n".join(logs) | |
| + "\n🔒 Arquivos temporários excluídos. Conformidade LGPD garantida 🔒" | |
| ) | |
| return total_md, total_logs | |
| def clear_session(self): | |
| self.processed_hashes.clear() | |
| return None, "", "🔒 Histórico e cache de dados limpos com sucesso." | |
| engine = DoclingAppProcessor() | |
| # ============================================================================ | |
| # CSS — LAYOUT CLAUDE DESIGN (radioterapia.ai) | |
| # ============================================================================ | |
| CSS_THEME = """ | |
| /* -------- Fontes -------- */ | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap'); | |
| /* -------- Tokens -------- */ | |
| :root, .dark { | |
| --bg-0: #060d1a; | |
| --bg-1: #0a1426; | |
| --bg-2: #0f1729; | |
| --bg-3: #16213a; | |
| --panel: #0c1322; | |
| --panel-2: #0f1729; | |
| --line: #1e293b; | |
| --line-strong: #2a3a55; | |
| --text: #e6edf7; | |
| --text-muted: #a3b1c6; | |
| --text-dim: #6b7a92; | |
| --accent: #4DA3FF; | |
| --accent-2: #8B9EE6; | |
| --accent-soft: rgba(77, 163, 255, 0.10); | |
| --accent-line: rgba(77, 163, 255, 0.35); | |
| --success: #5BC57F; | |
| --warn: #E1B23B; | |
| --error: #E96A5C; | |
| --r-md: 10px; | |
| --r-lg: 14px; | |
| --r-xl: 18px; | |
| --font-display: 'Inter', system-ui, sans-serif; | |
| --font-mono: 'JetBrains Mono', ui-monospace, monospace; | |
| --shadow-lift: 0 8px 32px -12px rgba(0, 0, 0, 0.5); | |
| } | |
| /* -------- Reset / base -------- */ | |
| html, body, .gradio-container { | |
| background-color: var(--bg-0) !important; | |
| color: var(--text) !important; | |
| font-family: var(--font-display) !important; | |
| } | |
| .gradio-container { | |
| max-width: 1320px !important; | |
| margin: 0 auto !important; | |
| padding: 0 !important; | |
| } | |
| footer { display: none !important; } | |
| /* Remove Gradio default backgrounds on blocks */ | |
| .gr-block, .gr-form, .gr-box, .gr-panel { | |
| background: transparent !important; | |
| border: none !important; | |
| } | |
| /* -------- Top bar -------- */ | |
| .rt-topbar { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 20px 36px; | |
| border-bottom: 1px solid var(--line); | |
| background: rgba(6, 13, 26, 0.7); | |
| backdrop-filter: blur(10px); | |
| position: sticky; | |
| top: 0; | |
| z-index: 10; | |
| } | |
| .rt-logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| font-family: var(--font-display); | |
| font-size: 15px; | |
| font-weight: 600; | |
| letter-spacing: -0.01em; | |
| color: var(--text); | |
| } | |
| .rt-logo .ai { color: var(--accent); } | |
| .rt-logo svg { display: block; } | |
| .rt-status-pill { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 12px; | |
| color: var(--text-muted); | |
| padding: 6px 12px; | |
| border-radius: 999px; | |
| border: 1px solid var(--line); | |
| background: var(--panel); | |
| } | |
| .rt-status-dot { | |
| width: 6px; height: 6px; | |
| border-radius: 50%; | |
| background: var(--success); | |
| box-shadow: 0 0 0 3px rgba(91, 197, 127, 0.25); | |
| } | |
| /* -------- Hero -------- */ | |
| .rt-hero-row { | |
| padding: 40px 36px 28px !important; | |
| max-width: 1320px; | |
| margin: 0 auto !important; | |
| align-items: center !important; | |
| gap: 36px !important; | |
| } | |
| .rt-hero-text { | |
| padding: 4px 0; | |
| } | |
| .rt-hero-image img { | |
| border-radius: var(--r-xl) !important; | |
| border: 1px solid var(--line-strong) !important; | |
| background: var(--panel) !important; | |
| box-shadow: var(--shadow-lift) !important; | |
| width: 100% !important; | |
| height: auto !important; | |
| display: block !important; | |
| } | |
| .rt-hero-image-placeholder { | |
| border-radius: var(--r-xl); | |
| border: 1px dashed var(--line-strong); | |
| background: var(--panel-2); | |
| box-shadow: var(--shadow-lift); | |
| padding: 60px 24px; | |
| text-align: center; | |
| color: var(--text-dim); | |
| font-family: var(--font-mono); | |
| font-size: 13px; | |
| letter-spacing: 0.05em; | |
| text-transform: uppercase; | |
| } | |
| .rt-eyebrow { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 6px 12px; | |
| border-radius: 999px; | |
| border: 1px solid var(--accent-line); | |
| background: var(--accent-soft); | |
| color: var(--accent); | |
| font-size: 12px; | |
| font-weight: 500; | |
| letter-spacing: 0.04em; | |
| text-transform: uppercase; | |
| margin-bottom: 20px; | |
| } | |
| .rt-h1 { | |
| margin: 0; | |
| font-family: var(--font-display); | |
| font-size: clamp(44px, 5.4vw, 72px); | |
| font-weight: 600; | |
| line-height: 1.02; | |
| letter-spacing: -0.035em; | |
| color: var(--text); | |
| } | |
| .rt-sub { | |
| margin: 14px 0 0; | |
| font-size: clamp(17px, 1.6vw, 20px); | |
| color: var(--text-muted); | |
| line-height: 1.45; | |
| max-width: 56ch; | |
| } | |
| .rt-features { | |
| display: flex; | |
| gap: 24px; | |
| margin-top: 28px; | |
| flex-wrap: wrap; | |
| } | |
| .rt-feature { | |
| padding-left: 14px; | |
| border-left: 2px solid var(--accent-line); | |
| } | |
| .rt-feature-k { | |
| font-size: 13px; | |
| color: var(--text); | |
| font-weight: 500; | |
| font-family: var(--font-mono); | |
| } | |
| .rt-feature-v { | |
| font-size: 12px; | |
| color: var(--text-dim); | |
| margin-top: 2px; | |
| } | |
| /* -------- Section headers -------- */ | |
| .rt-panel-head { | |
| margin-bottom: 14px; | |
| } | |
| .rt-panel-eyebrow { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| font-size: 11px; | |
| font-family: var(--font-mono); | |
| letter-spacing: 0.12em; | |
| text-transform: uppercase; | |
| color: var(--text-dim); | |
| margin-bottom: 8px; | |
| } | |
| .rt-panel-eyebrow .num { color: var(--accent); } | |
| .rt-panel-eyebrow .bar { height: 1px; width: 40px; background: var(--accent-line); } | |
| .rt-panel-title { | |
| margin: 0 0 4px; | |
| font-family: var(--font-display); | |
| font-size: 22px; | |
| font-weight: 600; | |
| letter-spacing: -0.015em; | |
| color: var(--text); | |
| } | |
| .rt-panel-sub { | |
| font-size: 13px; | |
| color: var(--text-muted); | |
| } | |
| /* -------- Panels / Cards -------- */ | |
| .rt-panel { | |
| border-radius: var(--r-xl) !important; | |
| border: 1px solid var(--line) !important; | |
| background: var(--panel) !important; | |
| padding: 24px !important; | |
| box-shadow: var(--shadow-lift) !important; | |
| } | |
| /* -------- File upload (Gradio) -------- */ | |
| .gr-file, .gr-file-upload, [data-testid="file"] { | |
| background: var(--panel-2) !important; | |
| border: 1.5px dashed var(--line-strong) !important; | |
| border-radius: var(--r-lg) !important; | |
| color: var(--text-muted) !important; | |
| transition: all 0.2s ease; | |
| } | |
| .gr-file:hover, [data-testid="file"]:hover { | |
| border-color: var(--accent) !important; | |
| background: var(--accent-soft) !important; | |
| } | |
| .gr-file label, [data-testid="file"] label { | |
| color: var(--text) !important; | |
| font-family: var(--font-display) !important; | |
| } | |
| /* -------- Textareas / inputs -------- */ | |
| textarea, input[type="text"], .gr-textbox textarea { | |
| background: var(--panel-2) !important; | |
| color: var(--text) !important; | |
| border: 1px solid var(--line) !important; | |
| border-radius: var(--r-md) !important; | |
| font-family: var(--font-mono) !important; | |
| font-size: 13px !important; | |
| line-height: 1.6 !important; | |
| } | |
| textarea:focus, .gr-textbox textarea:focus { | |
| border-color: var(--accent) !important; | |
| outline: none !important; | |
| box-shadow: 0 0 0 3px var(--accent-soft) !important; | |
| } | |
| label, .gr-textbox label, .gr-form label { | |
| color: var(--text-muted) !important; | |
| font-family: var(--font-display) !important; | |
| font-size: 12px !important; | |
| font-weight: 500 !important; | |
| letter-spacing: 0.04em !important; | |
| text-transform: uppercase !important; | |
| } | |
| /* -------- Buttons -------- */ | |
| button.gr-button, .gr-button { | |
| font-family: var(--font-display) !important; | |
| font-weight: 600 !important; | |
| border-radius: var(--r-md) !important; | |
| transition: all 0.2s ease !important; | |
| letter-spacing: -0.005em !important; | |
| } | |
| button.gr-button-primary, .gr-button-primary { | |
| background: var(--accent) !important; | |
| color: white !important; | |
| border: none !important; | |
| box-shadow: 0 8px 24px -8px rgba(77, 163, 255, 0.5) !important; | |
| } | |
| button.gr-button-primary:hover, .gr-button-primary:hover { | |
| background: #6BB4FF !important; | |
| transform: translateY(-1px); | |
| } | |
| button.gr-button-secondary, .gr-button-secondary { | |
| background: transparent !important; | |
| color: var(--text-muted) !important; | |
| border: 1px solid var(--line-strong) !important; | |
| } | |
| button.gr-button-secondary:hover, .gr-button-secondary:hover { | |
| color: var(--text) !important; | |
| border-color: var(--accent-line) !important; | |
| background: var(--accent-soft) !important; | |
| } | |
| button.gr-button-stop, .gr-button-stop { | |
| background: transparent !important; | |
| color: var(--text-muted) !important; | |
| border: 1px solid var(--line-strong) !important; | |
| } | |
| button.gr-button-stop:hover, .gr-button-stop:hover { | |
| color: var(--error) !important; | |
| border-color: var(--error) !important; | |
| } | |
| /* -------- Headings inside markdown blocks -------- */ | |
| .gradio-container h1, | |
| .gradio-container h2, | |
| .gradio-container h3, | |
| .gradio-container h4 { | |
| color: var(--text) !important; | |
| font-family: var(--font-display) !important; | |
| font-weight: 600 !important; | |
| letter-spacing: -0.015em !important; | |
| } | |
| .gradio-container h4 { | |
| font-size: 13px !important; | |
| color: var(--text-muted) !important; | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em !important; | |
| margin-bottom: 8px !important; | |
| } | |
| /* -------- Accordion -------- */ | |
| .gr-accordion, [data-testid="accordion"] { | |
| background: var(--panel) !important; | |
| border: 1px solid var(--line) !important; | |
| border-radius: var(--r-lg) !important; | |
| overflow: hidden; | |
| } | |
| .gr-accordion > button, [data-testid="accordion"] > button { | |
| color: var(--text) !important; | |
| font-family: var(--font-display) !important; | |
| font-weight: 500 !important; | |
| background: transparent !important; | |
| } | |
| /* -------- File pills row -------- */ | |
| .rt-pills { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| margin-top: 12px; | |
| } | |
| .rt-pill { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 6px 10px 6px 6px; | |
| border-radius: 999px; | |
| border: 1px solid var(--line); | |
| background: var(--panel-2); | |
| font-size: 12px; | |
| color: var(--text-muted); | |
| } | |
| .rt-pill-badge { | |
| padding: 2px 6px; | |
| border-radius: 6px; | |
| color: white; | |
| font-size: 9px; | |
| font-weight: 700; | |
| font-family: var(--font-mono); | |
| letter-spacing: 0.04em; | |
| } | |
| /* -------- Footer -------- */ | |
| .rt-footer { | |
| margin-top: 32px; | |
| padding: 20px 36px; | |
| border-top: 1px solid var(--line); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 16px; | |
| flex-wrap: wrap; | |
| font-size: 12px; | |
| color: var(--text-dim); | |
| font-family: var(--font-mono); | |
| letter-spacing: 0.04em; | |
| } | |
| /* -------- File download component -------- */ | |
| .gr-file-output { | |
| background: var(--panel-2) !important; | |
| border: 1px solid var(--line) !important; | |
| border-radius: var(--r-md) !important; | |
| } | |
| """ | |
| # ============================================================================ | |
| # COMPONENTES HTML — TOP BAR, HERO, FOOTER | |
| # ============================================================================ | |
| def image_to_data_uri(path: str): | |
| """Lê uma imagem do disco e devolve uma data URI base64 (ou None).""" | |
| if not os.path.exists(path): | |
| return None | |
| ext = path.lower().rsplit(".", 1)[-1] | |
| mime = { | |
| "png": "image/png", | |
| "jpg": "image/jpeg", | |
| "jpeg": "image/jpeg", | |
| "svg": "image/svg+xml", | |
| "webp": "image/webp", | |
| "gif": "image/gif", | |
| }.get(ext, "image/png") | |
| with open(path, "rb") as fh: | |
| b64 = base64.b64encode(fh.read()).decode("ascii") | |
| return f"data:{mime};base64,{b64}" | |
| # Logo dinâmico: usa logo.png se existir na raiz, senão usa o SVG inline. | |
| LOGO_FILE = "logo.png" | |
| _logo_uri = image_to_data_uri(LOGO_FILE) | |
| if _logo_uri: | |
| LOGO_BLOCK_HTML = ( | |
| f'<img src="{_logo_uri}" alt="radioterapia.ai" ' | |
| f'style="height: 28px; width: auto; display: block;" />' | |
| ) | |
| else: | |
| # Fallback: emblema SVG + wordmark (como no design Claude original) | |
| LOGO_BLOCK_HTML = """ | |
| <svg width="22" height="22" viewBox="0 0 40 40" fill="none" aria-hidden> | |
| <defs> | |
| <radialGradient id="rt-glow" cx="50%" cy="50%" r="50%"> | |
| <stop offset="0%" stop-color="#8B9EE6" stop-opacity="0.7"/> | |
| <stop offset="100%" stop-color="#4DA3FF" stop-opacity="0"/> | |
| </radialGradient> | |
| </defs> | |
| <circle cx="20" cy="20" r="19" stroke="#4DA3FF" stroke-opacity="0.35" stroke-width="0.8"/> | |
| <circle cx="20" cy="20" r="14" stroke="#8B9EE6" stroke-opacity="0.5" stroke-width="0.8"/> | |
| <circle cx="20" cy="20" r="9" stroke="#4DA3FF" stroke-opacity="0.7" stroke-width="0.8"/> | |
| <circle cx="20" cy="20" r="20" fill="url(#rt-glow)"/> | |
| <circle cx="20" cy="20" r="3" fill="#4DA3FF"/> | |
| <circle cx="32.5" cy="14.5" r="1.4" fill="#8B9EE6"/> | |
| </svg> | |
| <span>radioterapia<span class="ai">.ai</span></span> | |
| """ | |
| TOPBAR_HTML = f""" | |
| <div class="rt-topbar"> | |
| <div class="rt-logo"> | |
| {LOGO_BLOCK_HTML} | |
| </div> | |
| <div class="rt-status-pill"> | |
| <span class="rt-status-dot"></span> | |
| Motor Docling pronto | |
| </div> | |
| </div> | |
| """ | |
| HERO_TEXT_HTML = """ | |
| <div class="rt-hero-text"> | |
| <div class="rt-eyebrow"> | |
| <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M13 2 4 14h7l-1 8 9-12h-7l1-8z"/> | |
| </svg> | |
| <span>Motor de Inovação - radioterapia.ai</span> | |
| </div> | |
| <h1 class="rt-h1">Docling</h1> | |
| <p class="rt-sub"> | |
| Transcrição de documentos para texto. Extraia conteúdo estruturado de | |
| laudos, exames, fotos, etc. — para auxiliar a digitação para seu | |
| prontuário ou agentes. | |
| </p> | |
| <div class="rt-features"> | |
| <div class="rt-feature"> | |
| <div class="rt-feature-k">PDF · DOCX · PPTX</div> | |
| <div class="rt-feature-v">Documentos clínicos</div> | |
| </div> | |
| <div class="rt-feature"> | |
| <div class="rt-feature-k">JPG · PNG · TIFF</div> | |
| <div class="rt-feature-v">Laudos fotografados</div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| PILLS_HTML = """ | |
| <div class="rt-pills"> | |
| <div class="rt-pill"><span class="rt-pill-badge" style="background:#E0533A">PDF</span>PDFs</div> | |
| <div class="rt-pill"><span class="rt-pill-badge" style="background:#16A34A">JPG</span>Fotos de laudos</div> | |
| <div class="rt-pill"><span class="rt-pill-badge" style="background:#15803D">XLSX</span>Tabelas</div> | |
| <div class="rt-pill"><span class="rt-pill-badge" style="background:#E07B1E">PPTX</span>Apresentações</div> | |
| <div class="rt-pill"><span class="rt-pill-badge" style="background:#2563EB">DOCX</span>Documentos</div> | |
| </div> | |
| """ | |
| FOOTER_HTML = """ | |
| <div class="rt-footer"> | |
| <span>radioterapia.ai · docling engine v1.0</span> | |
| <span>Powered by IBM Docling · OSS</span> | |
| </div> | |
| """ | |
| def panel_head(num: str, title: str, sub: str) -> str: | |
| return f""" | |
| <div class="rt-panel-head"> | |
| <div class="rt-panel-eyebrow"> | |
| <span class="num">{num}</span> | |
| <span class="bar"></span> | |
| </div> | |
| <h2 class="rt-panel-title">{title}</h2> | |
| <div class="rt-panel-sub">{sub}</div> | |
| </div> | |
| """ | |
| # ============================================================================ | |
| # UI — GRADIO BLOCKS | |
| # ============================================================================ | |
| with gr.Blocks( | |
| title="Docling — Radioterapia.ai", | |
| css=CSS_THEME, | |
| theme=gr.themes.Base( | |
| primary_hue=gr.themes.colors.blue, | |
| neutral_hue=gr.themes.colors.slate, | |
| font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"], | |
| font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"], | |
| ), | |
| ) as app: | |
| # Hero — 2 colunas: texto + imagem do workflow | |
| with gr.Row(elem_classes="rt-hero-row"): | |
| with gr.Column(scale=11, min_width=320): | |
| gr.HTML(HERO_TEXT_HTML) | |
| with gr.Column(scale=10, min_width=280): | |
| # A imagem fica em /docling-workflow.png na raiz do repo. | |
| # Se o arquivo não existir, o componente exibe placeholder vazio. | |
| workflow_image_path = "docling-workflow.png" | |
| if os.path.exists(workflow_image_path): | |
| gr.Image( | |
| value=workflow_image_path, | |
| show_label=False, | |
| container=False, | |
| interactive=False, | |
| show_download_button=False, | |
| show_share_button=False, | |
| elem_classes="rt-hero-image", | |
| ) | |
| else: | |
| gr.HTML('<div class="rt-hero-image-placeholder">📊 Workflow Docling</div>') | |
| # ===== Linha principal: Upload + Resultado ===== | |
| with gr.Row(equal_height=False): | |
| # ---------- UPLOAD ---------- | |
| with gr.Column(scale=1, elem_classes="rt-panel"): | |
| gr.HTML(panel_head( | |
| "01", | |
| "Upload", | |
| "Anexe PDFs, fotos de laudos, tabelas e apresentações" | |
| )) | |
| file_input = gr.File( | |
| file_count="multiple", | |
| type="filepath", | |
| label="Arraste arquivos ou clique para selecionar", | |
| file_types=[".pdf", ".docx", ".pptx", ".xlsx", ".jpg", ".jpeg", ".png", ".tiff", ".html", ".md"], | |
| ) | |
| gr.HTML(PILLS_HTML) | |
| process_btn = gr.Button( | |
| "✨ Processar Documentos", | |
| variant="primary", | |
| size="lg", | |
| ) | |
| # ---------- RESULTADO ---------- | |
| with gr.Column(scale=1, elem_classes="rt-panel"): | |
| gr.HTML(panel_head( | |
| "02", | |
| "Resultado", | |
| "Texto extraído — em formato Markdown" | |
| )) | |
| md_output = gr.Textbox( | |
| lines=14, | |
| max_lines=22, | |
| label="output.md", | |
| placeholder="Aguardando documentos — o texto estruturado aparecerá aqui após o processamento.", | |
| show_copy_button=True, | |
| ) | |
| with gr.Row(): | |
| download_btn = gr.Button("⬇️ Baixar .md", size="sm") | |
| clear_btn = gr.Button("🗑️ Limpar Sessão", size="sm", variant="stop") | |
| download_file = gr.File(label="Download disponível", visible=False) | |
| # ===== Logs (fora do sistema de etapas) ===== | |
| with gr.Row(elem_classes="rt-panel"): | |
| with gr.Column(): | |
| gr.HTML(""" | |
| <div class="rt-panel-head"> | |
| <h2 class="rt-panel-title rt-panel-title-plain">Logs & comentários</h2> | |
| <div class="rt-panel-sub">Status de processamento, confiança e conformidade</div> | |
| </div> | |
| """) | |
| log_output = gr.Textbox( | |
| lines=6, | |
| max_lines=12, | |
| label="Status do Sistema", | |
| placeholder="$ aguardando arquivos…", | |
| ) | |
| # ===== Accordion "Sobre a ferramenta" ===== | |
| with gr.Accordion("Sobre a ferramenta", open=False): | |
| gr.Markdown(""" | |
| **🔒 Conformidade com a LGPD — Sem retenção de dados** | |
| * Arquivos processados estritamente em memória RAM volátil. | |
| * Exclusão forçada imediata após a leitura de cada documento do lote. | |
| * Sem gravação permanente ou retenção de dados sensíveis em servidor. | |
| * Deduplicação SHA-256 evita o reprocessamento de informações idênticas. | |
| **📚 Sobre o Docling** | |
| Incorpore a biblioteca **docling** em seus agentes médicos de transcrição. | |
| Saiba mais em: [https://www.docling.ai/](https://www.docling.ai/) | |
| """) | |
| # ===== Footer ===== | |
| gr.HTML(FOOTER_HTML) | |
| # ===== Handlers ===== | |
| def save_and_export_file(text): | |
| if not text or len(text.strip()) < 10: | |
| return gr.update(value=None, visible=False) | |
| temp_file = tempfile.NamedTemporaryFile( | |
| mode="w", suffix=".md", delete=False, encoding="utf-8" | |
| ) | |
| temp_file.write(text) | |
| temp_file.close() | |
| return gr.update(value=temp_file.name, visible=True) | |
| process_btn.click( | |
| fn=engine.process_batch, | |
| inputs=[file_input], | |
| outputs=[md_output, log_output], | |
| ) | |
| download_btn.click( | |
| fn=save_and_export_file, | |
| inputs=[md_output], | |
| outputs=[download_file], | |
| ) | |
| clear_btn.click( | |
| fn=engine.clear_session, | |
| outputs=[file_input, md_output, log_output], | |
| ) | |
| # ============================================================================ | |
| # LAUNCH | |
| # ============================================================================ | |
| if __name__ == "__main__": | |
| if not DOCLING_READY: | |
| print("⚠️ Aviso: Docling não está instalado. Verifique requirements.txt.") | |
| app.queue(max_size=20).launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| show_error=True, | |
| ) |