Skip to content

Identificação e inserção automática de xref no pipeline DOCX → SPS XML#109

Merged
gitnnolabs merged 12 commits into
scieloorg:mainfrom
Rossi-Luciano:add-xref-linking
Jun 1, 2026
Merged

Identificação e inserção automática de xref no pipeline DOCX → SPS XML#109
gitnnolabs merged 12 commits into
scieloorg:mainfrom
Rossi-Luciano:add-xref-linking

Conversation

@Rossi-Luciano
Copy link
Copy Markdown
Contributor

Contexto

O padrão SPS/JATS exige que toda citação bibliográfica no corpo do artigo esteja marcada com <xref ref-type="bibr" rid="Bn">, ligando-a à entrada correspondente na lista de referências. Anteriormente, a geração dessas tags dependia exclusivamente de um LLM para parsear as referências e identificar citações — mecanismo com cobertura baixa e imprevisível: sem o modelo disponível, nenhum <xref> era gerado.

Esta branch implementa um pipeline determinístico e independente de LLM para essa tarefa, usando o próprio DOCX como fonte de verdade. O LLM passa de mecanismo primário a camada de fallback.


Fluxo do pipeline

DOCX original
    │
    ▼
① mark_references(doc)                          [xref.py]
    ├─ Detecta estilo de citação (ABNT / Vancouver bracket / superscript)
    ├─ Adiciona bookmarks xref_B1…xref_Bn em cada entrada da lista de referências
    └─ Insere hyperlinks internos do Word: citação → bookmark correspondente
    │
    ▼
② validate_marks(doc)                           [xref.py]
    ├─ Verifica consistência: hyperlinks sem bookmark (erro) / bookmarks sem citação (aviso)
    └─ Grava resultado em xref_status (JSONField no modelo)
    │
    ▼
③ DOCX marcado salvo como marked_file           [tasks.py]
    └─ Disponível para download e revisão humana na interface Wagtail
    │
    ▼
④ read_marks(doc) → xref_map                   [tasks.py]
    └─ Extrai mapeamento {texto de citação → rid} dos hyperlinks do DOCX
       Ranges expandidos: "[26-27]" → rid="B26 B27"
    │
    ▼
⑤ Pré-processamento de stream_data_body        [tasks.py]
    ├─ Passe 1: xref_map — substituição exata pelos hyperlinks do DOCX
    └─ Passe 2: text_xref_fn — matching regex de Author (year) narrativo/parentético
    │
    ▼
⑥ get_xml() — geração do XML SPS               [xml.py]
    ├─ Passe 1: xref_map (segmentado — não reprocessa xrefs já inseridos)
    ├─ Passe 2: text_xref_fn (já segmentado internamente)
    └─ Passe 3: proccess_labeled_text (LLM, fallback, segmentado)
    │
    ▼
XML com <xref ref-type="bibr" rid="Bn"> em todas as citações identificadas

Os passes ⑤ e ⑥ operam por segmento: o parágrafo é dividido nas fronteiras de <xref> já existentes e cada camada processa apenas o texto puro, eliminando risco de double-wrapping e garantindo que citações num parágrafo parcialmente marcado não sejam ignoradas.


Cobertura por estilo de citação

Fixture Estilo Refs Links DOCX Cobertura DOCX Cobertura XML estimada
bn-2025-1870 ABNT narrativo 52 23 44% ~81%
bn-2025-1828 ABNT parentético 95 42 44% ~65–70%
ean-2025-0109-pt Vancouver superscript 31 27 87% ~87%
mr-2025-0709 Vancouver bracket 33 28 85% ~91%*

*B27 e B30 cobertos via expansão de range [26-27] / [29-30] → sem hyperlink direto no DOCX, mas presentes no rid do XML.

Por que não é 100%:

  • ABNT: citações em grupo temporal ("Yano 1981, ..., 2018") geram um único hyperlink para a primeira entrada; ambiguidades autor+ano sem co-autor explícito são conservadoramente não linkadas; refs institucionais sem padrão autor+ano (GBIF.org, SpeciesLink) são insolúveis por regex.
  • Vancouver: refs sem hyperlink são genuinamente não citadas no corpo.

Comparação com a versão anterior (apenas LLM via proccess_labeled_text):

Estilo Antes Depois
ABNT ~10–20% ~65–81%
Vancouver bracket ~30–50% ~91%
Vancouver superscript ~0–5% ~87%

O que foi implementado

markup_doc/xref.py — novo módulo

Core da feature. Contém toda a lógica de identificação de citações no DOCX:

  • Detecção de estilo de citação (ABNT, Vancouver bracket, Vancouver superscript)
  • mark_references(), validate_marks(), read_marks(), build_text_xref_replacer()
  • Detecção robusta de fim da seção de referências: lista de headings conhecidos + heurística ALL-CAPS + estilo Word Heading N

markup_doc/tasks.py

Integração do pipeline xref antes do processamento principal. Expansão de ranges Vancouver no xref_map. Pré-processamento de stream_data_body com os dois primeiros passes.

markup_doc/xml.py

Substituição do guard coarse if 'xref' not in paragraph por processamento por segmento via _apply_to_segments(), _apply_xref_map() e _apply_proccess_labeled_text(). Todos os parágrafos recebem os três passes; xrefs pré-existentes são preservados.

markup_doc/models.py

Campos marked_file (FileField) e xref_status (JSONField) em ArticleDocxMarkup. Widgets DownloadMarkedFileWidget, XrefStatusWidget e painel ReprocessButtonPanel. Proxy model ProcessedDocx com view dedicada no admin.

markup_doc/views.py + markup_doc/wagtail_hooks.py

Views download_marked_docx e reprocess. ProcessedDocxViewSet registrado no grupo de marcação editorial.

markup_doc/static/js/xref-button.js

Botão "Reprocessar" injetado na barra de ações do cabeçalho Wagtail nas views de ProcessedDocx e MarkupXML, com mensagem de confirmação contextual.

Correções de bugs colaterais

markup_doc/labeling_utils.py:

  • Regex de detecção da seção de referências mais precisa
  • resp_json = {} inicializado antes do bloco condicional (prevenia UnboundLocalError)
  • < literal escapado em append_fragment (prevenia quebra do parser XML)
  • Guarda de None em proccess_special_content

markuplib/function_docx.py:

  • is_numPr inicializado antes do loop (tabelas após listas eram descartadas silenciosamente)
  • Parágrafos adicionados a content independente de tabelas adjacentes

Migrações

  • 0003_articledocxmarkup_marked_file
  • 0004_articledocxmarkup_xref_status

🤖 Generated with Claude Code

@gitnnolabs
Copy link
Copy Markdown
Contributor

Revisando....

Rossi-Luciano and others added 3 commits June 1, 2026 09:02
…OCX → SPS XML)

Adiciona módulo xref.py com detecção de estilo de citação (ABNT narrativo,
ABNT parentético, Vancouver bracket, Vancouver superscript), marcação de
bookmarks e hyperlinks no DOCX, validação de consistência e extração do
mapeamento citação→rid.

Integra o pipeline em tasks.py: o DOCX é marcado antes do processamento
principal, o xref_map é aplicado em stream_data_body e repassado a get_xml().
Ranges Vancouver são expandidos para rid multi-valor (ex: "[26-27]" → "B26 B27").

Em xml.py, substitui o guard coarse `if 'xref' not in paragraph` por
processamento por segmento via _apply_to_segments(), eliminando risco de
double-wrapping e garantindo que citações em parágrafos parcialmente marcados
não sejam ignoradas. O LLM (proccess_labeled_text) passa a atuar como fallback.

Adiciona campos marked_file e xref_status ao modelo ArticleDocxMarkup, com
widgets visuais, proxy model ProcessedDocx e views download_marked_docx e
reprocess para suporte à revisão humana via interface Wagtail.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
labeling_utils.py:
- Regex de detecção da seção de referências mais precisa (refer[eê]nci|references?)
- resp_json inicializado antes do bloco condicional (evitava UnboundLocalError)
- Escapa '<' literal em append_fragment para não quebrar o parser XML
- Guarda None em proccess_special_content para search_special_id retornando None

function_docx.py:
- is_numPr inicializado antes do loop (tabelas após listas eram descartadas
  silenciosamente por flag herdada da iteração anterior)
- Parágrafos adicionados a content independente de tabelas adjacentes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Insere botão na barra de ações do cabeçalho nas views de edição de
ProcessedDocx e MarkupXML, com mensagem de confirmação contextual.
Usa MutationObserver como fallback para injeção no DOM quando o
cabeçalho ainda não está renderizado.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rossi-Luciano and others added 9 commits June 1, 2026 09:12
Remoção de guarda duplicada `if vals2 and vals2[0]:` que ficou sem
corpo após resolução de conflito de rebase, causando IndentationError.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dois acessos diretos a values["reftype"] sobreviveram à resolução de
conflito do rebase. Substituídos por values.get("reftype") para não
lançar KeyError quando o LLM não parseia o tipo da referência.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
O passe de xref_map em stream_data_body usava para.replace() cru,
sem segmentação. Para Vancouver superscript, chaves curtas como "2"
substituíam o dígito dentro de rid="B2" já criado, corrompendo o
atributo: rid="B<xref ref-type="bibr" rid="B2">2</xref>".

Substituído por _apply_xref_map_safe() com a mesma lógica segmentada
de xml.py: divide nos limites de <xref> existentes e aplica o replace
apenas nos segmentos de texto puro.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
O bug: _apply_xref_map acumulava replacements dentro do mesmo segmento.
Após substituir "20" → <xref rid="B20">20</xref>, a iteração seguinte
substituía "2" dentro do próprio rid="B20" já criado, produzindo
rid="B<xref rid="B2">2</xref>0" — XML inválido.

Fix: um _apply_to_segments por citação (não um for-loop dentro do
segmento). A cada iteração, os <xref> criados tornam-se fronteiras
que protegem as iterações seguintes.

Aplicado em tasks.py (_apply_xref_map_safe) e xml.py (_apply_xref_map).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…l.py

Artigos com figuras sem figlabel levantavam KeyError. Substituídos por
.get() com fallback vazio para todos os campos opcionais de elementos
especiais (fig, table, formula).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Figuras e tabelas sem rótulo levantavam KeyError em search_special_id.
Substituídos por .get() com fallback vazio e guards de comprimento antes
de indexar figid/tabid.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-type

Fallback para "bullet" quando o parágrafo não contém o padrão esperado.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fallback para string vazia quando o regex de content_list não encontra
o padrão [list ...][/list] no parágrafo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gitnnolabs gitnnolabs merged commit 2b35786 into scieloorg:main Jun 1, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants