Produção de Livros

1.370 palavras sumiram do livro e todo gate passou

Uma regra de legenda engoliu 1.370 palavras do texto do autor, e o gate que dizia que cada palavra do PDF existe na fonte passou limpo. O gate que acha o buraco, e como rodar.

JonathanJonathan
8 min de leitura1.556 palavras
Capa do artigo: 1.370 palavras sumiram do livro e todo gate passou (eBuz, Produção de Livros)

Um boneco de 201 páginas, de um livro que o autor já tinha escrito, estava publicado em um link, e todos os gates estavam verdes. Quando alguém comparou o PDF com o texto original, faltavam 1.370 palavras do autor.

Não foi invenção de texto, e sim o contrário. Uma regra do nosso gerador reconhecia legendas de figura e as tratava como legendas curtas de uma linha. Só que, ao lado de cada figura da apostila, o texto seguinte era parágrafo do autor, e a regra engolia até quatro linhas dele por figura.

O gate que existia perguntava: "toda palavra do PDF existe na fonte?". Passou. Porque ele só enxerga o que entrou no PDF, nunca o que saiu dele.

Por que o gate passou

Um gate de texto tem uma direção. A pergunta "toda palavra do PDF existe na fonte?" pega invenção: texto que a diagramação ou a IA acrescentou. Ela é cega para perda: texto que existia e sumiu. Para pegar a perda, a pergunta tem de ser a inversa: "toda palavra da fonte está no PDF?".

O mesmo erro de direção aparece em outro lugar. Em uma bateria de conferência, todos os gates rodaram sobre o texto-fonte e deram verde. O PDF, ao mesmo tempo, tinha 15 entidades HTML impressas como texto, um "Apêndice: Apêndice" duplicado e 10 negritos que sumiram. Gate que lê a fonte não vê o que o render estragou. A lição vale para os dois casos: confira o PDF final, e confira nas duas direções.

O que mais a troca do motor descobriu

Além das 1.370 palavras, achamos 538 palavras de rótulo de gráfico, corpo de tabela e legenda coladas no meio de parágrafos. A extração de texto jogava esses elementos no meio do fluxo. Também havia uma URL trocada por [endereço], o que mudava o texto do autor. E a origem de cada trecho, um código interno, aparecia na margem. Nenhum gate de fidelidade enxerga isso. Só olhar a página mostrou.

A regra que saiu disso, e que vale para qualquer livro com texto de autor: na dúvida, o texto fica no corpo. Sobrar uma linha de legenda é um defeito pequeno. Engolir texto do autor não é.

O gate de conservação

A ideia é simples. Pegue o texto do autor em um arquivo, com parágrafos separados por linha em branco. Extraia o texto do PDF. Confira, parágrafo por parágrafo, se ele chegou. Fazemos isso por janelas de oito palavras, que resistem à hifenização e a quebras de página no meio do parágrafo. Este é o script:

#!/usr/bin/env python3
"""Confere se o texto do autor chegou inteiro ao PDF. Mede a perda, não só a invenção.
Uso: python3 conserva.py fonte.txt livro.pdf
fonte.txt = o texto do autor, parágrafos separados por linha em branco.
Sai com 1 se algum parágrafo estiver faltando."""
import re, subprocess, sys, unicodedata

def base(s):                       # sem acento, sem caixa, sem pontuação, sem hífen e sem espaço
    s = unicodedata.normalize("NFD", s.lower())
    s = "".join(c for c in s if unicodedata.category(c) != "Mn")
    return re.sub(r"[^a-z0-9]", "", s)

def palavras(s):
    return re.findall(r"[^\W_]+", s.lower())

fonte = open(sys.argv[1], encoding="utf-8").read()
pdf = subprocess.run(["pdftotext", sys.argv[2], "-"], stdout=subprocess.PIPE, text=True).stdout
alvo = base(pdf)

JANELA, MINIMO = 8, 0.9
faltando, total_pal, perdidas = [], 0, 0
for par in [p.strip() for p in re.split(r"\n\s*\n", fonte) if p.strip()]:
    w = palavras(par)
    total_pal += len(w)
    if len(w) < JANELA:
        achou = base(par) in alvo
        if not achou: faltando.append((0.0, len(w), par)); perdidas += len(w)
        continue
    jan = [base(" ".join(w[i:i + JANELA])) for i in range(len(w) - JANELA + 1)]
    frac = sum(j in alvo for j in jan) / len(jan)
    if frac < MINIMO:
        faltando.append((frac, len(w), par)); perdidas += len(w)

print(f"palavras na fonte: {total_pal} | parágrafos com texto faltando: {len(faltando)} | palavras sob suspeita: {perdidas}")
for frac, n, par in faltando[:10]:
    print(f"  FALTA ({n} palavras, {frac:.0%} presente): {par[:70]}...")
sys.exit(1 if faltando else 0)

Testamos em um livro de 40 parágrafos e 1.143 palavras, gerado em duas versões. No PDF completo, o script saiu com 0 parágrafos faltando e código de saída 0. No PDF em que removemos dois parágrafos, ele apontou os dois, acusou 58 palavras sob suspeita e saiu com código 1.

Três causas comuns de perda

Quando o script aponta parágrafos faltando, a causa quase sempre é uma destas:

  1. Uma regra que reconhece elemento do texto. Legenda de figura, nota, citação em destaque. A regra decide o que é legenda e o que é parágrafo, e erra para o lado de engolir. Foi o nosso caso das 1.370 palavras.
  2. A extração misturando elementos. Rótulos de gráfico e corpo de tabela são jogados no meio do fluxo pelo extrator de texto. Na nossa conferência, 538 palavras de gráfico, tabela e legenda tinham aparecido como se fossem texto do autor. O sintoma é o inverso da perda: sobra texto.
  3. Uma limpeza que remove demais. Marcas internas, endereços e códigos são retirados do texto antes da diagramação. Se a regra de limpeza for larga, leva junto o que era do autor. Uma URL trocada por [endereço] mudou o texto dele.

Em todas, a correção é na origem, na regra que errou. Consertar o PDF à mão resolve uma vez e deixa o gerador errado para o próximo livro.

Passo a passo

  1. Guarde o texto do autor em um arquivo de texto simples, parágrafos separados por linha em branco. É a fonte da verdade.
  2. Gere o PDF final, o mesmo que vai ser enviado, e não uma versão intermediária.
  3. Rode o script de conservação.
  4. Leia cada parágrafo apontado. Pode ser perda real ou diferença de norma. Se for perda, corrija na origem (a regra do gerador), não no PDF.
  5. Rode a conferência inversa, que procura palavras do PDF que não existem na fonte, para pegar texto acrescentado. É o gate "toda palavra do PDF existe na fonte", que continua útil, só que não basta.
  6. Confira o que o texto não mostra. O negrito, por exemplo, pode ser lido do PDF com pdftohtml:
pdftohtml -xml -i -stdout livro.pdf | grep -o "<b>[^<]*</b>" | wc -l

Compare a contagem com os negritos do texto-fonte. Duas armadilhas, em que caímos. A hifenização quebra uma palavra no fim da linha ("visí" mais "vel") e dá falsa perda, então compare sem hífen e sem espaço, dos dois lados. E um boneco mostra só parte do texto, de propósito, então o gate precisa saber qual é o perfil do arquivo.

  1. Meça a perda também dentro do gerador. Se uma regra consome linhas de texto, ela deve contar quantas entraram e quantas saíram, e abortar quando a conta não fecha. No nosso gerador, essa conservação de linhas é parte do gate.

Os limites do script

  • Trechos repetidos. Se uma janela de oito palavras aparece em outro lugar do livro, ela "prova" presença sem provar. Em texto real isso é raro, mas em texto muito repetitivo reduz a sensibilidade. No nosso teste, os parágrafos eram muito parecidos entre si, e os dois removidos apareceram com 50% de presença.
  • Texto trocado por texto parecido. O script não vê uma palavra mudada por outra.
  • Tabelas e figuras. A extração de texto de tabelas pode embaralhar a ordem. Para elas, a conferência é visual.
  • A extração importa. Uma extração por outra biblioteca já perdeu dígitos soltos em um caso nosso. A prova usa o poppler.

O erro que custa caro

Considerar que um gate "verde" significa "texto inteiro". Cada gate responde a uma pergunta, e a pergunta tem direção. Antes de confiar em um gate, escreva a pergunta que ele faz e pergunte qual defeito ele não consegue ver. E, um passo antes: um gate que nunca foi visto reprovando não vale. Estrague o arquivo de propósito e veja o gate falhar. A técnica, o teste de mutação, aparece no checklist de preflight do miolo.

Quando NÃO usar

Se o texto do autor passa por edição de propósito (copidesque, revisão), a fonte deixa de ser o original. Compare então com a versão editada e aprovada, que vira a base de verdade. Se o texto vai direto para a página, sem regras que reconheçam elementos, o risco de uma regra engolir texto é menor. A conferência continua barata.

Alternativa sem o nosso ambiente

O script usa só o Python e o pdftotext, do poppler. Qualquer ferramenta de comparação de texto serve para a mesma ideia: diff entre o texto extraído e o original, ou o recurso de comparar documentos de um editor de texto. A direção é que importa.

O que o Claude faz hoje, e como fazer sem ele

Hoje o Claude roda a conferência do PDF final em cada boneco antes de promover o arquivo e relata os parágrafos apontados. Sem ele, você roda o script do artigo e lê cada ocorrência.

Onde entra na operação

Este é o gate do passo em que o boneco ou o miolo viram arquivo final. Ele convive com a conferência de sangria, de cor e de geometria, que estão no checklist de preflight do miolo. O desenho completo está em Como produzimos o miolo de um livro profissional sem InDesign. O compilador que gera o PDF está em Typst no lugar do InDesign.

Baixe, para guardar: checklist de preflight do miolo, com comando e limite por item (PDF).

Versões usadas neste artigo: veja a matriz de versões.

Perguntas frequentes

Por que o gate "toda palavra do PDF existe na fonte" não pegou o texto perdido?

Porque ele só olha o que entrou no PDF. Ele procura texto acrescentado, não texto removido. Para pegar perda, é preciso a pergunta inversa: toda palavra da fonte está no PDF? Os dois gates juntos cobrem as duas direções.

Como confiro se nenhuma palavra do autor sumiu da diagramação?

Guarde o texto do autor em um arquivo, extraia o texto do PDF final com pdftotext e compare parágrafo por parágrafo, por janelas de oito palavras, ignorando acento, caixa, pontuação e hífen. O script do artigo faz isso e sai com código 1 se algum parágrafo estiver faltando.

Posso conferir o texto do livro lendo o arquivo-fonte em vez do PDF?

Não é suficiente. Em um caso nosso, a bateria rodou limpa sobre o texto-fonte, mas o PDF tinha 15 entidades HTML impressas, um prefixo duplicado e 10 negritos perdidos. O que o render faz com o texto só se vê no PDF.

O que fazer quando o gate aponta um parágrafo faltando?

Abra a página, confirme se é perda real e corrija na origem, que costuma ser uma regra do gerador. Na dúvida, o texto fica no corpo: sobrar uma linha de legenda é um defeito pequeno, e engolir texto do autor não é.