roundb commited on
Commit
9d4b9e3
Β·
verified Β·
1 Parent(s): e071ade

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +193 -512
app.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
  Dashboard GLiNER2 β€” AnΓ‘lise de Projetos/Tarefas
3
- 489 registos reais | Gradio 6.x | MΓΊltiplas abas interativas
4
  """
5
 
6
  import gradio as gr
@@ -9,598 +9,279 @@ import plotly.graph_objects as go
9
  from plotly.subplots import make_subplots
10
  import json
11
  from collections import Counter
 
12
  import warnings
 
13
  warnings.filterwarnings("ignore")
14
 
15
- # ─── CARREGAR DADOS ─────────────────────────────────────────────────────────
16
- BASE = "d:/gliner2/hf"
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
- df = pd.read_csv(f"{BASE}/gliner2_resultado_489.csv",
19
- sep=";", encoding="utf-8-sig", on_bad_lines="skip")
20
 
21
- with open(f"{BASE}/gliner2_estatisticas_489.json", encoding="utf-8") as f:
 
 
 
 
 
 
 
 
 
 
 
22
  stats = json.load(f)
23
 
24
- with open(f"{BASE}/gliner2_extracao_489.json", encoding="utf-8") as f:
25
  estruturados = json.load(f)
26
 
27
- # Normalizar colaborador
 
 
 
28
  df["COLABORADOR"] = df["COLABORADOR"].fillna("").str.strip().str.title()
29
 
30
- # Identificar colunas chave
31
- col_designacao = [c for c in df.columns if 'DESIGNA' in c.upper()][0]
32
- col_obs = [c for c in df.columns if 'OBSERVA' in c.upper()][0]
33
- col_status = 'RB STATUS'
34
- col_tipo = 'TIPO'
35
- col_colab = 'COLABORADOR'
 
 
 
 
 
 
 
 
 
 
36
 
37
- # Paleta de cores
38
  CORES_FASE = {
39
  "pendente_validacao": "#F18F01",
40
- "concluido": "#2E86AB",
41
- "faturado": "#44BBA4",
42
- "em_progresso": "#A23B72",
43
- "cancelado": "#C73E1D",
44
  }
45
 
46
  FASE_LABELS = {
47
  "pendente_validacao": "Pendente ValidaΓ§Γ£o",
48
- "concluido": "ConcluΓ­do",
49
- "faturado": "Faturado",
50
- "em_progresso": "Em Progresso",
51
- "cancelado": "Cancelado",
52
  }
53
 
 
 
 
 
54
  def top_counter(series, n=15):
55
  all_vals = []
56
  for v in series.dropna():
57
- all_vals.extend([x.strip() for x in str(v).split(';') if x.strip()])
58
  return dict(Counter(all_vals).most_common(n))
59
 
60
- # ─── ABA 1: VISΓƒO GERAL ─────────────────────────────────────────────────────
 
 
 
61
  def fig_visao_geral():
 
62
  fig = make_subplots(
63
- rows=2, cols=3,
 
64
  subplot_titles=[
65
- "Fases dos Projetos (GLiNER2)",
66
- "DistribuiΓ§Γ£o por Tipo de Projeto",
67
  "Projetos por Colaborador",
68
- "AΓ§Γ΅es nas ObservaΓ§Γ΅es (NER)",
69
- "Tipos de Rede ExtraΓ­dos (NER)",
70
- "Semanas de ReferΓͺncia (NER)",
71
- ],
72
- specs=[
73
- [{"type": "pie"}, {"type": "bar"}, {"type": "bar"}],
74
- [{"type": "bar"}, {"type": "bar"}, {"type": "bar"}],
75
  ],
76
- vertical_spacing=0.18,
77
- horizontal_spacing=0.10,
78
  )
79
 
80
- # 1 β€” Fases (donut)
81
  fases = stats["fases_classificadas"]
82
- fig.add_trace(go.Pie(
83
- labels=[FASE_LABELS.get(k, k) for k in fases],
84
- values=list(fases.values()),
85
- marker_colors=[CORES_FASE.get(k, "#888") for k in fases],
86
- hole=0.38,
87
- textinfo="percent+label",
88
- textfont_size=10,
89
- showlegend=False,
90
- ), row=1, col=1)
91
-
92
- # 2 β€” Tipo de projeto
93
- tipo_counts = df[col_tipo].value_counts().head(10)
94
- fig.add_trace(go.Bar(
95
- x=tipo_counts.values, y=tipo_counts.index,
96
- orientation="h", marker_color="#2E86AB",
97
- text=tipo_counts.values, textposition="outside",
98
- showlegend=False,
99
- ), row=1, col=2)
100
-
101
- # 3 β€” Colaborador
102
- colab_counts = df[col_colab].replace("", pd.NA).dropna().value_counts().head(8)
103
- fig.add_trace(go.Bar(
104
- x=colab_counts.values, y=colab_counts.index,
105
- orientation="h", marker_color="#A23B72",
106
- text=colab_counts.values, textposition="outside",
107
- showlegend=False,
108
- ), row=1, col=3)
109
-
110
- # 4 β€” AΓ§Γ΅es
111
- acoes = stats["acoes_observacoes"]
112
- top_acoes = dict(list(acoes.items())[:8])
113
- fig.add_trace(go.Bar(
114
- x=list(top_acoes.values()), y=list(top_acoes.keys()),
115
- orientation="h", marker_color="#F18F01",
116
- text=list(top_acoes.values()), textposition="outside",
117
- showlegend=False,
118
- ), row=2, col=1)
119
-
120
- # 5 β€” Tipos de rede
121
- rede = stats["tipos_rede_extraidos"]
122
- top_rede = dict(list(rede.items())[:8])
123
- fig.add_trace(go.Bar(
124
- x=list(top_rede.keys()), y=list(top_rede.values()),
125
- marker_color="#44BBA4",
126
- text=list(top_rede.values()), textposition="outside",
127
- showlegend=False,
128
- ), row=2, col=2)
129
-
130
- # 6 β€” Semanas
131
- semanas = stats["semanas_referencia"]
132
- top_sem = dict(list(semanas.items())[:10])
133
- fig.add_trace(go.Bar(
134
- x=list(top_sem.keys()), y=list(top_sem.values()),
135
- marker_color="#C73E1D",
136
- text=list(top_sem.values()), textposition="outside",
137
- showlegend=False,
138
- ), row=2, col=3)
139
-
140
- fig.update_layout(
141
- height=700,
142
- title_text=f"<b>Dashboard GLiNER2 β€” {stats['total_registos']} Projetos Analisados</b>",
143
- title_font_size=16,
144
- paper_bgcolor="#f8f9fa",
145
- plot_bgcolor="#ffffff",
146
- margin=dict(t=80, b=30, l=10, r=10),
147
  )
148
- fig.update_xaxes(showgrid=False)
149
- fig.update_yaxes(showgrid=False)
150
- return fig
151
 
 
152
 
153
- # ─── ABA 2: ANÁLISE POR FASE ─────────────────────────────────────────────────
154
- def fig_fases(fase_sel):
155
- if fase_sel == "Todas":
156
- sub = df[df["FASE_CLASSIFICADA"].notna()]
157
- else:
158
- chave = {v: k for k, v in FASE_LABELS.items()}.get(fase_sel, fase_sel.lower().replace(" ", "_"))
159
- sub = df[df["FASE_CLASSIFICADA"] == chave]
 
 
160
 
161
- fig = make_subplots(
162
- rows=1, cols=2,
163
- subplot_titles=[f"Tipos β€” {fase_sel}", f"Colaboradores β€” {fase_sel}"],
164
- horizontal_spacing=0.12,
 
 
 
 
 
 
165
  )
166
- cor = CORES_FASE.get({v: k for k, v in FASE_LABELS.items()}.get(fase_sel, ""), "#2E86AB")
167
-
168
- tipo_c = sub[col_tipo].value_counts().head(10)
169
- fig.add_trace(go.Bar(
170
- x=tipo_c.values, y=tipo_c.index, orientation="h",
171
- marker_color=cor, text=tipo_c.values, textposition="outside", showlegend=False,
172
- ), row=1, col=1)
173
-
174
- col_c = sub[col_colab].replace("", pd.NA).dropna().value_counts().head(8)
175
- fig.add_trace(go.Bar(
176
- x=col_c.values, y=col_c.index, orientation="h",
177
- marker_color="#555555", text=col_c.values, textposition="outside", showlegend=False,
178
- ), row=1, col=2)
179
-
180
- fig.update_layout(
181
- height=420,
182
- title_text=f"<b>Fase: {fase_sel}</b> β€” {len(sub)} projetos",
183
- title_font_size=14,
184
- paper_bgcolor="#f8f9fa", plot_bgcolor="#ffffff",
185
- margin=dict(t=70, b=20, l=10, r=10),
186
  )
187
- fig.update_xaxes(showgrid=False)
188
- fig.update_yaxes(showgrid=False)
189
- return fig
190
 
 
191
 
192
- def tabela_fases(fase_sel):
193
- if fase_sel == "Todas":
194
- sub = df[df["FASE_CLASSIFICADA"].notna()]
195
- else:
196
- chave = {v: k for k, v in FASE_LABELS.items()}.get(fase_sel, fase_sel.lower().replace(" ", "_"))
197
- sub = df[df["FASE_CLASSIFICADA"] == chave]
198
 
199
- cols = ["SUB-CIP", "PROJETO", col_tipo, col_status, col_colab,
200
- "FASE_CLASSIFICADA", "NER_OBS_acao", "NER_OBS_semana"]
201
- return sub[cols].head(100).fillna("-").rename(columns={
202
- col_tipo: "TIPO", col_status: "STATUS", col_colab: "COLABORADOR",
203
- "FASE_CLASSIFICADA": "FASE (GLiNER2)",
204
- "NER_OBS_acao": "AÇÃO (NER)", "NER_OBS_semana": "SEMANA (NER)"
205
- })
206
 
 
 
 
207
 
208
- # ─── ABA 3: EXPLORADOR NER ───────────────────────────────────────────────────
209
  COL_NER_MAP = {
210
- "AΓ§Γ£o (ObservaΓ§Γ΅es)": "NER_OBS_acao",
211
- "Semana (ObservaΓ§Γ΅es)": "NER_OBS_semana",
212
- "Data (ObservaΓ§Γ΅es)": "NER_OBS_data",
213
- "Entidade Externa": "NER_OBS_entidade_externa",
214
- "Tipo de Rede (DesignaΓ§Γ£o)": "NER_DESIGN_tipo_rede",
215
- "Tipo de Trabalho": "NER_DESIGN_tipo_trabalho",
216
- "Cliente/Entidade": "NER_DESIGN_cliente_entidade",
217
- "CΓ³digo do Projeto": "NER_DESIGN_codigo_projeto",
218
  }
219
 
 
220
  def explorar_ner(coluna_ner, top_n):
221
- col = COL_NER_MAP.get(coluna_ner, "NER_OBS_acao")
 
 
222
  all_vals = []
 
223
  for v in df[col].dropna():
224
  all_vals.extend([x.strip() for x in str(v).split(";") if x.strip()])
 
225
  counter = Counter(all_vals)
226
  top = dict(counter.most_common(int(top_n)))
227
 
228
- if not top:
229
- fig = go.Figure()
230
- fig.update_layout(title_text="Sem dados para esta entidade", height=300)
231
- return fig, pd.DataFrame()
232
-
233
- fig = go.Figure(go.Bar(
234
- x=list(top.values()), y=list(top.keys()),
235
- orientation="h",
236
- marker=dict(color=list(top.values()), colorscale="Blues", showscale=False),
237
- text=list(top.values()), textposition="outside",
238
- ))
239
- fig.update_layout(
240
- height=max(350, int(top_n) * 30),
241
- title_text=f"<b>Top {top_n} β€” {coluna_ner}</b>",
242
- title_font_size=14,
243
- paper_bgcolor="#f8f9fa", plot_bgcolor="#ffffff",
244
- margin=dict(t=60, b=20, l=200, r=60),
245
- yaxis=dict(autorange="reversed"),
246
  )
247
- fig.update_xaxes(showgrid=False)
248
- fig.update_yaxes(showgrid=False)
249
 
250
- sub = df[df[col].notna()][["SUB-CIP", col_tipo, col_colab, "FASE_CLASSIFICADA", col_obs, col]].head(50).fillna("-")
251
- sub.columns = ["SUB-CIP", "TIPO", "COLABORADOR", "FASE (GLiNER2)", "OBSERVAÇÕES", coluna_ner]
 
 
252
  return fig, sub
253
 
254
 
255
- # ─── ABA 4: EXTRAÇÃO ESTRUTURADA JSON ───────────────────────────────────────
 
 
 
256
  def tabela_json():
 
257
  rows = []
 
258
  for item in estruturados:
 
259
  sub_cip = item.get("sub_cip", "")
 
260
  tarefas = item.get("extracao_gliner2", {}).get("tarefa", [])
261
- t = tarefas[0] if tarefas else {}
 
 
 
 
 
262
  rows.append({
263
- "SUB-CIP": sub_cip,
264
- "CΓ³digo Projeto": t.get("codigo_projeto") or "-",
265
- "Tipo de Rede": t.get("tipo_rede") or "-",
266
- "Colaborador": t.get("colaborador") or "-",
267
- "Status": t.get("status") or "-",
268
- "AΓ§Γ£o/ObservaΓ§Γ£o": t.get("acao_observacao") or "-",
269
- "Semana Ref.": t.get("semana_referencia") or "-",
270
  })
 
271
  return pd.DataFrame(rows)
272
 
273
 
274
- def fig_json_resumo():
275
- rows = []
276
- for item in estruturados:
277
- tarefas = item.get("extracao_gliner2", {}).get("tarefa", [])
278
- if tarefas:
279
- rows.append(tarefas[0])
280
- df_j = pd.DataFrame(rows)
281
-
282
- fig = make_subplots(rows=1, cols=2,
283
- subplot_titles=["Colaboradores (JSON)", "Status (JSON)"])
284
-
285
- if "colaborador" in df_j and df_j["colaborador"].notna().any():
286
- co = df_j["colaborador"].dropna().str.title().value_counts().head(8)
287
- fig.add_trace(go.Bar(x=co.index, y=co.values,
288
- marker_color="#A23B72", text=co.values, textposition="outside",
289
- showlegend=False), row=1, col=1)
290
-
291
- if "status" in df_j and df_j["status"].notna().any():
292
- st = df_j["status"].dropna().value_counts().head(8)
293
- fig.add_trace(go.Bar(x=st.index, y=st.values,
294
- marker_color="#2E86AB", text=st.values, textposition="outside",
295
- showlegend=False), row=1, col=2)
296
-
297
- fig.update_layout(height=350, paper_bgcolor="#f8f9fa",
298
- plot_bgcolor="#ffffff", margin=dict(t=50, b=20))
299
- fig.update_xaxes(showgrid=False, tickangle=30)
300
- fig.update_yaxes(showgrid=False)
301
- return fig
302
 
 
303
 
304
- # ─── ABA 5: MAPEAMENTO STATUS β†’ FASE ────────────────────────────────────────
305
- def fig_mapeamento():
306
- status_counts = stats["status_original"]
307
- mapa = stats["mapeamento_status_fase"]
308
-
309
- status_list = list(status_counts.keys())
310
- count_list = [status_counts[s] for s in status_list]
311
- fase_list = [mapa.get(s, "N/A") for s in status_list]
312
- cores = [CORES_FASE.get(f, "#888888") for f in fase_list]
313
- labels_fase = [FASE_LABELS.get(f, f) for f in fase_list]
314
-
315
- fig = go.Figure()
316
- fig.add_trace(go.Bar(
317
- x=count_list,
318
- y=status_list,
319
- orientation="h",
320
- marker_color=cores,
321
- text=[f"{lf} ({n})" for lf, n in zip(labels_fase, count_list)],
322
- textposition="inside",
323
- insidetextanchor="middle",
324
- textfont=dict(color="white", size=10),
325
- showlegend=False,
326
- hovertemplate="<b>%{y}</b><br>Fase: %{text}<extra></extra>",
327
- ))
328
-
329
- for k, v in CORES_FASE.items():
330
- fig.add_trace(go.Bar(
331
- name=FASE_LABELS.get(k, k), x=[None], y=[None],
332
- marker_color=v, showlegend=True
333
- ))
334
-
335
- fig.update_layout(
336
- height=560,
337
- title_text="<b>Status Original β†’ Fase (GLiNER2) β€” 18 status, 489 projetos</b>",
338
- title_font_size=14,
339
- paper_bgcolor="#f8f9fa", plot_bgcolor="#ffffff",
340
- xaxis=dict(title="NΓΊmero de Projetos"),
341
- yaxis=dict(autorange="reversed"),
342
- margin=dict(t=60, b=40, l=260, r=20),
343
- legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
344
- )
345
- return fig
346
 
 
347
 
348
- # ─── ABA 6: ANÁLISE TEMPORAL ─────────────────────────────────────────────────
349
- def fig_temporal():
350
- # DistribuiΓ§Γ£o de aΓ§Γ΅es por semana
351
- df_sem = df[df["NER_OBS_semana"].notna() & df["NER_OBS_acao"].notna()].copy()
352
-
353
- fig = make_subplots(rows=1, cols=2,
354
- subplot_titles=["AΓ§Γ΅es por Semana de ReferΓͺncia", "Entidades Externas Identificadas"])
355
-
356
- if not df_sem.empty:
357
- sem_acao = df_sem.groupby(["NER_OBS_semana", "NER_OBS_acao"]).size().reset_index(name="n")
358
- top_semanas = df_sem["NER_OBS_semana"].value_counts().head(8).index.tolist()
359
- sem_acao = sem_acao[sem_acao["NER_OBS_semana"].isin(top_semanas)]
360
- for acao in sem_acao["NER_OBS_acao"].unique()[:5]:
361
- sub_a = sem_acao[sem_acao["NER_OBS_acao"] == acao]
362
- fig.add_trace(go.Bar(
363
- x=sub_a["NER_OBS_semana"], y=sub_a["n"],
364
- name=acao, showlegend=True,
365
- ), row=1, col=1)
366
-
367
- entidades = stats.get("entidades_externas", {})
368
- top_ent = dict(list(entidades.items())[:10])
369
- if top_ent:
370
- fig.add_trace(go.Bar(
371
- x=list(top_ent.values()), y=list(top_ent.keys()),
372
- orientation="h", marker_color="#44BBA4",
373
- text=list(top_ent.values()), textposition="outside",
374
- showlegend=False,
375
- ), row=1, col=2)
376
-
377
- fig.update_layout(
378
- height=420, barmode="stack",
379
- title_text="<b>AnΓ‘lise Temporal e Entidades Externas (NER)</b>",
380
- title_font_size=14,
381
- paper_bgcolor="#f8f9fa", plot_bgcolor="#ffffff",
382
- margin=dict(t=70, b=30, l=10, r=10),
383
- )
384
- fig.update_xaxes(showgrid=False)
385
- fig.update_yaxes(showgrid=False)
386
- return fig
387
 
 
388
 
389
- # ─── ABA 7: EXPLICAÇÃO GLiNER2 ───────────────────────────────────────────────
390
- EXPLICACAO_HTML = """
391
- <div style="font-family: 'Segoe UI', Arial, sans-serif; max-width: 920px; margin: 0 auto; color: #1a1a2e;">
392
-
393
- <div style="background: linear-gradient(135deg, #1a5276, #2E86AB); border-radius: 12px; padding: 28px 32px; margin-bottom: 28px;">
394
- <h1 style="color: white; margin: 0 0 8px 0; font-size: 1.8em;">πŸ€– GLiNER2</h1>
395
- <p style="color: #d6eaf8; margin: 0; font-size: 1.05em;">
396
- <b>Generalist and Lightweight Named Entity Recognition 2</b><br>
397
- Um modelo de IA compacto que extrai informaΓ§Γ£o estruturada de texto livre β€” sem GPU, sem API, sem custos.
398
- </p>
399
- </div>
400
-
401
- <h2 style="color: #1a5276; border-bottom: 3px solid #2E86AB; padding-bottom: 6px;">πŸ” O Problema que o GLiNER2 Resolve</h2>
402
- <p>
403
- O ficheiro de projetos contΓ©m <b>489 registos reais</b> com campos de texto livre como
404
- <em>DesignaΓ§Γ£o do Projeto</em> e <em>ObservaΓ§Γ΅es</em>. Estes campos guardam informaΓ§Γ£o crΓ­tica β€”
405
- tipo de rede, aΓ§Γ΅es pendentes, entidades externas, semanas de referΓͺncia β€” mas numa forma
406
- impossΓ­vel de analisar diretamente com ferramentas tradicionais.
407
- </p>
408
- <div style="background: #fff3cd; border-left: 5px solid #ffc107; padding: 14px 18px; border-radius: 6px; margin: 16px 0;">
409
- <b>Exemplo real do ficheiro:</b><br>
410
- <code>"ATT VALID 17/02 PUSH MAIL ATT INFO MAIRIE 06/02 //"</code><br>
411
- <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>
412
- </div>
413
- <div style="background: #d1ecf1; border-left: 5px solid #17a2b8; padding: 14px 18px; border-radius: 6px; margin: 16px 0;">
414
- <b>Nota tΓ©cnica β€” Separador do ficheiro:</b><br>
415
- O ficheiro CSV usa <b>ponto-e-vΓ­rgula (;)</b> como separador e codificaΓ§Γ£o <b>latin-1</b>.
416
- Alguns campos (ex: "DATA LIMITE SLA") contΓͺm quebras de linha internas que faziam o pandas
417
- interpretar 1199 pseudo-linhas em vez das <b>489 reais</b>. O dashboard usa a leitura correta
418
- com filtro por padrΓ£o SUB-CIP (RBPT.YYYY.NNN.NNNN).
419
- </div>
420
-
421
- <h2 style="color: #1a5276; border-bottom: 3px solid #2E86AB; padding-bottom: 6px; margin-top: 28px;">βš™οΈ Como Funciona: Os 3 Modos Aplicados</h2>
422
-
423
- <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; margin: 16px 0;">
424
- <div style="background: #d1ecf1; border-radius: 10px; padding: 16px;">
425
- <h3 style="color: #0c5460; margin-top: 0;">πŸ“Œ Modo 1<br>NER nas DesignaΓ§Γ΅es</h3>
426
- <p style="font-size: 0.92em; color: #333;">
427
- Extrai o <b>tipo de rede</b> (BTHD, RAMI, ART 2 3...), o <b>tipo de trabalho</b> (TVX, GC, ETU...),
428
- o <b>cliente</b> (ENEDIS, ORANGE) e o <b>cΓ³digo do projeto</b> (VAN500664...) de textos como
429
- <em>"L, AO, 7C - 576 BTHD TVX GC DROIT AU TERRAIN"</em>.
430
- </p>
431
- <p style="font-size: 0.88em; color: #0c5460;"><b>Resultado:</b> BTHD domina com 73 projetos; ART 2 3 com 69.</p>
432
- </div>
433
- <div style="background: #d1ecf1; border-radius: 10px; padding: 16px;">
434
- <h3 style="color: #0c5460; margin-top: 0;">πŸ“ Modo 2<br>NER nas ObservaΓ§Γ΅es</h3>
435
- <p style="font-size: 0.92em; color: #333;">
436
- Extrai <b>aΓ§Γ΅es</b> (VALIDADO, ATT VALID, RELANCE), <b>semanas</b> (S07, S39),
437
- <b>datas</b> (17/02) e <b>entidades externas</b> (MAIRIE, PMV, Sogetrel)
438
- de notas humanas confusas e abreviadas.
439
- </p>
440
- <p style="font-size: 0.88em; color: #0c5460;"><b>Resultado:</b> "VALIDADO" lidera (163x); "ATT VALID" em 90 registos.</p>
441
- </div>
442
- <div style="background: #d1ecf1; border-radius: 10px; padding: 16px;">
443
- <h3 style="color: #0c5460; margin-top: 0;">🏷️ Modo 3<br>Classificação de Status</h3>
444
- <p style="font-size: 0.92em; color: #333;">
445
- Agrupa os <b>18 status distintos</b> em 5 fases de ciclo de vida
446
- sem qualquer regra manual β€” o modelo lΓͺ o texto e decide semanticamente.
447
- </p>
448
- <p style="font-size: 0.88em; color: #0c5460;"><b>Resultado:</b> 43% pendentes de validaΓ§Γ£o; 29% concluΓ­dos.</p>
449
- </div>
450
- </div>
451
-
452
- <h2 style="color: #1a5276; border-bottom: 3px solid #2E86AB; padding-bottom: 6px; margin-top: 28px;">πŸ“Š Resultados Obtidos</h2>
453
- <table style="width:100%; border-collapse: collapse; font-size: 0.95em;">
454
- <thead>
455
- <tr style="background: #2E86AB; color: white;">
456
- <th style="padding: 10px; text-align: left;">MΓ©trica</th>
457
- <th style="padding: 10px; text-align: center;">Valor</th>
458
- <th style="padding: 10px; text-align: left;">Significado</th>
459
- </tr>
460
- </thead>
461
- <tbody>
462
- <tr style="background: #f0f7ff;"><td style="padding: 9px 10px;">Total de registos reais</td><td style="padding: 9px 10px; text-align: center; font-weight: bold; color: #2E86AB;">489</td><td style="padding: 9px 10px;">Filtrado por padrΓ£o SUB-CIP vΓ‘lido</td></tr>
463
- <tr><td style="padding: 9px 10px;">DesignaΓ§Γ΅es ΓΊnicas processadas</td><td style="padding: 9px 10px; text-align: center; font-weight: bold; color: #2E86AB;">62</td><td style="padding: 9px 10px;">Tipos de projeto distintos</td></tr>
464
- <tr style="background: #f0f7ff;"><td style="padding: 9px 10px;">ObservaΓ§Γ΅es ΓΊnicas processadas</td><td style="padding: 9px 10px; text-align: center; font-weight: bold; color: #2E86AB;">232</td><td style="padding: 9px 10px;">Notas ΓΊnicas analisadas pelo NER</td></tr>
465
- <tr><td style="padding: 9px 10px;">Registos com aΓ§Γ£o identificada</td><td style="padding: 9px 10px; text-align: center; font-weight: bold; color: #F18F01;">163+ (VALIDADO)</td><td style="padding: 9px 10px;">AΓ§Γ΅es operacionais extraΓ­das</td></tr>
466
- <tr style="background: #f0f7ff;"><td style="padding: 9px 10px;">Projetos com fase classificada</td><td style="padding: 9px 10px; text-align: center; font-weight: bold; color: #44BBA4;">489 (100%)</td><td style="padding: 9px 10px;">Todos os status mapeados em fases</td></tr>
467
- <tr><td style="padding: 9px 10px;">Novas colunas adicionadas ao CSV</td><td style="padding: 9px 10px; text-align: center; font-weight: bold; color: #A23B72;">12</td><td style="padding: 9px 10px;">Dados estruturados prontos para BI</td></tr>
468
- </tbody>
469
- </table>
470
-
471
- <h2 style="color: #1a5276; border-bottom: 3px solid #2E86AB; padding-bottom: 6px; margin-top: 28px;">⚑ Por que GLiNER2 e não outra abordagem?</h2>
472
- <table style="width:100%; border-collapse: collapse; font-size: 0.93em;">
473
- <thead>
474
- <tr style="background: #2E86AB; color: white;">
475
- <th style="padding: 10px;">Abordagem</th>
476
- <th style="padding: 10px;">LimitaΓ§Γ£o</th>
477
- <th style="padding: 10px;">GLiNER2</th>
478
- </tr>
479
- </thead>
480
- <tbody>
481
- <tr style="background: #f8d7da;"><td style="padding: 9px 10px; font-weight: bold;">ExpressΓ΅es Regulares</td><td style="padding: 9px 10px;">Quebram com qualquer variaΓ§Γ£o no texto</td><td style="padding: 9px 10px; color: #155724;">βœ… Entende semΓ’ntica, nΓ£o padrΓ΅es rΓ­gidos</td></tr>
482
- <tr style="background: #fff3cd;"><td style="padding: 9px 10px; font-weight: bold;">ChatGPT / Claude (API)</td><td style="padding: 9px 10px;">Custo elevado + dados enviados para a nuvem</td><td style="padding: 9px 10px; color: #155724;">βœ… 100% local, gratuito, privado</td></tr>
483
- <tr style="background: #f8d7da;"><td style="padding: 9px 10px; font-weight: bold;">SpaCy / NER clΓ‘ssico</td><td style="padding: 9px 10px;">SΓ³ reconhece Pessoa/Local/Org genΓ©ricos</td><td style="padding: 9px 10px; color: #155724;">βœ… Schema personalizado em linguagem natural</td></tr>
484
- <tr style="background: #fff3cd;"><td style="padding: 9px 10px; font-weight: bold;">LLMs locais (Llama 7B+)</td><td style="padding: 9px 10px;">Requer GPU potente; lento em CPU</td><td style="padding: 9px 10px; color: #155724;">βœ… 205M parΓ’metros, rΓ‘pido em CPU</td></tr>
485
- </tbody>
486
- </table>
487
-
488
- <div style="background: linear-gradient(135deg, #d4edda, #c3e6cb); border-radius: 10px; padding: 20px 24px; margin-top: 28px;">
489
- <h3 style="color: #155724; margin-top: 0;">🎯 Conclusão</h3>
490
- <p style="color: #155724; margin: 0; font-size: 1em;">
491
- O GLiNER2 transformou o ficheiro CSV de <b>489 projetos</b> de um depΓ³sito de texto livre
492
- numa <b>base de dados estruturada e analisΓ‘vel</b>. As 12 novas colunas geradas estΓ£o prontas
493
- para alimentar qualquer ferramenta de Business Intelligence (Power BI, Tableau, Excel)
494
- β€” tudo processado localmente, sem qualquer custo de API.
495
- </p>
496
- </div>
497
-
498
- <div style="margin-top: 20px; padding: 12px 16px; background: #e8f4fd; border-radius: 8px; font-size: 0.88em; color: #1a5276;">
499
- <b>ReferΓͺncias:</b>
500
- Zaratiana et al. (2025) β€” <em>GLiNER2: An Efficient Multi-Task Information Extraction System</em>.
501
- arXiv:2507.18546 Β·
502
- <a href="https://github.com/fastino-ai/GLiNER2" target="_blank" style="color:#2E86AB;">github.com/fastino-ai/GLiNER2</a>
503
- </div>
504
- </div>
505
- """
506
 
507
- # ─── CONSTRUIR A APP GRADIO ──────────────────────────────────────────────────
508
- THEME = gr.themes.Soft(
509
- primary_hue="blue",
510
- secondary_hue="orange",
511
- font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "sans-serif"],
512
- )
513
 
514
- with gr.Blocks(
515
- theme=THEME,
516
- title="GLiNER2 Dashboard β€” 489 Projetos",
517
- css="""
518
- .gradio-container { max-width: 1200px !important; }
519
- .tab-nav button { font-size: 14px !important; font-weight: 600 !important; }
520
- footer { display: none !important; }
521
- """,
522
- ) as demo:
523
-
524
- gr.HTML("""
525
- <div style="background: linear-gradient(135deg, #1a5276 0%, #2E86AB 100%);
526
- padding: 22px 32px; border-radius: 12px; margin-bottom: 8px;">
527
- <h1 style="color: white; margin: 0; font-size: 1.9em; font-family: Inter, sans-serif;">
528
- πŸ”¬ GLiNER2 Dashboard
529
- </h1>
530
- <p style="color: #d6eaf8; margin: 6px 0 0 0; font-size: 1.05em;">
531
- AnΓ‘lise de <b>489 Projetos/Tarefas</b> com ExtraΓ§Γ£o de InformaΓ§Γ£o por InteligΓͺncia Artificial
532
- &nbsp;|&nbsp; Separador: <code>;</code> &nbsp;|&nbsp; Encoding: <code>latin-1</code>
533
- </p>
534
- </div>
535
- """)
536
 
537
- with gr.Tabs():
538
 
539
- with gr.Tab("πŸ“Š VisΓ£o Geral"):
540
- gr.Markdown("### Panorama completo dos 489 projetos analisados pelo GLiNER2")
541
- plot_geral = gr.Plot(show_label=False)
542
- demo.load(fn=fig_visao_geral, outputs=plot_geral)
543
 
544
- with gr.Tab("🏷️ AnÑlise por Fase"):
545
- gr.Markdown("### Explore os projetos por fase de ciclo de vida (classificada pelo GLiNER2)")
546
- fase_dd = gr.Dropdown(
547
- choices=["Todas"] + list(FASE_LABELS.values()),
548
- value="Todas", label="Filtrar por Fase",
549
  )
550
- plot_fase = gr.Plot(show_label=False)
551
- tbl_fase = gr.Dataframe(label="Registos da Fase", wrap=True, interactive=False)
552
- fase_dd.change(fn=fig_fases, inputs=fase_dd, outputs=plot_fase)
553
- fase_dd.change(fn=tabela_fases, inputs=fase_dd, outputs=tbl_fase)
554
- demo.load(fn=fig_fases, inputs=fase_dd, outputs=plot_fase)
555
- demo.load(fn=tabela_fases, inputs=fase_dd, outputs=tbl_fase)
556
 
557
- with gr.Tab("πŸ” Explorador NER"):
558
- gr.Markdown("### Explore as entidades extraΓ­das pelo GLiNER2 de cada campo de texto")
559
- with gr.Row():
560
- ner_dd = gr.Dropdown(
561
- choices=list(COL_NER_MAP.keys()),
562
- value="AΓ§Γ£o (ObservaΓ§Γ΅es)", label="Entidade NER", scale=2,
563
- )
564
- top_n = gr.Slider(5, 20, value=10, step=1, label="Top N", scale=1)
565
- plot_ner = gr.Plot(show_label=False)
566
- tbl_ner = gr.Dataframe(label="Registos com esta entidade", wrap=True, interactive=False)
567
- ner_dd.change(fn=explorar_ner, inputs=[ner_dd, top_n], outputs=[plot_ner, tbl_ner])
568
- top_n.change(fn=explorar_ner, inputs=[ner_dd, top_n], outputs=[plot_ner, tbl_ner])
569
- demo.load(fn=explorar_ner, inputs=[ner_dd, top_n], outputs=[plot_ner, tbl_ner])
570
-
571
- with gr.Tab("πŸ“‹ ExtraΓ§Γ£o JSON"):
572
- gr.Markdown("""
573
- ### ExtraΓ§Γ£o Estruturada (JSON) pelo GLiNER2
574
- O modelo leu o texto completo de cada projeto e extraiu automaticamente um objeto JSON
575
- com **cΓ³digo do projeto**, **tipo de rede**, **colaborador**, **status**, **aΓ§Γ£o** e **semana**.
576
- """)
577
- plot_json = gr.Plot(show_label=False)
578
- tbl_json = gr.Dataframe(label="ExtraΓ§Γ£o Estruturada (30 amostras)", wrap=True, interactive=False)
579
- demo.load(fn=fig_json_resumo, outputs=plot_json)
580
- demo.load(fn=tabela_json, outputs=tbl_json)
581
-
582
- with gr.Tab("πŸ—ΊοΈ Status β†’ Fase"):
583
- gr.Markdown("""
584
- ### Mapeamento Inteligente: 18 Status β†’ 5 Fases
585
- O GLiNER2 classificou semanticamente todos os status do ficheiro em fases de ciclo de vida.
586
- """)
587
- plot_mapa = gr.Plot(show_label=False)
588
- demo.load(fn=fig_mapeamento, outputs=plot_mapa)
589
-
590
- with gr.Tab("πŸ“… AnΓ‘lise Temporal"):
591
- gr.Markdown("### AΓ§Γ΅es por semana e entidades externas identificadas pelo NER")
592
- plot_temp = gr.Plot(show_label=False)
593
- demo.load(fn=fig_temporal, outputs=plot_temp)
594
-
595
- with gr.Tab("πŸ€– O que Γ© o GLiNER2?"):
596
- gr.HTML(EXPLICACAO_HTML)
597
-
598
- gr.HTML("""
599
- <div style="text-align: center; padding: 10px; color: #888; font-size: 0.85em; margin-top: 6px;">
600
- GLiNER2 Dashboard Β· <b>489 projetos reais</b> Β· Modelo: urchade/gliner_medium-v2.1 Β·
601
- Separador: <code>;</code> Β· Encoding: <code>latin-1</code> Β· Processado 100% localmente
602
- </div>
603
- """)
604
 
605
  if __name__ == "__main__":
606
- demo.launch(server_name="0.0.0.0", server_port=7861, share=False)
 
1
  """
2
  Dashboard GLiNER2 β€” AnΓ‘lise de Projetos/Tarefas
3
+ 489 registos reais | Gradio | MΓΊltiplas abas interativas
4
  """
5
 
6
  import gradio as gr
 
9
  from plotly.subplots import make_subplots
10
  import json
11
  from collections import Counter
12
+ from pathlib import Path
13
  import warnings
14
+
15
  warnings.filterwarnings("ignore")
16
 
17
+ # ─────────────────────────────────────────────
18
+ # CAMINHOS PORTÁTEIS
19
+ # ─────────────────────────────────────────────
20
+
21
+ BASE = Path(__file__).parent
22
+
23
+ CSV_FILE = BASE / "gliner2_resultado_489.csv"
24
+ STAT_FILE = BASE / "gliner2_estatisticas_489.json"
25
+ JSON_FILE = BASE / "gliner2_extracao_489.json"
26
+
27
+ if not CSV_FILE.exists():
28
+ raise FileNotFoundError(f"CSV nΓ£o encontrado: {CSV_FILE}")
29
+
30
+ if not STAT_FILE.exists():
31
+ raise FileNotFoundError(f"JSON estatΓ­sticas nΓ£o encontrado: {STAT_FILE}")
32
 
33
+ if not JSON_FILE.exists():
34
+ raise FileNotFoundError(f"JSON extraΓ§Γ£o nΓ£o encontrado: {JSON_FILE}")
35
 
36
+ # ─────────────────────────────────────────────
37
+ # CARREGAR DADOS
38
+ # ─────────────────────────────────────────────
39
+
40
+ df = pd.read_csv(
41
+ CSV_FILE,
42
+ sep=";",
43
+ encoding="latin-1",
44
+ on_bad_lines="skip"
45
+ )
46
+
47
+ with open(STAT_FILE, encoding="utf-8") as f:
48
  stats = json.load(f)
49
 
50
+ with open(JSON_FILE, encoding="utf-8") as f:
51
  estruturados = json.load(f)
52
 
53
+ # ─────────────────────────────────────────────
54
+ # NORMALIZAÇÃO
55
+ # ─────────────────────────────────────────────
56
+
57
  df["COLABORADOR"] = df["COLABORADOR"].fillna("").str.strip().str.title()
58
 
59
+ # procurar colunas automaticamente
60
+ def find_col(keyword):
61
+ for c in df.columns:
62
+ if keyword.upper() in c.upper():
63
+ return c
64
+ raise ValueError(f"Coluna contendo '{keyword}' nΓ£o encontrada")
65
+
66
+ col_designacao = find_col("DESIGNA")
67
+ col_obs = find_col("OBSERVA")
68
+ col_status = "RB STATUS"
69
+ col_tipo = "TIPO"
70
+ col_colab = "COLABORADOR"
71
+
72
+ # ─────────────────────────────────────────────
73
+ # CORES
74
+ # ─────────────────────────────────────────────
75
 
 
76
  CORES_FASE = {
77
  "pendente_validacao": "#F18F01",
78
+ "concluido": "#2E86AB",
79
+ "faturado": "#44BBA4",
80
+ "em_progresso": "#A23B72",
81
+ "cancelado": "#C73E1D",
82
  }
83
 
84
  FASE_LABELS = {
85
  "pendente_validacao": "Pendente ValidaΓ§Γ£o",
86
+ "concluido": "ConcluΓ­do",
87
+ "faturado": "Faturado",
88
+ "em_progresso": "Em Progresso",
89
+ "cancelado": "Cancelado",
90
  }
91
 
92
+ # ─────────────────────────────────────────────
93
+ # UTIL
94
+ # ─────────────────────────────────────────────
95
+
96
  def top_counter(series, n=15):
97
  all_vals = []
98
  for v in series.dropna():
99
+ all_vals.extend([x.strip() for x in str(v).split(";") if x.strip()])
100
  return dict(Counter(all_vals).most_common(n))
101
 
102
+ # ───────���─────────────────────────────────────
103
+ # FIGURA PRINCIPAL
104
+ # ─────────────────────────────────────────────
105
+
106
  def fig_visao_geral():
107
+
108
  fig = make_subplots(
109
+ rows=2,
110
+ cols=2,
111
  subplot_titles=[
112
+ "Fases dos Projetos",
113
+ "Tipos de Projeto",
114
  "Projetos por Colaborador",
115
+ "AΓ§Γ΅es nas ObservaΓ§Γ΅es",
 
 
 
 
 
 
116
  ],
 
 
117
  )
118
 
 
119
  fases = stats["fases_classificadas"]
120
+
121
+ fig.add_trace(
122
+ go.Pie(
123
+ labels=[FASE_LABELS.get(k, k) for k in fases],
124
+ values=list(fases.values()),
125
+ hole=0.4,
126
+ ),
127
+ row=1,
128
+ col=1,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  )
 
 
 
130
 
131
+ tipo_counts = df[col_tipo].value_counts().head(10)
132
 
133
+ fig.add_trace(
134
+ go.Bar(
135
+ x=tipo_counts.values,
136
+ y=tipo_counts.index,
137
+ orientation="h",
138
+ ),
139
+ row=1,
140
+ col=2,
141
+ )
142
 
143
+ colab_counts = df[col_colab].replace("", pd.NA).dropna().value_counts().head(8)
144
+
145
+ fig.add_trace(
146
+ go.Bar(
147
+ x=colab_counts.values,
148
+ y=colab_counts.index,
149
+ orientation="h",
150
+ ),
151
+ row=2,
152
+ col=1,
153
  )
154
+
155
+ acoes = stats["acoes_observacoes"]
156
+
157
+ fig.add_trace(
158
+ go.Bar(
159
+ x=list(acoes.values())[:10],
160
+ y=list(acoes.keys())[:10],
161
+ orientation="h",
162
+ ),
163
+ row=2,
164
+ col=2,
 
 
 
 
 
 
 
 
 
165
  )
 
 
 
166
 
167
+ fig.update_layout(height=650)
168
 
169
+ return fig
 
 
 
 
 
170
 
 
 
 
 
 
 
 
171
 
172
+ # ─────────────────────────────────────────────
173
+ # EXPLORADOR NER
174
+ # ─────────────────────────────────────────────
175
 
 
176
  COL_NER_MAP = {
177
+ "AΓ§Γ£o (ObservaΓ§Γ΅es)": "NER_OBS_acao",
178
+ "Semana (ObservaΓ§Γ΅es)": "NER_OBS_semana",
179
+ "Entidade": "NER_OBS_entidade_externa",
 
 
 
 
 
180
  }
181
 
182
+
183
  def explorar_ner(coluna_ner, top_n):
184
+
185
+ col = COL_NER_MAP[coluna_ner]
186
+
187
  all_vals = []
188
+
189
  for v in df[col].dropna():
190
  all_vals.extend([x.strip() for x in str(v).split(";") if x.strip()])
191
+
192
  counter = Counter(all_vals)
193
  top = dict(counter.most_common(int(top_n)))
194
 
195
+ fig = go.Figure(
196
+ go.Bar(
197
+ x=list(top.values()),
198
+ y=list(top.keys()),
199
+ orientation="h",
200
+ )
 
 
 
 
 
 
 
 
 
 
 
 
201
  )
 
 
202
 
203
+ fig.update_layout(height=400)
204
+
205
+ sub = df[df[col].notna()].head(50)
206
+
207
  return fig, sub
208
 
209
 
210
+ # ─────────────────────────────────────────────
211
+ # JSON ESTRUTURADO
212
+ # ─────────────────────────────────────────────
213
+
214
  def tabela_json():
215
+
216
  rows = []
217
+
218
  for item in estruturados:
219
+
220
  sub_cip = item.get("sub_cip", "")
221
+
222
  tarefas = item.get("extracao_gliner2", {}).get("tarefa", [])
223
+
224
+ if tarefas:
225
+ t = tarefas[0]
226
+ else:
227
+ t = {}
228
+
229
  rows.append({
230
+ "SUB-CIP": sub_cip,
231
+ "CΓ³digo": t.get("codigo_projeto"),
232
+ "Rede": t.get("tipo_rede"),
233
+ "Colaborador": t.get("colaborador"),
234
+ "Status": t.get("status"),
 
 
235
  })
236
+
237
  return pd.DataFrame(rows)
238
 
239
 
240
+ # ─────────────────────────────────────────────
241
+ # APP GRADIO
242
+ # ─────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
+ with gr.Blocks(title="GLiNER2 Dashboard") as demo:
245
 
246
+ gr.Markdown("# πŸ”¬ GLiNER2 Dashboard")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
+ with gr.Tabs():
249
 
250
+ with gr.Tab("πŸ“Š VisΓ£o Geral"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
 
252
+ plot_geral = gr.Plot()
253
 
254
+ demo.load(fn=fig_visao_geral, outputs=plot_geral)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
+ with gr.Tab("πŸ” Explorador NER"):
 
 
 
 
 
257
 
258
+ ner_dd = gr.Dropdown(
259
+ choices=list(COL_NER_MAP.keys()),
260
+ value="AΓ§Γ£o (ObservaΓ§Γ΅es)",
261
+ label="Entidade NER"
262
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
 
264
+ top_n = gr.Slider(5, 20, value=10)
265
 
266
+ plot_ner = gr.Plot()
267
+
268
+ tbl_ner = gr.Dataframe()
 
269
 
270
+ ner_dd.change(
271
+ fn=explorar_ner,
272
+ inputs=[ner_dd, top_n],
273
+ outputs=[plot_ner, tbl_ner],
 
274
  )
 
 
 
 
 
 
275
 
276
+ with gr.Tab("πŸ“‹ JSON Estruturado"):
277
+
278
+ tbl_json = gr.Dataframe()
279
+
280
+ demo.load(fn=tabela_json, outputs=tbl_json)
281
+
282
+ # ─────────────────────────────────────────────
283
+ # RUN
284
+ # ─────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
 
286
  if __name__ == "__main__":
287
+ demo.launch()