Guia LEORAD — organizar máscaras e autotextos com IA

Para você, médico(a): como usar

Jeito mais rápido: em leorad.com.br/organizar-mascaras ou no LEORAD (biblioteca → botão + → Organizar acervo com IA), clique na IA que você usa (ChatGPT, Claude ou Gemini): este guia vai copiado. Na conversa que abrir, cole (Ctrl+V), envie e depois anexe o seu material. Ali também dá para organizar sem sair do LEORAD, na aba Aqui no LEO.

Sem o botão:

  1. Abra o ChatGPT, o Claude ou o Gemini numa conversa nova.
  2. Anexe este arquivo e o seu material: documentos do Word, PDFs, planilhas, export de outro sistema ou texto colado.
  3. Escreva: "Organize minhas máscaras e autotextos seguindo o guia."

Nos dois casos, a IA vai fazer algumas perguntas, mostrar a organização proposta e, depois do seu ok, gerar um arquivo .json. No LEORAD: Biblioteca → botão + → Backup do LEO e escolha o .json (ele aceita .leo e .json).

Dicas:

  • Comece com um lote pequeno, de 5 a 10 máscaras, para conferir como fica. A importação cria tudo de uma vez, e desfazer é apagar item por item.
  • Não envie laudos de pacientes reais. Use só modelos, sem nome, data de nascimento ou número de exame.
  • Acervo grande? Peça um arquivo por região ou modalidade (um de Crânio, outro de Abdome…) e importe um de cada vez.

Para a IA: instruções

Você vai ajudar um(a) radiologista a transformar as máscaras (modelos de laudo) e os autotextos (frases prontas) dele(a) num arquivo .json que o LEORAD importa direto. Siga este guia à risca: o formato é estrito, e alguns erros fazem itens sumirem sem aviso na importação.

Regras de ouro

  1. Organize, não reescreva. Você pode criar pastas, padronizar nomes de máscara, criar atalhos e apontar duplicatas. O texto do laudo fica literal: não corrija, não resuma, não "melhore" termos médicos, não troque sinônimos. Se algo parecer errado (erro de digitação, lateralidade estranha, medida incoerente), aponte ao usuário e mantenha como está. Só edite o texto se o usuário pedir explicitamente.
  2. Preserve campos de preenchimento. Marcadores como ___, ***, [ ], ( ), xx mm ou <<medida>> ficam exatamente como vieram.
  3. Nada de dados de paciente. Se o material tiver nome de paciente, data de nascimento, número de prontuário/atendimento ou assinatura com CRM de laudo real, pare e avise. Pergunte se o usuário quer que você remova esses trechos ou se ele prefere mandar só os modelos.
  4. Mostre antes de gerar. Nunca gere o arquivo final sem o usuário aprovar a estrutura.
  5. Não peça senha, token, login nem chave. Nada disso é necessário.

Fluxo da conversa

Etapa 0 — Se ainda não há material. Muitas vezes o usuário chega por um link, e você lê este guia antes de ele anexar qualquer coisa. Nesse caso, não resuma o guia nem explique o formato. Responda em no máximo 4 linhas, em português e com tom cordial:

Oi! Vou organizar suas máscaras e autotextos para importar no LEORAD. Anexe aqui o seu material (Word, PDF, planilha ou texto colado). Pode ser tudo de uma vez. Só peço que sejam modelos, sem dados de pacientes.

E espere o material.

Etapa 1 — Entender o material. Leia tudo o que foi anexado e responda com um resumo curto: quantos textos encontrou, de que tipo e como parecem organizados. Pergunte só o que não der para deduzir:

  • O que é máscara (laudo completo) e o que é autotexto/frase (trecho para inserir no meio do laudo)? Se a separação estiver óbvia, proponha em vez de perguntar.
  • Como ele prefere as pastas: por região (Crânio, Tórax, Abdome…), por modalidade (TC, RM, US…) ou pela organização que já existe no material?
  • Qual nome dar à pasta principal (ex.: "Minhas máscaras", "Clínica X")?

Etapa 2 — Propor a estrutura. Mostre a árvore em texto, com o nome e o atalho de cada item, assim:

Máscaras — Dra. Ana
├── Crânio
│   ├── TC CRÂNIO NORMAL            (tccranio)
│   └── RM CRÂNIO NORMAL            (rmcranio)
└── Abdome
    └── US ABDOME TOTAL NORMAL      (usabdtotal)

Frases — Dra. Ana
└── Fígado
    └── ESTEATOSE LEVE              (esteatoseleve)

Liste também: duplicatas ou quase-duplicatas encontradas, textos que você não soube classificar e qualquer coisa que pareça errada no conteúdo. Espere o ok.

Etapa 3 — Gerar o arquivo. Monte o .json no formato abaixo, rode o validador (ou o checklist manual, se você não puder executar código) e corrija tudo o que ele apontar. Entregue:

  • o arquivo para download, com um nome como biblioteca-cranio.json;
  • se você não conseguir gerar arquivo, o JSON completo num único bloco de código, com a instrução: "copie, cole no Bloco de Notas e salve como biblioteca.json". No Bloco de Notas, o usuário deve escolher "Todos os arquivos" em Tipo e a codificação UTF-8;
  • um resumo de uma linha: quantas pastas, quantas máscaras e quantas frases.

Se o acervo for grande (mais de ~40 textos), gere um arquivo por pasta principal em vez de um arquivo gigante, para não cortar o conteúdo no meio.

Formato do arquivo

Um array JSON plano, sem aninhamento. A hierarquia é dada pelo campo parent, que aponta para o uuid de outro item do mesmo arquivo.

Pasta:

{ "uuid": "p-cranio", "name": "Crânio", "type": "mask", "order": 0, "parent": "raiz-mascaras" }

A pasta principal (raiz) não tem o campo parent.

Documento (máscara ou frase):

{
  "uuid": "d-tc-cranio",
  "name": "TC CRÂNIO NORMAL",
  "type": "mask",
  "order": 0,
  "parent": "p-cranio",
  "isLeaf": true,
  "text": "<p><strong>TOMOGRAFIA COMPUTADORIZADA DO CRÂNIO</strong></p><p><strong>Técnica:</strong></p><p>Aquisição volumétrica sem contraste.</p><p><strong>Análise:</strong></p><p>Parênquima encefálico com atenuação preservada.</p><p><strong>Impressão:</strong></p><p>Exame dentro dos padrões da normalidade.</p>",
  "instructions": "",
  "speechText": "tomografia de crânio normal",
  "shortcut": "tccranio"
}
camporegra
uuidIdentificador único dentro do arquivo. Qualquer texto serve (p1, d-tc-cranio…). O LEORAD gera os definitivos na importação.
nameNome que aparece na biblioteca. Máscaras em MAIÚSCULAS no padrão MODALIDADE REGIÃO ACHADO costumam ficar mais fáceis de achar. Não pode repetir dentro da mesma pasta, nem mudando só maiúscula/minúscula.
type"mask" para máscara, "phrase" para autotexto/frase. Define em qual aba o item entra.
orderPosição dentro da pasta: 0, 1, 2…
parentuuid da pasta onde o item fica. Obrigatório em tudo, menos na raiz.
isLeaftrue em documentos. Pastas não têm esse campo.
textCorpo do texto em HTML: um <p>…</p> por parágrafo e <strong> nos títulos de seção. Pode usar <em> e <u>. Não use \n, porque quebra de linha não aparece. Não use tabelas, estilos, classes nem cores. Nunca vazio.
shortcutAtalho para chamar o texto: minúsculo, sem espaço, sem acento, até 30 caracteres, e sem repetir no arquivo.
speechTextComo o médico falaria o nome ao ditar ("tomografia de crânio normal"). Pode ser "".
instructionsOrientação para a IA do LEORAD que não sai no laudo (ex.: "sempre citar a medida do maior nódulo"). HTML ou "". Só preencha se o material trouxer instruções explícitas.

Máscaras e frases ficam em árvores separadas. Um documento precisa ter o mesmo type da pasta onde está, e uma pasta o mesmo type da pasta-mãe. Por isso, crie uma raiz para máscaras ("type": "mask") e outra para frases ("type": "phrase").

O que faz item sumir sem aviso

A importação não mostra erro nestes casos: o item simplesmente não aparece.

  1. parent apontando para um uuid que não existe no arquivo.
  2. Documento sem parent (solto na raiz).
  3. Documento ou pasta com type diferente da pasta onde está.
  4. Dois itens com o mesmo name na mesma pasta, inclusive quando a diferença é só maiúscula/minúscula.
  5. text vazio.
  6. shortcut com mais de 30 caracteres.
  7. Mais de 8 níveis de pasta abaixo da raiz.

Validador (rode antes de entregar)

Se você pode executar Python, salve o JSON como biblioteca.json e rode este script. Se ele terminar com "Revise antes de importar", corrija e rode de novo.

import json, re, sys
from collections import Counter, defaultdict

caminho = sys.argv[1] if len(sys.argv) > 1 else "biblioteca.json"
with open(caminho, encoding="utf-8") as f:
    dados = json.load(f)
nos = dados["jsonContent"] if isinstance(dados, dict) else dados

erros, avisos = [], []
if not isinstance(nos, list):
    sys.exit("O arquivo precisa ser um array JSON [ ... ].")

por_uuid = {}
for n in nos:
    u = n.get("uuid")
    if not u:
        erros.append(f"item sem uuid: {n.get('name')!r}")
    elif u in por_uuid:
        erros.append(f"uuid repetido: {u}")
    por_uuid[u] = n

docs = [n for n in nos if n.get("isLeaf")]
pastas = [n for n in nos if not n.get("isLeaf")]

for n in nos:
    nome = n.get("name") or "(sem nome)"
    if not n.get("name"):
        erros.append(f"item sem name (uuid {n.get('uuid')})")
    if n.get("type") not in ("mask", "phrase"):
        erros.append(f"{nome}: type deve ser 'mask' ou 'phrase'")
    pai = n.get("parent")
    if pai and pai not in por_uuid:
        erros.append(f"{nome}: parent '{pai}' não existe no arquivo")
    elif pai and por_uuid[pai].get("isLeaf"):
        erros.append(f"{nome}: parent aponta para um documento, não para uma pasta")
    elif pai and por_uuid[pai].get("type") != n.get("type"):
        erros.append(f"{nome}: type '{n.get('type')}' diferente da pasta '{por_uuid[pai].get('name')}'")

for d in docs:
    nome = d.get("name")
    if not d.get("parent"):
        erros.append(f"{nome}: documento sem parent (precisa estar dentro de uma pasta)")
    texto = (d.get("text") or "").strip()
    if not texto:
        erros.append(f"{nome}: text vazio")
    elif not re.search(r"<[a-z]", texto, re.I):
        erros.append(f"{nome}: text sem HTML — use <p>…</p> por parágrafo")
    elif "\n" in texto:
        avisos.append(f"{nome}: text tem \\n — quebras de linha devem ser <p>")
    atalho = d.get("shortcut") or ""
    if len(atalho) > 30:
        erros.append(f"{nome}: shortcut com {len(atalho)} caracteres (máx. 30)")
    if atalho and not re.fullmatch(r"[a-z0-9._-]+", atalho):
        avisos.append(f"{nome}: shortcut '{atalho}' tem maiúscula, espaço ou acento")
    if re.search(r"\b(paciente|prontu[aá]rio|atendimento|nascimento|DN)\s*[:\-]", texto, re.I):
        avisos.append(f"{nome}: pode conter dado de paciente — confira")

repetidos = [s for s, c in Counter(d.get("shortcut") for d in docs if d.get("shortcut")).items() if c > 1]
for s in repetidos:
    erros.append(f"shortcut repetido: {s}")

irmaos = defaultdict(list)
for n in nos:
    irmaos[(n.get("parent"), n.get("type"), bool(n.get("isLeaf")))].append((n.get("name") or "").lower())
for (pai, _, _), nomes in irmaos.items():
    for nome, c in Counter(nomes).items():
        if c > 1:
            onde = por_uuid.get(pai, {}).get("name", "raiz")
            erros.append(f"nome repetido na pasta '{onde}': {nome}")

def profundidade(n):
    d, atual = 0, n
    while atual.get("parent") in por_uuid and d < 50:
        atual = por_uuid[atual["parent"]]
        d += 1
    return d

mais_funda = max([profundidade(p) for p in pastas] or [0])
if mais_funda > 8:
    erros.append(f"{mais_funda} níveis de pasta abaixo da raiz (máx. 8)")

print(f"pastas: {len(pastas)}")
print(f"máscaras: {sum(d.get('type') == 'mask' for d in docs)}")
print(f"frases: {sum(d.get('type') == 'phrase' for d in docs)}")
print(f"profundidade máxima: {mais_funda}")
for e in erros:
    print("ERRO  ", e)
for a in avisos:
    print("AVISO ", a)
print("\nRevise antes de importar." if erros else "\nPronto para importar.")

Checklist manual (se não puder rodar código)

Confira item por item antes de entregar:

  • O arquivo começa com [ e termina com ], e é JSON válido (aspas duplas, sem vírgula sobrando no fim).
  • Todo uuid é diferente dos outros.
  • Todo parent aponta para o uuid de uma pasta que está no arquivo.
  • Só as pastas raiz não têm parent, e nenhum documento fica sem pasta.
  • Máscaras estão só em pastas mask, e frases só em pastas phrase.
  • Nenhum nome se repete dentro da mesma pasta.
  • Todo text está em <p>…</p>, sem \n e nunca vazio.
  • Todo shortcut é minúsculo, sem espaço nem acento, tem até 30 caracteres e não se repete.
  • No máximo 8 níveis de pasta abaixo da raiz.

Modelo completo (importa como está)

[
  { "uuid": "raiz-m", "name": "Minhas máscaras", "type": "mask", "order": 0 },
  { "uuid": "p-cranio", "name": "Crânio", "type": "mask", "order": 0, "parent": "raiz-m" },
  {
    "uuid": "d-rm-cranio",
    "name": "RM CRÂNIO NORMAL",
    "type": "mask",
    "order": 0,
    "parent": "p-cranio",
    "isLeaf": true,
    "text": "<p><strong>RESSONÂNCIA MAGNÉTICA DO CRÂNIO</strong></p><p><strong>Análise:</strong></p><p>Parênquima encefálico com sinal e morfologia preservados.</p><p><strong>Impressão diagnóstica:</strong></p><p>Exame dentro dos padrões da normalidade.</p>",
    "instructions": "",
    "speechText": "ressonância de crânio normal",
    "shortcut": "rmcranio"
  },
  { "uuid": "raiz-f", "name": "Minhas frases", "type": "phrase", "order": 0 },
  { "uuid": "p-figado", "name": "Fígado", "type": "phrase", "order": 0, "parent": "raiz-f" },
  {
    "uuid": "f-esteatose",
    "name": "ESTEATOSE LEVE",
    "type": "phrase",
    "order": 0,
    "parent": "p-figado",
    "isLeaf": true,
    "text": "<p>Fígado com ecogenicidade discretamente aumentada, compatível com esteatose leve.</p>",
    "instructions": "",
    "speechText": "esteatose leve",
    "shortcut": "esteatoseleve"
  }
]

Lembretes ao usuário (inclua na entrega)

  • Importar: Biblioteca → botão + → Backup do LEO e escolher o .json (o importador aceita .leo e .json).
  • Se a pasta principal tiver o mesmo nome de uma que ele já tem, o LEORAD cria outra com um número no fim. Nada é sobrescrito.
  • Depois de importar, vale abrir duas ou três máscaras e conferir a formatação antes de importar os próximos lotes.