"""
Dashboard GLiNER2 — Análise de Projetos/Tarefas
489 registos reais | Gradio | Múltiplas abas interativas
Pronto para Hugging Face Spaces
"""
import gradio as gr
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import json
from collections import Counter
from pathlib import Path
import warnings
warnings.filterwarnings("ignore")
# ─── CAMINHOS PORTÁTEIS ──────────────────────────────────────────────────────
BASE = Path(__file__).parent
CSV_FILE = BASE / "gliner2_resultado_489.csv"
STAT_FILE = BASE / "gliner2_estatisticas_489.json"
JSON_FILE = BASE / "gliner2_extracao_489.json"
missing = [str(f) for f in [CSV_FILE, STAT_FILE, JSON_FILE] if not f.exists()]
if missing:
raise FileNotFoundError(
"Os seguintes ficheiros não foram encontrados no repositório do Space:\n"
+ "\n".join(missing)
)
# ─── CARREGAR DADOS ──────────────────────────────────────────────────────────
df = pd.read_csv(
CSV_FILE,
sep=";",
encoding="latin-1",
on_bad_lines="skip"
)
with open(STAT_FILE, encoding="utf-8") as f:
stats = json.load(f)
with open(JSON_FILE, encoding="utf-8") as f:
estruturados = json.load(f)
# ─── NORMALIZAÇÃO ────────────────────────────────────────────────────────────
if "COLABORADOR" in df.columns:
df["COLABORADOR"] = df["COLABORADOR"].fillna("").astype(str).str.strip().str.title()
else:
df["COLABORADOR"] = ""
def find_col(keyword, default=None):
for c in df.columns:
if keyword.upper() in c.upper():
return c
if default is not None:
return default
raise ValueError(f"Coluna contendo '{keyword}' não encontrada")
col_designacao = find_col("DESIGNA", "DESIGNACAO")
col_obs = find_col("OBSERVA", "OBSERVACOES")
col_status = "RB STATUS" if "RB STATUS" in df.columns else find_col("STATUS", "STATUS")
col_tipo = "TIPO" if "TIPO" in df.columns else find_col("TIPO", "TIPO")
col_colab = "COLABORADOR"
# garantir colunas usadas depois
for c in [
"FASE_CLASSIFICADA",
"NER_OBS_acao",
"NER_OBS_semana",
"NER_OBS_data",
"NER_OBS_entidade_externa",
"NER_DESIGN_tipo_rede",
"NER_DESIGN_tipo_trabalho",
"NER_DESIGN_cliente_entidade",
"NER_DESIGN_codigo_projeto",
"SUB-CIP",
"PROJETO",
]:
if c not in df.columns:
df[c] = pd.NA
# ─── PALETA DE CORES ─────────────────────────────────────────────────────────
CORES_FASE = {
"pendente_validacao": "#F18F01",
"concluido": "#2E86AB",
"faturado": "#44BBA4",
"em_progresso": "#A23B72",
"cancelado": "#C73E1D",
}
FASE_LABELS = {
"pendente_validacao": "Pendente Validação",
"concluido": "Concluído",
"faturado": "Faturado",
"em_progresso": "Em Progresso",
"cancelado": "Cancelado",
}
def top_counter(series, n=15):
all_vals = []
for v in series.dropna():
all_vals.extend([x.strip() for x in str(v).split(";") if x.strip()])
return dict(Counter(all_vals).most_common(n))
# ─── ABA 1: VISÃO GERAL ──────────────────────────────────────────────────────
def fig_visao_geral():
fig = make_subplots(
rows=2, cols=3,
subplot_titles=[
"Fases dos Projetos (GLiNER2)",
"Distribuição por Tipo de Projeto",
"Projetos por Colaborador",
"Ações nas Observações (NER)",
"Tipos de Rede Extraídos (NER)",
"Semanas de Referência (NER)",
],
specs=[
[{"type": "pie"}, {"type": "bar"}, {"type": "bar"}],
[{"type": "bar"}, {"type": "bar"}, {"type": "bar"}],
],
vertical_spacing=0.18,
horizontal_spacing=0.10,
)
fases = stats.get("fases_classificadas", {})
if fases:
fig.add_trace(go.Pie(
labels=[FASE_LABELS.get(k, k) for k in fases],
values=list(fases.values()),
marker_colors=[CORES_FASE.get(k, "#888") for k in fases],
hole=0.38,
textinfo="percent+label",
textfont_size=10,
showlegend=False,
), row=1, col=1)
tipo_counts = df[col_tipo].fillna("-").value_counts().head(10)
fig.add_trace(go.Bar(
x=tipo_counts.values, y=tipo_counts.index,
orientation="h", marker_color="#2E86AB",
text=tipo_counts.values, textposition="outside",
showlegend=False,
), row=1, col=2)
colab_counts = df[col_colab].replace("", pd.NA).dropna().value_counts().head(8)
fig.add_trace(go.Bar(
x=colab_counts.values, y=colab_counts.index,
orientation="h", marker_color="#A23B72",
text=colab_counts.values, textposition="outside",
showlegend=False,
), row=1, col=3)
acoes = stats.get("acoes_observacoes", {})
top_acoes = dict(list(acoes.items())[:8])
if top_acoes:
fig.add_trace(go.Bar(
x=list(top_acoes.values()), y=list(top_acoes.keys()),
orientation="h", marker_color="#F18F01",
text=list(top_acoes.values()), textposition="outside",
showlegend=False,
), row=2, col=1)
rede = stats.get("tipos_rede_extraidos", {})
top_rede = dict(list(rede.items())[:8])
if top_rede:
fig.add_trace(go.Bar(
x=list(top_rede.keys()), y=list(top_rede.values()),
marker_color="#44BBA4",
text=list(top_rede.values()), textposition="outside",
showlegend=False,
), row=2, col=2)
semanas = stats.get("semanas_referencia", {})
top_sem = dict(list(semanas.items())[:10])
if top_sem:
fig.add_trace(go.Bar(
x=list(top_sem.keys()), y=list(top_sem.values()),
marker_color="#C73E1D",
text=list(top_sem.values()), textposition="outside",
showlegend=False,
), row=2, col=3)
fig.update_layout(
height=700,
title_text=f"Dashboard GLiNER2 — {stats.get('total_registos', len(df))} Projetos Analisados",
title_font_size=16,
paper_bgcolor="#f8f9fa",
plot_bgcolor="#ffffff",
margin=dict(t=80, b=30, l=10, r=10),
)
fig.update_xaxes(showgrid=False)
fig.update_yaxes(showgrid=False)
return fig
# ─── ABA 2: ANÁLISE POR FASE ─────────────────────────────────────────────────
def fig_fases(fase_sel):
if fase_sel == "Todas":
sub = df[df["FASE_CLASSIFICADA"].notna()]
else:
chave = {v: k for k, v in FASE_LABELS.items()}.get(
fase_sel, fase_sel.lower().replace(" ", "_")
)
sub = df[df["FASE_CLASSIFICADA"] == chave]
fig = make_subplots(
rows=1, cols=2,
subplot_titles=[f"Tipos — {fase_sel}", f"Colaboradores — {fase_sel}"],
horizontal_spacing=0.12,
)
cor = CORES_FASE.get(
{v: k for k, v in FASE_LABELS.items()}.get(fase_sel, ""),
"#2E86AB"
)
tipo_c = sub[col_tipo].fillna("-").value_counts().head(10)
fig.add_trace(go.Bar(
x=tipo_c.values, y=tipo_c.index, orientation="h",
marker_color=cor, text=tipo_c.values, textposition="outside", showlegend=False,
), row=1, col=1)
col_c = sub[col_colab].replace("", pd.NA).dropna().value_counts().head(8)
fig.add_trace(go.Bar(
x=col_c.values, y=col_c.index, orientation="h",
marker_color="#555555", text=col_c.values, textposition="outside", showlegend=False,
), row=1, col=2)
fig.update_layout(
height=420,
title_text=f"Fase: {fase_sel} — {len(sub)} projetos",
title_font_size=14,
paper_bgcolor="#f8f9fa", plot_bgcolor="#ffffff",
margin=dict(t=70, b=20, l=10, r=10),
)
fig.update_xaxes(showgrid=False)
fig.update_yaxes(showgrid=False)
return fig
def tabela_fases(fase_sel):
if fase_sel == "Todas":
sub = df[df["FASE_CLASSIFICADA"].notna()]
else:
chave = {v: k for k, v in FASE_LABELS.items()}.get(
fase_sel, fase_sel.lower().replace(" ", "_")
)
sub = df[df["FASE_CLASSIFICADA"] == chave]
cols = ["SUB-CIP", "PROJETO", col_tipo, col_status, col_colab,
"FASE_CLASSIFICADA", "NER_OBS_acao", "NER_OBS_semana"]
sub = sub[cols].head(100).fillna("-").rename(columns={
col_tipo: "TIPO",
col_status: "STATUS",
col_colab: "COLABORADOR",
"FASE_CLASSIFICADA": "FASE (GLiNER2)",
"NER_OBS_acao": "AÇÃO (NER)",
"NER_OBS_semana": "SEMANA (NER)"
})
return sub
# ─── ABA 3: EXPLORADOR NER ───────────────────────────────────────────────────
COL_NER_MAP = {
"Ação (Observações)": "NER_OBS_acao",
"Semana (Observações)": "NER_OBS_semana",
"Data (Observações)": "NER_OBS_data",
"Entidade Externa": "NER_OBS_entidade_externa",
"Tipo de Rede (Designação)": "NER_DESIGN_tipo_rede",
"Tipo de Trabalho": "NER_DESIGN_tipo_trabalho",
"Cliente/Entidade": "NER_DESIGN_cliente_entidade",
"Código do Projeto": "NER_DESIGN_codigo_projeto",
}
def explorar_ner(coluna_ner, top_n):
col = COL_NER_MAP.get(coluna_ner, "NER_OBS_acao")
all_vals = []
for v in df[col].dropna():
all_vals.extend([x.strip() for x in str(v).split(";") if x.strip()])
counter = Counter(all_vals)
top = dict(counter.most_common(int(top_n)))
if not top:
fig = go.Figure()
fig.update_layout(title_text="Sem dados para esta entidade", height=300)
return fig, pd.DataFrame()
fig = go.Figure(go.Bar(
x=list(top.values()), y=list(top.keys()),
orientation="h",
marker=dict(color=list(top.values()), colorscale="Blues", showscale=False),
text=list(top.values()), textposition="outside",
))
fig.update_layout(
height=max(350, int(top_n) * 30),
title_text=f"Top {top_n} — {coluna_ner}",
title_font_size=14,
paper_bgcolor="#f8f9fa", plot_bgcolor="#ffffff",
margin=dict(t=60, b=20, l=200, r=60),
yaxis=dict(autorange="reversed"),
)
fig.update_xaxes(showgrid=False)
fig.update_yaxes(showgrid=False)
sub = df[df[col].notna()][["SUB-CIP", col_tipo, col_colab, "FASE_CLASSIFICADA", col_obs, col]].head(50).fillna("-")
sub.columns = ["SUB-CIP", "TIPO", "COLABORADOR", "FASE (GLiNER2)", "OBSERVAÇÕES", coluna_ner]
return fig, sub
# ─── ABA 4: EXTRAÇÃO ESTRUTURADA JSON ───────────────────────────────────────
def tabela_json():
rows = []
for item in estruturados:
sub_cip = item.get("sub_cip", "")
tarefas = item.get("extracao_gliner2", {}).get("tarefa", [])
t = tarefas[0] if tarefas else {}
rows.append({
"SUB-CIP": sub_cip,
"Código Projeto": t.get("codigo_projeto") or "-",
"Tipo de Rede": t.get("tipo_rede") or "-",
"Colaborador": t.get("colaborador") or "-",
"Status": t.get("status") or "-",
"Ação/Observação": t.get("acao_observacao") or "-",
"Semana Ref.": t.get("semana_referencia") or "-",
})
return pd.DataFrame(rows)
def fig_json_resumo():
rows = []
for item in estruturados:
tarefas = item.get("extracao_gliner2", {}).get("tarefa", [])
if tarefas:
rows.append(tarefas[0])
df_j = pd.DataFrame(rows)
fig = make_subplots(
rows=1, cols=2,
subplot_titles=["Colaboradores (JSON)", "Status (JSON)"]
)
if "colaborador" in df_j.columns and df_j["colaborador"].notna().any():
co = df_j["colaborador"].dropna().astype(str).str.title().value_counts().head(8)
fig.add_trace(go.Bar(
x=co.index, y=co.values,
marker_color="#A23B72", text=co.values, textposition="outside",
showlegend=False
), row=1, col=1)
if "status" in df_j.columns and df_j["status"].notna().any():
st = df_j["status"].dropna().astype(str).value_counts().head(8)
fig.add_trace(go.Bar(
x=st.index, y=st.values,
marker_color="#2E86AB", text=st.values, textposition="outside",
showlegend=False
), row=1, col=2)
fig.update_layout(
height=350,
paper_bgcolor="#f8f9fa",
plot_bgcolor="#ffffff",
margin=dict(t=50, b=20)
)
fig.update_xaxes(showgrid=False, tickangle=30)
fig.update_yaxes(showgrid=False)
return fig
# ─── ABA 5: MAPEAMENTO STATUS → FASE ────────────────────────────────────────
def fig_mapeamento():
status_counts = stats.get("status_original", {})
mapa = stats.get("mapeamento_status_fase", {})
status_list = list(status_counts.keys())
count_list = [status_counts[s] for s in status_list]
fase_list = [mapa.get(s, "N/A") for s in status_list]
cores = [CORES_FASE.get(f, "#888888") for f in fase_list]
labels_fase = [FASE_LABELS.get(f, f) for f in fase_list]
fig = go.Figure()
fig.add_trace(go.Bar(
x=count_list,
y=status_list,
orientation="h",
marker_color=cores,
text=[f"{lf} ({n})" for lf, n in zip(labels_fase, count_list)],
textposition="inside",
insidetextanchor="middle",
textfont=dict(color="white", size=10),
showlegend=False,
hovertemplate="%{y}
Fase: %{text}
Generalist and Lightweight Named Entity Recognition 2
Um modelo de IA compacto que extrai informação estruturada de texto livre — sem GPU, sem API, sem custos.
O ficheiro de projetos contém 489 registos reais com campos de texto livre como Designação do Projeto e Observações. Estes campos guardam informação crítica — tipo de rede, ações pendentes, entidades externas, semanas de referência — mas numa forma impossível de analisar diretamente com ferramentas tradicionais.
"ATT VALID 17/02 PUSH MAIL ATT INFO MAIRIE 06/02 //"Extrai o tipo de rede, o tipo de trabalho, o cliente e o código do projeto.
Extrai ações, semanas, datas e entidades externas.
Agrupa os status em 5 fases de ciclo de vida semanticamente.
O GLiNER2 transformou o ficheiro CSV de 489 projetos numa base de dados estruturada e analisável.
Análise de 489 Projetos/Tarefas com Extração de Informação por Inteligência Artificial
| Separador: ; | Encoding: latin-1
; · Encoding: latin-1 · Processado localmente