Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -11,9 +11,6 @@ import matplotlib.pyplot as plt
|
|
| 11 |
import re
|
| 12 |
import html
|
| 13 |
import requests
|
| 14 |
-
import torch
|
| 15 |
-
import spaces # ZeroGPU için gerekli kütüphane
|
| 16 |
-
|
| 17 |
from sentence_transformers import SentenceTransformer
|
| 18 |
from transformers import AutoTokenizer, AutoModelForSequenceClassification, TextClassificationPipeline, pipeline
|
| 19 |
import warnings
|
|
@@ -35,12 +32,18 @@ TRANSLATOR_NAME = "Helsinki-NLP/opus-mt-tc-big-en-tr"
|
|
| 35 |
# --- Ayarlar ---
|
| 36 |
NEUTRAL_CONFIDENCE_FLOOR = 0.55
|
| 37 |
|
| 38 |
-
# --- Sinyal Kelimeleri
|
| 39 |
NEGATIVE_CUES = { "crash", "crashes", "dump", "dumps", "plunge", "plunges", "tumble", "tumbles", "falls", "fall", "drop", "drops", "slump", "slumps", "sell-off", "selloff", "panic", "fear", "fears", "concern", "concerns", "pressure", "pressures", "risk", "risks", "risk-off", "lawsuit", "hacked", "hack", "breach", "ban", "banned", "crackdown", "probe", "investigation", "charges", "liquidation", "liquidations", "lag", "lags", "weak", "weaker", "over?", "collapse", "collapses", "recession", "loss", "losses" }
|
| 40 |
POSITIVE_CUES = { "surge", "surges", "pump", "pumps", "rally", "rallies", "soar", "soars", "breakout", "breaks out", "record", "ath", "all-time high", "wins", "approval", "approved", "etf", "inflows", "adoption", "partnership", "partners", "launch", "launches", "upgrade", "upgrades", "bull", "bullish", "rise", "rises", "beats", "rebound", "rebounds", "gain", "gains", "bullrun" }
|
| 41 |
-
LABEL_MAP = { "bullish": "OLUMLU", "bearish": "OLUMSUZ", "neutral": "NÖTR" }
|
| 42 |
|
| 43 |
-
# ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
def clean_html_tags(text):
|
| 45 |
if not text: return ""
|
| 46 |
text = html.unescape(text)
|
|
@@ -53,6 +56,31 @@ def clean_html_tags(text):
|
|
| 53 |
text = re.sub(r'\s+', ' ', text).strip()
|
| 54 |
return text
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
def _normalize_label(label: str) -> str:
|
| 57 |
l = (label or "").strip().lower()
|
| 58 |
if l in {"label_2"} or "bull" in l: return "bullish"
|
|
@@ -86,49 +114,51 @@ def fetch_feed_data(url):
|
|
| 86 |
return feedparser.parse(response.content) if response.status_code == 200 else None
|
| 87 |
except: return None
|
| 88 |
|
| 89 |
-
# ---
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
def initialize_models():
|
| 94 |
-
global model, sentiment_analyzer, translator
|
| 95 |
-
status_msg = []
|
| 96 |
-
|
| 97 |
-
# GPU var mı kontrolü (ZeroGPU'da her zaman vardır ama kod güvenliği için)
|
| 98 |
-
device = 0 if torch.cuda.is_available() else -1
|
| 99 |
|
| 100 |
-
|
| 101 |
-
if model is None:
|
| 102 |
-
# SentenceTransformer otomatik olarak GPU kullanır varsa
|
| 103 |
-
model = SentenceTransformer('all-MiniLM-L6-v2')
|
| 104 |
-
status_msg.append("✅ Embedding Modeli Hazır")
|
| 105 |
-
except Exception as e: status_msg.append(f"❌ Embedding: {str(e)}")
|
| 106 |
-
|
| 107 |
-
try:
|
| 108 |
-
if sentiment_analyzer is None:
|
| 109 |
-
tokenizer = AutoTokenizer.from_pretrained(CRYPTOBERT_NAME, use_fast=True)
|
| 110 |
-
clf_model = AutoModelForSequenceClassification.from_pretrained(CRYPTOBERT_NAME)
|
| 111 |
-
# device=0 diyerek GPU'ya zorluyoruz
|
| 112 |
-
sentiment_analyzer = TextClassificationPipeline(model=clf_model, tokenizer=tokenizer, device=device, max_length=128, truncation=True, padding="max_length")
|
| 113 |
-
status_msg.append("✅ Sentiment Modeli Hazır")
|
| 114 |
-
except Exception as e: status_msg.append(f"❌ Sentiment: {str(e)}")
|
| 115 |
-
|
| 116 |
-
try:
|
| 117 |
-
if translator is None:
|
| 118 |
-
# device=0 diyerek GPU'ya zorluyoruz
|
| 119 |
-
translator = pipeline("translation", model=TRANSLATOR_NAME, device=device)
|
| 120 |
-
status_msg.append("✅ Çeviri Modeli Hazır")
|
| 121 |
-
except Exception as e: status_msg.append(f"❌ Çeviri: {str(e)}")
|
| 122 |
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
-
# Haber Çekme ve Analiz (GPU kullanır)
|
| 126 |
-
@spaces.GPU(duration=60) # Bu işlem biraz sürebilir, süre tanıyalım
|
| 127 |
def fetch_news_wrapper():
|
| 128 |
global df, index, embeddings, sentiment_analyzer, model
|
| 129 |
|
| 130 |
if sentiment_analyzer is None:
|
| 131 |
-
|
| 132 |
|
| 133 |
RSS_URLS = [
|
| 134 |
"https://cointelegraph.com/rss",
|
|
@@ -142,7 +172,7 @@ def fetch_news_wrapper():
|
|
| 142 |
for url in RSS_URLS:
|
| 143 |
feed = fetch_feed_data(url)
|
| 144 |
if feed and feed.entries:
|
| 145 |
-
for entry in feed.entries[:
|
| 146 |
clean_title = clean_html_tags(entry.get("title", ""))
|
| 147 |
if clean_title:
|
| 148 |
all_entries.append({
|
|
@@ -157,70 +187,62 @@ def fetch_news_wrapper():
|
|
| 157 |
|
| 158 |
df = pd.DataFrame(all_entries).drop_duplicates(subset="title").reset_index(drop=True)
|
| 159 |
|
| 160 |
-
# Sentiment Analizi
|
| 161 |
-
|
| 162 |
-
scores = []
|
| 163 |
-
|
| 164 |
-
# Toplu işleme gerek yok, GPU hızlıdır, loop yeterli
|
| 165 |
-
for _, row in df.iterrows():
|
| 166 |
text = f"{row['title']}. {row['summary']}"[:1000]
|
| 167 |
try:
|
| 168 |
out = sentiment_analyzer(text)[0]
|
| 169 |
lbl, scr = _apply_post_rules(_normalize_label(out.get("label")), float(out.get("score")), text)
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
except:
|
| 173 |
-
sentiments.append("neutral")
|
| 174 |
-
scores.append(0.0)
|
| 175 |
|
| 176 |
-
df["sentiment_label"] =
|
| 177 |
-
df["sentiment_score"] = scores
|
| 178 |
|
| 179 |
-
# Faiss
|
| 180 |
corpus = df['title'].tolist()
|
| 181 |
embeddings = model.encode(corpus, show_progress_bar=False)
|
| 182 |
index = faiss.IndexFlatL2(embeddings.shape[1])
|
| 183 |
index.add(embeddings.astype('float32'))
|
| 184 |
|
| 185 |
-
|
|
|
|
| 186 |
html_feed = format_news_as_html(df)
|
| 187 |
choices = [(f"{i}. {t[:40]}...", i) for i, t in enumerate(df["title"])]
|
| 188 |
|
| 189 |
-
# Grafik
|
| 190 |
sentiment_counts = df["sentiment_label"].value_counts()
|
|
|
|
| 191 |
fig, ax = plt.subplots(figsize=(6, 3))
|
| 192 |
colors = {'bullish': '#4CAF50', 'bearish': '#F44336', 'neutral': '#FFC107'}
|
|
|
|
|
|
|
| 193 |
tr_labels = [LABEL_MAP.get(x, x) for x in sentiment_counts.index]
|
| 194 |
bar_colors = [colors.get(x, '#333') for x in sentiment_counts.index]
|
|
|
|
| 195 |
ax.bar(tr_labels, sentiment_counts.values, color=bar_colors)
|
| 196 |
ax.set_title("Piyasa Duygu Durumu")
|
| 197 |
plt.tight_layout()
|
| 198 |
|
| 199 |
return status_text, html_feed, gr.update(choices=choices, value=None), fig
|
| 200 |
|
| 201 |
-
# Çeviri
|
| 202 |
-
@spaces.GPU
|
| 203 |
def perform_translation(news_index):
|
| 204 |
-
global df
|
| 205 |
-
if translator is None: initialize_models()
|
| 206 |
-
|
| 207 |
if df is None or news_index is None: return "Lütfen bir haber seçin.", "..."
|
| 208 |
try:
|
| 209 |
idx = int(news_index)
|
| 210 |
row = df.iloc[idx]
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
title_tr = translator(row['title'][:512])[0]['translation_text']
|
| 214 |
-
|
| 215 |
-
summ = row['summary']
|
| 216 |
-
if not summ: summ = "Özet yok."
|
| 217 |
-
summary_tr = translator(summ[:512])[0]['translation_text']
|
| 218 |
-
|
| 219 |
return title_tr, summary_tr
|
| 220 |
except Exception as e: return f"Hata: {e}", "..."
|
| 221 |
|
| 222 |
-
|
| 223 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
def search_news(query):
|
| 225 |
global df, index, model
|
| 226 |
if df is None: return "Veri yok."
|
|
@@ -232,13 +254,14 @@ def search_news(query):
|
|
| 232 |
except: return "Arama hatası."
|
| 233 |
|
| 234 |
def analyze_coin(coin_name):
|
| 235 |
-
# Bu sadece filtreleme yaptığı için GPU'ya gerek yok
|
| 236 |
global df
|
| 237 |
if df is None: return None, "Veri yok."
|
| 238 |
filtered = df[df["title"].str.contains(coin_name, case=False, na=False)]
|
| 239 |
if len(filtered) == 0: return None, f"{coin_name} hakkında haber yok."
|
| 240 |
|
| 241 |
counts = filtered["sentiment_label"].value_counts()
|
|
|
|
|
|
|
| 242 |
labels_tr = [LABEL_MAP.get(x, x) for x in counts.index]
|
| 243 |
colors = ['#4CAF50' if x=='bullish' else '#F44336' if x=='bearish' else '#FFC107' for x in counts.index]
|
| 244 |
|
|
@@ -247,97 +270,126 @@ def analyze_coin(coin_name):
|
|
| 247 |
ax.set_title(f"{coin_name.upper()} Analizi")
|
| 248 |
return fig, format_news_as_html(filtered)
|
| 249 |
|
| 250 |
-
#
|
| 251 |
-
def format_news_as_html(dataframe):
|
| 252 |
-
if dataframe is None or len(dataframe) == 0:
|
| 253 |
-
return "<div style='padding:20px; text-align:center; color: #666;'>Haber yok. Lütfen 'Verileri Yenile' butonuna basın.</div>"
|
| 254 |
-
|
| 255 |
-
html_content = "<div class='news-feed-container'>"
|
| 256 |
-
for _, row in dataframe.iterrows():
|
| 257 |
-
sentiment = row['sentiment_label']
|
| 258 |
-
score = row['sentiment_score']
|
| 259 |
-
confidence_percent = int(score * 100)
|
| 260 |
-
tr_label = LABEL_MAP.get(sentiment, "NÖTR")
|
| 261 |
-
|
| 262 |
-
color_class = "neutral-card"
|
| 263 |
-
icon = "➖"
|
| 264 |
-
if sentiment == "bullish":
|
| 265 |
-
color_class = "bullish-card"
|
| 266 |
-
icon = "🚀"
|
| 267 |
-
elif sentiment == "bearish":
|
| 268 |
-
color_class = "bearish-card"
|
| 269 |
-
icon = "🔻"
|
| 270 |
-
|
| 271 |
-
html_content += f"""
|
| 272 |
-
<div class='news-card {color_class}'>
|
| 273 |
-
<div class='card-header'>
|
| 274 |
-
<span class='badge {sentiment}'>{icon} {tr_label} (%{confidence_percent})</span>
|
| 275 |
-
<span class='date'>{row['published'][:16]}</span>
|
| 276 |
-
</div>
|
| 277 |
-
<h3><a href="{row['link']}" target="_blank">{row['title']}</a></h3>
|
| 278 |
-
<p>{row['summary'][:160]}...</p>
|
| 279 |
-
</div>
|
| 280 |
-
"""
|
| 281 |
-
html_content += "</div>"
|
| 282 |
-
return html_content
|
| 283 |
-
|
| 284 |
-
# --- CSS ---
|
| 285 |
custom_css = """
|
| 286 |
-
|
| 287 |
-
.news-card
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
.bullish-card { border-left: 6px solid #4CAF50; }
|
| 292 |
.bearish-card { border-left: 6px solid #F44336; }
|
| 293 |
.neutral-card { border-left: 6px solid #FFC107; }
|
| 294 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
.bullish { background-color: #4CAF50; }
|
| 296 |
.bearish { background-color: #F44336; }
|
| 297 |
.neutral { background-color: #FFC107; color: #333; }
|
| 298 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
"""
|
| 300 |
|
| 301 |
-
# --- UI ---
|
| 302 |
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue"), css=custom_css, title="Crypto News AI") as app:
|
|
|
|
|
|
|
| 303 |
with gr.Row(elem_id="header"):
|
| 304 |
with gr.Column(scale=3):
|
| 305 |
-
gr.Markdown("# ⚡ AI Crypto Sentiment Dashboard
|
| 306 |
with gr.Column(scale=1):
|
| 307 |
init_btn = gr.Button("🚀 1. Modelleri Başlat", variant="primary", size="sm")
|
| 308 |
load_status = gr.Textbox(show_label=False, placeholder="Model Durumu...", lines=1)
|
|
|
|
| 309 |
gr.Markdown("---")
|
|
|
|
|
|
|
| 310 |
with gr.Row():
|
|
|
|
| 311 |
with gr.Column(scale=1, min_width=300):
|
|
|
|
| 312 |
with gr.Group():
|
| 313 |
gr.Markdown("### 📡 Veri Kontrol")
|
| 314 |
refresh_btn = gr.Button("🔄 2. Verileri Yenile", variant="secondary")
|
| 315 |
data_status = gr.Textbox(show_label=False, placeholder="Veri bekleniyor...", lines=1)
|
| 316 |
overview_plot = gr.Plot(label="Piyasa Özeti")
|
|
|
|
| 317 |
gr.Markdown("### 🔍 Hızlı Filtre")
|
| 318 |
with gr.Tabs():
|
| 319 |
with gr.Tab("Coin Ara"):
|
| 320 |
coin_input = gr.Textbox(placeholder="BTC, ETH, SOL...", show_label=False)
|
| 321 |
coin_btn = gr.Button("Analiz Et")
|
| 322 |
coin_plot = gr.Plot(show_label=False)
|
|
|
|
| 323 |
with gr.Tab("Haber Ara"):
|
| 324 |
search_input = gr.Textbox(placeholder="Konu girin...", show_label=False)
|
| 325 |
search_btn = gr.Button("Ara")
|
|
|
|
| 326 |
gr.Markdown("### 🇹🇷 Çeviri Aracı")
|
| 327 |
with gr.Group():
|
| 328 |
news_selector = gr.Dropdown(label="Haber Seçin", choices=[], type="value", interactive=True)
|
| 329 |
translate_btn = gr.Button("Türkçeye Çevir")
|
| 330 |
tr_title_out = gr.Textbox(label="Başlık (TR)", lines=2)
|
| 331 |
tr_summary_out = gr.Textbox(label="Özet (TR)", lines=4)
|
|
|
|
|
|
|
| 332 |
with gr.Column(scale=2):
|
| 333 |
gr.Markdown("### 📰 Canlı Haber Akışı")
|
| 334 |
news_feed_html = gr.HTML(label="Haberler", value="<div style='padding:20px; color:#666;'>Veriler yüklenince burada görünecek...</div>")
|
| 335 |
|
|
|
|
| 336 |
init_btn.click(initialize_models, outputs=load_status)
|
| 337 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
coin_btn.click(analyze_coin, inputs=coin_input, outputs=[coin_plot, news_feed_html])
|
| 339 |
search_btn.click(search_news, inputs=search_input, outputs=news_feed_html)
|
| 340 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
|
| 342 |
if __name__ == "__main__":
|
| 343 |
app.launch()
|
|
|
|
| 11 |
import re
|
| 12 |
import html
|
| 13 |
import requests
|
|
|
|
|
|
|
|
|
|
| 14 |
from sentence_transformers import SentenceTransformer
|
| 15 |
from transformers import AutoTokenizer, AutoModelForSequenceClassification, TextClassificationPipeline, pipeline
|
| 16 |
import warnings
|
|
|
|
| 32 |
# --- Ayarlar ---
|
| 33 |
NEUTRAL_CONFIDENCE_FLOOR = 0.55
|
| 34 |
|
| 35 |
+
# --- Sinyal Kelimeleri ---
|
| 36 |
NEGATIVE_CUES = { "crash", "crashes", "dump", "dumps", "plunge", "plunges", "tumble", "tumbles", "falls", "fall", "drop", "drops", "slump", "slumps", "sell-off", "selloff", "panic", "fear", "fears", "concern", "concerns", "pressure", "pressures", "risk", "risks", "risk-off", "lawsuit", "hacked", "hack", "breach", "ban", "banned", "crackdown", "probe", "investigation", "charges", "liquidation", "liquidations", "lag", "lags", "weak", "weaker", "over?", "collapse", "collapses", "recession", "loss", "losses" }
|
| 37 |
POSITIVE_CUES = { "surge", "surges", "pump", "pumps", "rally", "rallies", "soar", "soars", "breakout", "breaks out", "record", "ath", "all-time high", "wins", "approval", "approved", "etf", "inflows", "adoption", "partnership", "partners", "launch", "launches", "upgrade", "upgrades", "bull", "bullish", "rise", "rises", "beats", "rebound", "rebounds", "gain", "gains", "bullrun" }
|
|
|
|
| 38 |
|
| 39 |
+
# --- Çeviri Haritası (Görünüm İçin) ---
|
| 40 |
+
LABEL_MAP = {
|
| 41 |
+
"bullish": "OLUMLU",
|
| 42 |
+
"bearish": "OLUMSUZ",
|
| 43 |
+
"neutral": "NÖTR"
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
# --- TEMİZLEME VE YARDIMCI FONKSİYONLAR ---
|
| 47 |
def clean_html_tags(text):
|
| 48 |
if not text: return ""
|
| 49 |
text = html.unescape(text)
|
|
|
|
| 56 |
text = re.sub(r'\s+', ' ', text).strip()
|
| 57 |
return text
|
| 58 |
|
| 59 |
+
def initialize_models():
|
| 60 |
+
global model, sentiment_analyzer, translator
|
| 61 |
+
status_msg = []
|
| 62 |
+
try:
|
| 63 |
+
if model is None:
|
| 64 |
+
model = SentenceTransformer('all-MiniLM-L6-v2')
|
| 65 |
+
status_msg.append("✅ Embedding Modeli Hazır")
|
| 66 |
+
except Exception as e: status_msg.append(f"❌ Embedding: {str(e)}")
|
| 67 |
+
|
| 68 |
+
try:
|
| 69 |
+
if sentiment_analyzer is None:
|
| 70 |
+
tokenizer = AutoTokenizer.from_pretrained(CRYPTOBERT_NAME, use_fast=True)
|
| 71 |
+
clf_model = AutoModelForSequenceClassification.from_pretrained(CRYPTOBERT_NAME)
|
| 72 |
+
sentiment_analyzer = TextClassificationPipeline(model=clf_model, tokenizer=tokenizer, device=-1, max_length=128, truncation=True, padding="max_length")
|
| 73 |
+
status_msg.append("✅ Sentiment Modeli Hazır")
|
| 74 |
+
except Exception as e: status_msg.append(f"❌ Sentiment: {str(e)}")
|
| 75 |
+
|
| 76 |
+
try:
|
| 77 |
+
if translator is None:
|
| 78 |
+
translator = pipeline("translation", model=TRANSLATOR_NAME, device=-1)
|
| 79 |
+
status_msg.append("✅ Çeviri Modeli Hazır")
|
| 80 |
+
except Exception as e: status_msg.append(f"❌ Çeviri: {str(e)}")
|
| 81 |
+
|
| 82 |
+
return " | ".join(status_msg)
|
| 83 |
+
|
| 84 |
def _normalize_label(label: str) -> str:
|
| 85 |
l = (label or "").strip().lower()
|
| 86 |
if l in {"label_2"} or "bull" in l: return "bullish"
|
|
|
|
| 114 |
return feedparser.parse(response.content) if response.status_code == 200 else None
|
| 115 |
except: return None
|
| 116 |
|
| 117 |
+
# --- HTML FORMATLAMA FONKSİYONU ---
|
| 118 |
+
def format_news_as_html(dataframe):
|
| 119 |
+
if dataframe is None or len(dataframe) == 0:
|
| 120 |
+
return "<div style='padding:20px; text-align:center; color: #666;'>Haber yok. Lütfen 'Verileri Yenile' butonuna basın.</div>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
+
html_content = "<div class='news-feed-container'>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
+
for _, row in dataframe.iterrows():
|
| 125 |
+
sentiment = row['sentiment_label']
|
| 126 |
+
score = row['sentiment_score']
|
| 127 |
+
|
| 128 |
+
# Skor Formatı: 0.98 -> 98
|
| 129 |
+
confidence_percent = int(score * 100)
|
| 130 |
+
|
| 131 |
+
# Türkçe Etiket
|
| 132 |
+
tr_label = LABEL_MAP.get(sentiment, "NÖTR")
|
| 133 |
+
|
| 134 |
+
# Renk sınıfları
|
| 135 |
+
color_class = "neutral-card"
|
| 136 |
+
icon = "➖"
|
| 137 |
+
if sentiment == "bullish":
|
| 138 |
+
color_class = "bullish-card"
|
| 139 |
+
icon = "🚀"
|
| 140 |
+
elif sentiment == "bearish":
|
| 141 |
+
color_class = "bearish-card"
|
| 142 |
+
icon = "🔻"
|
| 143 |
+
|
| 144 |
+
html_content += f"""
|
| 145 |
+
<div class='news-card {color_class}'>
|
| 146 |
+
<div class='card-header'>
|
| 147 |
+
<span class='badge {sentiment}'>{icon} {tr_label} (%{confidence_percent})</span>
|
| 148 |
+
<span class='date'>{row['published'][:16]}</span>
|
| 149 |
+
</div>
|
| 150 |
+
<h3><a href="{row['link']}" target="_blank">{row['title']}</a></h3>
|
| 151 |
+
<p>{row['summary'][:160]}...</p>
|
| 152 |
+
</div>
|
| 153 |
+
"""
|
| 154 |
+
html_content += "</div>"
|
| 155 |
+
return html_content
|
| 156 |
|
|
|
|
|
|
|
| 157 |
def fetch_news_wrapper():
|
| 158 |
global df, index, embeddings, sentiment_analyzer, model
|
| 159 |
|
| 160 |
if sentiment_analyzer is None:
|
| 161 |
+
return "⚠️ Önce Modelleri Yükleyin!", "", gr.update(choices=[]), None
|
| 162 |
|
| 163 |
RSS_URLS = [
|
| 164 |
"https://cointelegraph.com/rss",
|
|
|
|
| 172 |
for url in RSS_URLS:
|
| 173 |
feed = fetch_feed_data(url)
|
| 174 |
if feed and feed.entries:
|
| 175 |
+
for entry in feed.entries[:20]:
|
| 176 |
clean_title = clean_html_tags(entry.get("title", ""))
|
| 177 |
if clean_title:
|
| 178 |
all_entries.append({
|
|
|
|
| 187 |
|
| 188 |
df = pd.DataFrame(all_entries).drop_duplicates(subset="title").reset_index(drop=True)
|
| 189 |
|
| 190 |
+
# Sentiment Analizi
|
| 191 |
+
def analyze_row(row):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
text = f"{row['title']}. {row['summary']}"[:1000]
|
| 193 |
try:
|
| 194 |
out = sentiment_analyzer(text)[0]
|
| 195 |
lbl, scr = _apply_post_rules(_normalize_label(out.get("label")), float(out.get("score")), text)
|
| 196 |
+
return lbl, scr
|
| 197 |
+
except: return "neutral", 0.0
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
+
df["sentiment_label"], df["sentiment_score"] = zip(*df.apply(analyze_row, axis=1))
|
|
|
|
| 200 |
|
| 201 |
+
# Faiss
|
| 202 |
corpus = df['title'].tolist()
|
| 203 |
embeddings = model.encode(corpus, show_progress_bar=False)
|
| 204 |
index = faiss.IndexFlatL2(embeddings.shape[1])
|
| 205 |
index.add(embeddings.astype('float32'))
|
| 206 |
|
| 207 |
+
# UI Çıktıları
|
| 208 |
+
status_text = f"✅ {len(df)} Haber Analiz Edildi"
|
| 209 |
html_feed = format_news_as_html(df)
|
| 210 |
choices = [(f"{i}. {t[:40]}...", i) for i, t in enumerate(df["title"])]
|
| 211 |
|
| 212 |
+
# Grafik oluştur (Türkçe Etiketli)
|
| 213 |
sentiment_counts = df["sentiment_label"].value_counts()
|
| 214 |
+
|
| 215 |
fig, ax = plt.subplots(figsize=(6, 3))
|
| 216 |
colors = {'bullish': '#4CAF50', 'bearish': '#F44336', 'neutral': '#FFC107'}
|
| 217 |
+
|
| 218 |
+
# Etiketleri Türkçeye çevir
|
| 219 |
tr_labels = [LABEL_MAP.get(x, x) for x in sentiment_counts.index]
|
| 220 |
bar_colors = [colors.get(x, '#333') for x in sentiment_counts.index]
|
| 221 |
+
|
| 222 |
ax.bar(tr_labels, sentiment_counts.values, color=bar_colors)
|
| 223 |
ax.set_title("Piyasa Duygu Durumu")
|
| 224 |
plt.tight_layout()
|
| 225 |
|
| 226 |
return status_text, html_feed, gr.update(choices=choices, value=None), fig
|
| 227 |
|
| 228 |
+
# --- Çeviri ve Arama Fonksiyonları ---
|
|
|
|
| 229 |
def perform_translation(news_index):
|
| 230 |
+
global df
|
|
|
|
|
|
|
| 231 |
if df is None or news_index is None: return "Lütfen bir haber seçin.", "..."
|
| 232 |
try:
|
| 233 |
idx = int(news_index)
|
| 234 |
row = df.iloc[idx]
|
| 235 |
+
title_tr = translate_text_en_to_tr(row['title'])
|
| 236 |
+
summary_tr = translate_text_en_to_tr(row['summary'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
return title_tr, summary_tr
|
| 238 |
except Exception as e: return f"Hata: {e}", "..."
|
| 239 |
|
| 240 |
+
def translate_text_en_to_tr(text):
|
| 241 |
+
global translator
|
| 242 |
+
if not text: return ""
|
| 243 |
+
try: return translator(text[:512])[0]['translation_text']
|
| 244 |
+
except: return "Çeviri hatası"
|
| 245 |
+
|
| 246 |
def search_news(query):
|
| 247 |
global df, index, model
|
| 248 |
if df is None: return "Veri yok."
|
|
|
|
| 254 |
except: return "Arama hatası."
|
| 255 |
|
| 256 |
def analyze_coin(coin_name):
|
|
|
|
| 257 |
global df
|
| 258 |
if df is None: return None, "Veri yok."
|
| 259 |
filtered = df[df["title"].str.contains(coin_name, case=False, na=False)]
|
| 260 |
if len(filtered) == 0: return None, f"{coin_name} hakkında haber yok."
|
| 261 |
|
| 262 |
counts = filtered["sentiment_label"].value_counts()
|
| 263 |
+
|
| 264 |
+
# Türkçe etiketler ve renkler
|
| 265 |
labels_tr = [LABEL_MAP.get(x, x) for x in counts.index]
|
| 266 |
colors = ['#4CAF50' if x=='bullish' else '#F44336' if x=='bearish' else '#FFC107' for x in counts.index]
|
| 267 |
|
|
|
|
| 270 |
ax.set_title(f"{coin_name.upper()} Analizi")
|
| 271 |
return fig, format_news_as_html(filtered)
|
| 272 |
|
| 273 |
+
# --- CSS STİLLERİ ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
custom_css = """
|
| 275 |
+
/* Genel Kart Yapısı */
|
| 276 |
+
.news-card {
|
| 277 |
+
background-color: #ffffff;
|
| 278 |
+
border-radius: 10px;
|
| 279 |
+
padding: 15px;
|
| 280 |
+
margin-bottom: 15px;
|
| 281 |
+
border: 1px solid #e0e0e0;
|
| 282 |
+
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
/* Yazı renklerini zorla koyu yap */
|
| 286 |
+
.news-card h3 a {
|
| 287 |
+
text-decoration: none;
|
| 288 |
+
color: #222222 !important;
|
| 289 |
+
font-weight: 700;
|
| 290 |
+
}
|
| 291 |
+
.news-card p {
|
| 292 |
+
color: #444444 !important;
|
| 293 |
+
font-size: 0.95em;
|
| 294 |
+
line-height: 1.5;
|
| 295 |
+
}
|
| 296 |
+
.card-header {
|
| 297 |
+
display: flex;
|
| 298 |
+
justify-content: space-between;
|
| 299 |
+
margin-bottom: 8px;
|
| 300 |
+
}
|
| 301 |
+
.date {
|
| 302 |
+
font-size: 0.8em;
|
| 303 |
+
color: #888888 !important;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
/* Renk Çizgileri */
|
| 307 |
.bullish-card { border-left: 6px solid #4CAF50; }
|
| 308 |
.bearish-card { border-left: 6px solid #F44336; }
|
| 309 |
.neutral-card { border-left: 6px solid #FFC107; }
|
| 310 |
+
|
| 311 |
+
/* Badge Tasarımı */
|
| 312 |
+
.badge {
|
| 313 |
+
padding: 3px 8px;
|
| 314 |
+
border-radius: 4px;
|
| 315 |
+
font-size: 0.75em;
|
| 316 |
+
font-weight: bold;
|
| 317 |
+
color: white;
|
| 318 |
+
}
|
| 319 |
.bullish { background-color: #4CAF50; }
|
| 320 |
.bearish { background-color: #F44336; }
|
| 321 |
.neutral { background-color: #FFC107; color: #333; }
|
| 322 |
+
|
| 323 |
+
.news-feed-container {
|
| 324 |
+
max-height: 800px;
|
| 325 |
+
overflow-y: auto;
|
| 326 |
+
padding-right: 10px;
|
| 327 |
+
}
|
| 328 |
"""
|
| 329 |
|
| 330 |
+
# --- UI TASARIMI ---
|
| 331 |
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue"), css=custom_css, title="Crypto News AI") as app:
|
| 332 |
+
|
| 333 |
+
# Üst Bar (Header)
|
| 334 |
with gr.Row(elem_id="header"):
|
| 335 |
with gr.Column(scale=3):
|
| 336 |
+
gr.Markdown("# ⚡ AI Crypto Sentiment Dashboard")
|
| 337 |
with gr.Column(scale=1):
|
| 338 |
init_btn = gr.Button("🚀 1. Modelleri Başlat", variant="primary", size="sm")
|
| 339 |
load_status = gr.Textbox(show_label=False, placeholder="Model Durumu...", lines=1)
|
| 340 |
+
|
| 341 |
gr.Markdown("---")
|
| 342 |
+
|
| 343 |
+
# Ana İçerik
|
| 344 |
with gr.Row():
|
| 345 |
+
# --- SOL SÜTUN (Kontrol & Analiz) ---
|
| 346 |
with gr.Column(scale=1, min_width=300):
|
| 347 |
+
|
| 348 |
with gr.Group():
|
| 349 |
gr.Markdown("### 📡 Veri Kontrol")
|
| 350 |
refresh_btn = gr.Button("🔄 2. Verileri Yenile", variant="secondary")
|
| 351 |
data_status = gr.Textbox(show_label=False, placeholder="Veri bekleniyor...", lines=1)
|
| 352 |
overview_plot = gr.Plot(label="Piyasa Özeti")
|
| 353 |
+
|
| 354 |
gr.Markdown("### 🔍 Hızlı Filtre")
|
| 355 |
with gr.Tabs():
|
| 356 |
with gr.Tab("Coin Ara"):
|
| 357 |
coin_input = gr.Textbox(placeholder="BTC, ETH, SOL...", show_label=False)
|
| 358 |
coin_btn = gr.Button("Analiz Et")
|
| 359 |
coin_plot = gr.Plot(show_label=False)
|
| 360 |
+
|
| 361 |
with gr.Tab("Haber Ara"):
|
| 362 |
search_input = gr.Textbox(placeholder="Konu girin...", show_label=False)
|
| 363 |
search_btn = gr.Button("Ara")
|
| 364 |
+
|
| 365 |
gr.Markdown("### 🇹🇷 Çeviri Aracı")
|
| 366 |
with gr.Group():
|
| 367 |
news_selector = gr.Dropdown(label="Haber Seçin", choices=[], type="value", interactive=True)
|
| 368 |
translate_btn = gr.Button("Türkçeye Çevir")
|
| 369 |
tr_title_out = gr.Textbox(label="Başlık (TR)", lines=2)
|
| 370 |
tr_summary_out = gr.Textbox(label="Özet (TR)", lines=4)
|
| 371 |
+
|
| 372 |
+
# --- SAĞ SÜTUN (Haber Akışı) ---
|
| 373 |
with gr.Column(scale=2):
|
| 374 |
gr.Markdown("### 📰 Canlı Haber Akışı")
|
| 375 |
news_feed_html = gr.HTML(label="Haberler", value="<div style='padding:20px; color:#666;'>Veriler yüklenince burada görünecek...</div>")
|
| 376 |
|
| 377 |
+
# --- ETKİLEŞİMLER (EVENTS) ---
|
| 378 |
init_btn.click(initialize_models, outputs=load_status)
|
| 379 |
+
|
| 380 |
+
refresh_btn.click(
|
| 381 |
+
fetch_news_wrapper,
|
| 382 |
+
outputs=[data_status, news_feed_html, news_selector, overview_plot]
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
coin_btn.click(analyze_coin, inputs=coin_input, outputs=[coin_plot, news_feed_html])
|
| 386 |
search_btn.click(search_news, inputs=search_input, outputs=news_feed_html)
|
| 387 |
+
|
| 388 |
+
translate_btn.click(
|
| 389 |
+
perform_translation,
|
| 390 |
+
inputs=news_selector,
|
| 391 |
+
outputs=[tr_title_out, tr_summary_out]
|
| 392 |
+
)
|
| 393 |
|
| 394 |
if __name__ == "__main__":
|
| 395 |
app.launch()
|