feat: linha do tempo processual dos autos vinculados Pet16704
Substitui o gitGraph do Mermaid (ilegível com 3.437 commits) por uma timeline interativa com eixo temporal real, filtros por instituição, tipo de documento e pessoas mencionadas (extraidas via NER local com pierreguillou/ner-bert-base-cased-pt-lenerbr).
This commit is contained in:
@@ -0,0 +1 @@
|
||||
.DS_Store
|
||||
@@ -0,0 +1,94 @@
|
||||
# Pet16704 — Linha do Tempo Processual
|
||||
|
||||
Visualização interativa da linha do tempo dos autos vinculados **Pet16704** (STF) — um ponto por
|
||||
documento, agrupado pela instituição de origem (Relatoria, PGR, Polícia Federal, Partes e Defesas,
|
||||
Secretaria e Cartório) e posicionado pela data real de juntada.
|
||||
|
||||
**Ver a página publicada:** (preencha aqui o link do GitHub Pages depois do primeiro deploy)
|
||||
|
||||
## Por que isso existe
|
||||
|
||||
O material de origem trazia um `gitGraph` do Mermaid com um commit por documento (3.437 no total).
|
||||
Isso não escala: o Mermaid posiciona commits em sequência fixa, sem eixo temporal real, e o SVG
|
||||
gerado saía em **16.368 × 40 px** — ilegível. As descrições também vinham truncadas em ~100
|
||||
caracteres pelas tags do Mermaid.
|
||||
|
||||
Este projeto troca aquele gráfico por uma timeline de verdade:
|
||||
|
||||
- eixo X com datas reais (não sequência de commits) e zoom/pan sobre o período;
|
||||
- uma faixa de visão geral (histograma mensal empilhado por instituição) para navegar entre o
|
||||
período parado (2018–2025) e a explosão de atividade em 2026;
|
||||
- textos completos, sem truncamento, extraídos de `TIMELINE.md`;
|
||||
- filtros por instituição, tipo de documento e pessoas mencionadas;
|
||||
- uma visão em tabela (ordenável, pesquisável) como alternativa acessível ao gráfico.
|
||||
|
||||
## Estrutura do repositório
|
||||
|
||||
```
|
||||
index.html página estática (self-contained: HTML + CSS + JS, sem build step)
|
||||
docs.json um registro por documento (id, instituição, data, texto completo, tipo, pessoas)
|
||||
people.json catálogo de pessoas identificadas: {key, count, role}
|
||||
pipeline/ scripts e dados intermediários da extração (ver abaixo)
|
||||
```
|
||||
|
||||
## Como os dados foram montados
|
||||
|
||||
1. **`docs.json`** — cruzamento de duas fontes do dump original (`gitgraph.md` para a instituição
|
||||
de cada documento, `TIMELINE.md` para data/texto completo/arquivo-fonte). Ver
|
||||
`pipeline/extract.py`.
|
||||
|
||||
2. **Tipo de documento** — extraído do nome do arquivo-fonte de cada documento (ex.: "Despacho",
|
||||
"Petição", "Procuração", "Certidão"), normalizado com acentuação correta.
|
||||
|
||||
3. **Pessoas mencionadas (`people.json`)** — reconhecimento de entidades feito **localmente, sem
|
||||
usar o Claude para o texto em si**, com o modelo
|
||||
[`pierreguillou/ner-bert-base-cased-pt-lenerbr`](https://huggingface.co/pierreguillou/ner-bert-base-cased-pt-lenerbr)
|
||||
(BERT treinado no corpus **LeNER-Br**, de textos jurídicos brasileiros). Rodado sobre os 3.437
|
||||
textos via `pipeline/run_ner.py`.
|
||||
|
||||
Depois da extração bruta (435 nomes distintos), foi feita uma limpeza:
|
||||
- separação de nomes que o modelo juntou erroneamente com "e" (ex. "Fulano e Beltrano" → duas
|
||||
pessoas), preservando sobrenomes compostos legítimos ("Castro e Silva");
|
||||
- remoção de nomes de escritórios de advocacia reconhecidos como pessoa por engano;
|
||||
- descarte de fragmentos de um único token (nomes incompletos, ex. só o primeiro nome);
|
||||
- junção de variações do mesmo nome por contenção de tokens (ex. "Daniel Vorcaro" →
|
||||
"Daniel Bueno Vorcaro").
|
||||
|
||||
Resultado: **292 pessoas distintas**. O papel processual de cada uma (Relator/Ministro,
|
||||
Outorgante/Parte, Peticionante, Advogado(a), Investigado(a), Delegado(a) PF, Senador(a),
|
||||
Deputado(a), Alvo de diligência, Perito(a), Testemunha) foi inferido por frases-gatilho perto de
|
||||
cada menção (ex. "Despacho do Relator X", "Procuração de X constituindo Y", "em desfavor de X").
|
||||
**63% das pessoas (185 de 292) ficaram sem papel classificado** ("Mencionado(a)") por falta de um
|
||||
sinal textual próximo confiável — isso é esperado e fica visível na própria página, não
|
||||
escondido.
|
||||
|
||||
**Limitações conhecidas:** isso é reconhecimento automático de entidades em texto livre, não uma
|
||||
base curada. Pode haver nomes não reconhecidos, variações da mesma pessoa não unificadas, e
|
||||
classificações de papel aproximadas. Trate como um ponto de partida para busca, não como fonte
|
||||
definitiva de identidade ou qualificação processual.
|
||||
|
||||
## Rodando localmente
|
||||
|
||||
Não há build step — é HTML/CSS/JS puro. Basta servir a pasta com qualquer servidor estático:
|
||||
|
||||
```bash
|
||||
python3 -m http.server 8000
|
||||
# abrir http://localhost:8000/
|
||||
```
|
||||
|
||||
Para regerar `docs.json` e `people.json` a partir dos dados originais (`gitgraph.md`,
|
||||
`TIMELINE.md`, ambos fora deste repositório), ver os scripts em `pipeline/`.
|
||||
|
||||
## Projeto relacionado
|
||||
|
||||
[**vorcaros**](https://rafapolo.github.io/vorcaros/) — visualização em grafo da rede societária
|
||||
(empresas e sócios) ligada às mesmas pessoas que aparecem nesta linha do tempo processual. Os dois
|
||||
projetos olham o mesmo caso por ângulos diferentes: um pela rede de negócios, este pelo andamento
|
||||
processual.
|
||||
|
||||
## Aviso
|
||||
|
||||
Este projeto reúne informações de um processo público em curso no STF. Nomes, datas e descrições
|
||||
refletem o conteúdo dos autos tal como disponibilizado; erros de extração automática (OCR, NER,
|
||||
digitação no material de origem) podem existir. Não constitui manifestação jurídica sobre culpa,
|
||||
inocência ou qualquer qualificação processual das pessoas mencionadas.
|
||||
+1261
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,144 @@
|
||||
import re, json, sys
|
||||
|
||||
root = "/Users/polux/tmp/Pet16704-pt2-md"
|
||||
|
||||
# 1. Parse gitgraph.md to get id -> branch (lane), preserving sequence order.
|
||||
branch_of = {}
|
||||
order_of = {}
|
||||
current_branch = "STF · Relatoria"
|
||||
idx = 0
|
||||
with open(f"{root}/gitgraph.md", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
m = re.match(r'checkout "([^"]*)"', line)
|
||||
if m:
|
||||
current_branch = m.group(1)
|
||||
continue
|
||||
m = re.match(r'commit id: "([^"]*)"', line)
|
||||
if m:
|
||||
cid = m.group(1)
|
||||
branch_of[cid] = current_branch
|
||||
order_of[cid] = idx
|
||||
idx += 1
|
||||
|
||||
print(f"gitgraph commits: {len(branch_of)}", file=sys.stderr)
|
||||
|
||||
# 2. Parse TIMELINE.md: sections "## <Case>" then lines "- **NNNNN** (YYYY-MM-DD) — text — [`file`](<path>)"
|
||||
records = []
|
||||
current_case = None
|
||||
line_re = re.compile(
|
||||
r'^- \*\*(\d+)\*\*(?: \((\d{4}-\d{2}-\d{2})\))? — (.*?) — \[`[^`]*`\]\(<([^>]*)>\)\s*$'
|
||||
)
|
||||
with open(f"{root}/TIMELINE.md", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.rstrip("\n")
|
||||
m = re.match(r'^## (.+)$', line)
|
||||
if m:
|
||||
current_case = m.group(1).strip()
|
||||
continue
|
||||
m = line_re.match(line)
|
||||
if m:
|
||||
num, date, text, path = m.groups()
|
||||
cid = f"{current_case}-{num}"
|
||||
records.append({
|
||||
"id": cid,
|
||||
"date": date,
|
||||
"text": text,
|
||||
"file": path,
|
||||
})
|
||||
|
||||
# disambiguate duplicate ids (multiple documents filed under the same number)
|
||||
# to match gitgraph.md's "-a"/"-b"/"-c" suffix convention
|
||||
from collections import Counter
|
||||
id_counts = Counter(r["id"] for r in records)
|
||||
seen = Counter()
|
||||
suffixes = "abcdefgh"
|
||||
for r in records:
|
||||
base = r["id"]
|
||||
if id_counts[base] > 1:
|
||||
r["id"] = f"{base}-{suffixes[seen[base]]}"
|
||||
seen[base] += 1
|
||||
|
||||
print(f"timeline records: {len(records)}", file=sys.stderr)
|
||||
|
||||
# 3a. Extract & normalize document type from the source filename.
|
||||
TYPE_MAP = {
|
||||
"Peticao": "Petição",
|
||||
"Peticao inicial": "Petição inicial",
|
||||
"Recibo de peticao eletronica": "Recibo de petição eletrônica",
|
||||
"Documentos comprobatorios": "Documento comprobatório",
|
||||
"Documento comprobatorio": "Documento comprobatório",
|
||||
"Comunicacao assinada": "Comunicação assinada",
|
||||
"Procuracao": "Procuração",
|
||||
"Despacho": "Despacho",
|
||||
"Certidao": "Certidão",
|
||||
"Vista a PGR": "Vista à PGR",
|
||||
"Intimacao": "Intimação",
|
||||
"Decisao monocratica": "Decisão monocrática",
|
||||
"Mandado de intimacao": "Mandado de intimação",
|
||||
"Termo de disponibilizacao de autos": "Termo de disponibilização de autos",
|
||||
"Documentos de identificacao": "Documentos de identificação",
|
||||
"Aviso de recebimento": "Aviso de recebimento",
|
||||
"Outras pecas": "Outras peças",
|
||||
"Manifestacao": "Manifestação",
|
||||
"Certidao de transito em julgado": "Certidão de trânsito em julgado",
|
||||
"Certidao de julgamento": "Certidão de julgamento",
|
||||
"Oficio": "Ofício",
|
||||
"Documento Sigiloso": "Documento sigiloso",
|
||||
"Manifestacao da PGR": "Manifestação da PGR",
|
||||
"Pedido de ingresso como interessado": "Pedido de ingresso como interessado",
|
||||
"Volume": "Volume",
|
||||
"Inteiro teor do acordao": "Inteiro teor do acórdão",
|
||||
"Inteiro teor do acordao (completo)": "Inteiro teor do acórdão",
|
||||
"Mandado de prisao": "Mandado de prisão",
|
||||
"Pedido de reconsideracao": "Pedido de reconsideração",
|
||||
"Informacao": "Informação",
|
||||
"Peticao de Interposicao de Agravo Regimental": "Petição de interposição de agravo regimental",
|
||||
"Peticao de renuncia ao mandato": "Petição de renúncia ao mandato",
|
||||
}
|
||||
|
||||
def extract_type(file_path):
|
||||
base = re.sub(r'\.md$', '', file_path.split('/')[-1])
|
||||
m = re.match(r'^\d+\s+(.*)$', base)
|
||||
if not m:
|
||||
return "Outro"
|
||||
rest = re.sub(r'_[0-9a-fA-F]{6,}$', '', m.group(1))
|
||||
if ' - ' in rest:
|
||||
rest = rest.split(' - ')[0]
|
||||
rest = rest.strip()
|
||||
return TYPE_MAP.get(rest, rest)
|
||||
|
||||
# 3b. Merge
|
||||
merged = []
|
||||
missing_branch = 0
|
||||
for r in records:
|
||||
branch = branch_of.get(r["id"])
|
||||
if branch is None:
|
||||
missing_branch += 1
|
||||
branch = "STF · Relatoria"
|
||||
merged.append({
|
||||
"id": r["id"],
|
||||
"branch": branch,
|
||||
"date": r["date"],
|
||||
"text": r["text"],
|
||||
"file": r["file"],
|
||||
"type": extract_type(r["file"]),
|
||||
"order": order_of.get(r["id"], -1),
|
||||
})
|
||||
|
||||
print(f"missing branch matches: {missing_branch}", file=sys.stderr)
|
||||
|
||||
# also report ids in gitgraph not in timeline (should just be "raiz")
|
||||
timeline_ids = {r["id"] for r in records}
|
||||
only_in_gitgraph = [cid for cid in branch_of if cid not in timeline_ids]
|
||||
print(f"only in gitgraph (not in timeline): {only_in_gitgraph[:10]} (total {len(only_in_gitgraph)})", file=sys.stderr)
|
||||
|
||||
merged.sort(key=lambda r: r["order"])
|
||||
|
||||
with open("/private/tmp/claude-501/-Users-polux-tmp-Pet16704-pt2-md/a294e67f-4337-4617-9903-54ea4711fb63/scratchpad/docs.json", "w", encoding="utf-8") as f:
|
||||
json.dump(merged, f, ensure_ascii=False)
|
||||
|
||||
branches = sorted(set(branch_of.values()))
|
||||
print("branches:", branches, file=sys.stderr)
|
||||
dated = sum(1 for r in merged if r["date"])
|
||||
print(f"dated: {dated}, undated: {len(merged)-dated}", file=sys.stderr)
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,54 @@
|
||||
import json, re, time
|
||||
from collections import Counter, defaultdict
|
||||
from transformers import AutoTokenizer, AutoModelForTokenClassification, pipeline
|
||||
|
||||
docs = json.load(open("docs.json"))
|
||||
texts = [d["text"] for d in docs]
|
||||
print(f"{len(texts)} documents to process", flush=True)
|
||||
|
||||
name = "pierreguillou/ner-bert-base-cased-pt-lenerbr"
|
||||
tok = AutoTokenizer.from_pretrained(name)
|
||||
model = AutoModelForTokenClassification.from_pretrained(name)
|
||||
nlp = pipeline("ner", model=model, tokenizer=tok, aggregation_strategy="simple", device=-1)
|
||||
|
||||
def merge_entities(ents, group="PESSOA"):
|
||||
ents = [e for e in ents if e["entity_group"] == group]
|
||||
ents.sort(key=lambda e: e["start"])
|
||||
merged = []
|
||||
for e in ents:
|
||||
if merged and e["start"] - merged[-1]["end"] <= 1:
|
||||
merged[-1]["end"] = e["end"]
|
||||
else:
|
||||
merged.append({"start": e["start"], "end": e["end"]})
|
||||
return merged
|
||||
|
||||
t0 = time.time()
|
||||
BATCH = 32
|
||||
all_people_per_doc = [] # list aligned with docs: list of names
|
||||
for i in range(0, len(texts), BATCH):
|
||||
batch = texts[i:i+BATCH]
|
||||
results = nlp(batch)
|
||||
for text, ents in zip(batch, results):
|
||||
merged = merge_entities(ents, "PESSOA")
|
||||
names = [text[m["start"]:m["end"]].strip() for m in merged]
|
||||
names = [n for n in names if len(n) >= 3]
|
||||
all_people_per_doc.append(names)
|
||||
if (i//BATCH) % 10 == 0:
|
||||
elapsed = time.time()-t0
|
||||
print(f"{i+len(batch)}/{len(texts)} docs, {elapsed:.1f}s elapsed", flush=True)
|
||||
|
||||
print("total time", time.time()-t0, "s")
|
||||
|
||||
# save raw per-doc results
|
||||
out = []
|
||||
for d, names in zip(docs, all_people_per_doc):
|
||||
out.append({"id": d["id"], "names": names})
|
||||
json.dump(out, open("ner_people_per_doc.json","w",encoding="utf-8"), ensure_ascii=False)
|
||||
|
||||
counter = Counter()
|
||||
for names in all_people_per_doc:
|
||||
for n in names:
|
||||
counter[n] += 1
|
||||
print("distinct raw person strings:", len(counter))
|
||||
for n,c in counter.most_common(50):
|
||||
print(c, n)
|
||||
Reference in New Issue
Block a user