import os # Hugging Face token uyarısını kapat os.environ["HF_HUB_DISABLE_IMPLICIT_TOKEN"] = "1" import gradio as gr import feedparser import pandas as pd import numpy as np import faiss import matplotlib.pyplot as plt import re import html import requests from sentence_transformers import SentenceTransformer from transformers import AutoTokenizer, AutoModelForSequenceClassification, TextClassificationPipeline, pipeline import warnings warnings.filterwarnings('ignore') # --- Global Değişkenler --- model = None sentiment_analyzer = None translator = None df = None index = None embeddings = None # --- Modeller --- CRYPTOBERT_NAME = "ElKulako/cryptobert" TRANSLATOR_NAME = "Helsinki-NLP/opus-mt-tc-big-en-tr" # --- Ayarlar --- NEUTRAL_CONFIDENCE_FLOOR = 0.55 # --- Sinyal Kelimeleri --- 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" } 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" } # --- Çeviri Haritası (Görünüm İçin) --- LABEL_MAP = { "bullish": "OLUMLU", "bearish": "OLUMSUZ", "neutral": "NÖTR" } # --- TEMİZLEME VE YARDIMCI FONKSİYONLAR --- def clean_html_tags(text): if not text: return "" text = html.unescape(text) text = re.sub(r'.*?', '', text, flags=re.DOTALL | re.IGNORECASE) text = re.sub(r'.*?', '', text, flags=re.DOTALL | re.IGNORECASE) text = re.sub(r'', '', text, flags=re.DOTALL) text = re.sub(r'<[^>]+>', ' ', text) text = re.sub(r'\bstyle="[^"]+"', '', text) text = re.sub(r'\bclass="[^"]+"', '', text) text = re.sub(r'\s+', ' ', text).strip() return text def initialize_models(): global model, sentiment_analyzer, translator status_msg = [] try: if model is None: model = SentenceTransformer('all-MiniLM-L6-v2') status_msg.append("✅ Embedding Modeli Hazır") except Exception as e: status_msg.append(f"❌ Embedding: {str(e)}") try: if sentiment_analyzer is None: tokenizer = AutoTokenizer.from_pretrained(CRYPTOBERT_NAME, use_fast=True) clf_model = AutoModelForSequenceClassification.from_pretrained(CRYPTOBERT_NAME) sentiment_analyzer = TextClassificationPipeline(model=clf_model, tokenizer=tokenizer, device=-1, max_length=128, truncation=True, padding="max_length") status_msg.append("✅ Sentiment Modeli Hazır") except Exception as e: status_msg.append(f"❌ Sentiment: {str(e)}") try: if translator is None: translator = pipeline("translation", model=TRANSLATOR_NAME, device=-1) status_msg.append("✅ Çeviri Modeli Hazır") except Exception as e: status_msg.append(f"❌ Çeviri: {str(e)}") return " | ".join(status_msg) def _normalize_label(label: str) -> str: l = (label or "").strip().lower() if l in {"label_2"} or "bull" in l: return "bullish" if l in {"label_0"} or "bear" in l: return "bearish" return "neutral" def _cue_score(text: str) -> int: t = (text or "").lower() pos = sum(1 for w in POSITIVE_CUES if w in t) neg = sum(1 for w in NEGATIVE_CUES if w in t) if re.search(r"is .* over\??", t): neg += 1 return pos - neg def _apply_post_rules(label: str, score: float, text: str) -> tuple[str, float]: cue = _cue_score(text) if label == "neutral": if cue <= -2: return "bearish", max(0.60, score) if cue >= 2: return "bullish", max(0.60, score) if label == "neutral" and score < NEUTRAL_CONFIDENCE_FLOOR: if cue > 0: return "bullish", min(0.60, score + 0.10) if cue < 0: return "bearish", min(0.60, score + 0.10) return label, score if label == "bullish" and cue < -1 and score < 0.70: return "neutral", score * 0.9 if label == "bearish" and cue > 1 and score < 0.70: return "neutral", score * 0.9 return label, score def fetch_feed_data(url): headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36", "Accept": "application/rss+xml, text/xml" } try: response = requests.get(url, headers=headers, timeout=10) return feedparser.parse(response.content) if response.status_code == 200 else None except: return None # --- HTML FORMATLAMA FONKSİYONU --- def format_news_as_html(dataframe): if dataframe is None or len(dataframe) == 0: return "
Haber yok. Lütfen 'Verileri Yenile' butonuna basın.
" html_content = "
" for _, row in dataframe.iterrows(): sentiment = row['sentiment_label'] score = row['sentiment_score'] # Skor Formatı: 0.98 -> 98 confidence_percent = int(score * 100) # Türkçe Etiket tr_label = LABEL_MAP.get(sentiment, "NÖTR") # Renk sınıfları color_class = "neutral-card" icon = "➖" if sentiment == "bullish": color_class = "bullish-card" icon = "🚀" elif sentiment == "bearish": color_class = "bearish-card" icon = "🔻" html_content += f"""
{icon} {tr_label} (%{confidence_percent}) {row['published'][:16]}

{row['title']}

{row['summary'][:160]}...

""" html_content += "
" return html_content def fetch_news_wrapper(): global df, index, embeddings, sentiment_analyzer, model if sentiment_analyzer is None: return "⚠️ Önce Modelleri Yükleyin!", "", gr.update(choices=[]), None RSS_URLS = [ "https://cointelegraph.com/rss", "https://cryptonews.com/news/feed", "https://www.coindesk.com/arc/outboundfeeds/rss/", "https://tr.investing.com/rss/302.rss" ] all_entries = [] for url in RSS_URLS: feed = fetch_feed_data(url) if feed and feed.entries: for entry in feed.entries[:20]: clean_title = clean_html_tags(entry.get("title", "")) if clean_title: all_entries.append({ "title": clean_title, "link": entry.get("link", ""), "summary": clean_html_tags(entry.get("summary", "")), "published": entry.get("published", "") }) if not all_entries: return "❌ Haber alınamadı.", "", gr.update(choices=[]), None df = pd.DataFrame(all_entries).drop_duplicates(subset="title").reset_index(drop=True) # Sentiment Analizi def analyze_row(row): text = f"{row['title']}. {row['summary']}"[:1000] try: out = sentiment_analyzer(text)[0] lbl, scr = _apply_post_rules(_normalize_label(out.get("label")), float(out.get("score")), text) return lbl, scr except: return "neutral", 0.0 df["sentiment_label"], df["sentiment_score"] = zip(*df.apply(analyze_row, axis=1)) # Faiss corpus = df['title'].tolist() embeddings = model.encode(corpus, show_progress_bar=False) index = faiss.IndexFlatL2(embeddings.shape[1]) index.add(embeddings.astype('float32')) # UI Çıktıları status_text = f"✅ {len(df)} Haber Analiz Edildi" html_feed = format_news_as_html(df) choices = [(f"{i}. {t[:40]}...", i) for i, t in enumerate(df["title"])] # Grafik oluştur (Türkçe Etiketli) sentiment_counts = df["sentiment_label"].value_counts() fig, ax = plt.subplots(figsize=(6, 3)) colors = {'bullish': '#4CAF50', 'bearish': '#F44336', 'neutral': '#FFC107'} # Etiketleri Türkçeye çevir tr_labels = [LABEL_MAP.get(x, x) for x in sentiment_counts.index] bar_colors = [colors.get(x, '#333') for x in sentiment_counts.index] ax.bar(tr_labels, sentiment_counts.values, color=bar_colors) ax.set_title("Piyasa Duygu Durumu") plt.tight_layout() return status_text, html_feed, gr.update(choices=choices, value=None), fig # --- Çeviri ve Arama Fonksiyonları --- def perform_translation(news_index): global df if df is None or news_index is None: return "Lütfen bir haber seçin.", "..." try: idx = int(news_index) row = df.iloc[idx] title_tr = translate_text_en_to_tr(row['title']) summary_tr = translate_text_en_to_tr(row['summary']) return title_tr, summary_tr except Exception as e: return f"Hata: {e}", "..." def translate_text_en_to_tr(text): global translator if not text: return "" try: return translator(text[:512])[0]['translation_text'] except: return "Çeviri hatası" def search_news(query): global df, index, model if df is None: return "Veri yok." try: q_vec = model.encode([query]) D, I = index.search(q_vec.astype('float32'), 5) results = df.iloc[I[0]] return format_news_as_html(results) except: return "Arama hatası." def analyze_coin(coin_name): global df if df is None: return None, "Veri yok." filtered = df[df["title"].str.contains(coin_name, case=False, na=False)] if len(filtered) == 0: return None, f"{coin_name} hakkında haber yok." counts = filtered["sentiment_label"].value_counts() # Türkçe etiketler ve renkler labels_tr = [LABEL_MAP.get(x, x) for x in counts.index] colors = ['#4CAF50' if x=='bullish' else '#F44336' if x=='bearish' else '#FFC107' for x in counts.index] fig, ax = plt.subplots(figsize=(5, 5)) ax.pie(counts.values, labels=labels_tr, autopct='%1.1f%%', colors=colors) ax.set_title(f"{coin_name.upper()} Analizi") return fig, format_news_as_html(filtered) # --- CSS STİLLERİ --- custom_css = """ /* Genel Kart Yapısı */ .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); } /* Yazı renklerini zorla koyu yap */ .news-card h3 a { text-decoration: none; color: #222222 !important; font-weight: 700; } .news-card p { color: #444444 !important; font-size: 0.95em; line-height: 1.5; } .card-header { display: flex; justify-content: space-between; margin-bottom: 8px; } .date { font-size: 0.8em; color: #888888 !important; } /* Renk Çizgileri */ .bullish-card { border-left: 6px solid #4CAF50; } .bearish-card { border-left: 6px solid #F44336; } .neutral-card { border-left: 6px solid #FFC107; } /* Badge Tasarımı */ .badge { padding: 3px 8px; border-radius: 4px; font-size: 0.75em; font-weight: bold; color: white; } .bullish { background-color: #4CAF50; } .bearish { background-color: #F44336; } .neutral { background-color: #FFC107; color: #333; } .news-feed-container { max-height: 800px; overflow-y: auto; padding-right: 10px; } """ # --- UI TASARIMI --- with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue"), css=custom_css, title="Crypto News AI") as app: # Üst Bar (Header) with gr.Row(elem_id="header"): with gr.Column(scale=3): gr.Markdown("# ⚡ AI Crypto Sentiment Dashboard") with gr.Column(scale=1): init_btn = gr.Button("🚀 1. Modelleri Başlat", variant="primary", size="sm") load_status = gr.Textbox(show_label=False, placeholder="Model Durumu...", lines=1) gr.Markdown("---") # Ana İçerik with gr.Row(): # --- SOL SÜTUN (Kontrol & Analiz) --- with gr.Column(scale=1, min_width=300): with gr.Group(): gr.Markdown("### 📡 Veri Kontrol") refresh_btn = gr.Button("🔄 2. Verileri Yenile", variant="secondary") data_status = gr.Textbox(show_label=False, placeholder="Veri bekleniyor...", lines=1) overview_plot = gr.Plot(label="Piyasa Özeti") gr.Markdown("### 🔍 Hızlı Filtre") with gr.Tabs(): with gr.Tab("Coin Ara"): coin_input = gr.Textbox(placeholder="BTC, ETH, SOL...", show_label=False) coin_btn = gr.Button("Analiz Et") coin_plot = gr.Plot(show_label=False) with gr.Tab("Haber Ara"): search_input = gr.Textbox(placeholder="Konu girin...", show_label=False) search_btn = gr.Button("Ara") gr.Markdown("### 🇹🇷 Çeviri Aracı") with gr.Group(): news_selector = gr.Dropdown(label="Haber Seçin", choices=[], type="value", interactive=True) translate_btn = gr.Button("Türkçeye Çevir") tr_title_out = gr.Textbox(label="Başlık (TR)", lines=2) tr_summary_out = gr.Textbox(label="Özet (TR)", lines=4) # --- SAĞ SÜTUN (Haber Akışı) --- with gr.Column(scale=2): gr.Markdown("### 📰 Canlı Haber Akışı") news_feed_html = gr.HTML(label="Haberler", value="
Veriler yüklenince burada görünecek...
") # --- ETKİLEŞİMLER (EVENTS) --- init_btn.click(initialize_models, outputs=load_status) refresh_btn.click( fetch_news_wrapper, outputs=[data_status, news_feed_html, news_selector, overview_plot] ) coin_btn.click(analyze_coin, inputs=coin_input, outputs=[coin_plot, news_feed_html]) search_btn.click(search_news, inputs=search_input, outputs=news_feed_html) translate_btn.click( perform_translation, inputs=news_selector, outputs=[tr_title_out, tr_summary_out] ) if __name__ == "__main__": app.launch()