Mrttbn commited on
Commit
a9ac494
·
verified ·
1 Parent(s): 4730840

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +173 -121
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 (Aynı) ---
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
- # --- Yardımcı Fonksiyonlar ---
 
 
 
 
 
 
 
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
- # --- GPU İŞLEMLERİ ---
90
-
91
- # Modelleri Yükle (GPU Ayarlı)
92
- @spaces.GPU # Modelleri yüklerken de GPU context'i olsun
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
- try:
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
- return " | ".join(status_msg)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- initialize_models() # Eğer yüklenmediyse yükle
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[:25]: # GPU olduğu için sayıyı biraz artırabiliriz
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 Döngüsü
161
- sentiments = []
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
- sentiments.append(lbl)
171
- scores.append(scr)
172
- except:
173
- sentiments.append("neutral")
174
- scores.append(0.0)
175
 
176
- df["sentiment_label"] = sentiments
177
- df["sentiment_score"] = scores
178
 
179
- # Faiss & Embeddings
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
- status_text = f"✅ {len(df)} Haber Analiz Edildi (GPU Hızlandırmalı ⚡)"
 
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 (GPU Kesin Gerekli)
202
- @spaces.GPU
203
  def perform_translation(news_index):
204
- global df, translator
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
- # GPU'da çeviri çok hızlıdır
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
- # Arama (CPU yeterli ama GPU varsa kullansın)
223
- @spaces.GPU
 
 
 
 
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
- # HTML Formatlama (Görsel İşler)
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
- .news-card { background-color: #ffffff; border-radius: 10px; padding: 15px; margin-bottom: 15px; border: 1px solid #e0e0e0; box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
287
- .news-card h3 a { text-decoration: none; color: #222222 !important; font-weight: 700; }
288
- .news-card p { color: #444444 !important; font-size: 0.95em; line-height: 1.5; }
289
- .card-header { display: flex; justify-content: space-between; margin-bottom: 8px; }
290
- .date { font-size: 0.8em; color: #888888 !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- .badge { padding: 3px 8px; border-radius: 4px; font-size: 0.75em; font-weight: bold; color: white; }
 
 
 
 
 
 
 
 
295
  .bullish { background-color: #4CAF50; }
296
  .bearish { background-color: #F44336; }
297
  .neutral { background-color: #FFC107; color: #333; }
298
- .news-feed-container { max-height: 800px; overflow-y: auto; padding-right: 10px; }
 
 
 
 
 
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 (ZeroGPU)")
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
- refresh_btn.click(fetch_news_wrapper, outputs=[data_status, news_feed_html, news_selector, overview_plot])
 
 
 
 
 
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
- translate_btn.click(perform_translation, inputs=news_selector, outputs=[tr_title_out, tr_summary_out])
 
 
 
 
 
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()