import io import os import subprocess import time from datetime import datetime import pandas as pd import requests import streamlit as st try: import psutil except ImportError: psutil = None st.set_page_config( page_title="Sentinel — İçerik Moderasyon", layout="wide", initial_sidebar_state="expanded", ) st.markdown( """ """, unsafe_allow_html=True, ) API_URL = os.getenv("SENTINEL_API_URL", "https://moztrk-sentinel-api.hf.space/analyze") VERDICT_COLORS = { "CRITICAL": "#e03030", "HIGH": "#e07020", "MEDIUM": "#d4a017", "LOW": "#3a7bd4", "NONE": "#2ea84a", } VERDICT_ICONS = {"CRITICAL": "🚨", "HIGH": "🤬", "MEDIUM": "◆", "LOW": "▲", "NONE": "✓"} if "last_latency_ms" not in st.session_state: st.session_state["last_latency_ms"] = None if "last_metrics" not in st.session_state: st.session_state["last_metrics"] = None def get_gpu_info(): try: result = subprocess.check_output( [ "nvidia-smi", "--query-gpu=name,utilization.gpu,temperature.gpu,memory.used,memory.total", "--format=csv,noheader,nounits", ], encoding="utf-8", stderr=subprocess.STDOUT, ) line = result.strip().splitlines()[0] name, util, temp, mem_used, mem_total = [p.strip() for p in line.split(",", maxsplit=4)] return { "name": name, "load": int(float(util)), "temp": int(float(temp)), "vram_used": int(float(mem_used)), "vram_total": int(float(mem_total)), } except Exception: return None def capture_process_metrics(): gpu_data = get_gpu_info() cpu_val = 0.0 ram_pct = 0.0 if psutil is not None: cpu_val = psutil.cpu_percent(interval=0.1) ram_pct = psutil.virtual_memory().percent return { "cpu": round(cpu_val, 1), "ram_pct": round(ram_pct, 1), "vram_used": str(gpu_data["vram_used"]) if gpu_data else "0", "gpu_load": str(gpu_data["load"]) if gpu_data else "0", "timestamp": time.strftime("%H:%M:%S"), } def resolve_api_endpoints(api_url_raw: str): base = (api_url_raw or "").strip().rstrip("/") if base.endswith("/analyze"): root = base[: -len("/analyze")] elif base.endswith("/analyze-batch"): root = base[: -len("/analyze-batch")] else: root = base analyze_url = f"{root}/analyze" batch_url = f"{root}/analyze-batch" return analyze_url, batch_url def verdict_css_class(decision): d = decision.upper() if "TEMIZ" in d or "CLEAR" in d: return "TEMIZ" if "HAKARET" in d or "INSULT" in d: return "SALDIRGAN" if "NEFRET" in d or "IDENTITY" in d: return "NEFRET" if "KÜFÜR" in d or "KUFUR" in d or "PROFANITY" in d: return "KUFUR" if "SALDIRGAN" in d or "TOXIC" in d: return "SALDIRGAN" if "İNCELEME" in d or "INCELEME" in d or "REVIEW" in d: return "INCELEME" if "SPAM" in d or "GİBBERİSH" in d: return "SPAM" return "TEMIZ" def risk_color(val): if val > 0.7: return "#e03030" if val > 0.4: return "#d4a017" if val > 0.15: return "#f0a020" return "#2ea84a" def score_bar(label, value, color="#1a6cf7"): pct = min(max(value * 100, 0), 100) return f"""
{label}%{pct:.1f}
""" def badge_html(risk): cls = { "CRITICAL": "badge-CRITICAL", "HIGH": "badge-HIGH", "MEDIUM": "badge-MEDIUM", "LOW": "badge-LOW", "NONE": "badge-NONE", }.get(risk.upper(), "badge-NONE") return f'{risk}' def inline_bar_html(value, color): w = min(max(value * 60, 0), 60) return f'%{value * 100:.0f}' def generate_docx_report(res_df, total_time, platform_dil): try: from docx import Document from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.oxml import OxmlElement from docx.oxml.ns import qn from docx.shared import Cm, Pt, RGBColor except ImportError: return None doc = Document() for section in doc.sections: section.top_margin = Cm(1.8) section.bottom_margin = Cm(1.8) section.left_margin = Cm(2.0) section.right_margin = Cm(2.0) def set_cell_bg(cell, hex_color): tc = cell._tc tc_pr = tc.get_or_add_tcPr() shd = OxmlElement("w:shd") shd.set(qn("w:val"), "clear") shd.set(qn("w:color"), "auto") shd.set(qn("w:fill"), hex_color) tc_pr.append(shd) def add_run(para, text, bold=False, size=10, color="000000", italic=False): run = para.add_run(text) run.bold = bold run.italic = italic run.font.size = Pt(size) run.font.color.rgb = RGBColor(int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16)) return run title_para = doc.add_paragraph() title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER add_run(title_para, "SENTINEL AI - Moderasyon Analiz Raporu", bold=True, size=18, color="1F4E79") sub_para = doc.add_paragraph() sub_para.alignment = WD_ALIGN_PARAGRAPH.CENTER ts = datetime.now().strftime("%d.%m.%Y %H:%M") add_run( sub_para, f"Platform: {platform_dil.upper()} | Olusturulma: {ts} | {len(res_df)} kayit | {total_time:.1f}s", size=9, color="888888", ) doc.add_paragraph() counts = res_df["Karar"].value_counts() sum_para = doc.add_paragraph() add_run(sum_para, "OZET", bold=True, size=11, color="1F4E79") sum_tbl = doc.add_table(rows=1, cols=len(counts) + 1) sum_tbl.style = "Table Grid" hdr = sum_tbl.rows[0].cells set_cell_bg(hdr[0], "1F4E79") p = hdr[0].paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.CENTER add_run(p, "Metrik", bold=True, size=9, color="FFFFFF") karar_colors = { "TEMIZ": "2EA84A", "KÜFÜR": "D4A017", "KUFUR": "D4A017", "PROFANITY": "D4A017", "SALDIRGAN": "D4A017", "TOXIC": "D4A017", "NEFRET": "E07020", "INCELEME": "3A7BD4", "SPAM": "8030D4", "GIBBERISH": "8030D4", } for i, (karar, cnt) in enumerate(counts.items()): cell = hdr[i + 1] set_cell_bg(cell, "0D1220") p2 = cell.paragraphs[0] p2.alignment = WD_ALIGN_PARAGRAPH.CENTER c = next((v for k, v in karar_colors.items() if k in karar.upper()), "888888") add_run(p2, f"{cnt}", bold=True, size=14, color=c) p3 = cell.add_paragraph() p3.alignment = WD_ALIGN_PARAGRAPH.CENTER add_run(p3, karar[:16], size=7, color="888888") doc.add_paragraph() detail_para = doc.add_paragraph() add_run(detail_para, "DETAYLI ANALIZ SONUCLARI", bold=True, size=11, color="1F4E79") cols = ["#", "Metin", "Normalize", "Karar", "Risk", "Saldirganlik", "Nefret", "Tehdit", "Hits"] tbl = doc.add_table(rows=1, cols=len(cols)) tbl.style = "Table Grid" for i, col_name in enumerate(cols): cell = tbl.rows[0].cells[i] set_cell_bg(cell, "1F4E79") p = cell.paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.CENTER add_run(p, col_name, bold=True, size=8, color="FFFFFF") for idx, row in res_df.iterrows(): tr = tbl.add_row() cells = tr.cells risk_str = str(row.get("Risk", "")).upper() row_colors = { "CRITICAL": "1F0C0C", "HIGH": "1A0E03", "MEDIUM": "141002", "LOW": "07091A", "NONE": "050F07", } row_fill = row_colors.get(risk_str, "0D1220") set_cell_bg(cells[0], row_fill) p = cells[0].paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.CENTER add_run(p, str(idx + 1), size=8, color="4A6080") set_cell_bg(cells[1], row_fill) p = cells[1].paragraphs[0] add_run(p, str(row.get("Metin", ""))[:120], size=8, color="C9D1E0") set_cell_bg(cells[2], row_fill) p = cells[2].paragraphs[0] add_run(p, str(row.get("Normalize", ""))[:60], size=7, color="6A8CB0", italic=True) set_cell_bg(cells[3], row_fill) p = cells[3].paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.CENTER karar = str(row.get("Karar", "")) c = next((v for k, v in karar_colors.items() if k in karar.upper()), "888888") add_run(p, karar[:20], bold=True, size=8, color=c) set_cell_bg(cells[4], row_fill) p = cells[4].paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.CENTER risk_colors = { "CRITICAL": "E03030", "HIGH": "E07020", "MEDIUM": "D4A017", "LOW": "3A7BD4", "NONE": "2EA84A", } rc = risk_colors.get(risk_str, "888888") add_run(p, risk_str, bold=True, size=8, color=rc) for col_i, field in [(5, "Saldırganlık"), (6, "Nefret"), (7, "Tehdit")]: set_cell_bg(cells[col_i], row_fill) p = cells[col_i].paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.CENTER score = float(row.get(field, 0.0)) add_run(p, f"%{score * 100:.1f}", size=8, color=risk_color(score).replace("#", "")) set_cell_bg(cells[8], row_fill) p = cells[8].paragraphs[0] hits = str(row.get("Hits", "")).strip("[]'\"") add_run(p, hits if hits else "-", size=7, color="E05050" if hits else "2A3D55") widths_cm = [0.7, 4.5, 3.0, 2.8, 1.5, 1.4, 1.4, 1.4, 2.0] for i, w in enumerate(widths_cm): for row in tbl.rows: row.cells[i].width = Cm(w) doc.add_paragraph() inceleme = res_df[res_df["Karar"].str.contains("İNCELEME|INCELEME|REVIEW", na=False)] if len(inceleme): q_para = doc.add_paragraph() add_run(q_para, f"INCELEME KUYRUGU - {len(inceleme)} Icerik", bold=True, size=11, color="3A7BD4") for _, row in inceleme.iterrows(): q_tbl = doc.add_table(rows=1, cols=1) q_tbl.style = "Table Grid" cell = q_tbl.rows[0].cells[0] set_cell_bg(cell, "060A13") p = cell.paragraphs[0] add_run(p, str(row.get("Metin", ""))[:200], size=9, color="C9D1E0") p2 = cell.add_paragraph() add_run( p2, f"Risk: {row.get('Risk', '')} | Saldirganlik: %{float(row.get('Saldırganlık', 0)) * 100:.0f} | {row.get('Gerekçe', '')}", size=8, color="4A6080", italic=True, ) doc.add_paragraph() footer_p = doc.add_paragraph() footer_p.alignment = WD_ALIGN_PARAGRAPH.CENTER add_run(footer_p, "Sentinel AI - Dahili Kullanim - " + datetime.now().strftime("%Y"), size=8, color="2A3D55") buf = io.BytesIO() doc.save(buf) buf.seek(0) return buf st.markdown( """
Sentinel
İçerik Moderasyon Sistemi
ONLINE
""", unsafe_allow_html=True, ) with st.sidebar: st.markdown( """
Sistem Konfigürasyonu
""", unsafe_allow_html=True, ) st.markdown( """
Platform Dili
""", unsafe_allow_html=True, ) platform_dil = st.radio( "Platform dili", ["tr", "en"], format_func=lambda x: "Türkçe · TR Pipeline" if x == "tr" else "English · EN Pipeline", label_visibility="collapsed", ) st.markdown("
", unsafe_allow_html=True) st.markdown( """
API Endpoint
""", unsafe_allow_html=True, ) api_url = st.text_input("API", value=API_URL, label_visibility="collapsed") st.markdown("

", unsafe_allow_html=True) st.markdown( """
TR PIPELINE
──────────────
is_spam() evrensel filtre
Küfür listesi lookup
BERTurk offensive 42K
Detoxify multilingual

EN PIPELINE
──────────────
is_spam() evrensel filtre
Gibberish Detector
Detoxify original 6-label
""", unsafe_allow_html=True, ) st.markdown("---") st.markdown("### 🖥️ Sistem Monitörü") if psutil is None: st.warning("psutil yüklü değil. Kurulum: pip install psutil") else: cpu_load = psutil.cpu_percent(interval=0.2) ram = psutil.virtual_memory() ram_used_gb = ram.used / (1024**3) col1, col2 = st.columns(2) col1.metric("CPU Yükü", f"%{cpu_load:.0f}") col2.metric("RAM", f"{ram_used_gb:.1f} GB", f"%{ram.percent:.0f}", delta_color="inverse") gpu = get_gpu_info() if gpu: st.markdown(f"**GPU:** {gpu['name']}") col3, col4 = st.columns(2) col3.metric("GPU Yükü", f"%{gpu['load']}") col4.metric("GPU Isı", f"{gpu['temp']}°C") vram_pct = 0.0 if gpu["vram_total"] > 0: vram_pct = min(max(gpu["vram_used"] / gpu["vram_total"], 0.0), 1.0) st.write(f"VRAM: {gpu['vram_used']}MB / {gpu['vram_total']}MB") st.progress(vram_pct) else: st.warning("GPU bilgisi alınamadı (nvidia-smi erişimi yok).") st.markdown("---") live_latency = st.session_state.get("last_latency_ms") if live_latency is None: st.info("🚀 **Model Latency:** N/A\n\n🛡️ **Sentinel v2.9 Active**") else: st.info(f"🚀 **Model Latency:** ~{live_latency:.0f}ms/req\n\n🛡️ **Sentinel v2.9 Active**") st.markdown("---") if st.session_state.get("last_metrics"): m = st.session_state["last_metrics"] st.markdown("### ⚡ Son İşlem Performansı") st.caption(f"Saat: {m['timestamp']} (İstek anındaki veriler)") col5, col6 = st.columns(2) col5.metric("İşlem CPU", f"%{m['cpu']}") col6.metric("İşlem RAM", f"%{m['ram_pct']}") col7, col8 = st.columns(2) col7.metric("GPU Yükü", f"%{m['gpu_load']}") col8.metric("VRAM", f"{m['vram_used']} MB") st.success("Analiz işlemi için performans verisi kaydedildi.") else: st.info("Performans verisi için analiz başlatın.") tab1, tab2 = st.tabs([" Tek Metin Analizi ", " Toplu Analiz "]) with tab1: st.markdown("
", unsafe_allow_html=True) user_input = st.text_area( "Analiz metni", height=120, placeholder="Analiz edilecek metni buraya yazın...", label_visibility="collapsed", ) col_btn, col_info = st.columns([2, 5]) with col_btn: analyze_btn = st.button("Analiz Et", use_container_width=True) with col_info: st.markdown( """
Spam → Dil → Küfür → Model → Karar
""", unsafe_allow_html=True, ) if analyze_btn: if not user_input.strip(): st.warning("Analiz için metin gerekli.") else: with st.spinner(""): try: t0 = time.time() analyze_url, _ = resolve_api_endpoints(api_url) resp = requests.post(analyze_url, json={"text": user_input, "platform_dil": platform_dil}, timeout=30) st.session_state["last_metrics"] = capture_process_metrics() elapsed = (time.time() - t0) * 1000 except requests.RequestException as e: st.error(f"API bağlantı hatası: {e}") st.stop() if resp.status_code != 200: st.error(f"API {resp.status_code} döndü.") st.stop() r = resp.json() decision = r.get("decision", "—") reason = r.get("reason", "—") risk = r.get("risk_level", "None") risk_u = str(risk).upper() lang = r.get("language", platform_dil).upper() cleaned = r.get("cleaned_text", "") details = r.get("details", {}) latency = r.get("latency_ms", round(elapsed, 1)) st.session_state["last_latency_ms"] = float(latency) backend_perf = r.get("performance") if isinstance(backend_perf, dict): st.session_state["last_metrics"] = { "cpu": backend_perf.get("cpu", 0), "ram_pct": backend_perf.get("ram_pct", 0), "vram_used": str(backend_perf.get("vram_used", 0)), "gpu_load": str(backend_perf.get("gpu_load", 0)), "timestamp": backend_perf.get("timestamp", time.strftime("%H:%M:%S")), } vcls = verdict_css_class(decision) vcolor = VERDICT_COLORS.get(risk_u, "#2ea84a") vicon = VERDICT_ICONS.get(risk_u, "✓") st.markdown( f"""
{vicon}  {decision} [{lang}]
{reason}
""", unsafe_allow_html=True, ) lat_class = "low" if latency < 200 else ("med" if latency < 500 else "high") risk_class = { "CRITICAL": "high", "HIGH": "high", "MEDIUM": "med", "LOW": "med", "NONE": "low", }.get(risk_u, "low") st.markdown( f"""
Risk Seviyesi
{risk}
Gecikme
{latency:.0f} ms
Pipeline
{lang}
Normalize Edilen Metin
{cleaned}
""", unsafe_allow_html=True, ) hits = details.get("hits", []) or [] insult_hits = details.get("insult_hits", []) or [] if hits or insult_hits: tags = "".join(f'⚡ {h}' for h in hits) tags += "".join( f'⚠ {h}' for h in insult_hits ) st.markdown( f"""
Kara Liste Eşleşmeleri
{tags}
""", unsafe_allow_html=True, ) col_scores, col_models = st.columns([1, 1.2]) with col_scores: st.markdown( """
Sinyal Analizi
""", unsafe_allow_html=True, ) bars = "" if lang == "TR": off = details.get("off_score", 0.0) ia = details.get("detox", {}).get("identity_attack", 0.0) thr = details.get("threat", 0.0) bars += score_bar("Saldırganlık", off, risk_color(off)) bars += score_bar("Nefret (identity_attack)", ia, risk_color(ia)) bars += score_bar("Tehdit", thr, risk_color(thr)) else: dtx = details.get("detox", {}) for key, lbl in [ ("toxicity", "Toxicity"), ("threat", "Threat"), ("insult", "Insult"), ("identity_attack", "Identity Attack"), ("severe_toxicity", "Severe Toxicity"), ("obscene", "Obscene"), ]: v = dtx.get(key, 0.0) bars += score_bar(lbl, v, risk_color(v)) st.markdown(bars, unsafe_allow_html=True) with col_models: st.markdown( """
Model Kaynak Analizi (Source)
""", unsafe_allow_html=True, ) rows_html = "" if lang == "TR": m_list = [ ("BERTurk Offensive", "N/A", details.get("off_score", 0.0)), ("Detoxify (TR)", "Analyzed", details.get("detox", {}).get("toxicity", 0.0)), ] else: m_list = [ ("Detoxify (Original)", "Analyzed", details.get("detox", {}).get("toxicity", 0.0)), ( "Gibberish Detector", details.get("gibberish_label", "N/A"), details.get("gibberish_score", 0.0) or 0.0, ), ] for m_name, m_dec, m_score in m_list: try: m_score = float(m_score) except (TypeError, ValueError): m_score = 0.0 c = risk_color(m_score) rows_html += f"""
{m_name} {m_dec} (%{m_score * 100:.1f})
""" st.markdown(rows_html, unsafe_allow_html=True) with tab2: st.markdown("
", unsafe_allow_html=True) st.markdown( """
Veri Seti Yükle
""", unsafe_allow_html=True, ) uploaded = st.file_uploader("Dosya", type=["csv", "xlsx"], label_visibility="collapsed") if uploaded: df = pd.read_csv(uploaded) if uploaded.name.endswith(".csv") else pd.read_excel(uploaded) if len(df) == 0: st.warning("Dosya boş.") st.stop() st.markdown( f"""
{len(df)} satır yüklendi
""", unsafe_allow_html=True, ) col_name = st.selectbox("Analiz sütunu:", df.columns) if st.button("Toplu Analizi Başlat", use_container_width=False): progress = st.progress(0) status_text = st.empty() results = [] t0 = time.time() texts_payload = [str(text) for text in df[col_name]] _, batch_url = resolve_api_endpoints(api_url) progress.progress(0.2) status_text.markdown( f"""Batch isteği gönderiliyor... ({len(texts_payload)} satır)""", unsafe_allow_html=True, ) try: resp = requests.post( batch_url, json={"texts": texts_payload, "platform_dil": platform_dil, "batch_size": 16}, timeout=300, ) payload = resp.json() if resp.status_code == 200 else {} except requests.RequestException as exc: st.error(f"Batch API bağlantı hatası: {exc}") st.stop() if resp.status_code != 200: st.error(f"Batch API {resp.status_code} döndü.") st.stop() items = payload.get("results", []) if isinstance(payload, dict) else [] if len(items) != len(texts_payload): st.warning(f"Batch sonuç sayısı beklenenden farklı: {len(items)} / {len(texts_payload)}") for text, r in zip(texts_payload, items): details = r.get("details", {}) hits_all = list(details.get("hits", []) or []) + list(details.get("insult_hits", []) or []) results.append( { "Metin": text, "Normalize": r.get("cleaned_text", ""), "Dil": r.get("language", "—").upper(), "Karar": r.get("decision", "—"), "Risk": r.get("risk_level", "—"), "Gerekçe": r.get("reason", "—"), "Saldırganlık": round(float(details.get("off_score", 0.0)), 4), "Nefret": round(float(details.get("detox", {}).get("identity_attack", 0.0)), 4), "Tehdit": round(float(details.get("threat", details.get("detox", {}).get("threat", 0.0))), 4), "Hits": ", ".join(hits_all) if hits_all else "", } ) progress.progress(1.0) status_text.markdown( f"""{len(results)} / {len(df)} işlendi""", unsafe_allow_html=True, ) elapsed = time.time() - t0 res_df = pd.DataFrame(results) if len(df) > 0: st.session_state["last_latency_ms"] = (elapsed * 1000.0) / len(df) status_text.empty() progress.empty() st.markdown( f"""
{len(df)} satır {elapsed:.1f}s içinde analiz edildi
""", unsafe_allow_html=True, ) counts = res_df["Karar"].value_counts() karar_colors_ui = { "TEMIZ": "#2ea84a", "CLEAR": "#2ea84a", "KÜFÜR": "#d4a017", "KUFUR": "#d4a017", "PROFANITY": "#d4a017", "SALDIRGAN": "#d4a017", "TOXIC": "#d4a017", "NEFRET": "#e07020", "IDENTITY": "#e07020", "İNCELEME": "#3a7bd4", "INCELEME": "#3a7bd4", "REVIEW": "#3a7bd4", "SPAM": "#8030d4", "GİBBERİSH": "#8030d4", } cols_summary = st.columns(min(len(counts), 6)) for i, (karar, cnt) in enumerate(counts.items()): if i < 6: vc = next((v for k, v in karar_colors_ui.items() if k in karar.upper()), "#888888") with cols_summary[i]: st.markdown( f"""
{cnt}
{karar[:18]}
""", unsafe_allow_html=True, ) st.markdown("
", unsafe_allow_html=True) st.markdown( """
Detaylı Analiz Tablosu
""", unsafe_allow_html=True, ) table_rows = "" for idx, row in res_df.iterrows(): risk_str = str(row.get("Risk", "")).upper() row_bg = { "CRITICAL": "#1f0c0c", "HIGH": "#1a0e03", "MEDIUM": "#141002", "LOW": "#07091a", "NONE": "#050f07", }.get(risk_str, "#0d1220") karar_str = str(row.get("Karar", "")) kc = next((v for k, v in karar_colors_ui.items() if k in karar_str.upper()), "#888888") sal = float(row.get("Saldırganlık", 0.0)) nef = float(row.get("Nefret", 0.0)) thr = float(row.get("Tehdit", 0.0)) hits_str = str(row.get("Hits", "")).strip() hits_html = "" if hits_str: for h in hits_str.split(","): h = h.strip() if h: hits_html += f'{h}' else: hits_html = '' metin_full = str(row.get("Metin", "")) metin_short = metin_full[:60] + "..." if len(metin_full) > 60 else metin_full normalize = str(row.get("Normalize", ""))[:50] table_rows += f""" {idx + 1} {metin_short} {normalize} {karar_str[:22]} {badge_html(risk_str)} {inline_bar_html(sal, risk_color(sal))} {inline_bar_html(nef, risk_color(nef))} {inline_bar_html(thr, risk_color(thr))} {hits_html} {str(row.get("Gerekçe", ""))[:60]} """ st.markdown( f"""
{table_rows}
#MetinNormalizeKarar RiskSaldırganlıkNefretTehdit HitsGerekçe
""", unsafe_allow_html=True, ) st.markdown("
", unsafe_allow_html=True) col_chart, col_stats = st.columns([1, 1]) with col_chart: st.markdown( """
Dağılım
""", unsafe_allow_html=True, ) st.bar_chart(counts) with col_stats: st.markdown( """
İstatistikler
""", unsafe_allow_html=True, ) total = len(res_df) zararli = total - len(res_df[res_df["Karar"].str.contains("TEMİZ|CLEAR", na=False)]) st.markdown( f"""
Toplam kayıt {total}
Zararlı içerik {zararli} (%{zararli / total * 100:.1f})
Ortalama süre {elapsed / total * 1000:.0f}ms / satır
Hits bulundu {len(res_df[res_df['Hits'].str.len() > 0])} kayıt
İnceleme kuyruğu {len(res_df[res_df['Karar'].str.contains('İNCELEME|INCELEME', na=False)])} içerik
""", unsafe_allow_html=True, ) st.markdown("
", unsafe_allow_html=True) inceleme = res_df[res_df["Karar"].str.contains("İNCELEME|INCELEME|REVIEW", na=False)] if len(inceleme): st.markdown( f"""
İnceleme Kuyruğu — {len(inceleme)} İçerik
""", unsafe_allow_html=True, ) for i, (_, row) in enumerate(inceleme.iterrows()): sal = float(row.get("Saldırganlık", 0.0)) st.markdown( f"""
{i + 1:02d}
{str(row.get('Metin', ''))}
Risk: {row.get('Risk', '')}  |  Saldırganlık: %{sal * 100:.0f}  |  {row.get('Gerekçe', '')}
""", unsafe_allow_html=True, ) st.markdown("
", unsafe_allow_html=True) st.markdown( """
Raporu İndir
""", unsafe_allow_html=True, ) col_dl1, col_dl2, _ = st.columns([1, 1, 4]) with col_dl1: csv_bytes = res_df.to_csv(index=False).encode("utf-8") st.download_button( "⬇ CSV", data=csv_bytes, file_name=f"sentinel_raporu_{datetime.now().strftime('%Y%m%d_%H%M')}.csv", mime="text/csv", use_container_width=True, ) with col_dl2: docx_buf = generate_docx_report(res_df, elapsed, platform_dil) if docx_buf: st.download_button( "⬇ DOCX", data=docx_buf, file_name=f"sentinel_raporu_{datetime.now().strftime('%Y%m%d_%H%M')}.docx", mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document", use_container_width=True, ) else: st.warning("python-docx yüklü değil: pip install python-docx")