Download app.py from roundb/Gliner2: direct link, hf CLI and curl.
- Browser
- Download file 25.4 kB
-
https://huggingface.co/spaces/roundb/Gliner2/resolve/main/app.py
- Command line
-
hf download hf://spaces/roundb/Gliner2/app.py
-
curl -L -o app.py https://huggingface.co/spaces/roundb/Gliner2/resolve/main/app.py
25.4 kB
| """ | |
| 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"<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 | |
| # ─── 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"<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 | |
| # ─── 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"<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 | |
| # ─── 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="<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 | |
| # ─── 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="<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 | |
| # ─── ABA 7: EXPLICAÇÃO GLiNER2 ─────────────────────────────────────────────── | |
| 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> | |
| """ | |
| # ─── 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(""" | |
| <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) |