"""Normalizadores de campo único: espécie/suporte (1.5), características físicas (1.5/4.4), direitos (4.2), idioma (4.3), lugar (1.3.1 data tópica) e assuntos (8.1).""" from __future__ import annotations import re import unicodedata CORROMPIDO = re.compile(r'[<>{}]|ns:|PALAVRAS-CHAVE|^[0-9a-f]{6,}|\d[a-f]\d[a-f]\d') def sem_acento(s: str) -> str: return ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn') def corrompido(v: str | None) -> bool: return bool(v) and bool(CORROMPIDO.search(v)) # ── 1.5 espécie e suporte (a partir de dc.type) ───────────────────────────── ESPECIES = [ # (padrão, gênero, espécie) (r'^negativo', 'iconográfico', 'negativo fotográfico'), (r'^diapositivo', 'iconográfico', 'diapositivo'), (r'reprodu[cç][aã]o fotomec', 'iconográfico', 'reprodução fotomecânica'), (r'^(fotografia|vista panor)', 'iconográfico', 'fotografia'), (r'^gravura', 'iconográfico', 'gravura'), (r'^desenho', 'iconográfico', 'desenho'), (r'^[áa]lbum', 'iconográfico', 'álbum fotográfico'), (r'^manuscrito', 'textual', 'manuscrito'), (r'^livro', 'bibliográfico', 'livro'), (r'^artigo de peri', 'bibliográfico', 'periódico'), ] def especie(tipo: str | None) -> dict: """{'genero', 'especie', 'suporte', 'presumida'}. Sem dc.type (ou com lixo), presume fotografia — o acervo inteiro é um repositório fotográfico — e marca.""" t = (tipo or '').strip() if t and not corrompido(t): base, _, sup = t.partition('/') b = sem_acento(base.strip().lower()) for pat, gen, esp in ESPECIES: if re.search(pat, b) or re.search(pat, base.strip().lower()): suporte = sup.strip().lower() or None if esp == 'negativo fotográfico' and 'flex' in b: suporte = 'flexível' return {'genero': gen, 'especie': esp, 'suporte': suporte, 'presumida': False} return {'genero': 'iconográfico', 'especie': 'fotografia', 'suporte': None, 'presumida': True, 'original': t or None} # ── características físicas que algumas instituições põem em dc.description ── FISICO = re.compile( r'(?i)^\s*(p\s*&\s*b|p/b|preto e branco|cor(ida)?|colorid[ao]|s[ée]pia|' r'albumina|gelatina|prata|col[oó]dio|cianotipia|platina|carv[aã]o|ambr[oó]tipo|daguerre[oó]tipo|' r'negativo|acetato|nitrato|vidro|papel|cart[aã]o|\d+[,.]?\d*\s*x\s*\d+|processo:|segunda gera)') def e_fisico(v: str) -> bool: return bool(FISICO.match(v)) and len(v) < 120 def cor(fisico: list[str]) -> str | None: t = ' '.join(fisico).lower() if re.search(r'p\s*&\s*b|p/b|preto e branco', t): return 'p&b' if re.search(r'colorid|\bcor\b', t): return 'cor' return None # ── 4.2 condições de reprodução ───────────────────────────────────────────── def direitos(textos: list[str], instituicao: str) -> dict: """{'categoria', 'detentor', 'texto', 'originais'}.""" limpos = list(dict.fromkeys(t.strip() for t in textos if t and t.strip() and t.strip() != 'None')) junto = sem_acento(' '.join(limpos).lower()) if re.search(r'dominio publico|omínio público|omi?nio publico', junto): cat = 'domínio público' elif 'liberado para uso de natureza cultural' in junto: cat = 'uso cultural liberado' elif re.search(r'solicitar|requer libera', junto): cat = 'requer autorização do detentor' else: cat = 'consultar o detentor' detentor = None for t in limpos: a = sem_acento(t.lower()) if corrompido(t) or re.search(r'dominio publico|solicitar|requer|liberado', a) or len(t) < 6: continue if 'bibli' in a and 'nac' in a: detentor = 'Biblioteca Nacional (Brasil)' elif 'moreira' in a and 'salles' in a and '/' not in t: detentor = 'Instituto Moreira Salles' else: detentor = t break detentor = detentor or instituicao frases = { 'domínio público': 'Imagem em domínio público.', 'uso cultural liberado': 'Uso liberado para fins de natureza cultural.', 'requer autorização do detentor': 'Reprodução sujeita a autorização do detentor dos direitos.', 'consultar o detentor': 'Condições de reprodução não informadas na origem; consultar o detentor.', } return {'categoria': cat, 'detentor': detentor, 'texto': f'{frases[cat]} Detentor indicado: {detentor}.', 'originais': limpos or None} # ── 4.3 idioma ─────────────────────────────────────────────────────────────── NOMES_IDIOMA = {'por': 'português', 'fre': 'francês', 'eng': 'inglês', 'lat': 'latim', 'ger': 'alemão', 'ita': 'italiano', 'ukr': 'ucraniano', 'hun': 'húngaro', 'spa': 'espanhol'} def idiomas(vs: list[str]) -> list[dict]: out = [] for v in vs: c = (v or '').strip().lower() if c in NOMES_IDIOMA and c not in [o['codigo'] for o in out]: out.append({'codigo': c, 'nome': NOMES_IDIOMA[c]}) return out # ── 1.3.1 data tópica e 8.1 lugares ───────────────────────────────────────── UF = {'AC', 'AL', 'AP', 'AM', 'BA', 'CE', 'DF', 'ES', 'GO', 'MA', 'MT', 'MS', 'MG', 'PA', 'PB', 'PR', 'PE', 'PI', 'RJ', 'RN', 'RS', 'RO', 'RR', 'SC', 'SP', 'SE', 'TO'} ESTADOS = {'rio de janeiro': 'RJ', 'sao paulo': 'SP', 'bahia': 'BA', 'pernambuco': 'PE', 'minas gerais': 'MG', 'para': 'PA', 'amazonas': 'AM', 'parana': 'PR', 'rio grande do sul': 'RS', 'santa catarina': 'SC', 'ceara': 'CE', 'maranhao': 'MA', 'espirito santo': 'ES', 'goias': 'GO', 'mato grosso': 'MT', 'paraiba': 'PB', 'alagoas': 'AL', 'sergipe': 'SE', 'piaui': 'PI', 'rio grande do norte': 'RN'} PAISES = {'brasil', 'brazil'} def lugar(coberturas: list[str]) -> str | None: """A forma mais específica de cidade: 'Cidade (UF)'. Junta 'Rio de Janeiro' + 'RJ'.""" cs = [c.strip() for c in coberturas if c and c.strip() and not corrompido(c)] for c in cs: if m := re.fullmatch(r'(.+?)\s*(?:\(|,\s*|\s-\s*)([A-Z]{2})\)?', c): if m[2] in UF and sem_acento(m[1].lower()) not in PAISES: return f'{m[1].strip()} ({m[2]})' ufs = [c for c in cs if c in UF] for c in cs: k = sem_acento(c.lower()) if k in PAISES or c in UF or '(estado)' in k or 'state' in k: continue if re.search(r'\(([^)]+)\)$', c): # "Paris (França)", "Rio de Janeiro (Brazil)" nome, pais = re.match(r'(.+?)\s*\(([^)]+)\)$', c).groups() if sem_acento(pais.lower()) in PAISES: uf = ESTADOS.get(sem_acento(nome.lower())) return f'{nome} ({uf})' if uf else nome return c uf = ufs[0] if ufs else ESTADOS.get(k) return f'{c} ({uf})' if uf else c return None # ── 8.1 assuntos: separa as traduções inglesas que o DSpace da BN repete ───── EN = {'the', 'and', 'of', 'in', 'on', 'women', 'men', 'children', 'streets', 'street', 'buildings', 'building', 'churches', 'church', 'views', 'view', 'slaves', 'railroads', 'railroad', 'harbors', 'harbor', 'landscapes', 'portraits', 'bridges', 'rivers', 'houses', 'squares', 'parks', 'gardens', 'mountains', 'beaches', 'ships', 'soldiers', 'history', 'photographs', 'people', 'indians', 'coffee', 'workers', 'palaces', 'monuments', 'waterfalls', 'boats', 'plantations', 'aerial', 'military', 'city', 'cities', 'public', 'social', 'life', 'customs', 'description', 'travel', 'transportation', 'agriculture', 'industry', 'architecture', 'fountains', 'hospitals', 'schools', 'markets', 'lakes', 'bays', 'islands', 'forts', 'wars', 'war', 'emperors', 'kings', 'queens', 'nobility', 'family', 'families', 'politicians'} PT = {'de', 'da', 'do', 'das', 'dos', 'e', 'em', 'no', 'na', 'nos', 'nas', 'para', 'com'} def idioma_termo(t: str) -> str: if re.search(r'[ãõçáéíóúâêô]', t.lower()): return 'por' palavras = set(re.findall(r'[a-z]+', t.lower())) if palavras & PT: return 'por' if palavras & EN or re.search(r'\((?:brazil|state)', t.lower()): return 'eng' return 'por'