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:
2026-09-13 10:37:10 +02:00
commit 891d58ec55
10 changed files with 1559 additions and 0 deletions
+144
View File
@@ -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
+54
View File
@@ -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)