Compare commits

..
8 Commits
Author SHA1 Message Date
retoor aa071db7ae chore: move hidden sentiment div below article excerpt in newspaper template
The diff relocates the hidden sentiment display element from above the article-meta section to a position after the excerpt text within the article-content div, preserving its hidden state while adjusting DOM order for cleaner template structure.
2025-10-06 07:38:43 +00:00
retoor 719b31c465 chore: replace hidden input with hidden div for sentiment display in newspaper template 2025-10-06 07:37:27 +00:00
retoor 4efb11dc6a feat: add sentiment analysis module using VADER with classify and score functions
Implement a new sentiment.py module that provides VADER-based sentiment analysis. The module includes an `analyze_sentiment_vader` function that classifies text as Positive, Negative, or Neutral based on compound score thresholds (>=0.05, <=-0.05), and a convenience `analyze` function wrapping the analyzer instance. Returns compound score and detailed polarity scores alongside classification.
2025-10-06 07:35:13 +00:00
retoor 422e4bf0c9 feat: integrate vaderSentiment analysis into article ingestion and search endpoints 2025-10-06 07:35:02 +00:00
retoor 6416b18ca6 fix: assign first_newspaper variable before loop to fix newspaper context in newspaper_latest endpoint 2025-10-06 06:05:47 +00:00
retoor 367b5043cb feat: aggregate articles across newspapers and limit to 30 with whitespace cleanup 2025-10-06 06:04:14 +00:00
retoor 3e804dcea9 feat: integrate ChromaDB for persistent article indexing and deduplication in sync tasks 2025-10-06 05:48:53 +00:00
retoor 93e1c282aa chore: scaffold initial project structure with FastAPI app, SQLite DB, and Jinja2 templates 2025-10-02 19:17:36 +00:00
4 changed files with 48 additions and 2 deletions
+2
View File
@@ -7,3 +7,5 @@ aiohttp==3.9.1
feedparser==6.0.10 feedparser==6.0.10
websockets==12.0 websockets==12.0
trafilatura==1.6.2 trafilatura==1.6.2
vaderSentiment
+7 -1
View File
@@ -4,6 +4,7 @@ from fastapi.templating import Jinja2Templates
import dataset import dataset
import json import json
import aiohttp import aiohttp
import sentiment
import feedparser import feedparser
import asyncio import asyncio
from datetime import datetime from datetime import datetime
@@ -356,15 +357,17 @@ async def websocket_sync(websocket: WebSocket):
'last_synchronized': datetime.now().isoformat() 'last_synchronized': datetime.now().isoformat()
} }
existing = articles_table.find_one(guid=article_data['guid']) existing = articles_table.find_one(guid=article_data['guid'])
if not existing: if not existing:
new_articles.append(article_data) new_articles.append(article_data)
articles_count += 1 articles_count += 1
article_data['sentiment'] = json.dumps(sentiment.analyze(entry.get('description', '') or entry.get('summary', '')))
articles_table.upsert(article_data, ['guid']) articles_table.upsert(article_data, ['guid'])
# Index the article to ChromaDB # Index the article to ChromaDB
doc_content = f"{article_data.get('title', '')}\n{article_data.get('description', '')}" doc_content = f"{article_data.get('title', '')}\n{article_data.get('description', '')}"
metadata = {key: str(value) for key, value in article_data.items() if key != 'content'} # Exclude large content from metadata metadata = {key: str(value) for key, value in article_data.items() if key != 'content'} # Exclude large content from metadata
chroma_collection.upsert( chroma_collection.upsert(
documents=[doc_content], documents=[doc_content],
@@ -490,6 +493,7 @@ async def search_articles(
for i, doc_id in enumerate(results['ids'][0]): for i, doc_id in enumerate(results['ids'][0]):
res = results['metadatas'][0][i] res = results['metadatas'][0][i]
res['distance'] = results['distances'][0][i] res['distance'] = results['distances'][0][i]
res['sentiment'] = sentiment.analyze(res.get('description', '') or res.get('content', '') or res.get('title', ''))
formatted_results.append(res) formatted_results.append(res)
return JSONResponse(content={"results": formatted_results}) return JSONResponse(content={"results": formatted_results})
@@ -565,6 +569,8 @@ async def newspaper_latest(request: Request):
for article in articles: for article in articles:
for key, value in article.items(): for key, value in article.items():
article[key] = str(value).strip().replace(' ', '') article[key] = str(value).strip().replace(' ', '')
article['sentiment'] = sentiment.analyze(article.get('description', '') or article.get('content', '') or res.get('title', ''))
return templates.TemplateResponse("newspaper_view.html", { return templates.TemplateResponse("newspaper_view.html", {
"request": request, "request": request,
"newspaper": first_newspaper, "newspaper": first_newspaper,
+35
View File
@@ -0,0 +1,35 @@
import json
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
def analyze_sentiment_vader(text, analyzer):
"""
Analyzes text using VADER and returns a dictionary with the results.
Args:
text (str): The text content to analyze.
analyzer (SentimentIntensityAnalyzer): An instantiated VADER analyzer.
Returns:
dict: A dictionary containing the sentiment classification, compound score,
and detailed scores (positive, neutral, negative).
"""
scores = analyzer.polarity_scores(text)
compound_score = scores['compound']
if compound_score >= 0.05:
sentiment = 'Positive'
elif compound_score <= -0.05:
sentiment = 'Negative'
else:
sentiment = 'Neutral'
return {
'sentiment': sentiment,
'score': compound_score,
'details': scores
}
vader_analyzer = SentimentIntensityAnalyzer()
def analyze(content):
return analyze_sentiment_vader(content, vader_analyzer)
+3
View File
@@ -164,6 +164,7 @@
<h2 class="article-title"> <h2 class="article-title">
<a href="{{ article.link }}" target="_blank">{{ article.title }}</a> <a href="{{ article.link }}" target="_blank">{{ article.title }}</a>
</h2> </h2>
<div class="article-meta"> <div class="article-meta">
<span class="article-source">{{ article.feed_name }}</span> <span class="article-source">{{ article.feed_name }}</span>
{% if article.author %} {% if article.author %}
@@ -178,6 +179,8 @@
{% set clean_text = full_content|striptags %} {% set clean_text = full_content|striptags %}
{% set display_text = clean_text[:500] if article.content else clean_text[:300] %} {% set display_text = clean_text[:500] if article.content else clean_text[:300] %}
{{ display_text }}{% if clean_text|length > (500 if article.content else 300) %}...{% endif %} {{ display_text }}{% if clean_text|length > (500 if article.content else 300) %}...{% endif %}
<div class="article-sentiment" style="display: none">Sentiment: {{ article.sentiment }}</div>
</div> </div>
{% set words = full_content.split() %} {% set words = full_content.split() %}