| """ |
| 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") |
|
|
| |
| 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) |
| ) |
|
|
| |
| 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) |
|
|
| |
| 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" |
|
|
| |
| 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 |
|
|
| |
| 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)) |
|
|
| |
| 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"<b>Dashboard GLiNER2 β {stats.get('total_registos', len(df))} Projetos Analisados</b>", |
| 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 |
|
|
| |
| 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"<b>Fase: {fase_sel}</b> β {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 |
|
|
| |
| 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"<b>Top {top_n} β {coluna_ner}</b>", |
| 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 |
|
|
| |
| 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 |
|
|
| |
| 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="<b>%{y}</b><br>Fase: %{text}<extra></extra>", |
| )) |
|
|
| 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="<b>Status Original β Fase (GLiNER2)</b>", |
| 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 |
|
|
| |
| 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="<b>AnΓ‘lise Temporal e Entidades Externas (NER)</b>", |
| 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 |
|
|
| |
| EXPLICACAO_HTML = """ |
| <div style="font-family: 'Segoe UI', Arial, sans-serif; max-width: 920px; margin: 0 auto; color: #1a1a2e;"> |
| |
| <div style="background: linear-gradient(135deg, #1a5276, #2E86AB); border-radius: 12px; padding: 28px 32px; margin-bottom: 28px;"> |
| <h1 style="color: white; margin: 0 0 8px 0; font-size: 1.8em;">π€ GLiNER2</h1> |
| <p style="color: #d6eaf8; margin: 0; font-size: 1.05em;"> |
| <b>Generalist and Lightweight Named Entity Recognition 2</b><br> |
| Um modelo de IA compacto que extrai informaΓ§Γ£o estruturada de texto livre β sem GPU, sem API, sem custos. |
| </p> |
| </div> |
| |
| <h2 style="color: #1a5276; border-bottom: 3px solid #2E86AB; padding-bottom: 6px;">π O Problema que o GLiNER2 Resolve</h2> |
| <p> |
| O ficheiro de projetos contΓ©m <b>489 registos reais</b> com campos de texto livre como |
| <em>DesignaΓ§Γ£o do Projeto</em> e <em>ObservaΓ§Γ΅es</em>. 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. |
| </p> |
| <div style="background: #fff3cd; border-left: 5px solid #ffc107; padding: 14px 18px; border-radius: 6px; margin: 16px 0;"> |
| <b>Exemplo real do ficheiro:</b><br> |
| <code>"ATT VALID 17/02 PUSH MAIL ATT INFO MAIRIE 06/02 //"</code><br> |
| <span style="color:#856404;">β Sem GLiNER2: apenas texto. Com GLiNER2: <b>AΓ§Γ£o=ATT VALID</b>, <b>Data=17/02</b>, <b>Entidade=MAIRIE</b></span> |
| </div> |
| <div style="background: #d1ecf1; border-left: 5px solid #17a2b8; padding: 14px 18px; border-radius: 6px; margin: 16px 0;"> |
| <b>Nota tΓ©cnica β Separador do ficheiro:</b><br> |
| O ficheiro CSV usa <b>ponto-e-vΓrgula (;)</b> como separador e codificaΓ§Γ£o <b>latin-1</b>. |
| Alguns campos contΓͺm quebras de linha internas. O dashboard usa a leitura correta |
| com filtro por padrΓ£o SUB-CIP. |
| </div> |
| |
| <h2 style="color: #1a5276; border-bottom: 3px solid #2E86AB; padding-bottom: 6px; margin-top: 28px;">βοΈ Como Funciona: Os 3 Modos Aplicados</h2> |
| |
| <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; margin: 16px 0;"> |
| <div style="background: #d1ecf1; border-radius: 10px; padding: 16px;"> |
| <h3 style="color: #0c5460; margin-top: 0;">π Modo 1<br>NER nas DesignaΓ§Γ΅es</h3> |
| <p style="font-size: 0.92em; color: #333;"> |
| Extrai o <b>tipo de rede</b>, o <b>tipo de trabalho</b>, |
| o <b>cliente</b> e o <b>cΓ³digo do projeto</b>. |
| </p> |
| </div> |
| <div style="background: #d1ecf1; border-radius: 10px; padding: 16px;"> |
| <h3 style="color: #0c5460; margin-top: 0;">π Modo 2<br>NER nas ObservaΓ§Γ΅es</h3> |
| <p style="font-size: 0.92em; color: #333;"> |
| Extrai <b>aΓ§Γ΅es</b>, <b>semanas</b>, <b>datas</b> e <b>entidades externas</b>. |
| </p> |
| </div> |
| <div style="background: #d1ecf1; border-radius: 10px; padding: 16px;"> |
| <h3 style="color: #0c5460; margin-top: 0;">π·οΈ Modo 3<br>ClassificaΓ§Γ£o de Status</h3> |
| <p style="font-size: 0.92em; color: #333;"> |
| Agrupa os status em 5 fases de ciclo de vida semanticamente. |
| </p> |
| </div> |
| </div> |
| |
| <div style="background: linear-gradient(135deg, #d4edda, #c3e6cb); border-radius: 10px; padding: 20px 24px; margin-top: 28px;"> |
| <h3 style="color: #155724; margin-top: 0;">π― ConclusΓ£o</h3> |
| <p style="color: #155724; margin: 0; font-size: 1em;"> |
| O GLiNER2 transformou o ficheiro CSV de <b>489 projetos</b> numa <b>base de dados estruturada e analisΓ‘vel</b>. |
| </p> |
| </div> |
| </div> |
| """ |
|
|
| |
| 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(""" |
| <div style="background: linear-gradient(135deg, #1a5276 0%, #2E86AB 100%); |
| padding: 22px 32px; border-radius: 12px; margin-bottom: 8px;"> |
| <h1 style="color: white; margin: 0; font-size: 1.9em; font-family: Inter, sans-serif;"> |
| π¬ GLiNER2 Dashboard |
| </h1> |
| <p style="color: #d6eaf8; margin: 6px 0 0 0; font-size: 1.05em;"> |
| AnΓ‘lise de <b>489 Projetos/Tarefas</b> com ExtraΓ§Γ£o de InformaΓ§Γ£o por InteligΓͺncia Artificial |
| | Separador: <code>;</code> | Encoding: <code>latin-1</code> |
| </p> |
| </div> |
| """) |
|
|
| 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(""" |
| <div style="text-align: center; padding: 10px; color: #888; font-size: 0.85em; margin-top: 6px;"> |
| GLiNER2 Dashboard Β· <b>489 projetos reais</b> Β· |
| Separador: <code>;</code> Β· Encoding: <code>latin-1</code> Β· Processado localmente |
| </div> |
| """) |
|
|
| demo.launch(server_name="0.0.0.0", server_port=7860, share=False) |