Spaces:
Runtime error
Runtime error
Upload 10 files
Browse files- .gitignore +3 -0
- LICENSE +21 -0
- README.md +68 -11
- app.py +122 -0
- main.py +95 -0
- requirements.txt +8 -0
- static/script.js +32 -0
- static/style.css +117 -0
- templates/about.html +33 -0
- templates/index.html +57 -0
.gitignore
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
__pycache__/
|
2 |
+
.venv/
|
3 |
+
.idea/
|
LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
MIT License
|
2 |
+
|
3 |
+
Copyright (c) 2025 ihaveaplan66
|
4 |
+
|
5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6 |
+
of this software and associated documentation files (the "Software"), to deal
|
7 |
+
in the Software without restriction, including without limitation the rights
|
8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9 |
+
copies of the Software, and to permit persons to whom the Software is
|
10 |
+
furnished to do so, subject to the following conditions:
|
11 |
+
|
12 |
+
The above copyright notice and this permission notice shall be included in all
|
13 |
+
copies or substantial portions of the Software.
|
14 |
+
|
15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21 |
+
SOFTWARE.
|
README.md
CHANGED
@@ -1,11 +1,68 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# NewsAnalyzer
|
2 |
+
|
3 |
+
[Live Demo](https://huggingface.co/spaces/YOUR_USERNAME/NewsAnalyzer)
|
4 |
+
|
5 |
+
NewsAnalyzer is a web application that analyzes news articles using natural language processing (NLP) techniques. It helps users quickly understand the sentiment, key topics, and overall trends in the latest news based on any search query.
|
6 |
+
|
7 |
+
## Features
|
8 |
+
|
9 |
+
- Search for recent news articles using NewsAPI
|
10 |
+
- Perform sentiment analysis to determine whether each article is positive or negative
|
11 |
+
- Automatically categorize articles into topics such as business, technology, politics, and more
|
12 |
+
- Generate concise summaries for each article
|
13 |
+
- Extract the most frequently mentioned words and display them in a word cloud
|
14 |
+
- Visualize sentiment distribution and trending words in clear charts
|
15 |
+
- Cache recent searches for improved performance
|
16 |
+
|
17 |
+
## Technologies
|
18 |
+
|
19 |
+
- **Flask** for backend logic and routing
|
20 |
+
- **Hugging Face Transformers** for NLP tasks:
|
21 |
+
- Sentiment analysis: [distilbert-base-uncased-finetuned-sst-2-english](https://huggingface.co/distilbert-base-uncased-finetuned-sst-2-english)
|
22 |
+
- Topic classification: [cardiffnlp/tweet-topic-21-multi](https://huggingface.co/cardiffnlp/tweet-topic-21-multi)
|
23 |
+
- Summarization: [facebook/bart-large-cnn](https://huggingface.co/facebook/bart-large-cnn)
|
24 |
+
- **NLTK** for tokenization and text preprocessing
|
25 |
+
- **Matplotlib** and **WordCloud** for visualizations
|
26 |
+
- **NewsAPI** for retrieving real-time news articles
|
27 |
+
|
28 |
+
## Installation
|
29 |
+
|
30 |
+
1. Clone the repository:
|
31 |
+
```bash
|
32 |
+
git clone https://github.com/ihaveaplan66/news-analyzer.git
|
33 |
+
cd NewsAnalyzer
|
34 |
+
```
|
35 |
+
|
36 |
+
2. Create a virtual environment and activate it:
|
37 |
+
```bash
|
38 |
+
python -m venv venv
|
39 |
+
source venv/bin/activate # Windows: venv\Scripts\activate
|
40 |
+
```
|
41 |
+
|
42 |
+
3. Install dependencies:
|
43 |
+
```bash
|
44 |
+
pip install -r requirements.txt
|
45 |
+
```
|
46 |
+
|
47 |
+
4. Set your NewsAPI key in `app.py`:
|
48 |
+
```python
|
49 |
+
api_key = "your_newsapi_key_here"
|
50 |
+
```
|
51 |
+
|
52 |
+
5. Run the application:
|
53 |
+
```bash
|
54 |
+
python app.py
|
55 |
+
```
|
56 |
+
|
57 |
+
6. Open the application in your browser:
|
58 |
+
```
|
59 |
+
http://localhost:5000
|
60 |
+
```
|
61 |
+
|
62 |
+
## License
|
63 |
+
|
64 |
+
This project is part of my personal portfolio and is provided under the MIT License.
|
65 |
+
|
66 |
+
## Author
|
67 |
+
|
68 |
+
Created by Volodymyr Shereperov
|
app.py
ADDED
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, render_template, request, Response, url_for
|
2 |
+
from main import analyze_news, extract_trending_words
|
3 |
+
import io
|
4 |
+
import time
|
5 |
+
import matplotlib.pyplot as plt
|
6 |
+
from wordcloud import WordCloud
|
7 |
+
|
8 |
+
|
9 |
+
app = Flask(__name__)
|
10 |
+
cache = {}
|
11 |
+
plt.switch_backend("Agg")
|
12 |
+
|
13 |
+
def get_cached_data(cache_key):
|
14 |
+
cached = cache.get(cache_key)
|
15 |
+
if cached and time.time() - cached["timestamp"] < 600:
|
16 |
+
print(f"Cache hit for {cache_key}")
|
17 |
+
return cached["results"], cached["trending_words"]
|
18 |
+
return None, None
|
19 |
+
|
20 |
+
@app.route("/", methods=["GET", "POST"])
|
21 |
+
def index():
|
22 |
+
results, trending_words = [], []
|
23 |
+
sentiment_chart = None
|
24 |
+
query = ""
|
25 |
+
|
26 |
+
if request.method == "POST":
|
27 |
+
query = request.form["query"]
|
28 |
+
num_articles = int(request.form["num_articles"])
|
29 |
+
api_key = "f80d8a6206cd472baeb21f04786b2626"
|
30 |
+
|
31 |
+
cache_key = f"{query}_{num_articles}"
|
32 |
+
results, trending_words = get_cached_data(cache_key)
|
33 |
+
|
34 |
+
if results is None:
|
35 |
+
print(f"No cache for {cache_key}, fetching new data...")
|
36 |
+
results = analyze_news(query, api_key, num_articles)
|
37 |
+
texts = [article["title"] + " " + article.get("summary", "") for article in results]
|
38 |
+
trending_words = extract_trending_words(texts)
|
39 |
+
|
40 |
+
cache[cache_key] = {
|
41 |
+
"results": results,
|
42 |
+
"trending_words": trending_words,
|
43 |
+
"timestamp": time.time()
|
44 |
+
}
|
45 |
+
|
46 |
+
if results:
|
47 |
+
sentiment_chart = url_for('sentiment_chart_route')
|
48 |
+
|
49 |
+
return render_template("index.html", results=results, sentiment_chart=sentiment_chart, query=query, trending_words=trending_words)
|
50 |
+
|
51 |
+
@app.route("/sentiment_chart")
|
52 |
+
def sentiment_chart_route():
|
53 |
+
if not cache:
|
54 |
+
return "No sentiment data", 404
|
55 |
+
|
56 |
+
last_query = list(cache.keys())[-1]
|
57 |
+
cached = cache.get(last_query)
|
58 |
+
|
59 |
+
if not cached:
|
60 |
+
return "No sentiment data", 404
|
61 |
+
|
62 |
+
results = cached["results"]
|
63 |
+
sentiments = [article["sentiment"] for article in results]
|
64 |
+
|
65 |
+
sentiment_counts = dict((x, sentiments.count(x)) for x in set(sentiments))
|
66 |
+
|
67 |
+
labels = list(sentiment_counts.keys())
|
68 |
+
values = list(sentiment_counts.values())
|
69 |
+
|
70 |
+
color_map = {
|
71 |
+
"POSITIVE": "#28a745",
|
72 |
+
"NEGATIVE": "#c82333"
|
73 |
+
}
|
74 |
+
|
75 |
+
colors = [color_map[label] for label in labels]
|
76 |
+
total = sum(values)
|
77 |
+
|
78 |
+
plt.figure(figsize=(3, 3))
|
79 |
+
plt.pie(values, autopct=lambda pct: f'{int(pct * total / 100)} ({pct:.1f}%)', startangle=140, colors=colors)
|
80 |
+
plt.axis('equal')
|
81 |
+
|
82 |
+
img = io.BytesIO()
|
83 |
+
plt.savefig(img, format="png", bbox_inches="tight", transparent = True)
|
84 |
+
plt.close()
|
85 |
+
img.seek(0)
|
86 |
+
|
87 |
+
return Response(img.getvalue(), mimetype="image/png")
|
88 |
+
|
89 |
+
@app.route("/wordcloud_chart")
|
90 |
+
def wordcloud_chart():
|
91 |
+
if not cache:
|
92 |
+
return "No word data", 404
|
93 |
+
|
94 |
+
last_query = list(cache.keys())[-1]
|
95 |
+
cached = cache.get(last_query)
|
96 |
+
|
97 |
+
if not cached:
|
98 |
+
return "No word data", 404
|
99 |
+
|
100 |
+
results = cached["results"]
|
101 |
+
texts = [article["title"] + " " + article.get("summary", "") for article in results]
|
102 |
+
|
103 |
+
text = " ".join(texts)
|
104 |
+
|
105 |
+
plt.figure(figsize=(16, 8), dpi=150)
|
106 |
+
wordcloud = WordCloud(width=1600, height=800, colormap="Blues", background_color='#222').generate(text)
|
107 |
+
plt.imshow(wordcloud, interpolation="bilinear")
|
108 |
+
plt.axis("off")
|
109 |
+
|
110 |
+
img = io.BytesIO()
|
111 |
+
plt.savefig(img, format="png", bbox_inches="tight", pad_inches=0, dpi=150)
|
112 |
+
plt.close()
|
113 |
+
img.seek(0)
|
114 |
+
|
115 |
+
return Response(img.getvalue(), mimetype="image/png")
|
116 |
+
|
117 |
+
@app.route("/about")
|
118 |
+
def about():
|
119 |
+
return render_template("about.html")
|
120 |
+
|
121 |
+
if __name__ == "__main__":
|
122 |
+
app.run(host="0.0.0.0", port=7860, debug=True)
|
main.py
ADDED
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
from collections import Counter
|
3 |
+
from transformers import pipeline
|
4 |
+
import nltk
|
5 |
+
from nltk.tokenize import word_tokenize
|
6 |
+
from nltk.corpus import stopwords
|
7 |
+
import string
|
8 |
+
from transformers import AutoModelForSequenceClassification, AutoTokenizer
|
9 |
+
import torch
|
10 |
+
|
11 |
+
nltk.download('punkt')
|
12 |
+
nltk.download('stopwords')
|
13 |
+
nltk.download('averaged_perceptron_tagger')
|
14 |
+
nltk.download('punkt_tab')
|
15 |
+
|
16 |
+
|
17 |
+
# 1. Function for getting news via NewsAPI
|
18 |
+
def get_news(query, api_key, num_articles=5):
|
19 |
+
url = f'https://newsapi.org/v2/everything?q={query}&apiKey={api_key}&language=en&pageSize={num_articles}'
|
20 |
+
response = requests.get(url)
|
21 |
+
if response.status_code == 200:
|
22 |
+
return response.json()['articles']
|
23 |
+
return []
|
24 |
+
|
25 |
+
|
26 |
+
# 2. Analyzing tone with Hugging Face
|
27 |
+
tone_analyzer = pipeline("sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english", revision="714eb0f")
|
28 |
+
|
29 |
+
def analyze_sentiment(text):
|
30 |
+
return tone_analyzer(text)[0]
|
31 |
+
|
32 |
+
|
33 |
+
# 3. Define category
|
34 |
+
|
35 |
+
category_model = AutoModelForSequenceClassification.from_pretrained("cardiffnlp/tweet-topic-21-multi")
|
36 |
+
category_tokenizer = AutoTokenizer.from_pretrained("cardiffnlp/tweet-topic-21-multi")
|
37 |
+
labels = ['art', 'business', 'entertainment', 'environment', 'fashion', 'finance', 'food',
|
38 |
+
'health', 'law', 'media', 'military', 'music', 'politics', 'religion', 'sci/tech',
|
39 |
+
'sports', 'travel', 'weather', 'world news', 'none']
|
40 |
+
|
41 |
+
def classify_category(text):
|
42 |
+
inputs = category_tokenizer(text, return_tensors="pt", truncation=True, padding=True)
|
43 |
+
outputs = category_model(**inputs)
|
44 |
+
predicted_class = torch.argmax(outputs.logits, dim=1).item()
|
45 |
+
return labels[predicted_class]
|
46 |
+
|
47 |
+
|
48 |
+
# 4. Summarization
|
49 |
+
summarizer = pipeline("summarization", model="facebook/bart-large-cnn")
|
50 |
+
|
51 |
+
def split_text(text, max_tokens=512):
|
52 |
+
words = text.split()
|
53 |
+
return [' '.join(words[i:i+max_tokens]) for i in range(0, len(words), max_tokens)]
|
54 |
+
|
55 |
+
def summarize_text(text):
|
56 |
+
chunks = split_text(text)
|
57 |
+
summaries = [summarizer(chunk, max_length=100, min_length=30, do_sample=False)[0]['summary_text'] for chunk in chunks]
|
58 |
+
return ' '.join(summaries)
|
59 |
+
|
60 |
+
|
61 |
+
# 5. Search for trending words
|
62 |
+
def extract_trending_words(texts):
|
63 |
+
text = ' '.join(texts).lower()
|
64 |
+
words = word_tokenize(text)
|
65 |
+
words = [word for word in words if word not in stopwords.words('english') and word not in string.punctuation and len(word) > 1]
|
66 |
+
word_freq = Counter(words)
|
67 |
+
return word_freq.most_common(10)
|
68 |
+
|
69 |
+
# 6. The main process of analyzing news
|
70 |
+
def analyze_news(query, api_key, num_articles=5):
|
71 |
+
articles = get_news(query, api_key, num_articles)
|
72 |
+
|
73 |
+
if not articles:
|
74 |
+
return []
|
75 |
+
|
76 |
+
news_results = []
|
77 |
+
for article in articles:
|
78 |
+
title = article.get('title', 'No Title')
|
79 |
+
description = article.get('description', '') or ''
|
80 |
+
url = article.get('url', '#')
|
81 |
+
|
82 |
+
sentiment = analyze_sentiment(title + " " + description)['label']
|
83 |
+
category = classify_category(title + " " + description)
|
84 |
+
summary = summarize_text(title + " " + description)
|
85 |
+
|
86 |
+
news_results.append({
|
87 |
+
"title": title,
|
88 |
+
"url": url,
|
89 |
+
"sentiment": sentiment,
|
90 |
+
"category": category,
|
91 |
+
"summary": summary
|
92 |
+
})
|
93 |
+
|
94 |
+
return news_results
|
95 |
+
|
requirements.txt
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Flask==3.0.0
|
2 |
+
transformers==4.39.1
|
3 |
+
torch==2.2.1
|
4 |
+
nltk==3.8.1
|
5 |
+
matplotlib==3.8.2
|
6 |
+
wordcloud==1.9.3
|
7 |
+
requests==2.31.0
|
8 |
+
pillow==10.2.0
|
static/script.js
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
let chartsLoaded = { sentiment: false, wordcloud: false };
|
2 |
+
let chartInterval = setInterval(checkCharts, 2000);
|
3 |
+
|
4 |
+
function checkCharts() {
|
5 |
+
fetch('/sentiment_chart')
|
6 |
+
.then(response => {
|
7 |
+
if (response.ok) {
|
8 |
+
document.getElementById('sentimentChart').src = '/sentiment_chart';
|
9 |
+
document.getElementById('sentimentChart').style.display = 'block';
|
10 |
+
document.getElementById('sentimentChartMessage').style.display = 'none';
|
11 |
+
chartsLoaded.sentiment = true;
|
12 |
+
stopIfChartsLoaded();
|
13 |
+
}
|
14 |
+
});
|
15 |
+
|
16 |
+
fetch('/wordcloud_chart')
|
17 |
+
.then(response => {
|
18 |
+
if (response.ok) {
|
19 |
+
document.getElementById('wordcloudChart').src = '/wordcloud_chart';
|
20 |
+
document.getElementById('wordcloudChart').style.display = 'block';
|
21 |
+
document.getElementById('wordcloudChartMessage').style.display = 'none';
|
22 |
+
chartsLoaded.wordcloud = true;
|
23 |
+
stopIfChartsLoaded();
|
24 |
+
}
|
25 |
+
});
|
26 |
+
}
|
27 |
+
|
28 |
+
function stopIfChartsLoaded() {
|
29 |
+
if (chartsLoaded.sentiment && chartsLoaded.wordcloud) {
|
30 |
+
clearInterval(chartInterval);
|
31 |
+
}
|
32 |
+
}
|
static/style.css
ADDED
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
body {
|
2 |
+
font-family: Arial, sans-serif;
|
3 |
+
background-color: #181818;
|
4 |
+
color: #e0e0e0;
|
5 |
+
display: flex;
|
6 |
+
justify-content: center;
|
7 |
+
align-items: center;
|
8 |
+
flex-direction: column;
|
9 |
+
min-height: 100vh;
|
10 |
+
height: 100%;
|
11 |
+
margin: 0;
|
12 |
+
padding: 0;
|
13 |
+
}
|
14 |
+
|
15 |
+
.container {
|
16 |
+
background: #222;
|
17 |
+
padding: 20px;
|
18 |
+
border-radius: 10px;
|
19 |
+
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.5);
|
20 |
+
text-align: center;
|
21 |
+
width: 90%;
|
22 |
+
max-width: 600px;
|
23 |
+
margin-bottom: 20px;
|
24 |
+
}
|
25 |
+
|
26 |
+
.first-container {
|
27 |
+
margin-top: 20px;
|
28 |
+
}
|
29 |
+
|
30 |
+
.res-container {
|
31 |
+
text-align: left;
|
32 |
+
}
|
33 |
+
|
34 |
+
h1 {
|
35 |
+
color: #f0f0f0;
|
36 |
+
}
|
37 |
+
|
38 |
+
h3 {
|
39 |
+
margin-top: 40px;
|
40 |
+
}
|
41 |
+
|
42 |
+
p.meta-info {
|
43 |
+
color: #bbb;
|
44 |
+
font-size: 14px;
|
45 |
+
line-height: 0
|
46 |
+
}
|
47 |
+
|
48 |
+
textarea, input {
|
49 |
+
width: 100%;
|
50 |
+
padding: 10px;
|
51 |
+
border: 1px solid #444;
|
52 |
+
border-radius: 5px;
|
53 |
+
background: #333;
|
54 |
+
color: #e0e0e0;
|
55 |
+
resize: none;
|
56 |
+
font-size: 16px;
|
57 |
+
outline: none;
|
58 |
+
box-sizing: border-box;
|
59 |
+
margin-bottom: 10px;
|
60 |
+
font-family: inherit;
|
61 |
+
}
|
62 |
+
|
63 |
+
button {
|
64 |
+
padding: 10px 15px;
|
65 |
+
border: none;
|
66 |
+
background: #007bff;
|
67 |
+
color: white;
|
68 |
+
font-size: 16px;
|
69 |
+
cursor: pointer;
|
70 |
+
border-radius: 5px;
|
71 |
+
transition: 0.3s;
|
72 |
+
width: 100%;
|
73 |
+
}
|
74 |
+
|
75 |
+
button:hover {
|
76 |
+
background: #0056b3;
|
77 |
+
}
|
78 |
+
|
79 |
+
input[type=number]::-webkit-inner-spin-button,
|
80 |
+
input[type=number]::-webkit-outer-spin-button {
|
81 |
+
-webkit-appearance: none;
|
82 |
+
margin: 0;
|
83 |
+
}
|
84 |
+
|
85 |
+
a {
|
86 |
+
color: inherit;
|
87 |
+
text-decoration: none;
|
88 |
+
}
|
89 |
+
|
90 |
+
a:hover {
|
91 |
+
text-decoration: underline;
|
92 |
+
}
|
93 |
+
|
94 |
+
img {
|
95 |
+
display: block;
|
96 |
+
margin: 0 auto;
|
97 |
+
}
|
98 |
+
|
99 |
+
.about-container {
|
100 |
+
margin-top: 20px;
|
101 |
+
}
|
102 |
+
|
103 |
+
.about-container li {
|
104 |
+
margin-bottom: 15px;
|
105 |
+
text-align: left;
|
106 |
+
}
|
107 |
+
|
108 |
+
.about-container p {
|
109 |
+
text-align: left;
|
110 |
+
}
|
111 |
+
|
112 |
+
footer {
|
113 |
+
text-align: center;
|
114 |
+
margin-bottom: 20px;
|
115 |
+
color: #bbb;
|
116 |
+
font-size: 14px;
|
117 |
+
}
|
templates/about.html
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>About - NewsAnalyzer</title>
|
7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
8 |
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='80' font-size='80' %3E📰%3C/text%3E%3C/svg%3E">
|
9 |
+
</head>
|
10 |
+
<body>
|
11 |
+
<div class="container about-container">
|
12 |
+
<h1>About NewsAnalyzer</h1>
|
13 |
+
<p>
|
14 |
+
NewsAnalyzer is an AI-powered tool designed to help you quickly understand the latest news trends. Simply enter your search query, and NewsAnalyzer will gather and analyze articles to deliver valuable insights.
|
15 |
+
</p>
|
16 |
+
<p>Here's how it works:</p>
|
17 |
+
<ul>
|
18 |
+
<li><b>News Collection:</b> News articles are fetched from NewsAPI based on your search query.</li>
|
19 |
+
<li><b>Sentiment Analysis:</b> Each article is evaluated to determine whether its tone is positive or negative.</li>
|
20 |
+
<li><b>Category Classification:</b> Articles are automatically categorized into topics like business, technology, politics, and more.</li>
|
21 |
+
<li><b>Summarization:</b> Long descriptions are summarized to give you only the most important details.</li>
|
22 |
+
<li><b>Trending Words:</b> The most frequently mentioned words across articles are extracted to highlight key topics.</li>
|
23 |
+
<li><b>Charts:</b> Sentiment distribution and trending words are visualized with clear, easy-to-understand charts.</li>
|
24 |
+
</ul>
|
25 |
+
<p>Technologies:</p>
|
26 |
+
<p>NewsAnalyzer combines real-time data collection with machine learning models from Hugging Face, using Flask to seamlessly connect the data processing, analysis, and visualization into a single streamlined experience.</p>
|
27 |
+
<a href="/" style="color: #bbb;">← Back to Home</a>
|
28 |
+
</div>
|
29 |
+
</body>
|
30 |
+
<footer>
|
31 |
+
Created by Volodymyr Shereperov | <a href="https://github.com/ihaveaplan66/news-analyzer" target="_blank">Source Code on GitHub</a>
|
32 |
+
</footer>
|
33 |
+
</html>
|
templates/index.html
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>NewsAnalyzer</title>
|
7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
8 |
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='80' font-size='80' %3E📰%3C/text%3E%3C/svg%3E">
|
9 |
+
</head>
|
10 |
+
<body>
|
11 |
+
<div class="container first-container">
|
12 |
+
<h1>NewsAnalyzer</h1>
|
13 |
+
|
14 |
+
<form method="POST">
|
15 |
+
<textarea name="query" placeholder="Enter search query (e.g., Tesla)" required></textarea>
|
16 |
+
<input type="number" name="num_articles" min="1" max="20" value="5" required placeholder="Number of articles (1-20)">
|
17 |
+
<button type="submit">Analyze</button>
|
18 |
+
</form>
|
19 |
+
|
20 |
+
<p style="margin-top: 10px; font-size: 14px; color: #aaa;">
|
21 |
+
<a href="/about" style="color: #bbb;">How does it work?</a>
|
22 |
+
</p>
|
23 |
+
</div>
|
24 |
+
|
25 |
+
{% if results %}
|
26 |
+
<div class="container res-container">
|
27 |
+
<h2 style="text-align: center">Analysis for '{{ query }}'</h2>
|
28 |
+
{% for article in results %}
|
29 |
+
<h3><b><a href="{{ article.url }}" target="_blank">{{ article.title }}</a></b></h3>
|
30 |
+
<p class="meta-info">{{ article.category }} · {{ article.sentiment }}</p>
|
31 |
+
<p>{{ article.summary }}</p>
|
32 |
+
{% endfor %}
|
33 |
+
</div>
|
34 |
+
|
35 |
+
<div class="container">
|
36 |
+
<h2>Sentiment Analysis</h2>
|
37 |
+
<img id="sentimentChart" src="" alt="Sentiment Analysis" style="display:none; max-width:100%;">
|
38 |
+
<p id="sentimentChartMessage" style="text-align: center; color: #aaa;">Charts are loading...</p>
|
39 |
+
</div>
|
40 |
+
|
41 |
+
<div class="container">
|
42 |
+
<h2>Trending Words</h2>
|
43 |
+
<ul style="list-style: none; padding: 0; text-align: center;">
|
44 |
+
{% for word, count in trending_words %}
|
45 |
+
<li style="display: inline-block; margin: 5px; color: #bbb;">{{ word }} ({{ count }})</li>
|
46 |
+
{% endfor %}
|
47 |
+
</ul>
|
48 |
+
<img id="wordcloudChart" src="" alt="Word Cloud" style="display:none; max-width:100%;">
|
49 |
+
<p id="wordcloudChartMessage" style="text-align: center; color: #aaa;">Charts are loading...</p>
|
50 |
+
</div>
|
51 |
+
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
52 |
+
{% endif %}
|
53 |
+
</body>
|
54 |
+
<footer>
|
55 |
+
Created by Volodymyr Shereperov | <a href="https://github.com/ihaveaplan66/news-analyzer" target="_blank">Source Code on GitHub</a>
|
56 |
+
</footer>
|
57 |
+
</html>
|