feat: derivadas de acesso com tamanhos pela NARA e CONARQ (medio 1.200, grande 2.000)
- pipeline/deriva-acesso.py gera medio/ e grande/ da matriz no finland, em WebP sRGB, sem ampliar, com retomada - MANIFEST e CONARQ.md documentam matriz, derivada de navegação e de acesso (Res. CONARQ 31/2010) e os tamanhos da NARA 2004 - INDEX.md revisto; teste-upload.txt e falhas-500 antigos saem do bucket
This commit is contained in:
@@ -254,7 +254,22 @@ Versionados no git: textos, revisão, autoridades e pendências. `unidades.jsonl
|
||||
- **Valores corrompidos na origem** (fragmentos de XML ou hash no lugar do
|
||||
texto): desconsiderados e anotados em 6.2.
|
||||
|
||||
### 5.6 Pendências fora do código
|
||||
### 5.6 Matriz e derivadas (Resolução CONARQ 31/2010)
|
||||
|
||||
A NOBRADE não trata de imagem; quem trata é a Resolução 31/2010
|
||||
(*Recomendações para digitalização de documentos arquivísticos
|
||||
permanentes*). Ela separa **matriz digital** (alta resolução, TIFF ≥ 300 dpi
|
||||
em escala 1:1, acesso restrito) de **derivadas**: de acesso (JPEG/PNG,
|
||||
formato aberto, "em variados tamanhos de acordo com a banda") e de
|
||||
navegação (thumbnail). Não fixa pixels para as derivadas.
|
||||
|
||||
Aqui a matriz é a imagem de preservação de cada instituição (`preservacao/`).
|
||||
Os tamanhos das derivadas seguem a NARA 2004, citada pela própria
|
||||
recomendação: `thumb256/` 256 px (navegação), `medio/` 1.200 px (acesso
|
||||
recomendado) e `grande/` 2.000 px (acesso ampliado), em WebP sRGB, sem
|
||||
ampliar. Tabela completa em `MANIFEST.md` → "Tamanhos das derivadas".
|
||||
|
||||
### 5.7 Pendências fora do código
|
||||
|
||||
- [ ] Pedir ao CONARQ o código da entidade custodiadora (proc. 1.1.1). O
|
||||
rascunho para o formulário CODEARQ está em
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# INDEX — Brasiliana Fotográfica → S3
|
||||
|
||||
> Índice do que foi coletado. Ver `MANIFEST.md` pra estrutura/checksums e `tasks/PLANO.md` pro estado ao vivo dos downloads (as imagens continuam sendo baixadas depois deste índice ser gerado — os metadados abaixo já são definitivos).
|
||||
> Índice do que foi coletado (coleta concluída em 2026-09-13; índice revisto em 2026-10-01). Ver `MANIFEST.md` para a estrutura do bucket, checksums, derivadas e a descrição NOBRADE em `descricao/`.
|
||||
|
||||
## Acervo DSpace (brasilianafotografica.bn.gov.br/brasiliana)
|
||||
|
||||
|
||||
+32
-4
@@ -78,8 +78,11 @@ s3://brasiliana/
|
||||
{handle-com-underscore}.webp miniatura DERIVADA por nós (não é
|
||||
material de origem — ver abaixo)
|
||||
derivados/medio/
|
||||
{handle-com-underscore}.webp imagem média (~1000 px) DERIVADA, usada
|
||||
no cartão do mural
|
||||
{handle-com-underscore}.webp derivada de acesso, 1.200 px, DERIVADA,
|
||||
aberta no visor do mural
|
||||
derivados/grande/
|
||||
{handle-com-underscore}.webp derivada de acesso ampliada, 2.000 px,
|
||||
carregada no zoom do visor
|
||||
```
|
||||
|
||||
### Sobre `derivados/`
|
||||
@@ -97,8 +100,33 @@ imagem de preservação quando ela existe e, nos 69 itens sem ela, do
|
||||
então o conjunto é uniforme.
|
||||
|
||||
Cobre **13.059 dos 13.060 handles do catálogo (99,99%)**. O único ausente
|
||||
é `7523`, cujos bitstreams a origem serve com HTTP 500. `derivados/medio/`
|
||||
tem a mesma cobertura (13.059).
|
||||
é `7523`, cujos bitstreams a origem serve com HTTP 500. `derivados/medio/` e
|
||||
`derivados/grande/` têm a mesma cobertura (13.059).
|
||||
|
||||
#### Tamanhos das derivadas (definidos em 2026-10-01)
|
||||
|
||||
Nos termos do CONARQ (Resolução 31/2010, *Recomendações para digitalização
|
||||
de documentos arquivísticos permanentes*), a imagem de preservação faz o
|
||||
papel de **matriz digital**, e o que está em `derivados/` são **derivadas**:
|
||||
a de navegação (thumbnail) e as de acesso, que o CONARQ pede em formato
|
||||
aberto e "em variados tamanhos de acordo com a banda" — sem fixar pixels.
|
||||
Os pixels seguem a NARA (*Technical Guidelines for Digitizing Archival
|
||||
Materials for Electronic Access*, 2004, tabela de fotografias), que é a
|
||||
referência citada pela própria recomendação do CONARQ:
|
||||
|
||||
| Pasta | Lado maior | CONARQ | NARA 2004 (fotografias) | Uso no mural |
|
||||
|---|--:|---|---|---|
|
||||
| `thumb256/` | 256 px | derivada de navegação | thumbnail (≤ 200 px) | fonte do atlas 3D (blocos de 128 px) |
|
||||
| `medio/` | 1.200 px | derivada de acesso | *Access – Recommended* (800–1.200 px) | imagem aberta no visor |
|
||||
| `grande/` | 2.000 px | derivada de acesso | *Larger Alternative* (1.200–2.000 px) | carregada só no zoom |
|
||||
|
||||
Regras comuns: WebP (formato aberto; o CONARQ recomenda JPEG/PNG mas admite
|
||||
outros quando o acesso exige), espaço de cor **sRGB** (pedido pela NARA para
|
||||
acesso), metadados embutidos removidos, e **nunca ampliar** — a matriz menor
|
||||
que o alvo fica no próprio tamanho. Também chega ao nível 3–4 do Europeana
|
||||
Publishing Framework (≥ 0,95 megapixel). Gerado por
|
||||
`pipeline/deriva-acesso.py` a partir da matriz (ou, nos 68 itens sem ela, da
|
||||
imagem servida pelo DSpace).
|
||||
|
||||
Contagem de itens: o `catalogo.jsonl` final tem **13.060 handles distintos**,
|
||||
todos itens reais. O falso-positivo `20.500.12156.1/1` (página raiz da
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Derivadas de acesso (CONARQ, Res. 31/2010) a partir da matriz de preservação.
|
||||
|
||||
Gera, para cada item, duas cópias WebP em sRGB, sem metadados embutidos e sem
|
||||
nunca ampliar (o que for menor que o alvo fica no tamanho original):
|
||||
|
||||
derivados/medio/<handle>.webp 1.200 px no lado maior NARA 2004 "Access – Recommended"
|
||||
derivados/grande/<handle>.webp 2.000 px no lado maior NARA 2004 "Larger Alternative"
|
||||
|
||||
A miniatura (derivados/thumb256/, 256 px) já existe e não muda. Ver a tabela
|
||||
de tamanhos em MANIFEST.md e CONARQ.md.
|
||||
|
||||
Roda no finland (tráfego intra-Hetzner, o bucket é hel1):
|
||||
scp pipeline/deriva-acesso.py derivar.jsonl finland:/root/brasiliana/
|
||||
ssh finland 'cd /root/brasiliana && set -a && . /root/.s3env && set +a && \
|
||||
nohup python3 deriva-acesso.py derivar.jsonl > deriva-acesso.log 2>&1 &'
|
||||
|
||||
`derivar.jsonl`: uma linha {handle, fonte} por item, fonte = chave no bucket da
|
||||
imagem de preservação (ou, sem ela, da imagem servida pelo DSpace). O progresso
|
||||
vai para `deriva-acesso.resultados.jsonl`; rodar de novo retoma de onde parou.
|
||||
Opções: --seco (gera em /tmp e não sobe), --limite N, --threads N.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
import boto3
|
||||
|
||||
BUCKET = os.environ.get('ARCHIVE_BUCKET', 'brasiliana')
|
||||
ENDPOINT = os.environ.get('S3_ENDPOINT', 'https://hel1.your-objectstorage.com')
|
||||
ALVOS = [ # (pasta, lado maior em px, qualidade WebP)
|
||||
('medio', 1200, 80),
|
||||
('grande', 2000, 82),
|
||||
]
|
||||
CACHE = 'public, max-age=604800'
|
||||
|
||||
trava = threading.Lock()
|
||||
|
||||
|
||||
def processa(s3, item, seco, saida_seca):
|
||||
h = item['handle']
|
||||
pasta = h.replace('/', '_')
|
||||
tmp = tempfile.mkdtemp(prefix='deriva-')
|
||||
try:
|
||||
ext = os.path.splitext(item['fonte'])[1] or '.img'
|
||||
src = os.path.join(tmp, 'src' + ext)
|
||||
s3.download_file(BUCKET, item['fonte'], src)
|
||||
feitos = []
|
||||
for nome, lado, q in ALVOS:
|
||||
out = os.path.join(tmp, f'{nome}.webp')
|
||||
# "WxH>" = só reduz; sRGB na saída (NARA pede sRGB nas derivadas de acesso)
|
||||
r = subprocess.run(['vipsthumbnail', src, '--size', f'{lado}x{lado}>',
|
||||
'--export-profile', 'srgb', '-o', f'{out}[Q={q},strip]'],
|
||||
capture_output=True, text=True, timeout=600)
|
||||
if r.returncode != 0 or not os.path.exists(out):
|
||||
# perfil de cor ausente ou ilegível: tenta sem conversão de perfil
|
||||
r = subprocess.run(['vipsthumbnail', src, '--size', f'{lado}x{lado}>',
|
||||
'-o', f'{out}[Q={q},strip]'], capture_output=True, text=True, timeout=600)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f'vipsthumbnail {nome}: {r.stderr.strip()[:300]}')
|
||||
dims = subprocess.run(['vipsheader', '-f', 'width', out], capture_output=True, text=True).stdout.strip(), \
|
||||
subprocess.run(['vipsheader', '-f', 'height', out], capture_output=True, text=True).stdout.strip()
|
||||
dado = open(out, 'rb').read()
|
||||
chave = f'derivados/{nome}/{pasta}.webp'
|
||||
if seco:
|
||||
shutil.copy(out, os.path.join(saida_seca, f'{pasta}.{nome}.webp'))
|
||||
else:
|
||||
s3.put_object(Bucket=BUCKET, Key=chave, Body=dado, ACL='public-read',
|
||||
ContentType='image/webp', CacheControl=CACHE)
|
||||
feitos.append({'tipo': f'derivado-{nome}', 'chave': chave, 'bytes': len(dado),
|
||||
'sha256': hashlib.sha256(dado).hexdigest(), 'w': int(dims[0]), 'h': int(dims[1])})
|
||||
return {'handle': h, 'fonte': item['fonte'], 'ok': True, 'arquivos': feitos,
|
||||
'quando': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}
|
||||
except Exception as e: # noqa: BLE001 — um item ruim não para a fila
|
||||
return {'handle': h, 'fonte': item['fonte'], 'ok': False, 'erro': f'{type(e).__name__}: {e}'[:400]}
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('lista')
|
||||
ap.add_argument('--seco', action='store_true')
|
||||
ap.add_argument('--limite', type=int, default=0)
|
||||
ap.add_argument('--threads', type=int, default=4)
|
||||
ap.add_argument('--resultados', default='deriva-acesso.resultados.jsonl')
|
||||
a = ap.parse_args()
|
||||
|
||||
itens = [json.loads(l) for l in open(a.lista, encoding='utf-8') if l.strip()]
|
||||
feitos = set()
|
||||
if os.path.exists(a.resultados) and not a.seco:
|
||||
for l in open(a.resultados, encoding='utf-8'):
|
||||
r = json.loads(l)
|
||||
if r.get('ok'):
|
||||
feitos.add(r['handle'])
|
||||
fila = [i for i in itens if i['handle'] not in feitos]
|
||||
if a.limite:
|
||||
fila = fila[:a.limite]
|
||||
saida_seca = tempfile.mkdtemp(prefix='deriva-seco-') if a.seco else None
|
||||
print(f'{len(itens)} itens, {len(feitos)} já feitos, {len(fila)} na fila'
|
||||
+ (f' (seco → {saida_seca})' if a.seco else ''), flush=True)
|
||||
|
||||
s3 = boto3.client('s3', endpoint_url=ENDPOINT)
|
||||
t0, n, erros = time.time(), 0, 0
|
||||
with ThreadPoolExecutor(a.threads) as ex, open(a.resultados if not a.seco else os.devnull, 'a', encoding='utf-8') as res:
|
||||
for fut in as_completed([ex.submit(processa, s3, i, a.seco, saida_seca) for i in fila]):
|
||||
r = fut.result()
|
||||
n += 1
|
||||
erros += not r['ok']
|
||||
with trava:
|
||||
res.write(json.dumps(r, ensure_ascii=False) + '\n')
|
||||
res.flush()
|
||||
if not r['ok']:
|
||||
print(f"ERRO {r['handle']}: {r['erro']}", flush=True)
|
||||
if n % 100 == 0 or n == len(fila):
|
||||
dt = time.time() - t0
|
||||
print(f'{n}/{len(fila)} · {erros} erros · {n / dt * 60:.0f}/min · '
|
||||
f'faltam ~{(len(fila) - n) / max(n / dt, 1e-9) / 60:.0f} min', flush=True)
|
||||
print('FIM', flush=True)
|
||||
return 1 if erros else 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -24,6 +24,12 @@ ENTIDADE_SITE = 'https://xn--2dk.xyz/brasiliana/'
|
||||
COLECAO = 'BF'
|
||||
COLECAO_TITULO = 'Coleção Brasiliana Fotográfica (cópia digital)'
|
||||
RESPONSAVEL = 'rafapolo'
|
||||
|
||||
# Derivadas de acesso no bucket (CONARQ Res. 31/2010; tamanhos pela NARA 2004):
|
||||
# thumb256/ 256 px (navegação) · medio/ 1.200 px (acesso) · grande/ 2.000 px
|
||||
# (acesso ampliado). GRANDE liga a troca para grande/ no zoom do visor —
|
||||
# só True depois que pipeline/deriva-acesso.py cobriu o acervo inteiro.
|
||||
GRANDE = False
|
||||
BUCKET = 's3://brasiliana'
|
||||
|
||||
REGRAS = ('NOBRADE: Norma brasileira de descrição arquivística (CONARQ, 2006); '
|
||||
|
||||
@@ -11,12 +11,13 @@ Campos novos em meta (curtos: o mapa inteiro desce para o navegador):
|
||||
ai índice em `autoridades` (2.1, forma autorizada ISAAR(CPF))
|
||||
dq 1 quando a data foi atribuída pelo arquivista (proc. 1.3.3.2)
|
||||
No topo do mapa: `series`, `autoridades`, `instituicoes` (community → nome
|
||||
autorizado) e `colecao` (código e título do nível 1).
|
||||
autorizado), `colecao` (código e título do nível 1) e `grande` (há derivada
|
||||
de 2.000 px para o zoom).
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from .config import COLECAO_TITULO, INSTITUICOES, RAIZ, SAIDA, codigo
|
||||
from .config import COLECAO_TITULO, GRANDE, INSTITUICOES, RAIZ, SAIDA, codigo
|
||||
|
||||
MAPA = RAIZ / 'web' / 'atlas' / 'atlas-map.json'
|
||||
|
||||
@@ -48,6 +49,7 @@ def main():
|
||||
d['autoridades'] = formas
|
||||
d['instituicoes'] = {k: v[1] for k, v in INSTITUICOES.items()}
|
||||
d['colecao'] = {'codigo': codigo('BF'), 'titulo': COLECAO_TITULO}
|
||||
d['grande'] = GRANDE # o visor só pede derivados/grande/ quando isto é verdade
|
||||
feitos = 0
|
||||
for h, e in d['map'].items():
|
||||
u = dossies.get(h)
|
||||
|
||||
+3
-2
@@ -1541,7 +1541,8 @@ function citation(data, meta, inst, aut) {
|
||||
// anchored under the cursor / between the fingers, and panning is clamped so
|
||||
// the image never leaves the stage. A sharper copy (derivados/grande) is
|
||||
// swapped in once the zoom passes what the medium copy can show, keeping the
|
||||
// on-screen size exactly where it was.
|
||||
// on-screen size exactly where it was. Sizes: medio 1.200 px, grande 2.000 px
|
||||
// (CONARQ derivadas de acesso; NARA 2004 "Access – Recommended" / "Larger Alternative").
|
||||
const wrapEl = document.getElementById('card-wrap');
|
||||
const stage = document.getElementById('stage');
|
||||
const stImg = document.getElementById('card-img');
|
||||
@@ -1604,7 +1605,7 @@ function stageLoad(data) {
|
||||
Z.handle = h; Z.touched = false; Z.grande = false; Z.nw = 0;
|
||||
stImg.classList.remove('ready');
|
||||
stImg.alt = (data.meta || {}).title || '';
|
||||
// the atlas tile first (instant, blurry), then the medium copy (~1000 px)
|
||||
// the atlas tile first (instant, blurry), then the medium copy (1.200 px, NARA "access")
|
||||
const entry = atlasEntries[h], page = atlasSources[entry?.atlas || 0];
|
||||
let got = false;
|
||||
const big = new Image();
|
||||
|
||||
Reference in New Issue
Block a user