Gliner2 / app.py
roundb's picture
Update app.py
7f00249 verified
"""
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
&nbsp;|&nbsp; Separador: <code>;</code> &nbsp;|&nbsp; 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)