Spaces:
Sleeping
Sleeping
| 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( | |
| """ | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap'); | |
| html, body, [class*="css"] { | |
| font-family: 'IBM Plex Sans', sans-serif; | |
| background-color: #0a0e17; | |
| color: #c9d1e0; | |
| } | |
| [data-testid="stSidebar"] { | |
| background: #0d1220; | |
| border-right: 1px solid #1e2d45; | |
| min-width: 300px !important; | |
| max-width: 300px !important; | |
| width: 300px !important; | |
| margin-left: 0 !important; | |
| transform: translateX(0) !important; | |
| flex-shrink: 0 !important; | |
| } | |
| [data-testid="stSidebar"][aria-expanded="false"] { | |
| min-width: 300px !important; | |
| max-width: 300px !important; | |
| width: 300px !important; | |
| margin-left: 0 !important; | |
| transform: translateX(0) !important; | |
| } | |
| [data-testid="stSidebar"][aria-expanded="true"] { | |
| min-width: 300px !important; | |
| max-width: 300px !important; | |
| width: 300px !important; | |
| } | |
| [data-testid="stSidebarContent"] { | |
| display: block !important; | |
| visibility: visible !important; | |
| opacity: 1 !important; | |
| } | |
| [data-testid="stSidebar"] * { color: #8a9bc0 !important; } | |
| [data-testid="stSidebar"] .stRadio label { color: #c9d1e0 !important; } | |
| [data-testid="collapsedControl"], | |
| [data-testid="stSidebarCollapseButton"], | |
| button[title="Close sidebar"], | |
| button[title="Open sidebar"] { display: none !important; } | |
| #MainMenu, footer, header { visibility: hidden; } | |
| .block-container { padding-top: 1.5rem; padding-bottom: 2rem; } | |
| .sentinel-header { | |
| display: flex; align-items: center; gap: 16px; | |
| padding: 20px 0 28px 0; | |
| border-bottom: 1px solid #1e2d45; | |
| margin-bottom: 28px; | |
| } | |
| .sentinel-logo { | |
| width: 44px; height: 44px; | |
| background: linear-gradient(135deg, #1a6cf7, #0d3d8e); | |
| border-radius: 10px; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 22px; | |
| } | |
| .sentinel-title { font-family:'IBM Plex Mono',monospace; font-size:22px; font-weight:600; color:#e8eef8; } | |
| .sentinel-sub { font-size:12px; color:#6f86ab; font-family:'IBM Plex Mono',monospace; letter-spacing:1px; text-transform:uppercase; } | |
| .status-pill { | |
| margin-left:auto; background:#0a1f0e; border:1px solid #1a5c28; | |
| color:#3ddc5f; font-family:'IBM Plex Mono',monospace; | |
| font-size:11px; padding:4px 12px; border-radius:20px; | |
| } | |
| .status-dot { display:inline-block; width:7px; height:7px; background:#3ddc5f; border-radius:50%; margin-right:6px; animation:pulse 2s infinite; } | |
| @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.3} } | |
| .verdict-card { border-radius:12px; padding:24px 28px; margin-bottom:20px; border:1px solid; position:relative; overflow:hidden; } | |
| .verdict-card::before { content:''; position:absolute; top:0; left:0; width:4px; height:100%; } | |
| .verdict-TEMIZ { background:#050f07; border-color:#1a4d25; } .verdict-TEMIZ::before { background:#2ea84a; } | |
| .verdict-KUFUR { background:#0f0c02; border-color:#4d3d08; } .verdict-KUFUR::before { background:#d4a017; } | |
| .verdict-SALDIRGAN{ background:#0f0c02; border-color:#4d3d08; } .verdict-SALDIRGAN::before{ background:#d4a017; } | |
| .verdict-TOXIC { background:#0f0c02; border-color:#4d3d08; } .verdict-TOXIC::before { background:#d4a017; } | |
| .verdict-NEFRET { background:#120a02; border-color:#5c2e0a; } .verdict-NEFRET::before { background:#e07020; } | |
| .verdict-INCELEME { background:#060a13; border-color:#1a2d5c; } .verdict-INCELEME::before { background:#3a7bd4; } | |
| .verdict-SPAM { background:#080810; border-color:#2a1a4d; } .verdict-SPAM::before { background:#8030d4; } | |
| .verdict-label { font-family:'IBM Plex Mono',monospace; font-size:26px; font-weight:600; margin-bottom:6px; } | |
| .verdict-reason { font-size:14px; color:#6a7f9a; font-family:'IBM Plex Mono',monospace; } | |
| .metric-row { display:flex; gap:12px; margin-bottom:20px; } | |
| .metric-card { flex:1; background:#0d1220; border:1px solid #1e2d45; border-radius:10px; padding:16px 20px; } | |
| .metric-label { font-family:'IBM Plex Mono',monospace; font-size:11px; color:#7690b8; text-transform:uppercase; letter-spacing:1px; margin-bottom:8px; } | |
| .metric-value { font-family:'IBM Plex Mono',monospace; font-size:24px; font-weight:600; color:#e8eef8; } | |
| .metric-value.low{color:#2ea84a} .metric-value.med{color:#d4a017} .metric-value.high{color:#e03030} | |
| .score-row { margin-bottom:14px; } | |
| .score-label { display:flex; justify-content:space-between; font-family:'IBM Plex Mono',monospace; font-size:12px; color:#8ea7cb; margin-bottom:5px; } | |
| .score-track { height:5px; background:#1a2535; border-radius:3px; overflow:hidden; } | |
| .score-fill { height:100%; border-radius:3px; } | |
| .stTextArea textarea { background:#0d1220 !important; border:1px solid #1e2d45 !important; border-radius:10px !important; color:#c9d1e0 !important; font-family:'IBM Plex Sans',sans-serif !important; font-size:15px !important; padding:14px !important; } | |
| .stTextArea textarea:focus { border-color:#1a6cf7 !important; } | |
| .stButton button { background:#1a6cf7 !important; color:white !important; border:none !important; border-radius:8px !important; font-family:'IBM Plex Sans',sans-serif !important; font-weight:500 !important; font-size:14px !important; padding:10px 24px !important; } | |
| .stButton button:hover { background:#1557cc !important; } | |
| .stTabs [data-baseweb="tab-list"] { background:transparent !important; border-bottom:1px solid #1e2d45 !important; } | |
| .stTabs [data-baseweb="tab"] { background:transparent !important; color:#4a6080 !important; font-family:'IBM Plex Mono',monospace !important; font-size:13px !important; padding:10px 20px !important; border-bottom:2px solid transparent !important; } | |
| .stTabs [aria-selected="true"] { color:#1a6cf7 !important; border-bottom-color:#1a6cf7 !important; background:transparent !important; } | |
| [data-testid="stFileUploader"] { background:#0d1220 !important; border:1px dashed #1e2d45 !important; border-radius:10px !important; } | |
| .stRadio label { background:#111827 !important; border:1px solid #1e2d45 !important; border-radius:8px !important; padding:10px 14px !important; } | |
| .stRadio label:has(input:checked) { border-color:#1a6cf7 !important; background:#0d1a33 !important; } | |
| hr { border-color:#1e2d45 !important; } | |
| .stTextInput input { background:#0d1220 !important; border:1px solid #1e2d45 !important; color:#c9d1e0 !important; border-radius:8px !important; font-family:'IBM Plex Mono',monospace !important; font-size:12px !important; } | |
| [data-testid="stDataFrame"] { border:1px solid #1e2d45 !important; border-radius:10px !important; overflow:hidden !important; } | |
| .stProgress > div > div { background:#1a6cf7 !important; } | |
| .report-table { width:100%; border-collapse:collapse; font-family:'IBM Plex Mono',monospace; font-size:12px; } | |
| .report-table th { | |
| text-align:left; padding:10px 14px; | |
| color:#4a6080; font-weight:600; font-size:10px; | |
| letter-spacing:1.2px; text-transform:uppercase; | |
| background:#0d1220; border-bottom:1px solid #1e2d45; | |
| position:sticky; top:0; z-index:10; | |
| } | |
| .report-table td { padding:10px 14px; border-bottom:1px solid #0f1826; vertical-align:middle; } | |
| .report-table tr:hover td { background:#0d1525; } | |
| .risk-badge { | |
| display:inline-block; padding:2px 10px; border-radius:12px; | |
| font-size:10px; font-weight:600; letter-spacing:0.8px; | |
| font-family:'IBM Plex Mono',monospace; | |
| } | |
| .badge-CRITICAL { background:#1f0c0c; color:#e03030; border:1px solid #5c1a1a; } | |
| .badge-HIGH { background:#1a0e03; color:#e07020; border:1px solid #5c2e0a; } | |
| .badge-MEDIUM { background:#141002; color:#d4a017; border:1px solid #4d3d08; } | |
| .badge-LOW { background:#07091a; color:#3a7bd4; border:1px solid #1a2d5c; } | |
| .badge-NONE { background:#050f07; color:#2ea84a; border:1px solid #1a4d25; } | |
| .inline-bar { | |
| display:inline-block; height:4px; border-radius:2px; | |
| vertical-align:middle; margin-right:4px; | |
| } | |
| .hits-tag { | |
| display:inline-block; background:#1f0e0e; border:1px solid #5c1a1a; | |
| color:#e05050; font-size:10px; padding:1px 6px; border-radius:4px; margin:1px; | |
| } | |
| .karar-cell { font-weight:600; font-size:11px; } | |
| .metin-cell { color:#8a9bc0; max-width:280px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } | |
| .skor-cell { color:#6a8cb0; font-size:11px; } | |
| .summary-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(140px, 1fr)); gap:12px; margin-bottom:24px; } | |
| .summary-card { background:#0d1220; border:1px solid #1e2d45; border-radius:10px; padding:16px; text-align:center; } | |
| .summary-count { font-family:'IBM Plex Mono',monospace; font-size:36px; font-weight:700; margin-bottom:4px; } | |
| .summary-label { font-family:'IBM Plex Mono',monospace; font-size:10px; color:#4a6080; text-transform:uppercase; letter-spacing:1px; } | |
| .queue-card { | |
| background:#060a13; border:1px solid #1a2d5c; border-radius:10px; | |
| padding:16px; margin-bottom:10px; | |
| display:flex; gap:16px; align-items:flex-start; | |
| } | |
| .queue-index { font-family:'IBM Plex Mono',monospace; font-size:11px; color:#2a3d55; min-width:28px; } | |
| .queue-text { color:#c9d1e0; font-size:13px; line-height:1.5; flex:1; } | |
| .queue-meta { font-family:'IBM Plex Mono',monospace; font-size:10px; color:#4a6080; margin-top:4px; } | |
| </style> | |
| """, | |
| 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"""<div class=\"score-row\"> | |
| <div class=\"score-label\"><span>{label}</span><span style=\"color:{color};font-weight:600\">%{pct:.1f}</span></div> | |
| <div class=\"score-track\"><div class=\"score-fill\" style=\"width:{pct}%;background:{color}\"></div></div> | |
| </div>""" | |
| 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'<span class="risk-badge {cls}">{risk}</span>' | |
| def inline_bar_html(value, color): | |
| w = min(max(value * 60, 0), 60) | |
| return f'<span class="inline-bar" style="width:{w}px;background:{color}"></span><span style="color:{color};font-size:11px">%{value * 100:.0f}</span>' | |
| 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( | |
| """ | |
| <div class="sentinel-header"> | |
| <div class="sentinel-logo">⬡</div> | |
| <div> | |
| <div class="sentinel-title">Sentinel</div> | |
| <div class="sentinel-sub">İçerik Moderasyon Sistemi</div> | |
| </div> | |
| <div class="status-pill"><span class="status-dot"></span>ONLINE</div> | |
| </div> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| with st.sidebar: | |
| st.markdown( | |
| """<div style="padding:8px 0 20px 0; border-bottom:1px solid #1e2d45; margin-bottom:20px;"> | |
| <div style="font-family:'IBM Plex Mono',monospace; font-size:11px; color:#4a6080; letter-spacing:1.5px; text-transform:uppercase; margin-bottom:16px;">Sistem Konfigürasyonu</div> | |
| </div>""", | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown( | |
| """<div style="font-family:'IBM Plex Mono',monospace; font-size:11px; color:#4a6080; text-transform:uppercase; letter-spacing:1px; margin-bottom:10px;">Platform Dili</div>""", | |
| 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("<br>", unsafe_allow_html=True) | |
| st.markdown( | |
| """<div style="font-family:'IBM Plex Mono',monospace; font-size:11px; color:#4a6080; text-transform:uppercase; letter-spacing:1px; margin-bottom:10px;">API Endpoint</div>""", | |
| unsafe_allow_html=True, | |
| ) | |
| api_url = st.text_input("API", value=API_URL, label_visibility="collapsed") | |
| st.markdown("<br><br>", unsafe_allow_html=True) | |
| st.markdown( | |
| """<div style="font-family:'IBM Plex Mono',monospace; font-size:11px; color:#2a3d55; line-height:1.8;"> | |
| TR PIPELINE<br><span style="color:#4a6289">──────────────</span><br> | |
| <span style="color:#6f8fbf">▸</span> is_spam() evrensel filtre<br> | |
| <span style="color:#6f8fbf">▸</span> Küfür listesi lookup<br> | |
| <span style="color:#6f8fbf">▸</span> BERTurk offensive 42K<br> | |
| <span style="color:#6f8fbf">▸</span> Detoxify multilingual<br><br> | |
| EN PIPELINE<br><span style="color:#4a6289">──────────────</span><br> | |
| <span style="color:#6f8fbf">▸</span> is_spam() evrensel filtre<br> | |
| <span style="color:#6f8fbf">▸</span> Gibberish Detector<br> | |
| <span style="color:#6f8fbf">▸</span> Detoxify original 6-label | |
| </div>""", | |
| 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("<br>", 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( | |
| """<div style="padding:10px 0; font-family:'IBM Plex Mono',monospace; font-size:11px; color:#8ea7cb; line-height:1.8;">Spam → Dil → Küfür → Model → Karar</div>""", | |
| 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"""<div class="verdict-card verdict-{vcls}"> | |
| <div class="verdict-label" style="color:{vcolor}">{vicon} {decision} | |
| <span style="font-size:14px;color:#2a3d55;margin-left:12px;">[{lang}]</span> | |
| </div> | |
| <div class="verdict-reason">{reason}</div> | |
| </div>""", | |
| 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"""<div class="metric-row"> | |
| <div class="metric-card"><div class="metric-label">Risk Seviyesi</div><div class="metric-value {risk_class}">{risk}</div></div> | |
| <div class="metric-card"><div class="metric-label">Gecikme</div><div class="metric-value {lat_class}">{latency:.0f} ms</div></div> | |
| <div class="metric-card"><div class="metric-label">Pipeline</div><div class="metric-value" style="font-size:18px;">{lang}</div></div> | |
| <div class="metric-card" style="flex:2"><div class="metric-label">Normalize Edilen Metin</div> | |
| <div style="font-family:'IBM Plex Mono',monospace;font-size:13px;color:#6a8cb0;margin-top:6px;word-break:break-all;">{cleaned}</div> | |
| </div> | |
| </div>""", | |
| unsafe_allow_html=True, | |
| ) | |
| hits = details.get("hits", []) or [] | |
| insult_hits = details.get("insult_hits", []) or [] | |
| if hits or insult_hits: | |
| tags = "".join(f'<span class="hits-tag">⚡ {h}</span>' for h in hits) | |
| tags += "".join( | |
| f'<span class="hits-tag" style="color:#d4a017;border-color:#5c3d08;background:#1a1002">⚠ {h}</span>' | |
| for h in insult_hits | |
| ) | |
| st.markdown( | |
| f"""<div style="margin-bottom:16px;"> | |
| <div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:#4a6080;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px;">Kara Liste Eşleşmeleri</div> | |
| {tags} | |
| </div>""", | |
| unsafe_allow_html=True, | |
| ) | |
| col_scores, col_models = st.columns([1, 1.2]) | |
| with col_scores: | |
| st.markdown( | |
| """<div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:#4a6080;text-transform:uppercase;letter-spacing:1px;margin-bottom:14px;">Sinyal Analizi</div>""", | |
| 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( | |
| """<div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:#4a6080;text-transform:uppercase;letter-spacing:1px;margin-bottom:14px;">Model Kaynak Analizi (Source)</div>""", | |
| 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"""<div style="background:#0d1220;border:1px solid #1e2d45;border-radius:8px;padding:10px;margin-bottom:8px;"> | |
| <div style="display:flex;justify-content:space-between;align-items:center;gap:10px;"> | |
| <span style="font-size:12px;font-weight:600;color:#e8eef8;">{m_name}</span> | |
| <span style="font-size:10px;color:{c};background:{c}22;padding:2px 8px;border-radius:4px;border:1px solid {c}44;white-space:nowrap;"> | |
| {m_dec} (%{m_score * 100:.1f}) | |
| </span> | |
| </div> | |
| </div>""" | |
| st.markdown(rows_html, unsafe_allow_html=True) | |
| with tab2: | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| st.markdown( | |
| """<div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:#4a6080;text-transform:uppercase;letter-spacing:1px;margin-bottom:16px;">Veri Seti Yükle</div>""", | |
| 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"""<div style="font-family:'IBM Plex Mono',monospace;font-size:12px;color:#4a6080;margin-bottom:16px;">{len(df)} satır yüklendi</div>""", | |
| 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"""<span style="font-family:'IBM Plex Mono',monospace;font-size:12px;color:#4a6080;">Batch isteği gönderiliyor... ({len(texts_payload)} satır)</span>""", | |
| 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"""<span style="font-family:'IBM Plex Mono',monospace;font-size:12px;color:#4a6080;">{len(results)} / {len(df)} işlendi</span>""", | |
| 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"""<div style="font-family:'IBM Plex Mono',monospace;font-size:12px;color:#2ea84a;margin:12px 0;"> | |
| {len(df)} satır {elapsed:.1f}s içinde analiz edildi</div>""", | |
| 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"""<div class="metric-card" style="text-align:center;"> | |
| <div class="summary-count" style="color:{vc}">{cnt}</div> | |
| <div class="summary-label">{karar[:18]}</div> | |
| </div>""", | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| st.markdown( | |
| """<div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:#4a6080;text-transform:uppercase;letter-spacing:1px;margin-bottom:12px;">Detaylı Analiz Tablosu</div>""", | |
| 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'<span class="hits-tag">{h}</span>' | |
| else: | |
| hits_html = '<span style="color:#2a3d55;font-size:10px;">—</span>' | |
| 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""" | |
| <tr style="background:{row_bg}"> | |
| <td style="color:#2a3d55;text-align:center;font-size:11px;">{idx + 1}</td> | |
| <td class="metin-cell" title="{metin_full}">{metin_short}</td> | |
| <td style="color:#4a6080;font-size:10px;font-style:italic;">{normalize}</td> | |
| <td class="karar-cell" style="color:{kc}">{karar_str[:22]}</td> | |
| <td>{badge_html(risk_str)}</td> | |
| <td class="skor-cell">{inline_bar_html(sal, risk_color(sal))}</td> | |
| <td class="skor-cell">{inline_bar_html(nef, risk_color(nef))}</td> | |
| <td class="skor-cell">{inline_bar_html(thr, risk_color(thr))}</td> | |
| <td>{hits_html}</td> | |
| <td style="color:#4a6080;font-size:10px;max-width:180px;">{str(row.get("Gerekçe", ""))[:60]}</td> | |
| </tr>""" | |
| st.markdown( | |
| f""" | |
| <div style="overflow-x:auto;overflow-y:auto;max-height:520px;border:1px solid #1e2d45;border-radius:10px;"> | |
| <table class="report-table"> | |
| <thead> | |
| <tr> | |
| <th>#</th><th>Metin</th><th>Normalize</th><th>Karar</th> | |
| <th>Risk</th><th>Saldırganlık</th><th>Nefret</th><th>Tehdit</th> | |
| <th>Hits</th><th>Gerekçe</th> | |
| </tr> | |
| </thead> | |
| <tbody>{table_rows}</tbody> | |
| </table> | |
| </div>""", | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| col_chart, col_stats = st.columns([1, 1]) | |
| with col_chart: | |
| st.markdown( | |
| """<div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:#4a6080;text-transform:uppercase;letter-spacing:1px;margin-bottom:10px;">Dağılım</div>""", | |
| unsafe_allow_html=True, | |
| ) | |
| st.bar_chart(counts) | |
| with col_stats: | |
| st.markdown( | |
| """<div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:#4a6080;text-transform:uppercase;letter-spacing:1px;margin-bottom:10px;">İstatistikler</div>""", | |
| 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""" | |
| <div style="font-family:'IBM Plex Mono',monospace;font-size:13px;line-height:2.2;color:#8a9bc0;"> | |
| <span style="color:#4a6080">Toplam kayıt </span> {total}<br> | |
| <span style="color:#4a6080">Zararlı içerik</span> <span style="color:#e03030">{zararli}</span> (%{zararli / total * 100:.1f})<br> | |
| <span style="color:#4a6080">Ortalama süre </span> {elapsed / total * 1000:.0f}ms / satır<br> | |
| <span style="color:#4a6080">Hits bulundu </span> {len(res_df[res_df['Hits'].str.len() > 0])} kayıt<br> | |
| <span style="color:#4a6080">İnceleme kuyruğu</span> {len(res_df[res_df['Karar'].str.contains('İNCELEME|INCELEME', na=False)])} içerik | |
| </div>""", | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| inceleme = res_df[res_df["Karar"].str.contains("İNCELEME|INCELEME|REVIEW", na=False)] | |
| if len(inceleme): | |
| st.markdown( | |
| f"""<div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:#3a7bd4;text-transform:uppercase;letter-spacing:1px;margin-bottom:12px;">İnceleme Kuyruğu — {len(inceleme)} İçerik</div>""", | |
| unsafe_allow_html=True, | |
| ) | |
| for i, (_, row) in enumerate(inceleme.iterrows()): | |
| sal = float(row.get("Saldırganlık", 0.0)) | |
| st.markdown( | |
| f"""<div class="queue-card"> | |
| <div class="queue-index">{i + 1:02d}</div> | |
| <div> | |
| <div class="queue-text">{str(row.get('Metin', ''))}</div> | |
| <div class="queue-meta"> | |
| Risk: {row.get('Risk', '')} | | |
| Saldırganlık: %{sal * 100:.0f} | | |
| {row.get('Gerekçe', '')} | |
| </div> | |
| </div> | |
| </div>""", | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| st.markdown( | |
| """<div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:#4a6080;text-transform:uppercase;letter-spacing:1px;margin-bottom:12px;">Raporu İndir</div>""", | |
| 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") | |