""" 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}", )) for k, v in CORES_FASE.items(): fig.add_trace(go.Bar( name=FASE_LABELS.get(k, k), x=[None], y=[None], marker_color=v, showlegend=True )) fig.update_layout( height=560, title_text="Status Original → Fase (GLiNER2)", title_font_size=14, paper_bgcolor="#f8f9fa", plot_bgcolor="#ffffff", xaxis=dict(title="Número de Projetos"), yaxis=dict(autorange="reversed"), margin=dict(t=60, b=40, l=260, r=20), legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1), ) return fig # ─── ABA 6: ANÁLISE TEMPORAL ───────────────────────────────────────────────── def fig_temporal(): df_sem = df[df["NER_OBS_semana"].notna() & df["NER_OBS_acao"].notna()].copy() fig = make_subplots( rows=1, cols=2, subplot_titles=["Ações por Semana de Referência", "Entidades Externas Identificadas"] ) if not df_sem.empty: sem_acao = df_sem.groupby(["NER_OBS_semana", "NER_OBS_acao"]).size().reset_index(name="n") top_semanas = df_sem["NER_OBS_semana"].value_counts().head(8).index.tolist() sem_acao = sem_acao[sem_acao["NER_OBS_semana"].isin(top_semanas)] for acao in sem_acao["NER_OBS_acao"].dropna().unique()[:5]: sub_a = sem_acao[sem_acao["NER_OBS_acao"] == acao] fig.add_trace(go.Bar( x=sub_a["NER_OBS_semana"], y=sub_a["n"], name=acao, showlegend=True, ), row=1, col=1) entidades = stats.get("entidades_externas", {}) top_ent = dict(list(entidades.items())[:10]) if top_ent: fig.add_trace(go.Bar( x=list(top_ent.values()), y=list(top_ent.keys()), orientation="h", marker_color="#44BBA4", text=list(top_ent.values()), textposition="outside", showlegend=False, ), row=1, col=2) fig.update_layout( height=420, barmode="stack", title_text="Análise Temporal e Entidades Externas (NER)", title_font_size=14, paper_bgcolor="#f8f9fa", plot_bgcolor="#ffffff", margin=dict(t=70, b=30, l=10, r=10), ) fig.update_xaxes(showgrid=False) fig.update_yaxes(showgrid=False) return fig # ─── ABA 7: EXPLICAÇÃO GLiNER2 ─────────────────────────────────────────────── EXPLICACAO_HTML = """

🤖 GLiNER2

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 Problema que o GLiNER2 Resolve

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.

Exemplo real do ficheiro:
"ATT VALID 17/02 PUSH MAIL ATT INFO MAIRIE 06/02 //"
→ Sem GLiNER2: apenas texto. Com GLiNER2: Ação=ATT VALID, Data=17/02, Entidade=MAIRIE
Nota técnica — Separador do ficheiro:
O ficheiro CSV usa ponto-e-vírgula (;) como separador e codificação latin-1. Alguns campos contêm quebras de linha internas. O dashboard usa a leitura correta com filtro por padrão SUB-CIP.

⚙️ Como Funciona: Os 3 Modos Aplicados

📌 Modo 1
NER nas Designações

Extrai o tipo de rede, o tipo de trabalho, o cliente e o código do projeto.

📝 Modo 2
NER nas Observações

Extrai ações, semanas, datas e entidades externas.

🏷️ Modo 3
Classificação de Status

Agrupa os status em 5 fases de ciclo de vida semanticamente.

🎯 Conclusão

O GLiNER2 transformou o ficheiro CSV de 489 projetos numa base de dados estruturada e analisável.

""" # ─── CONSTRUIR A APP GRADIO ────────────────────────────────────────────────── THEME = gr.themes.Soft( primary_hue="blue", secondary_hue="orange", font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "sans-serif"], ) with gr.Blocks( theme=THEME, title="GLiNER2 Dashboard — 489 Projetos", css=""" .gradio-container { max-width: 1200px !important; } .tab-nav button { font-size: 14px !important; font-weight: 600 !important; } footer { display: none !important; } """, ) as demo: gr.HTML("""

🔬 GLiNER2 Dashboard

Análise de 489 Projetos/Tarefas com Extração de Informação por Inteligência Artificial  |  Separador: ;  |  Encoding: latin-1

""") with gr.Tabs(): with gr.Tab("📊 Visão Geral"): gr.Markdown("### Panorama completo dos 489 projetos analisados pelo GLiNER2") plot_geral = gr.Plot(show_label=False) demo.load(fn=fig_visao_geral, outputs=plot_geral) with gr.Tab("🏷️ Análise por Fase"): gr.Markdown("### Explore os projetos por fase de ciclo de vida (classificada pelo GLiNER2)") fase_dd = gr.Dropdown( choices=["Todas"] + list(FASE_LABELS.values()), value="Todas", label="Filtrar por Fase", ) plot_fase = gr.Plot(show_label=False) tbl_fase = gr.Dataframe(label="Registos da Fase", wrap=True, interactive=False) fase_dd.change(fn=fig_fases, inputs=fase_dd, outputs=plot_fase) fase_dd.change(fn=tabela_fases, inputs=fase_dd, outputs=tbl_fase) demo.load(fn=fig_fases, inputs=fase_dd, outputs=plot_fase) demo.load(fn=tabela_fases, inputs=fase_dd, outputs=tbl_fase) with gr.Tab("🔍 Explorador NER"): gr.Markdown("### Explore as entidades extraídas pelo GLiNER2 de cada campo de texto") with gr.Row(): ner_dd = gr.Dropdown( choices=list(COL_NER_MAP.keys()), value="Ação (Observações)", label="Entidade NER", scale=2, ) top_n = gr.Slider(5, 20, value=10, step=1, label="Top N", scale=1) plot_ner = gr.Plot(show_label=False) tbl_ner = gr.Dataframe(label="Registos com esta entidade", wrap=True, interactive=False) ner_dd.change(fn=explorar_ner, inputs=[ner_dd, top_n], outputs=[plot_ner, tbl_ner]) top_n.change(fn=explorar_ner, inputs=[ner_dd, top_n], outputs=[plot_ner, tbl_ner]) demo.load(fn=explorar_ner, inputs=[ner_dd, top_n], outputs=[plot_ner, tbl_ner]) with gr.Tab("📋 Extração JSON"): gr.Markdown(""" ### Extração Estruturada (JSON) pelo GLiNER2 O modelo leu o texto completo de cada projeto e extraiu automaticamente um objeto JSON com **código do projeto**, **tipo de rede**, **colaborador**, **status**, **ação** e **semana**. """) plot_json = gr.Plot(show_label=False) tbl_json = gr.Dataframe(label="Extração Estruturada", wrap=True, interactive=False) demo.load(fn=fig_json_resumo, outputs=plot_json) demo.load(fn=tabela_json, outputs=tbl_json) with gr.Tab("🗺️ Status → Fase"): gr.Markdown(""" ### Mapeamento Inteligente: Status → Fase O GLiNER2 classificou semanticamente todos os status do ficheiro em fases de ciclo de vida. """) plot_mapa = gr.Plot(show_label=False) demo.load(fn=fig_mapeamento, outputs=plot_mapa) with gr.Tab("📅 Análise Temporal"): gr.Markdown("### Ações por semana e entidades externas identificadas pelo NER") plot_temp = gr.Plot(show_label=False) demo.load(fn=fig_temporal, outputs=plot_temp) with gr.Tab("🤖 O que é o GLiNER2?"): gr.HTML(EXPLICACAO_HTML) gr.HTML("""
GLiNER2 Dashboard · 489 projetos reais · Separador: ; · Encoding: latin-1 · Processado localmente
""") demo.launch(server_name="0.0.0.0", server_port=7860, share=False)