joao-oak commited on
Commit
b97b213
·
1 Parent(s): e9d1ac9

initial commit

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.csv filter=lfs diff=lfs merge=lfs -text
37
+ *.png filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # Set environment variables
4
+ ENV PORT=7860
5
+ ENV TRANSFORMERS_CACHE=/app/models
6
+ ENV HF_HOME=/app/models
7
+
8
+ # Create app and model directories
9
+ WORKDIR /code
10
+ RUN mkdir -p /app/models && chmod -R 777 /app/models
11
+
12
+ # Install dependencies
13
+ COPY requirements.txt .
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ # Pre-download the SentenceTransformer model
17
+ RUN python3 -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('sentence-transformers/paraphrase-multilingual-mpnet-base-v2')"
18
+
19
+ # Copy the rest of the app code
20
+ COPY . .
21
+
22
+ # Expose port
23
+ EXPOSE $PORT
24
+
25
+ # Run the app
26
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,943 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dash
2
+ from dash import dcc, html, Input, Output, State
3
+ import dash_bootstrap_components as dbc
4
+ import plotly.express as px
5
+ import pandas as pd
6
+ import dash_daq as daq
7
+ import plotly.graph_objects as go
8
+ import base64
9
+ from openai import OpenAI
10
+ from dash.exceptions import PreventUpdate
11
+ import os
12
+ # os.environ["TRANSFORMERS_CACHE"] = "/app/models"
13
+ # os.environ["HF_HOME"] = "/app/models"
14
+ import utils as u
15
+ import requests
16
+ import bs4
17
+ import re
18
+ from langchain_huggingface import HuggingFaceEmbeddings
19
+
20
+ info_text = """
21
+ ### Funcionalidades:
22
+
23
+ - A secção das definições permite-lhe escolher o tipo gráfico de dispersão: 1D, 2D, ou 3D.
24
+ - A ferramenta classifica artigos consoante quatro eixos: viés político, fiabilidade, objetividade e legibilidade.
25
+ - Todos os eixos podem ser escolhidos na secção das definições, bem como o seu intervalo de valores.
26
+ - Um filtro de fontes está também disponível para melhor análise.
27
+ - Na última parte da secção, um botão permite agrupar os dados e calcular a média por fonte, em vez dos dados de nível de artigo.
28
+ - O gráfico em si oferece duas funções interativas:
29
+ - Dados específicos da notícia são mostrados com a passagem do rato por cima do respetivo ponto.
30
+ - Ao clicar num ponto, o corpo do artigo é exibido numa janela pop-up.
31
+ - Por baixo do gráfico, um artigo pode ser adicionado para classificação em tempo real através de um ficheiro .txt ou upload de URL. Certifique-se de que qualquer artigo de uma URL é de uma fontes indicadas e não está bloqueado a subscrição.
32
+
33
+ ### Classificação:
34
+
35
+ - A classificação de cada eixo é realizada usando um modelo de linguagem de grande escala (LLM).
36
+ - O modelo gera um descritor para cada artigo e para cada eixo.
37
+ - Exemplos de descritores podem ser encontrados nos dados de passagem dos pontos já presentes no gráfico.
38
+ - O descritor gerado para cada eixo é então comparado com uma base de dados de descritores obtidos a partir de um conjunto de dados etiquetados.
39
+ - Com base na similaridade com os descritores na base de dados, o valor do eixo é calculado para cada eixo de forma independente.
40
+ - Para o eixo de legibilidade, o valor é obtido usando métricas de legibilidade estabelecidas e mapeando-as para a escala do eixo.
41
+ """
42
+
43
+
44
+ # Dash app
45
+ app = dash.Dash(__name__, title='Media Bias Chart', external_stylesheets=[dbc.themes.BOOTSTRAP])
46
+ server = app.server
47
+
48
+ data = pd.read_csv("./demo_data.csv")
49
+ data = data[["Título", "Texto", "Fonte", "Descritor de Viés Político", "Descritor de Fiabilidade", "Descritor de Objetividade", "Viés Político", "Fiabilidade", "Objetividade", "Legibilidade"]]
50
+
51
+ embedding_model = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-mpnet-base-v2")
52
+ political_bias_db, reliability_db, objectivity_db = u.setup_db()
53
+
54
+ app.layout = html.Div([
55
+
56
+ dcc.Store(id="data-store", data=data.to_dict("records"), storage_type="session"),
57
+ html.Div([
58
+
59
+ # dcc.Store(id='news-data', data=data.to_dict('records')),
60
+
61
+ html.H2("Definições", style={'textAlign': 'center'}),
62
+
63
+ # Dropdown for selecting chart type
64
+ html.Label("Selecione o tipo de gráfico:"),
65
+ dcc.Dropdown(
66
+ id="chart-type",
67
+ options=[
68
+ {"label": "Gráfico de Dispersão 1D", "value": "1D"},
69
+ {"label": "Gráfico de Dispersão 2D", "value": "2D"},
70
+ {"label": "Gráfico de Dispersão 3D", "value": "3D"}
71
+ ],
72
+ value="2D",
73
+ clearable=False,
74
+ style={'marginBottom': '40px'}
75
+ ),
76
+
77
+ # Dropdowns for selecting axes
78
+ html.Div([
79
+ html.Label("Selecione o eixo X:"),
80
+ dcc.Dropdown(
81
+ id="x-axis",
82
+ options=[{"label": col, "value": col} for col in data.columns[6:]],
83
+ value="Viés Político",
84
+ clearable=False,
85
+ style={'marginBottom': '10px'},
86
+ ),
87
+ dcc.RangeSlider(
88
+ id="x-filter",
89
+ min=-100,
90
+ max=100,
91
+ step=5,
92
+ marks={-100: '-100', -50: '-50', 0: '0', 50: '50', 100: '100'},
93
+ value=[-100, 100],
94
+ allowCross=False,
95
+ className="range-slider"
96
+ )
97
+ ], id="x-axis-container", style={'marginBottom': '20px'}),
98
+
99
+ html.Div([
100
+ html.Label("Selecione o eixo Y:"),
101
+ dcc.Dropdown(
102
+ id="y-axis",
103
+ options=[{"label": col, "value": col} for col in data.columns[6:]],
104
+ value="Fiabilidade",
105
+ clearable=False,
106
+ style={'marginBottom': '10px'},
107
+ ),
108
+ dcc.RangeSlider(
109
+ id="y-filter",
110
+ min=-100,
111
+ max=100,
112
+ step=5,
113
+ marks={-100: '-100', -50: '-50', 0: '0', 50: '50', 100: '100'},
114
+ value=[-100, 100],
115
+ allowCross=False,
116
+ className="range-slider"
117
+ )
118
+ ], id="y-axis-container", style={'marginBottom': '20px'}),
119
+
120
+ html.Div([
121
+ html.Label("Selecione o eixo Z:"),
122
+ dcc.Dropdown(
123
+ id="z-axis",
124
+ options=[{"label": col, "value": col} for col in data.columns[6:]],
125
+ value="Objetividade",
126
+ clearable=False,
127
+ style={'marginBottom': '10px'},
128
+ ),
129
+ dcc.RangeSlider(
130
+ id="z-filter",
131
+ min=-100,
132
+ max=100,
133
+ step=5,
134
+ marks={-100: '-100', -50: '-50', 0: '0', 50: '50', 100: '100'},
135
+ value=[-100, 100],
136
+ allowCross=False,
137
+ className="range-slider"
138
+ )
139
+ ], id="z-axis-container", style={'marginBottom': '30px'}),
140
+
141
+ # Checkbox filter for sources
142
+ html.Label("Filtrar por Fonte:"),
143
+ dcc.Dropdown(
144
+ id="source-filter",
145
+ options=([{"label": "Selecionar Todos", "value": "ALL"}] +
146
+ [{"label": src, "value": src} for src in data["Fonte"].unique()]),
147
+ value=data["Fonte"].unique().tolist(),
148
+ multi=True,
149
+ placeholder="Selectione a fonte...",
150
+ style={'font-family' : 'Arial', 'marginBottom': '20px'}
151
+ ),
152
+
153
+ # Toggle button to group by source
154
+ daq.ToggleSwitch(
155
+ id="group-toggle",
156
+ label="Agrupar por fonte (Média)",
157
+ size=60
158
+ ),
159
+ ],
160
+ style={
161
+ 'width': '25%', 'padding': '20px', 'backgroundColor': '#f8f9fa',
162
+ 'position': 'fixed', 'height': '100vh', 'overflowY': 'auto', 'font-family': 'Calibri'
163
+ }
164
+ ),
165
+
166
+ html.Div([
167
+ html.Div(
168
+ dbc.Button("Informação", id="open-info", size="lg", n_clicks=0,
169
+ style={
170
+ "backgroundColor": "#E5ECF6",
171
+ "color": "black",
172
+ "border": "none",
173
+ # "font-weight": "bold"
174
+ }),
175
+ className="d-flex justify-content-end"
176
+ ),
177
+
178
+ dbc.Modal(
179
+ [
180
+ # dbc.ModalHeader(dbc.ModalTitle("How to Use This App")),
181
+ dbc.ModalBody(dcc.Markdown(info_text)),
182
+ dbc.ModalFooter(
183
+ dbc.Button("Fechar", id="close-info", className="ms-auto", n_clicks=0)
184
+ ),
185
+ ],
186
+ id="modal-info",
187
+ is_open=False,
188
+ ),
189
+
190
+ # Graph
191
+ html.H1("Portuguese Media Charts", style={'textAlign': 'center', 'font-family': 'Calibri', 'font-weight': 'bold'}),
192
+ dcc.Graph(id="news-plot", style={'height': '800px'}),
193
+
194
+ dbc.Modal(
195
+ [
196
+ dbc.ModalHeader(dbc.ModalTitle(id="modal-title")),
197
+ dbc.ModalBody(id='modal-body'),
198
+ dbc.ModalFooter(
199
+ dbc.Button("Fechar", id="close", className="ms-auto", n_clicks=0)
200
+ ),
201
+ ],
202
+ id="modal",
203
+ is_open=False,
204
+ ),
205
+
206
+ dcc.Loading(
207
+ id="upload-loading",
208
+ type="circle",
209
+ fullscreen=False,
210
+ children=[
211
+
212
+ # for article input in .txt
213
+ dcc.Upload(
214
+ id='upload-article',
215
+ children=html.Div([
216
+ '📄 Arraste ou Selecione um Artigo para Classificar (formato .txt)'
217
+ ]),
218
+ style={
219
+ 'width': '100%',
220
+ 'height': '60px',
221
+ 'lineHeight': '60px',
222
+ 'borderWidth': '1px',
223
+ 'borderStyle': 'dashed',
224
+ 'borderRadius': '5px',
225
+ 'textAlign': 'center',
226
+ 'margin': '10px 0',
227
+ 'font-family': 'Calibri',
228
+ 'margin-bottom': '30px'
229
+ },
230
+ accept='.txt'
231
+ ),
232
+
233
+ # for article input in url format
234
+ html.Div([
235
+ html.Label([
236
+ html.Span("Ou Insira um Link ", style={'font-size': '18px', 'font-family': 'Calibri'}),
237
+ html.Span("(Apenas para Expresso, Público, Eco Sapo, e Diário de Notícias)", style={'font-size': '12px', 'font-family': 'Calibri'})
238
+ ]),
239
+ dcc.Input(
240
+ id='url-input',
241
+ type='url',
242
+ placeholder='https://exemplo.com/artigo-notícias',
243
+ style={
244
+ 'width': '98.3%',
245
+ 'padding': '10px',
246
+ 'margin': '10px 0',
247
+ 'border': '1px solid #ccc',
248
+ 'borderRadius': '5px',
249
+ 'font-family': 'Calibri'
250
+ }
251
+ ),
252
+ html.Button('Submeter Link', id='submit-url-button', n_clicks=0, style={
253
+ 'margin': '10px 0',
254
+ 'font-family': 'Calibri'
255
+ }),
256
+ ]),
257
+
258
+ html.Div(id='upload-feedback', style={'margin': '10px 0'}),
259
+ ]
260
+ )
261
+ ],
262
+ style={'marginLeft': '27%', 'padding': '20px'}
263
+ #style={'width': '75%', 'padding': '20px', 'backgroundColor': '#f8f9fa', 'position': 'fixed', 'left': '25%', 'top': '0', 'bottom': '0', 'overflowY': 'auto'}
264
+ )
265
+ ])
266
+
267
+ # to update the chart dynamically
268
+ @app.callback(
269
+ Output("news-plot", "figure", allow_duplicate=True),
270
+ [Input("chart-type", "value"),
271
+ Input("x-axis", "value"),
272
+ Input("y-axis", "value"),
273
+ Input("z-axis", "value"),
274
+ Input("x-filter", "value"),
275
+ Input("y-filter", "value"),
276
+ Input("z-filter", "value"),
277
+ Input("source-filter", "value"),
278
+ Input("group-toggle", "value")],
279
+ State("data-store", "data"),
280
+ prevent_initial_call=True
281
+ )
282
+ def update_chart(chart_type, x_axis, y_axis, z_axis, x_range, y_range, z_range, selected_sources, group_toggle, data_records):
283
+
284
+ data = pd.DataFrame(data_records)
285
+
286
+ axis_extremities ={
287
+ "Viés Político" : ["Enviesado à Esquerda", "Enviesado à Direita", -100, 100],
288
+ "Fiabilidade" : ["Não fiável", "Fiável", -100, 100],
289
+ "Objetividade" : ["Baseado em Opinião", "Factual/Objetivo", -100, 100],
290
+ "Legibilidade" : ["Difícil de ler", "Fácil de ler", 0, 100]
291
+ }
292
+
293
+ all_sources = data["Fonte"].unique().tolist()
294
+ if set(selected_sources) == set(all_sources) or not selected_sources:
295
+ filtered_data = data
296
+ else:
297
+ filtered_data = data[data["Fonte"].isin(selected_sources)]
298
+
299
+ if group_toggle:
300
+ filtered_data_1 = filtered_data[filtered_data["Fonte"] != "Utilizador"].groupby("Fonte").mean().reset_index()
301
+ filtered_data_2 = filtered_data[filtered_data["Fonte"] == "Utilizador"][["Fonte", "Viés Político", "Fiabilidade", "Objetividade", "Legibilidade"]]
302
+ filtered_data = pd.concat([filtered_data_1, filtered_data_2], ignore_index=True)
303
+ filtered_data["Título"] = filtered_data["Fonte"]
304
+
305
+ # Apply range filters for each axis
306
+ filtered_data = filtered_data[(filtered_data[x_axis] >= x_range[0]) &
307
+ (filtered_data[x_axis] <= x_range[1])]
308
+
309
+ # Only apply Y and Z filters for the relevant chart types
310
+ if chart_type in ["2D", "3D"]:
311
+ filtered_data = filtered_data[(filtered_data[y_axis] >= y_range[0]) &
312
+ (filtered_data[y_axis] <= y_range[1])]
313
+
314
+ if chart_type == "3D":
315
+ filtered_data = filtered_data[(filtered_data[z_axis] >= z_range[0]) &
316
+ (filtered_data[z_axis] <= z_range[1])]
317
+
318
+ if chart_type == "1D":
319
+
320
+ to_hover = ["Fonte"]
321
+
322
+ if not group_toggle:
323
+ if x_axis != "Legibilidade":
324
+ to_hover.append(f"Descritor de {x_axis}")
325
+
326
+ # 1D plot
327
+ filtered_data['Custom Size'] = filtered_data["Fonte"].apply(lambda x: 0 if x == "Utilizador" else 12)
328
+ fig = px.scatter(filtered_data, x=x_axis, y=[0] * len(filtered_data), #text="Título",
329
+ # color="reliability score", # size="size_fixed",
330
+ color="Fonte",
331
+ color_continuous_scale="Viridis",
332
+ title=f"1D Scatter Plot: {x_axis}",
333
+ hover_name="Título",
334
+ hover_data={col: True for col in to_hover} | {"Custom Size": False},
335
+ size='Custom Size',
336
+ size_max=10,
337
+ opacity=0.8)
338
+
339
+ fig.update_yaxes(visible=False, showticklabels=False) # Hide Y-axis
340
+ # fig.update_traces(marker=dict(opacity=0.7, line=dict(width=1, color='black')))
341
+
342
+ if group_toggle:
343
+ fig.update_traces(marker=dict(opacity=0, size=0)) # to remove the original marker dots
344
+ for i, row in filtered_data.iterrows():
345
+ img_path = u.get_source_image(row["Fonte"])
346
+ img_data = u.get_img_data(img_path)
347
+
348
+ fig.add_layout_image(
349
+ dict(
350
+ source=f'data:image/png;base64,{img_data}',
351
+ x=row[x_axis],
352
+ y=0,
353
+ xref="x",
354
+ yref="y",
355
+ sizex=6,
356
+ sizey=6,
357
+ xanchor="center",
358
+ yanchor="middle",
359
+ opacity=0.8,
360
+ )
361
+ )
362
+ else:
363
+ for _, row in filtered_data.iterrows():
364
+ if row["Fonte"] == "Utilizador":
365
+ img_data = u.get_img_data("logos/x.png")
366
+
367
+ fig.add_layout_image(
368
+ dict(
369
+ source=f'data:image/png;base64,{img_data}',
370
+ x=row[x_axis],
371
+ y=0,
372
+ xref="x",
373
+ yref="y",
374
+ sizex=7,
375
+ sizey=7,
376
+ xanchor="center",
377
+ yanchor="middle"
378
+ )
379
+ )
380
+
381
+ # X-axis left annotation (outside)
382
+ fig.add_annotation(
383
+ x=0.13, # Far left in paper coords
384
+ y=0, # Bottom
385
+ xref="paper",
386
+ yref="paper",
387
+ text=f"⬅️ {axis_extremities[x_axis][0]}",
388
+ showarrow=False,
389
+ xanchor="right",
390
+ yanchor="top",
391
+ font=dict(size=12),
392
+ yshift=-25 # Further outside the plot
393
+ )
394
+
395
+ # X-axis right annotation (outside)
396
+ fig.add_annotation(
397
+ x=0.87, # Far right
398
+ y=0,
399
+ xref="paper",
400
+ yref="paper",
401
+ text=f"{axis_extremities[x_axis][1]} ➡️",
402
+ showarrow=False,
403
+ xanchor="left",
404
+ yanchor="top",
405
+ font=dict(size=12),
406
+ yshift=-25
407
+ )
408
+
409
+
410
+ fig.update_layout(
411
+ xaxis=dict(
412
+ range=[axis_extremities[x_axis][2], axis_extremities[x_axis][3]]
413
+ ))
414
+
415
+ elif chart_type == "2D":
416
+
417
+ to_hover = ["Fonte"]
418
+
419
+ if not group_toggle:
420
+ if x_axis != "Legibilidade":
421
+ to_hover.append(f"Descritor de {x_axis}")
422
+ if y_axis != "Legibilidade":
423
+ to_hover.append(f"Descritor de {y_axis}")
424
+
425
+ # 2D plot
426
+ filtered_data['Custom Size'] = filtered_data["Fonte"].apply(lambda x: 0 if x == "Utilizador" else 10)
427
+ fig = px.scatter(filtered_data, x=x_axis, y=y_axis, # text="Título",
428
+ # color="reliability score", # size="size_fixed",
429
+ color_continuous_scale="Viridis",
430
+ color="Fonte",
431
+ title=f"2D Scatter: {x_axis} vs {y_axis}",
432
+ hover_name="Título",
433
+ size='Custom Size',
434
+ hover_data={col: True for col in to_hover} | {"Custom Size": False},
435
+ size_max=10,
436
+ opacity=0.8)
437
+
438
+ if group_toggle:
439
+ fig.update_traces(marker=dict(opacity=0, size=0)) # to remove the original marker dots
440
+ for i, row in filtered_data.iterrows():
441
+ img_path = u.get_source_image(row["Fonte"])
442
+ img_data = u.get_img_data(img_path)
443
+
444
+ fig.add_layout_image(
445
+ dict(
446
+ source=f'data:image/png;base64,{img_data}',
447
+ x=row[x_axis],
448
+ y=row[y_axis],
449
+ xref="x",
450
+ yref="y",
451
+ sizex=10,
452
+ sizey=10,
453
+ xanchor="center",
454
+ yanchor="middle",
455
+ opacity=0.8,
456
+ )
457
+ )
458
+ else:
459
+ for _, row in filtered_data.iterrows():
460
+ if row["Fonte"] == "Utilizador":
461
+ img_data = u.get_img_data("logos/x.png")
462
+
463
+ fig.add_layout_image(
464
+ dict(
465
+ source=f'data:image/png;base64,{img_data}',
466
+ x=row[x_axis],
467
+ y=row[y_axis],
468
+ xref="x",
469
+ yref="y",
470
+ sizex=7,
471
+ sizey=7,
472
+ xanchor="center",
473
+ yanchor="middle"
474
+ )
475
+ )
476
+
477
+ # X-axis left annotation (outside)
478
+ fig.add_annotation(
479
+ x=0.15, # Far left in paper coords
480
+ y=0, # Bottom
481
+ xref="paper",
482
+ yref="paper",
483
+ text=f"⬅️ {axis_extremities[x_axis][0]}",
484
+ showarrow=False,
485
+ xanchor="right",
486
+ yanchor="top",
487
+ font=dict(size=12),
488
+ yshift=-25 # Further outside the plot
489
+ )
490
+
491
+ # X-axis right annotation (outside)
492
+ fig.add_annotation(
493
+ x=0.85, # Far right
494
+ y=0,
495
+ xref="paper",
496
+ yref="paper",
497
+ text=f"{axis_extremities[x_axis][1]} ➡️",
498
+ showarrow=False,
499
+ xanchor="left",
500
+ yanchor="top",
501
+ font=dict(size=12),
502
+ yshift=-25
503
+ )
504
+
505
+ # Y-axis bottom annotation (outside)
506
+ fig.add_annotation(
507
+ x=0,
508
+ y=0, # Bottom
509
+ xref="paper",
510
+ yref="paper",
511
+ text=f"⬅️ {axis_extremities[y_axis][0]}",
512
+ showarrow=False,
513
+ xanchor="right",
514
+ yanchor="bottom",
515
+ font=dict(size=12),
516
+ xshift=-40, # Move left outside
517
+ textangle=-90
518
+ )
519
+
520
+ # Y-axis top annotation (outside)
521
+ fig.add_annotation(
522
+ x=0,
523
+ y=1, # Top
524
+ xref="paper",
525
+ yref="paper",
526
+ text=f"{axis_extremities[y_axis][1]} ➡️",
527
+ showarrow=False,
528
+ xanchor="right",
529
+ yanchor="top",
530
+ font=dict(size=12),
531
+ xshift=-40,
532
+ textangle=-90
533
+ )
534
+
535
+ fig.update_layout(
536
+ xaxis=dict(
537
+ range=[axis_extremities[x_axis][2], axis_extremities[x_axis][3]]
538
+ ),
539
+ yaxis=dict(
540
+ range=[axis_extremities[y_axis][2], axis_extremities[y_axis][3]]
541
+ )
542
+ )
543
+
544
+ elif chart_type == "3D":
545
+
546
+ to_hover = ["Fonte"]
547
+
548
+ if not group_toggle:
549
+ if x_axis != "Legibilidade":
550
+ to_hover.append(f"Descritor de {x_axis}")
551
+ if y_axis != "Legibilidade":
552
+ to_hover.append(f"Descritor de {y_axis}")
553
+ if z_axis != "Legibilidade":
554
+ to_hover.append(f"Descritor de {z_axis}")
555
+
556
+ user_upload = "Utilizador"
557
+ highlight_df = filtered_data[filtered_data["Fonte"] == user_upload]
558
+ other_df = filtered_data[filtered_data["Fonte"] != user_upload]
559
+
560
+ source_list = other_df["Fonte"].unique()
561
+ color_map = {source: f"hsl({i * 360 / len(source_list)}, 70%, 50%)" for i, source in enumerate(source_list)}
562
+ color_array = other_df["Fonte"].map(color_map)
563
+
564
+ fig = go.Figure()
565
+
566
+ # 3D plot
567
+ if group_toggle:
568
+ fig = px.scatter_3d(other_df, x=x_axis, y=y_axis, z=z_axis,
569
+ color="Fonte", # Color points by source
570
+ color_continuous_scale="Viridis",
571
+ # symbol="Fonte",
572
+ title=f"Gráfico de Dispersão 3D: {x_axis} vs {y_axis} vs {z_axis}",
573
+ hover_name='Title')
574
+
575
+ fig.update_traces(marker=dict(size=10, opacity=0.8, line=dict(width=1, color='black')))
576
+
577
+ highlight_trace = px.scatter_3d(highlight_df, x=x_axis, y=y_axis, z=z_axis,
578
+ color_discrete_sequence=["black"],
579
+ hover_name="Título")
580
+
581
+ highlight_trace.update_traces(
582
+ marker=dict(size=10),
583
+ name="Utilizador",
584
+ showlegend=True)
585
+ for trace in highlight_trace.data:
586
+ fig.add_trace(trace)
587
+
588
+ else:
589
+ fig = px.scatter_3d(other_df, x=x_axis, y=y_axis, z=z_axis,
590
+ color="Fonte",
591
+ color_continuous_scale="Viridis",
592
+ title=f"Gráfico de Dispersão 3D: {x_axis} vs {y_axis} vs {z_axis}",
593
+ hover_name="Título",
594
+ hover_data=to_hover)
595
+ fig.update_traces(marker=dict(size=5, opacity=0.8))
596
+
597
+ highlight_trace = px.scatter_3d(highlight_df, x=x_axis, y=y_axis, z=z_axis,
598
+ color_discrete_sequence=["black"],
599
+ hover_name="Título",
600
+ hover_data=to_hover)
601
+
602
+ highlight_trace.update_traces(
603
+ marker=dict(size=10),
604
+ name="Utilizador",
605
+ showlegend=True)
606
+ for trace in highlight_trace.data:
607
+ fig.add_trace(trace)
608
+
609
+ fig.update_layout(
610
+ scene=dict(
611
+ xaxis=dict(
612
+ title=x_axis,
613
+ range=[axis_extremities[x_axis][2], axis_extremities[x_axis][3]]
614
+ ),
615
+ yaxis=dict(
616
+ title=y_axis,
617
+ range=[axis_extremities[y_axis][2], axis_extremities[y_axis][3]]
618
+ ),
619
+ zaxis=dict(
620
+ title=z_axis,
621
+ range=[axis_extremities[z_axis][2], axis_extremities[z_axis][3]]
622
+ )
623
+ )
624
+ )
625
+
626
+ return fig
627
+
628
+ # # To update the sources in the source filter
629
+ # @app.callback(
630
+ # Output("source-filter", "options"),
631
+ # Output("source-filter", "value"),
632
+ # Input("news-data", "data")
633
+ # )
634
+ # def update_source_dropdown(data):
635
+
636
+ # df = pd.DataFrame(data)
637
+ # unique_sources = df["Fonte"].unique().tolist()
638
+
639
+ # options = [{"label": "Select All", "value": "ALL"}] + [
640
+ # {"label": src, "value": src} for src in unique_sources
641
+ # ]
642
+
643
+ # return options, unique_sources
644
+
645
+ # To update the source filter
646
+ @app.callback(
647
+ Output("source-filter", "value", allow_duplicate=True),
648
+ [Input("source-filter", "value")],
649
+ State("source-filter", "options"),
650
+ prevent_initial_call=True
651
+ )
652
+ def update_source_selection(selected_sources, options):
653
+ all_sources = data["Fonte"].unique().tolist()
654
+
655
+ if "ALL" in selected_sources:
656
+ # If "Select All" is clicked, return all sources
657
+ if "Utilizador" in [src["value"] for src in options]:
658
+ return all_sources + ["Utilizador"]
659
+ return all_sources
660
+
661
+ else:
662
+ # else return selected sources normally
663
+ return selected_sources
664
+
665
+ # To updated the graph size based on chart type
666
+ @app.callback(
667
+ Output("news-plot", "style"),
668
+ Input("chart-type", "value")
669
+ )
670
+ def update_graph_height(chart_type):
671
+ if chart_type == "1D":
672
+ return {"height": "400px"}
673
+ elif chart_type == "2D":
674
+ return {"height": "800px"}
675
+ elif chart_type == "3D":
676
+ return {"height": "1000px"}
677
+ return {"height": "800px"}
678
+
679
+ # To disable Y-axis and Z-axis dropdowns based on chart type
680
+ @app.callback(
681
+ [Output("y-axis", "disabled"),
682
+ Output("z-axis", "disabled")],
683
+ [Input("chart-type", "value")]
684
+ )
685
+ def update_dropdown_states(chart_type):
686
+ if chart_type == "1D":
687
+ return True, True
688
+ elif chart_type == "2D":
689
+ return False, True
690
+ else:
691
+ return False, False
692
+
693
+ # To hide Y-axis and Z-axis dropdowns based on chart type
694
+ @app.callback(
695
+ [Output("y-axis-container", "style"),
696
+ Output("z-axis-container", "style")],
697
+ [Input("chart-type", "value")]
698
+ )
699
+ def update_dropdown_visibility(chart_type):
700
+ base_style = {'marginBottom': '10px'}
701
+ disabled_style = {'marginBottom': '10px', 'opacity': '0.5'}
702
+
703
+ if chart_type == "1D":
704
+ return disabled_style, disabled_style
705
+
706
+ elif chart_type == "2D":
707
+ return base_style, disabled_style
708
+
709
+ else:
710
+ return base_style, base_style
711
+
712
+ # show/hide filters based on chart type
713
+ @app.callback(
714
+ [Output("x-filter", "disabled"),
715
+ Output("y-filter", "disabled"),
716
+ Output("z-filter", "disabled")],
717
+ [Input("chart-type", "value"),
718
+ Input("x-axis", "value"),
719
+ Input("y-axis", "value"),
720
+ Input("z-axis", "value")]
721
+ )
722
+ def update_filter_availability(chart_type, x_axis, y_axis, z_axis):
723
+
724
+ x_disabled = False
725
+ y_disabled = chart_type == "1D"
726
+ z_disabled = chart_type != "3D"
727
+
728
+ return x_disabled, y_disabled, z_disabled
729
+
730
+ # To update the range sliders based on selected axis
731
+ @app.callback(
732
+ [Output("x-filter", "min"), Output("x-filter", "max"),
733
+ Output("x-filter", "marks"), Output("x-filter", "value"),
734
+ Output("y-filter", "min"), Output("y-filter", "max"),
735
+ Output("y-filter", "marks"), Output("y-filter", "value"),
736
+ Output("z-filter", "min"), Output("z-filter", "max"),
737
+ Output("z-filter", "marks"), Output("z-filter", "value")],
738
+ [Input("x-axis", "value"), Input("y-axis", "value"), Input("z-axis", "value")]
739
+ )
740
+ def update_range_sliders(x_axis, y_axis, z_axis):
741
+ axis_data = {
742
+ "Viés Político": [-100, 100, {-100: "-100", -50: "-50", 0: "0", 50: "50", 100: "100"}],
743
+ "Fiabilidade": [-100, 100, {-100: "-100", -50: "-50", 0: "0", 50: "50", 100: "100"}],
744
+ "Objetividade": [-100, 100, {-100: "-100", -50: "-50", 0: "0", 50: "50", 100: "100"}],
745
+ "Legibilidade": [0, 100, {0: "0", 25: "25", 50: "50", 75: "75", 100: "100"}]
746
+ }
747
+
748
+ x_min, x_max, x_marks = axis_data[x_axis]
749
+ y_min, y_max, y_marks = axis_data[y_axis]
750
+ z_min, z_max, z_marks = axis_data[z_axis]
751
+
752
+ return (x_min, x_max, x_marks, [x_min, x_max],
753
+ y_min, y_max, y_marks, [y_min, y_max],
754
+ z_min, z_max, z_marks, [z_min, z_max])
755
+
756
+ @app.callback(
757
+ Output('upload-feedback', 'children', allow_duplicate=True),
758
+ Output("news-plot", "figure", allow_duplicate=True),
759
+ Output("source-filter", "options", allow_duplicate=True),
760
+ Output("source-filter", "value", allow_duplicate=True),
761
+ Output("data-store", "data", allow_duplicate=True),
762
+ Input('upload-article', 'contents'),
763
+ State('upload-article', 'filename'),
764
+ State("source-filter", "options"),
765
+ State("source-filter", "value"),
766
+ State("data-store", "data"),
767
+ prevent_initial_call=True
768
+ )
769
+ def classify_article(contents, filename, options, selected_sources, data_records):
770
+ data = pd.DataFrame(data_records)
771
+ if contents is None:
772
+ raise PreventUpdate
773
+
774
+ # Decode .txt content
775
+ content_type, content_string = contents.split(',')
776
+ decoded = base64.b64decode(content_string).decode('utf-8')
777
+
778
+ openai = OpenAI(
779
+ api_key=os.environ.get("API_KEY"),
780
+ base_url="https://api.deepinfra.com/v1/openai",
781
+ )
782
+
783
+ try:
784
+ # political_bias_db, reliability_db, objectivity_db = u.setup_db()
785
+
786
+ descriptor_political_bias = u.get_descriptor(decoded, "political_bias", u.prompts, openai)
787
+ descriptor_reliability = u.get_descriptor(decoded, "reliability", u.prompts, openai)
788
+ descriptor_objectivity = u.get_descriptor(decoded, "objectivity", u.prompts, openai)
789
+
790
+ political_bias_score = u.get_score(descriptor_political_bias, embedding_model, 0.5, political_bias_db, 50)
791
+ reliability_score = u.get_score(descriptor_reliability, embedding_model, 0.2, reliability_db, 7505)
792
+ objectivity_score = u.get_score(descriptor_objectivity, embedding_model, 0.2, objectivity_db, 9000)
793
+ reliability_score = u.classify_readability(decoded)
794
+
795
+ new_point = {
796
+ "Título": filename,
797
+ "Texto": decoded,
798
+ "Fonte": "Utilizador",
799
+ "Descritor de Viés Político": descriptor_political_bias,
800
+ "Descritor de Fiabilidade": descriptor_reliability,
801
+ "Descritor de Objetividade": descriptor_objectivity,
802
+ "Viés Político": political_bias_score,
803
+ "Fiabilidade": reliability_score,
804
+ "Objetividade": objectivity_score,
805
+ "Legibilidade": reliability_score
806
+ }
807
+
808
+ data = pd.concat([pd.DataFrame(data), pd.DataFrame([new_point])], ignore_index=True)
809
+
810
+ unique_sources = selected_sources + [new_point["Fonte"]]
811
+ options = options + [{"label": new_point["Fonte"], "value": new_point["Fonte"]}]
812
+
813
+ return f"✅ Classificado e adicionado '{filename}'", dash.no_update, options, unique_sources, data.to_dict("records")
814
+
815
+
816
+ except Exception as e:
817
+
818
+ return f"❌ Erro a classificar o artigo: {e}", dash.no_update, dash.no_update, dash.no_update, dash.no_update
819
+
820
+ @app.callback(
821
+ Output('upload-feedback', 'children'),
822
+ Output("news-plot", "figure"),
823
+ Output("source-filter", "options"),
824
+ Output("source-filter", "value"),
825
+ Output("data-store", "data"),
826
+ Input('submit-url-button', 'n_clicks'),
827
+ State('url-input', 'value'),
828
+ State("source-filter", "options"),
829
+ State("source-filter", "value"),
830
+ State("data-store", "data")
831
+ )
832
+ def classify_url(n_clicks, url, options, selected_sources, data_records):
833
+ data = pd.DataFrame(data_records)
834
+ if n_clicks > 0 and url:
835
+
836
+ res = requests.get(url)
837
+ soup = bs4.BeautifulSoup(res.text, 'lxml')
838
+
839
+ if "expresso.pt" in url:
840
+ outlet = "Expresso"
841
+ title = soup.select('h1')[0].text
842
+ text = soup.select('.full-article-fragment.full-article-body.article-content.first')[0].getText()
843
+
844
+ elif "publico.pt" in url:
845
+ outlet = "Público"
846
+ title = soup.select('.headline.story__headline')[0].getText()
847
+ match = re.search(r'\s*(.*?)\s*$', title.strip())
848
+ if match:
849
+ title = match.group(1)
850
+ text = soup.select('.story__body')[0].getText()
851
+
852
+ elif "eco.sapo.pt" in url:
853
+ outlet = "Eco Sapo"
854
+ title = soup.select('.title')[0].get_text()
855
+ title = re.sub(r'\s+', ' ', title).strip()
856
+ text = soup.select('.entry__content')[0].get_text()
857
+
858
+ elif "dn.pt" in url:
859
+ outlet = "Diário de Notícias"
860
+ title = soup.select('.arrow-component.arr--story-headline.story-headline-m_wrapper__1Wey6')[0].getText()
861
+ text = soup.select('.arr--story-page-card-wrapper')[0].getText()
862
+
863
+ openai = OpenAI(
864
+ api_key=os.environ.get("API_KEY"),
865
+ base_url="https://api.deepinfra.com/v1/openai",
866
+ )
867
+
868
+ try:
869
+ # political_bias_db, reliability_db, objectivity_db = u.setup_db()
870
+
871
+ descriptor_political_bias = u.get_descriptor(text, "political_bias", u.prompts, openai)
872
+ descriptor_reliability = u.get_descriptor(text, "reliability", u.prompts, openai)
873
+ descriptor_objectivity = u.get_descriptor(text, "objectivity", u.prompts, openai)
874
+
875
+ political_bias_score = u.get_score(descriptor_political_bias, embedding_model, 0.5, political_bias_db, 50)
876
+ reliability_score = u.get_score(descriptor_reliability, embedding_model, 0.2, reliability_db, 7505)
877
+ objectivity_score = u.get_score(descriptor_objectivity, embedding_model, 0.2, objectivity_db, 9000)
878
+ reliability_score = u.classify_readability(text)
879
+
880
+ new_point = {
881
+ "Título": f"{outlet}: {title}",
882
+ "Text": text,
883
+ "Fonte": "Utilizador",
884
+ "Descritor de Viés Político": descriptor_political_bias,
885
+ "Descritor de Fiabilidade": descriptor_reliability,
886
+ "Descritor de Objetividade": descriptor_objectivity,
887
+ "Viés Político": political_bias_score,
888
+ "Fiabilidade": reliability_score,
889
+ "Objetividade": objectivity_score,
890
+ "Legibilidade": reliability_score
891
+ }
892
+
893
+ data = pd.concat([pd.DataFrame(data), pd.DataFrame([new_point])], ignore_index=True)
894
+
895
+ unique_sources = selected_sources + [new_point["Fonte"]]
896
+ options = options + [{"label": new_point["Fonte"], "value": new_point["Fonte"]}]
897
+
898
+ return f"✅ O artigo pedido foi classificao e adicionado.", dash.no_update, options, unique_sources, data.to_dict("records")
899
+
900
+
901
+ except Exception as e:
902
+
903
+ return f"❌ Erro a classificar o artigo: {e}", dash.no_update, dash.no_update, dash.no_update, dash.no_update
904
+
905
+ else:
906
+ raise PreventUpdate
907
+
908
+ # to open window with article text
909
+ @app.callback(
910
+ Output("modal", "is_open"),
911
+ Output("modal-body", "children"),
912
+ Output("modal-title", "children"),
913
+ Input("news-plot", "clickData"),
914
+ Input("close", "n_clicks"),
915
+ State("modal", "is_open"),
916
+ State("data-store", "data")
917
+ )
918
+ def display_modal(clickData, close_clicks, is_open, data_records):
919
+ data = pd.DataFrame(data_records)
920
+ ctx = dash.callback_context
921
+
922
+ if ctx.triggered_id == "news-plot" and clickData:
923
+ title = clickData['points'][0]['hovertext']
924
+ data_index = data[data["Título"] == title].index
925
+ news_text = data.loc[data_index, 'Texto']
926
+ return True, news_text, title
927
+ elif ctx.triggered_id == "close" and is_open:
928
+ return False, None, None
929
+ return is_open, None, None
930
+
931
+ # to open informational window
932
+ @app.callback(
933
+ Output("modal-info", "is_open"),
934
+ [Input("open-info", "n_clicks"), Input("close-info", "n_clicks")],
935
+ [State("modal-info", "is_open")],
936
+ )
937
+ def toggle_modal(n1, n2, is_open):
938
+ if n1 or n2:
939
+ return not is_open
940
+ return is_open
941
+
942
+ if __name__ == '__main__':
943
+ app.run(host="0.0.0.0", port=7860)
demo_data.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:14de21cb200a398ff83263e34fa24006684e51a5e832c44721f962f976d6f790
3
+ size 534135
logos/bombeiros.png ADDED

Git LFS Details

  • SHA256: 22a4d35fbe283c65f1eb800a95d443f5c69a27c0bb526bffe0dd719d75af0790
  • Pointer size: 131 Bytes
  • Size of remote file: 163 kB
logos/cm.jpg ADDED
logos/default.png ADDED

Git LFS Details

  • SHA256: a77ab8f3f6073fcc1185d3cb1e4cec41996a1155b96ac498ea8814eea6c9bba8
  • Pointer size: 129 Bytes
  • Size of remote file: 2.88 kB
logos/direita.png ADDED

Git LFS Details

  • SHA256: dede9e58850a69554b268414cc10c0f00e34a88e794194d8de6d367a27e686a6
  • Pointer size: 130 Bytes
  • Size of remote file: 74.5 kB
logos/dn.png ADDED

Git LFS Details

  • SHA256: 82c3fbead3a5c942743542b2599d633ae96efa3151eba5798fee9e80f66d0477
  • Pointer size: 131 Bytes
  • Size of remote file: 132 kB
logos/expresso.png ADDED

Git LFS Details

  • SHA256: b249594569d58127538b0ad923fe0d38b675e4192cfc83f30387037e47b40a03
  • Pointer size: 130 Bytes
  • Size of remote file: 11.1 kB
logos/extra.png ADDED

Git LFS Details

  • SHA256: 5676502e58388e95f050c216dfd9c96bec546b806ba5b70d264c7ea8cff6c300
  • Pointer size: 130 Bytes
  • Size of remote file: 16.1 kB
logos/jn.png ADDED

Git LFS Details

  • SHA256: d9251da217b7974cd08fc7ded1c6613d57bd35d3460b62ea4ca1332f7accb58e
  • Pointer size: 129 Bytes
  • Size of remote file: 8.09 kB
logos/lusa.jpg ADDED
logos/magazine.png ADDED

Git LFS Details

  • SHA256: 624af680e5715807a8a7544b1e5c26b7a0e8aacf2fb592128827258b34838f9c
  • Pointer size: 129 Bytes
  • Size of remote file: 1.84 kB
logos/negocios.png ADDED

Git LFS Details

  • SHA256: 7d818b07dbb3dd1560acbce3ac7fb4cb7c939ed035872b3fb7b208aaae491b58
  • Pointer size: 129 Bytes
  • Size of remote file: 4.84 kB
logos/publico.png ADDED

Git LFS Details

  • SHA256: 72236da1aad33e35035e615338c2e4c08779f00a2c12bd9fbd610ce0f62c4f62
  • Pointer size: 130 Bytes
  • Size of remote file: 49.8 kB
logos/sic.png ADDED

Git LFS Details

  • SHA256: a069718023a6584c6646df478e696830648f8db45d8681745d00557513ee5a83
  • Pointer size: 129 Bytes
  • Size of remote file: 5.65 kB
logos/tsf.png ADDED

Git LFS Details

  • SHA256: cf0bc71b97b008168535ad7e7e26c9a15e0ebdaf878c607738e231e9dc54df3d
  • Pointer size: 131 Bytes
  • Size of remote file: 103 kB
logos/tugapress.png ADDED

Git LFS Details

  • SHA256: 3e8882a41507b204a11e37be46f5e5920a02b6146e7f254f0e376d9f96a2f0ea
  • Pointer size: 130 Bytes
  • Size of remote file: 35.1 kB
logos/x.png ADDED

Git LFS Details

  • SHA256: 29f843226f3f8f09846ecefbdb251b2b4a1801c3ded7f4bab8afab547a922f44
  • Pointer size: 131 Bytes
  • Size of remote file: 216 kB
objectivity_db.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c97962c1d7d6ba4fd74e6fc4f1e5e64aeef91f8afed600f0711d934ce315f008
3
+ size 155334033
political_db.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0af893206be882deb5064dd06dedeca80d4a2667c036d4006b269af3b2eaafe6
3
+ size 78119550
reliability_db.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8b1f2e8a361aa57a684fc757724a8f79b9667d851297f3075a357632b6b8fa85
3
+ size 125966914
requirements.txt ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ dash==3.0.4
2
+ dash-daq==0.6.0
3
+ pandas==1.5.3
4
+ plotly==6.0.1
5
+ openai==1.65.5
6
+ requests==2.32.3
7
+ requests-oauthlib==2.0.0
8
+ requests-toolbelt==1.0.0
9
+ beautifulsoup4==4.12.3
10
+ regex==2024.9.11
11
+ dataclasses-json==0.6.5
12
+ jsonpatch==1.33
13
+ jsonpointer==3.0.0
14
+ orjson==3.10.15
15
+ numpy==1.26.4
16
+ langchain-community==0.3.12
17
+ langchain==0.3.18
18
+ langchain-core==0.3.34
19
+ langchain-huggingface==0.1.2
20
+ langchain-text-splitters==0.3.6
21
+ gunicorn
22
+ lxml==5.2.1
23
+ sentence_transformers==3.4.1
24
+ chromadb==0.6.3
25
+ dash-bootstrap-components==2.0.3
scores_ari.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:073be0035a1ef26cced046a59d4689032630a9d8d17a906f84450546f756ad73
3
+ size 48104
scores_fk.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ffd40c12d0d88792d747ad2392ffa0ff92f354dc20bae787af87b17ac2b9a619
3
+ size 48104
scores_fre.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1f036c041a38492f6d8b2cbff869f58ac8c695f4c7de6e641f131669c783b8dc
3
+ size 48104
scores_g.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7b9940d479497f47666caed5e9db6f5943208a706a3e60354931319fa7ba0df4
3
+ size 48128
utils.py ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import json
3
+ import pandas as pd
4
+ import re
5
+ import numpy as np
6
+ import ast
7
+
8
+ def get_img_data(img_path):
9
+ with open(img_path, 'rb') as f:
10
+ img_data = f.read()
11
+ return base64.b64encode(img_data).decode('ascii')
12
+
13
+ def get_source_image(source):
14
+ image_map = {
15
+ "Jornal de Notícias": "./logos/jn.png",
16
+ "CM Jornal": "./logos/cm.jpg",
17
+ "Jornal de Negócios": "./logos/negocios.png",
18
+ "Diário de Notícias": "./logos/dn.png",
19
+ "Expresso": "./logos/expresso.png",
20
+ "SIC Notícias": "./logos/sic.png",
21
+ "Público": "./logos/publico.png",
22
+ "TSF": "./logos/tsf.png",
23
+ "Lusa": "./logos/lusa.jpg",
24
+ "Direita Política": "./logos/direita.png",
25
+ "Magazine Lusa": "./logos/magazine.png",
26
+ "Tuga Press": "./logos/tugapress.png",
27
+ "Semanário Extra": "./logos/extra.png",
28
+ "Bombeiros24": "./logos/bombeiros.png",
29
+ "User Upload": "./logos/x.png",
30
+ }
31
+ return image_map.get(source, "./logos/default.png")
32
+
33
+ def process_json(output):
34
+ json_object = json.loads(re.findall(r'\{.*?\}', output)[0])
35
+ return json_object
36
+
37
+ prompts = { "political_bias" : {
38
+ "system" : ("És especialista em política Portuguesa. Vais interpretar texto e identificar enviesamento político. "
39
+ "Em Portugal, o Partido Socialista (PS), Bloco de Esquerda (BE), Livre, e Partido Comunista Português (PCP) são os partidos de esquerda. "
40
+ "O Partido Social Democrata (PSD), Iniciativa Liberal (IL), CDS, e Chega são os partidos de direita."),
41
+ "user" : ('Um indicador de viés político é aqui definido como um rótulo descritivo que represente a presença de enviesamento político num texto.\n '
42
+ 'O que vais detetar é especificamente enviesamento político. \n\n'
43
+ 'A saída deve ser sempre um JSON com o seguinte formato:\n'
44
+ '{"indicador": "descrição do viés identificado"}'
45
+ 'Exemplos:\n'
46
+ '{"indicador": "Refere-se à imigração como algo negativo para a sociedade."}\n'
47
+ '{"indicador": "Apoia a intervenção do governo na economida e sociedade."}\n'
48
+ '{"indicador": "Menciona as vantagens de impostos baixos para o crescimento económico."}\n'
49
+ '{"indicador": "Critica o capitalismo."}\n'
50
+ '{"indicador": "Desvaloriza o estado social e valoriza o mérito individual."}\n'
51
+ '{"indicador": "Critica negativamente a posição de certos partidos de direita."}\n\n'
52
+ 'Extrai **apenas um novo indicador** de viés político do seguinte texto e formata a saída **exatamente como nos exemplos**:\n\n')
53
+ },
54
+
55
+ "reliability" : {
56
+ "system" : ('Tu és um assistente especializado em avaliar a fiabilidade de artigos com base na sua linguagem, estrutura e transparência. '
57
+ 'A tua tarefa é identificar indicadores de fiabilidade e apresentar a resposta num formato JSON bem definido.\n'
58
+ 'Diretrizes:\n'
59
+ '- Considera **apenas elementos formais** do texto (linguagem, tom, estrutura, uso de fontes). **Não avalies a veracidade do conteúdo.**\n'
60
+ '- Identifica um único indicador por vez.\n'
61
+ '- A resposta deve ter o seguinte formato: {"indicador": "descrição do indicador identificado"}\n'
62
+ "Quando receberes um artigo, analisa a sua forma e extrai apenas um indicador do nível de fiabilidade, seguindo rigorosamente o formato especificado."),
63
+ "user" : ('Um indicador de fiabilidade é aqui definido como um rótulo descritivo que represente a presença de elementos num artigo que podem indicar maior ou menor credibilidade.\n'
64
+ 'O que vais detetar são especificamente sinais linguísticos, estruturais ou estilísticos que afetam credibilidade percebida do artigo.\n'
65
+ 'A saída deve ser sempre um JSON com o seguinte formato:\n'
66
+ '{"indicador": "descrição do indicador identificado"}\n'
67
+ 'Exemplos:\n'
68
+ '{"indicador": "Apresenta fontes verificáveis para as informações mencionadas."}\n'
69
+ '{"indicador": "Utiliza linguagem sensacionalista para atrair atenção."}\n'
70
+ '{"indicador": "Utiliza uma linguagem neutra e objetiva."}\n'
71
+ '{"indicador": "Faz afirmações fortes sem citar fontes verificáveis."}\n'
72
+ '{"indicador": "Evita exageros ou distorções ao apresentar os fatos."}\n'
73
+ '{"indicador": "Apresenta erros gramaticais e ortográficos."}\n'
74
+ '{"indicador": "O texto está bem estruturado e sem erros gramaticais."}\n'
75
+ '{"indicador": "Uso excessivo de linguagem emocional e adjetivos carregados."}\n\n'
76
+ 'Extrai **apenas um novo indicador** do nível de fiabilidade do seguinte texto e formata a saída **exatamente como nos exemplos**:\n')
77
+ },
78
+
79
+ "objectivity" : {
80
+ "system" : ('Tu és um assistente especializado em avaliar objetividade de artigos com base na sua linguagem, estrutura e transparência. '
81
+ 'A tua tarefa é identificar indicadores de objetividade/subjetividade e apresentar a resposta num formato JSON bem definido.\n'
82
+ 'Diretrizes:\n'
83
+ '- Considera **apenas elementos formais** do texto (linguagem, terminologia, tom, estrutura). **Não avalies a veracidade do conteúdo.**\n'
84
+ '- Identifica um único indicador por vez.\n'
85
+ '- A resposta deve ter o seguinte formato: {"indicador": "descrição do indicador identificado"}\n'
86
+ "Quando receberes um artigo, analisa a sua forma e extrai apenas um indicador de objetividade/subjetividade, seguindo rigorosamente o formato especificado."),
87
+ "user" : ('Um indicador de objetividade é aqui definido como um rótulo descritivo que represente a presença de elementos num artigo que contribuem para a sua maior ou menor imparcialidade e rigor.\n'
88
+ 'O que vais detetar são especificamente sinais linguísticos, estruturais ou estilísticos que afetam a objetividade percebida do artigo.\n'
89
+ 'A saída deve ser sempre um JSON com o seguinte formato:\n'
90
+ '{"indicador": "descrição do indicador identificado"}\n'
91
+ 'Exemplos:\n'
92
+ '{"indicador": "Apresenta dados concretos e verificáveis para fundamentar as informações."}\n'
93
+ '{"indicador": "Utiliza linguagem opinativa, expressando juízos de valor."}\n'
94
+ '{"indicador": "Evita linguagem emocional ou adjetivos subjetivos."}\n'
95
+ '{"indicador": "Revela preferência explícita por um ponto de vista sem apresentar contrapontos."}\n'
96
+ '{"indicador": "Utiliza um tom neutro e descritivo, sem expressar opinião."}\n'
97
+ '{"indicador": "Inclui suposições ou generalizações sem suporte em dados verificáveis."}\n'
98
+ '{"indicador": "Inclui referências a fontes credíveis e verificáveis."}\n'
99
+ '{"indicador": "Apresenta argumentos persuasivos em vez de informações neutras."}\n\n'
100
+ 'Extrai **apenas um novo indicador** de objetividade do seguinte texto e formata a saída **exatamente como nos exemplos**:\n')
101
+ },
102
+
103
+ "sensationalism" : {
104
+ "system" : ('Gere exatamente cinco títulos jornalísticos para a notícia fornecida, com níveis crescentes de clickbait.\n'
105
+ 'Segue estas diretrizes rigorosamente:\n'
106
+ '- O primeiro título deve ser puramente factual, sem qualquer clickbait.\n'
107
+ '- O terceiro título deve ter um leve grau de clickbait, mas ainda parecer um título convencional.\n'
108
+ '- O quinto título deve ter um nível claramente elevado de clickbait, mas sem exageros irreais ou sensacionalismo extremo. Deve continuar adequado a um jornal minimamente credível.\n'
109
+ '- Os títulos devem ser usáveis em meios jornalísticos reais e manter a coerência com o conteúdo da notícia.\n'
110
+ '- Retorna a resposta exclusivamente no seguinte formato JSON, sem qualquer outro texto adicional:\n'
111
+ '[{"1" : "título gerado"}, {"2" : "título gerado"}, {"3" : "título gerado"}, {"4" : "título gerado"}, {"5" : "título gerado"}]'),
112
+ "user" : ('Analisa a notícia abaixo e gera exatamente 5 títulos, cada um com nível crescente de clickbait. '
113
+ 'Como referência, o primeiro título deve ser factual e sem qualquer clickbait, o terceiro deve ter ligeiro clickbait e o '
114
+ 'quinto seria o único título com nível claramente elevado de clickbait. Ainda assim, todos os títulos devem ser passíveis '
115
+ 'de serem usados como título de uma notícia num jornal minimamente credível, por isso nenhum dos títulos deve ter um nível '
116
+ 'de clickbait quase satírico de tão exagerado e irrealista que é.\n'
117
+ 'A saída deve ser **só** um JSON com apenas os 5 títulos gerados usando o **exatamente** seguinte formato:\n'
118
+ '[{"1" : "título gerado"}, {"2" : "título gerado"}, {"3" : "título gerado"}, {"4" : "título gerado"}, {"5" : "título gerado"}].\n\n')
119
+ }
120
+ }
121
+
122
+ def process_descriptor_json(descriptor):
123
+ json_object = json.loads(re.findall(r'\{.*?\}', descriptor)[0])
124
+ return json_object["indicador"]
125
+
126
+ def cosine_similarity(vec1, vec2):
127
+ vec1 = np.array(vec1)
128
+ vec2 = np.array(vec2)
129
+
130
+ dot_product = np.dot(vec1, vec2)
131
+ norm1 = np.linalg.norm(vec1)
132
+ norm2 = np.linalg.norm(vec2)
133
+
134
+ if norm1 == 0 or norm2 == 0:
135
+ return 0.0
136
+
137
+ return dot_product / (norm1 * norm2)
138
+
139
+ def get_score(descriptor, model, threshold, db_df, k):
140
+
141
+ # embedding descriptor
142
+ descriptor_embedding = model.embed_query(descriptor)
143
+
144
+ db_df["similarity_scores"] = db_df["embeddings"].apply(lambda x:cosine_similarity(x, descriptor_embedding))
145
+
146
+ # filtering out indicators with similarity lower than threshold
147
+ db_df = db_df[db_df["similarity_scores"] >= threshold].sort_values(by="similarity_scores", ascending=False).head(k)
148
+
149
+ if len(db_df) == 0:
150
+ return 0
151
+
152
+ count_unbiased = (db_df["score"] == 0).sum()
153
+
154
+ if count_unbiased >= 3:
155
+ return 0
156
+
157
+ # Axis score
158
+ db_df["scores"] = db_df["score"] * db_df["similarity_scores"]
159
+
160
+ return db_df["scores"].sum() / db_df["similarity_scores"].sum()
161
+
162
+ # for political_bias, objectivity, and reliability
163
+ def get_descriptor(article, bias_axis, prompts, openai):
164
+ system_prompt = prompts[bias_axis]["system"]
165
+ user_prompt = prompts[bias_axis]["user"]
166
+
167
+ user_prompt_complete = user_prompt + article
168
+
169
+ chat_completion = openai.chat.completions.create(
170
+ model="meta-llama/Llama-3.3-70B-Instruct",
171
+ messages=[
172
+ {"role": "system", "content": system_prompt},
173
+ {"role": "user", "content": user_prompt_complete},
174
+ ],
175
+ temperature=0
176
+ )
177
+
178
+ descriptor = chat_completion.choices[0].message.content
179
+
180
+ try:
181
+ processed_descriptor = process_descriptor_json(descriptor)
182
+ except:
183
+ processed_descriptor = descriptor
184
+
185
+ return processed_descriptor
186
+
187
+ def setup_db():
188
+
189
+ political_bias_db = pd.read_csv('political_db.csv', converters={"embeddings": lambda x: np.array(ast.literal_eval(x))})
190
+ reliability_db = pd.read_csv('reliability_db.csv', converters={"embeddings": lambda x: np.array(ast.literal_eval(x))})
191
+ objectivity_db = pd.read_csv('objectivity_db.csv', converters={"embeddings": lambda x: np.array(ast.literal_eval(x))})
192
+
193
+ return political_bias_db, reliability_db, objectivity_db
194
+
195
+ # Readability
196
+ def count_words(text):
197
+ words = text.split()
198
+ return len(words)
199
+
200
+ def count_syllables(word):
201
+ vogal = ['a', 'ã', 'â', 'á', 'à', 'e', 'é', 'ê', 'i', 'í', 'o', 'ô', 'õ', 'ó', 'ò', 'u', 'ú']
202
+ ditongo = ['ae', 'ãe', 'ai', 'ao', 'ão', 'au', 'ea', 'ei', 'eo', 'eu', 'éu', 'ia', 'ie', 'io', 'iu', 'õe', 'oi', 'ói', 'ou', 'ua', 'ue', 'uê', 'ui', 'uo']
203
+ tritongo = ['uai', 'uei', 'uão', 'uõe', 'uiu', 'uou']
204
+
205
+ count = 0
206
+ for i in range(len(word)):
207
+ if word[i].lower() in vogal:
208
+ count += 1
209
+ if i > 1 and word[i-2:i+1].lower() in tritongo:
210
+ count -= 2
211
+ elif i > 0 and word[i-1:i+1].lower() in ditongo:
212
+ count -= 1
213
+ return count
214
+
215
+ def count_sentences(text):
216
+ return text.count('.') + text.count('!') + text.count('?') - 3*text.count('...')
217
+
218
+ def percentile_of_number(num, lst, inverted=False):
219
+ count = sum(1 for i in lst if i < num)
220
+ percentile = (count / len(lst)) * 100
221
+ if inverted:
222
+ percentile = 100 - percentile
223
+ return percentile
224
+
225
+ def classify_readability(article):
226
+ scores_fre = np.load("./scores_fre.npy")
227
+ scores_ari = np.load("./scores_ari.npy")
228
+ scores_fk = np.load("./scores_fk.npy")
229
+ scores_g = np.load("./scores_g.npy")
230
+
231
+ words = count_words(article)
232
+ syllables = count_syllables(article)
233
+ sentences = count_sentences(article)
234
+ characters = len(article)
235
+
236
+ # The higher, the more readable
237
+ flesch_reading_ease = 227 - 1.04 * (words / sentences) - 72 * (syllables / words)
238
+ gulpease = 89 + 300 * (sentences/words) - 10 * (characters / words)
239
+
240
+ # The lower, the more readable
241
+ automated_readability_index = 0.44 * (words/sentences) + 4.6 * (characters / words) - 20
242
+ flesch_kincaid = 0.36 * (words / sentences) + 10.4 * (syllables / words) - 18
243
+
244
+ p_fre = percentile_of_number(flesch_reading_ease, scores_fre)
245
+ p_ari = percentile_of_number(automated_readability_index, scores_ari, inverted=True)
246
+ p_fk = percentile_of_number(flesch_kincaid, scores_fk, inverted=True)
247
+ p_g = percentile_of_number(gulpease, scores_g)
248
+ return np.mean([p_fre, p_ari, p_fk, p_g])