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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user