Spaces:
Sleeping
Sleeping
initial commit
Browse files- .gitattributes +2 -0
- Dockerfile +26 -0
- app.py +943 -0
- demo_data.csv +3 -0
- logos/bombeiros.png +3 -0
- logos/cm.jpg +0 -0
- logos/default.png +3 -0
- logos/direita.png +3 -0
- logos/dn.png +3 -0
- logos/expresso.png +3 -0
- logos/extra.png +3 -0
- logos/jn.png +3 -0
- logos/lusa.jpg +0 -0
- logos/magazine.png +3 -0
- logos/negocios.png +3 -0
- logos/publico.png +3 -0
- logos/sic.png +3 -0
- logos/tsf.png +3 -0
- logos/tugapress.png +3 -0
- logos/x.png +3 -0
- objectivity_db.csv +3 -0
- political_db.csv +3 -0
- reliability_db.csv +3 -0
- requirements.txt +25 -0
- scores_ari.npy +3 -0
- scores_fk.npy +3 -0
- scores_fre.npy +3 -0
- scores_g.npy +3 -0
- utils.py +248 -0
.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
|
logos/cm.jpg
ADDED
|
logos/default.png
ADDED
|
Git LFS Details
|
logos/direita.png
ADDED
|
Git LFS Details
|
logos/dn.png
ADDED
|
Git LFS Details
|
logos/expresso.png
ADDED
|
Git LFS Details
|
logos/extra.png
ADDED
|
Git LFS Details
|
logos/jn.png
ADDED
|
Git LFS Details
|
logos/lusa.jpg
ADDED
|
logos/magazine.png
ADDED
|
Git LFS Details
|
logos/negocios.png
ADDED
|
Git LFS Details
|
logos/publico.png
ADDED
|
Git LFS Details
|
logos/sic.png
ADDED
|
Git LFS Details
|
logos/tsf.png
ADDED
|
Git LFS Details
|
logos/tugapress.png
ADDED
|
Git LFS Details
|
logos/x.png
ADDED
|
Git LFS Details
|
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])
|