Files
brasiliano/tasks/PLANO.md
T

24 KiB
Raw Blame History

PLANO — Brasiliana Fotográfica → S3

Comece por aqui. Documento de transferência: uma sessão nova consegue retomar o trabalho só com este arquivo. Detalhe técnico completo em README.md.

Objetivo: os 13.036 itens da Brasiliana Fotográfica — metadados e imagens em qualidade de preservação — depositados em s3://brasiliana, organizados como acervo arquivístico (procedência + checksums).

Autorização: a Biblioteca Nacional foi consultada. Confirmou que o material é público e que não tem equipe técnica para fornecer um dump; a coleta por scraping é o caminho combinado. Ritmo cortês e identificação continuam valendo.

Reorganização de pastas em 2026-09-10. Scripts + estado (catálogo, .sqlite, .log, .tmp/, credenciais) mudaram de raiz para pipeline/; visualizações foram para web/; exemplo-item/ virou examples/exemplo-item/; censo.py virou archive/censo.py. Todo comando de terminal abaixo já foi atualizado para o novo caminho. Detalhe completo em README.md → "Estrutura do repositório".

Mudança de plano em 2026-09-10 (importante, ver 2.6): o achado 2.4 abaixo não se sustentou. Cloudflare passou a bloquear bitstream via curl também pra itens sem cache de borda — não só HTML. Tudo (Fases 2 e 3) roda pelo navegador agora, trilha única, não mais A/B separadas por tipo de nome. A Fase 4 (legado) segue via curl normalmente — domínio diferente (objdigital.bn.br), sem esse bloqueio.


1. Estado em 2026-09-10

Fase 1 (metadados) ✅ completa — 13.059 registros, 0 erros. 1 falso-positivo conhecido (.../1, é a página raiz da comunidade, não um item — excluir na contagem final)
Catálogo pipeline/catalogo.jsonl, re-exportado depois dos reparos (152 itens com all_preservation_urls)
Download principal (1 imagem/item) 🟡 em andamento no navegador
Thumbnails 🟡 em andamento no navegador
Extras (itens com múltiplos bitstreams, 115 itens) ✅ completo
Fase 4 (legado objdigital) 🟡 em andamento, local (baixador.py --somente-legado), 2.707 itens com legacy_url
subidor.py rodando em pipeline/, --vigiar, roteia preservacao/thumbnail/legado certo

Não existe mais aba do Brave rodando isso — é uma aba de Chrome comum, conectada via extensão Claude-in-Chrome. coletor.js/coletor-resume.js grava em IndexedDB a cada item; fechar a aba não perde nada, mas o loop para e precisa recolar o script certo pra retomar (ver tasks/TODO.md pros comandos exatos de cada trilha ativa).


2. Cinco achados que não devem ser redescobertos

Cada um destes custou horas. Nenhum é óbvio, e vários são contraintuitivos.

2.1 O sitemap está sete anos desatualizado

htmlmap/sitemap são de abril/2019 e listam ~5.300 dos 13.036 itens. Enumerar por eles perde 59% do acervo sem erro aparente — o crawl termina limpo, pela metade. Fonte correta: browse?type=title&rpp=100&offset=N, percorrido por comunidade (o que de quebra etiqueta a instituição de origem).

2.2 O arquivo de preservação está no DSpace, não no objdigital

O DC.identifier aponta para objdigital.bn.br e parece ser o master. É o contrário: o bitstream do DSpace tem 5533×4065 (7,4 MB) contra ~2600×2100 (147 KB) do objdigital. O maior medido: 8679×6484, 34,7 MB — EXIF mostra Hasselblad H3D II-39MS a 7216×5412. Inverter isso arquiva o acervo a um quarto da resolução, e nada avisa.

O PDF de 71,9 MB do item 14582 no objdigital não é a mesma imagem melhor: é outro objeto, um álbum de várias páginas.

2.3 O Cloudflare bloqueia automação de navegador — não IP, não geografia

Medido na mesma máquina, mesmo IP, mesmo instante:

Cliente Páginas HTML
Chrome/Brave do usuário ✅ 200
fetch() de dentro da página ✅ 200 (>100 seguidas)
Playwright headed ❌ 200 na 1ª, 403 depois
Playwright headless ❌ 403
curl / httpx ❌ 403
Servidor (Hetzner Helsinki) ❌ 403

Proxies não ajudam — o bloqueio dispara na segunda requisição da mesma sessão, não por origem. Testado e descartado. Não repita.

2.4 Bitstreams passam por curl; páginas HTML não

bitstream/... responde 200 a curl -L simples, até com UA de curl. Só o HTML é desafiado. É isso que permite baixar os 116 GB fora do navegador, num servidor.

2.5 Nome de arquivo com percent-encoding leva 403

Testado com cinco user-agents (Chrome, Safari, Firefox, Googlebot, curl): todos 403 numa URL com %28/%20, enquanto curl/8.7.1 puro dá 200 numa URL de nome simples. O gatilho é o caminho, não o cliente — variar user-agent não resolve. O navegador busca os dois casos sem problema.

Daí as duas trilhas — modelo original, superado, ver 2.6. Itens da BN usam imagem.jpg (trilha A); os do IMS usam nomes descritivos com espaços e acentos (trilha B).

2.6 O achado 2.4 não se sustentou — Cloudflare bloqueia bitstream também

Testado ao vivo em 2026-09-10: item nunca antes acessado, buscado pelo navegador (200 OK) e imediatamente depois via curl na Finlândia — 403, página real do Cloudflare ("Attention Required"). O único item que passava por curl (6066) só passava porque já tinha sido baixado com sucesso antes e ficou em cache de borda (cf-cache-status: HIT) — sobra de um caso, não regra geral. Pré-aquecer a URL pelo navegador antes de tentar via curl não transfere o cache pra tornar o curl aceito (testado, falha igual).

Consequência: não existe mais trilha A via servidor. Tudo — nome simples ou com encoding — precisa passar pelo navegador vivo (fetch() de dentro da página). baixador.py na Finlândia fica só pra Fase 4 (objdigital, domínio diferente, sem esse bloqueio — ver 2.11).

2.7 Itens com múltiplos bitstreams reais — não é caso raro

115 itens (de ~13.058) têm 2 a 6 imagens reais no bundle ORIGINAL, não uma. citation_pdf_url não é gerado pra esses (o gerador de citação do DSpace não sabe qual é "a" imagem quando há mais de uma) — por isso ficam sem preservation_url na extração simples. Sintoma: full_table tem dc.description mencionando álbum/página (ex.: "Álbum 0259"), igual ao caso do PDF de 71,9 MB do achado 2.2 — mesmo fenômeno, mas dentro do próprio DSpace, não só no objdigital.

Como enumerar as imagens reais de um item, em ordem de confiabilidade:

  1. Contar .file-wrapper na página normal do item (não ?show=full) — bate com o número real de bitstreams.
  2. Tentar sequências /bitstream/{PREFIX}/{h}/{seq}/qualquer-nome.jpg pra seq de 1 até N — funciona quando as sequências são contíguas a partir de 1 (maioria dos casos).
  3. Quando (2) falha (sequências não contíguas): pegar o nome real do arquivo a partir do <img src> de cada .file-wrapper (é a miniatura, nome com .jpg duplicado no final — ex. nome.jpg.jpg), tirar o último .jpg, e buscar /bitstream/handle/{PREFIX}/{h}/{nome-real} (rota por nome exato, não por sequência).

preservation_url = a maior imagem encontrada (compatibilidade com o pipeline); all_preservation_urls/all_preservation_sizes guardam todas, ordenadas da maior pra menor.

Exceção conhecida sem conserto: handle 20.500.12156.1/7523 — HTTP 500 do próprio servidor da BN ao buscar os bitstreams (não é problema de nome/padrão). Provável arquivo corrompido do lado deles.

2.8b Achado 2.8 falha ~62% das vezes em alguns lotes — bitstream de LICENÇA, não thumbnail

seq_master+1 às vezes é o bitstream de LICENÇA do DSpace (texto puro, "LICENÇA DE VEICULAÇÃO E DISTRIBUIÇÃO..."), não a miniatura — visto em Marinha e IMS, 2.655 de 4.278 thumbnails (62%!) baixados errado num lote, 146 já tinham subido pro bucket antes de descobrir (limpo). O filtro de tamanho do v1 (> 2MB → rejeita) não pega isso: o texto da licença é pequeno o bastante (781 bytes) pra passar. Fix: validar magic bytes (JPEG FFD8/PNG) antes de aceitar, tentar seq+2/+3 se falhar, e cair pro método do achado 2.7 (<img src> da página) como último recurso — baixador-thumb.js v2. Também blindado no subidor.py: rejeita qualquer upload cujo conteúdo não comece com magic bytes de imagem, independente do nome do arquivo (defesa em camadas).

Lição: tamanho pequeno não é prova de "é a miniatura" — só magic bytes provam que é imagem de verdade.

2.8 Miniatura gerada pelo DSpace: sequência N+1 (quando contígua)

Pra item com bitstream original na sequência N, a miniatura que o DSpace gera automaticamente costuma ficar na sequência N+1 — confirmado no handle 6066: seq 1 = master (7,7 MB), seq 2 = thumb (56 KB). Mesma rota filename-agnostic do achado 2.4 original (o nome na URL não importa, só a sequência). Quando as sequências não são contíguas (itens do achado 2.7 via método 3), a miniatura de cada imagem é o próprio <img src> do .file-wrapper correspondente — não precisa de request extra.

2.9 objdigital às vezes tem PDF (álbum), não só JPG — extração pegava o identificador errado

Alguns itens têm mais de um DC.identifier apontando pro objdigital: um .htm (página de landing) e um .pdf (o álbum de verdade, múltiplas páginas — mesmo fenômeno do PDF de 71,9 MB do item 14582, achado 2.2). A extração original pegava o primeiro identificador que batesse com "objdigital", que pra 44 itens (a maioria uma sequência do Museu Aeroespacial, 14526-14570, mais o 584) era o .htm, não o .pdf — perdendo o conteúdo real. De 235 itens com PDF disponível, 191 já estavam certos por acaso (o PDF vinha primeiro na lista); só 44 precisaram de correção manual (feita em catalogo.jsonl e no IndexedDB, antes do baixador.py --somente-legado alcançar esses handles — nada foi contaminado no bucket). Vale re-checar outros itens com múltiplos DC.identifier de objdigital por precaução, não só os que têm .pdf.

Recheque feito (2026-09-10): mapeamos todos os padrões de extensão em 2.231 itens com 2+ identifiers objdigital. Achados adicionais:

  • 5 itens (1081, 1082, 1084, 4077, 4084) têm só HTML, sem .jpg/.pdf nenhum — são páginas de um visualizador de zoom profundo (OpenSeadragon/DZI, pirâmide de blocos, não um arquivo único). 3 já tinham sido baixados e subidos como se fossem imagem (arquivos de ~2,4 KB, só a página HTML do visualizador) — limpo do bucket, legacy_url zerado pros 5 (não vale a complexidade de espelhar a pirâmide de tiles pra tão poucos itens).
  • 11 itens têm .sid (MrSID, formato comprimido de imagem grande) além do .jpg — não capturado, ficou como gap conhecido (baixo volume, não priorizado).

2.10 O crawl da Fase 1 não é 100% completo — achado via referência cruzada com o WordPress

Cruzando os handles citados nos 636 posts do WordPress contra o catálogo, 2 itens reais (1799 "Princesa Isabel menina", 4798 "Vista panorâmica de Canudos...") existem no site mas não foram enumerados pela Fase 1 — ambos sem citation_pdf_url, formato de metadado antigo (um é de 2015-04-16, uma das primeiras entradas do sistema). Adicionados manualmente ao catálogo depois de descobertos.

Isso só foi achado por acaso (porque um post do blog citava esses handles especificamente) — não existe verificação exaustiva de que a enumeração por browse?type=title capturou 100% dos itens de cada comunidade. Pode haver mais itens faltando que nenhum post referencia. Não investigado a fundo: seria preciso comparar a contagem de cada comunidade contra alguma fonte independente (a BN não expõe OAI-PMH nem count via API, até onde sabemos).

2.11 O objdigital só responde em HTTPS — não era bloqueio

legacy_url na fonte vem como http://objdigital.bn.br/.... Conexão HTTP simples (porta 80) é resetada (Recv failure: Connection reset by peer), local e da Finlândia. HTTPS funciona liso com curl puro, local (não testado da Finlândia — motivo original de "não alcançável" pode ser outro, não investigado). Fix: forçar https:// no legacy_url antes de baixar (já aplicado em baixador.py).

2.12 Caractere # no nome do arquivo corrompe URL no IndexedDB

78 itens (mesmo álbum "Album 0035/0259/0260") têm # literal no nome real do arquivo (ex.: Alb 0260 199 CT #.jpg). Algum processo que passou esse nome por new URL(...).pathname sofreu o efeito clássico: # não percentado é tratado como início de fragmento da URL, e tudo depois dele é descartado — a URL salva no IndexedDB virou .../Alb%20...%20CT%20 (sem sequência, sem #, sem .jpg), 404 garantido. O catalogo.jsonl local nunca foi tocado por esse bug (tem a URL certa, com %23) — fix é só reescrever o campo no IndexedDB a partir do catálogo (pipeline/reparo-hash-url.js).

Lição: nunca usar new URL()/.pathname pra manipular nomes de arquivo vindos de fonte externa sem antes confirmar que não têm #, ? ou outros caracteres especiais de URL não codificados.

2.13 O mesmo # quebra de novo — desta vez no redirect do próprio DSpace

Mesmo com a URL do achado 2.12 corrigida no IndexedDB (%23 correto, forma antiga /bitstream/PREFIX/ID/SEQ/arquivo), os 78 itens continuavam dando 404. O DSpace responde a essa forma antiga com um redirect para a forma canônica nova (/bitstream/handle/PREFIX/ID/arquivo, sem número de sequência) — e o Location do redirect tem o # sem escapar de novo, então o fetch() do navegador trunca no fragmento ao seguir o redirect (finalUrl chega faltando #.jpg, 404). Fix: pedir direto a forma canônica (/bitstream/handle/...), pulando o redirect — testado, 200, JPEG válido. preservation_url desses 78 itens reescrito no IndexedDB para a forma canônica.

Lição: com # no nome do arquivo, não confiar em nenhum redirect do servidor — ele pode ter o mesmo bug do lado de lá. Preferir sempre a URL final/canônica quando ela for conhecida.

2.14 Dá pra ler o progresso do BAIXA/BAIXA_THUMB sem o Chrome conectado

O progresso das trilhas via navegador (localStorage.bras_baixados, bras_thumbs_v2) sobrevive num arquivo leveldb comum no disco — ~/Library/Application Support/Google/Chrome/Default/Local Storage/leveldb — mesmo com a extensão Claude-in-Chrome desconectada (ex.: conflito de conta OAuth, sessão anterior perdida). Basta copiar o diretório (evita brigar com o LOCK que o Chrome mantém aberto) e ler com uma lib leveldb qualquer (classic-level via npm, sem dependência nativa externa). O valor vem com 1 byte de prefixo indicando encoding (0x00=UTF-16LE, 0x01=UTF-8/Latin1) antes do JSON de verdade. Script pronto: pipeline/tools/ler-localstorage.js.

Lição: perder a conexão com a extensão do navegador não significa perder visibilidade do progresso — boa parte do estado do app mora em arquivos comuns no disco do usuário, não só "dentro do navegador".


3. O acervo

DSpace 4.2, handles 20.500.12156.1/*. 13.058 itens reais (13.059 descobertos no crawl, menos 1 falso-positivo — .../1 é a página raiz da comunidade, não um item). Contagem original por comunidade abaixo era de 13.036; a diferença é ruído de borda (comunidades ganham/perdem item ao longo do tempo), não erro de coleta.

Instituição Itens Instituição Itens
Instituto Moreira Salles 5.133 Fiocruz 548
Biblioteca Nacional 2.726 Marinha (DPHDM) 456
Museu Aeroespacial 1.143 Arquivo Nacional 383
Museu da República 906 Arq. Geral da Cidade do RJ 248
Museu Histórico Nacional 649 Leibniz-Institut, Leipzig 216
IHGB 206 Fund. Joaquim Nabuco 190
Museu Goeldi 95 FGV CPDOC 79
IBGE 58

~139,5 GB projetados (recalculado em 2026-09-10 com médias reais de 357-474 arquivos já no bucket por categoria, não mais os 6 originais): preservação+extras ~136,9 GB (13.152 arquivos, 10,7 MB médio), thumbnails ~0,9 GB, legado ~1,5 GB (taxa de sucesso ~75%, muito 404 genuíno), metadados+WordPress+catálogo <0,2 GB combinados.


4. Runbook

Fase 1 — metadados (navegador) ✅ COMPLETA

Cole pipeline/coletor-resume.js (pendentes + erros juntos, numa passada só) numa aba em https://brasilianafotografica.bn.gov.br/brasiliana/. Acompanhar com await BRAS.status(). Deixe a aba ativa na janela dela: o Chromium estrangula setTimeout em abas de fundo até 1×/min, e o loop passa a rastejar sem erro nenhum. Ao terminar: await BRAS.exportar().

Fases 2 e 3 — trilha única via navegador (não tem mais trilha A/servidor)

Ver achado 2.6. Na mesma aba (mesmo IndexedDB bras), cole em sequência:

// pipeline/baixador-navegador.js -- imagem principal, todos os itens
BAIXA.quantos(); BAIXA.iniciar();
// pipeline/baixador-thumb.js -- miniaturas
BAIXA_THUMB.quantos(); BAIXA_THUMB.iniciar();
// pipeline/baixador-extras.js -- 2a+ imagem dos itens com multiplos bitstreams
// (rodar reparo-multibitstream.js antes, se ainda nao rodou)
BAIXA_EXTRA.quantos(); BAIXA_EXTRA.iniciar();

Em paralelo, em outro terminal:

cd pipeline
set -a; . .s3env; set +a
python3 subidor.py --vigiar --catalogo catalogo.jsonl

Downloads automáticos do Chrome exigem permissão em chrome://settings/content/automaticDownloads (adicionar https://brasilianafotografica.bn.gov.br) — sem isso o download "funciona" no JS (sem erro) mas nenhum arquivo chega no disco. Vale pro domínio inteiro, uma vez só.

Fase 4 — cópias legadas (máquina local)

cd pipeline
set -a; . .s3env; set +a
python3 baixador.py --catalogo catalogo.jsonl --somente-legado

Via curl direto, sem navegador (ver achado 2.11). ~2.707 itens têm legacy_url, ~328 KB em média.


5. Arquivos e credenciais

Layout do repo (detalhe em README.md → "Estrutura do repositório"): scripts + estado ao vivo em pipeline/, visualizações em web/, item de referência em examples/, código morto em archive/.

Arquivo O quê
tasks/PLANO.md Este documento
README.md Referência técnica completa (raiz do projeto)
pipeline/coletor.js Fase 1 — console do navegador, IndexedDB, autoinício
pipeline/baixador.py Fases 2 e 4 — curl → S3, um arquivo por vez
pipeline/baixador-navegador.js Fase 3 — navegador → pasta Downloads
pipeline/subidor.py Fase 3 — Downloads → S3, apaga na hora
archive/censo.py Versão Playwright — não funciona, mantida pelo parsing
pipeline/.s3env.exemplo Modelo; chaves reais em /Users/polux/Projetos/tocador/.env

S3: hel1.your-objectstorage.com, bucket brasiliana (Hetzner). Credenciais em /Users/polux/Projetos/tocador/.env (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY). Cópia local em pipeline/.s3env. Na Finlândia: /root/.s3env.

Servidor: ssh finland → 89.167.95.136 (Hetzner Helsinki, root, 31 GB livres).


6. Estrutura no bucket

s3://brasiliana/
  MANIFEST.md                          o que é, como foi coletado, quando
  catalogo.jsonl                       uma linha por item
  itens/20.500.12156.1_6066/
      metadata.json                    Dublin Core + URL de origem + data UTC
      preservacao/imagem.jpg           5533×4065, 7,4 MB
      legado/icon843660.jpg            147 KB, referência de origem
      manifest-sha256.txt              checksum de cada arquivo

Não negociável: SHA-256 de tudo e URL de origem + data/hora de captura. Sem os dois é pasta de imagens, não arquivo.


7. Pendências

  1. Terminar a Fase 1 e exportar o catálogo.
  2. Medir a fração da trilha B (BAIXA.quantos()) — de graça, sem requisição.
  3. Confirmar a projeção de ~116 GB (hoje: 6 arquivos medidos).
  4. Decidir sobre os PDFs de álbum do objdigital — objetos distintos, o de 71,9 MB provavelmente não é caso isolado.
  5. Gerar MANIFEST.md e catalogo.jsonl no bucket ao final.

8. Direitos

Majoritariamente domínio público por antiguidade, mas as 15 instituições têm políticas distintas. dc.rights e a instituição de origem são preservados por item: a decisão de redistribuir é item a item, não do acervo em bloco. Arquivar para si é uma coisa; republicar é outra.

9. Descrição arquivística (NOBRADE)

Resumo da norma e decisões de aplicação em CONARQ.md. Implementação no branch feat/nobrade.

# Tarefa Entrega Depende de
0 — Decisões
0.1 Definir nome da entidade custodiadora e código provisório (BR XYZ) pipeline/nobrade/config.py —
0.2 Pedir código de entidade ao CONARQ e-mail enviado 0.1
0.3 Fixar a convenção do código de referência (BR XYZ BF.IMS.GF.12345) CONARQ.md §5.2 0.1
1 — Normalizadores (cada um gera uma tabela de revisão)
1.1 Datas: mais de 20 formatos de dc.date → forma NOBRADE ([1860?], [18--]) + ISO 8601 + original descricao/revisao/datas.tsv —
1.2 Atribuir data aproximada aos s.d. a partir do fotógrafo coluna em datas.tsv 1.1, 1.4
1.3 Séries: 2.657 combinações de dc.source → séries canônicas por instituição descricao/revisao/series.tsv —
1.4 Autoridades: fotógrafos (dc.contributor) e colecionadores, com variantes deduplicadas descricao/autoridades.jsonl —
1.5 Suporte/espécie: dc.type → gênero + espécie + suporte descricao/revisao/tipos.tsv —
1.6 Direitos: dc.rights → ~4 categorias, mantendo o original descricao/revisao/direitos.tsv —
1.7 Data tópica: dc.coverage → cidade (UF) campo 1.3 —
1.8 Assuntos: dc.subject separado em pt/en campo 8.1 —
2 — Montagem da hierarquia
2.1 nobrade: níveis 0→5 com id, parent_id, nivel, codigo descricao/unidades.jsonl 0.3, 1.3
2.2 Preencher os elementos por nível a partir dos normalizadores campos "1.3", "2.1"… 1.x, 2.1
2.3 Nível 5 para os handles com várias imagens registros filhos 2.1
2.4 Somar dimensão e suporte nos níveis 1–3 campo 1.5 agregado 2.2
3 — Textos dos níveis altos
3.1 Nível 1: história arquivística, procedência, acesso, arranjo descricao/textos/nivel1.md —
3.2 Nível 2: breve histórico das 16 instituições descricao/textos/secoes/ —
3.3 Nível 3: âmbito e conteúdo das séries principais descricao/textos/series/ 1.3
3.4 Biografias de fotógrafos a partir dos posts do WordPress campo 2.2 das autoridades 1.4
4 — Relações e fontes
4.1 5.1 Originais: instituição + handle + Sophia + objdigital campo automático 2.1
4.2 5.2 Cópias: preservação, legado, miniaturas no S3 campo automático 2.1
4.3 5.4 Publicações: posts do WordPress → handles citados campo 5.4 —
5 — Validação
5.1 Validador dos 7 obrigatórios por nível descricao/pendencias.md 2.x
5.2 Rodar, corrigir e repetir até zerar as pendências relatório limpo 5.1
6 — Exportação e publicação
6.1 Exportar EAD 3 descricao/ead/brasiliana.xml 5.2
6.2 Exportar EAC-CPF descricao/eac/*.xml 1.4
6.3 Validar contra os XSD xmllint limpo 6.1, 6.2
6.4 Subir descricao/ para s3://brasiliana/descricao/ e atualizar o MANIFEST objetos no bucket 6.3
6.5 (opcional) CSV de importação do AtoM descricao/atom.csv 5.2
7 — Mural
7.1 Cartão com código de referência, data normalizada, suporte, "original em" web/wall.html 2.2
7.2 Filtro por série (nível 3) web/wall.html 1.3
7.3 Publicar em xyz/brasiliana commit 7.x