Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import feedparser | |
| import urllib.parse | |
| from groq import Groq, RateLimitError | |
| import pandas as pd | |
| import json | |
| import os | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| from datetime import datetime, timedelta | |
| # --- 1. Page Config (Mobile Optimized) --- | |
| st.set_page_config(page_title="Omni-Watch T&S Master", page_icon="๐ก๏ธ", layout="wide") | |
| # --- 2. API Key Load --- | |
| api_key = os.environ.get("GROQ_API_KEY") | |
| if not api_key: | |
| try: api_key = st.secrets["GROQ_API_KEY"] | |
| except: pass | |
| if not api_key: | |
| st.error("๐ GROQ_API_KEY Missing. Please check your deployment settings.") | |
| st.stop() | |
| client = Groq(api_key=api_key) | |
| # ๐จ [CRITICAL] Robust Model Hierarchy (Auto-Switching) | |
| # 429 ์๋ฌ๋ 400(๋ชจ๋ธ ์ข ๋ฃ) ์๋ฌ ๋ฐ์ ์, ์๋์ผ๋ก ๋ค์ ์์ ๋ชจ๋ธ๋ก ์ ํํ์ฌ ์ค๋จ ์์ด ๋ถ์ํฉ๋๋ค. | |
| MODEL_HIERARCHY = [ | |
| "llama-3.3-70b-versatile", # 1์์: ์ต์ SOTA ๋ชจ๋ธ | |
| "llama-3.1-70b-versatile", # 2์์: ๊ณ ์ฑ๋ฅ ๋ฐฑ์ | |
| "mixtral-8x7b-32768", # 3์์: ์์ ์ฑ ์์ฃผ | |
| "llama-3.1-8b-instant" # 4์์: ์ด๊ณ ์ ๋น์์ฉ | |
| ] | |
| # --- 3. Configuration --- | |
| REGION_MAP = { | |
| "Asia & Pacific": ["KR", "JP", "CN", "VN", "IN", "AU"], | |
| "Europe": ["GB", "FR", "DE", "UA", "RU"], | |
| "Americas": ["US", "CA", "BR", "MX"], | |
| "ME & Africa": ["IL", "SA", "AE", "TR"] | |
| } | |
| # --- 4. Core Functions --- | |
| # [Smart AI Wrapper] ์๋ฌ ํธ๋ค๋ง ๋ฐ ๋ชจ๋ธ ์๋ ์ ํ ๋ก์ง | |
| def get_ai_response(system_msg, user_content, json_mode=True): | |
| for model in MODEL_HIERARCHY: | |
| try: | |
| kwargs = { | |
| "model": model, | |
| "messages": [{"role": "system", "content": system_msg}, {"role": "user", "content": user_content}], | |
| } | |
| if json_mode: | |
| kwargs["response_format"] = {"type": "json_object"} | |
| res = client.chat.completions.create(**kwargs) | |
| return res | |
| except RateLimitError: | |
| continue # ํ๋ ์ด๊ณผ ์ ๋ค์ ๋ชจ๋ธ ์๋ | |
| except Exception as e: | |
| # ๋ชจ๋ธ ์ข ๋ฃ(400)๋ ์ฐพ์ ์ ์์(404) ์๋ฌ ์์๋ ๋ค์ ๋ชจ๋ธ ์๋ | |
| if "model_decommissioned" in str(e) or "404" in str(e) or "400" in str(e): | |
| continue | |
| st.error(f"โ ๏ธ Error with {model}: {e}") # ๊ทธ ์ธ ์น๋ช ์ ์๋ฌ๋ ์ถ๋ ฅ | |
| return None | |
| st.error("๐จ All AI models are currently unavailable. Please check API Status.") | |
| return None | |
| def fetch_extensive_news(query, geo="US", limit=60, period="7d"): | |
| time_filter = f" when:{period}" | |
| encoded = urllib.parse.quote(query + time_filter) | |
| url = f"https://news.google.com/rss/search?q={encoded}&hl=en&gl={geo}&ceid={geo}:en" | |
| feed = feedparser.parse(url) | |
| articles = [] | |
| for e in feed.entries[:limit]: | |
| articles.append({"title": e.title, "source": e.source.title if 'source' in e else "G-News", "link": e.link}) | |
| return articles | |
| def render_gauge(score, title): | |
| # ์์ธ ๋ถ์์ฉ ๊ฒ์ด์ง ์ฐจํธ (์๋ฃธ ๋ฏธ์ฌ์ฉ) | |
| fig = go.Figure(go.Indicator( | |
| mode = "gauge+number", | |
| value = score, | |
| domain = {'x': [0, 1], 'y': [0, 1]}, | |
| title = {'text': title, 'font': {'size': 18, 'color': "#FF4B4B"}}, | |
| gauge = { | |
| 'axis': {'range': [0, 100], 'tickwidth': 1}, | |
| 'bar': {'color': "#FF4B4B"}, | |
| 'steps': [{'range': [0, 100], 'color': "#ffebee"}], | |
| } | |
| )) | |
| fig.update_layout(height=180, margin=dict(l=10, r=10, t=40, b=10), paper_bgcolor="rgba(0,0,0,0)") | |
| return fig | |
| # --- 5. ๐จ GLOBAL WAR ROOM (Policy Focused, Numeric Only) --- | |
| st.title("๐จ OMNI-WATCH: T&S WAR ROOM") | |
| st.caption(f"Policy Risk Monitoring: {datetime.now().strftime('%H:%M:%S')} UTC") | |
| if st.button("๐ Refresh"): | |
| if 'hot_issues' in st.session_state: del st.session_state.hot_issues | |
| if 'hot_issues' not in st.session_state: | |
| with st.spinner("Scanning 12h Global Feeds for Violations..."): | |
| # ๊ฒ์์ด: T&S ์๋ฐ ๊ฐ๋ฅ์ฑ์ด ๋์ ํค์๋ | |
| raw_news = fetch_extensive_news("violence OR hate speech OR disinformation OR scandal OR protest", limit=40, period="12h") | |
| if not raw_news: | |
| st.warning("No critical incidents found in the last 12h.") | |
| st.session_state.hot_issues = [] | |
| else: | |
| news_context = "\n".join([f"Event: {n['title']}" for n in raw_news]) | |
| # PROMPT: ์ฌ๊ฑด ๊ฐ์์ ์๋ฐ ์ฌํญ์ ๋ช ํํ ๊ตฌ๋ถ | |
| system_msg = """ | |
| Identify TOP 3 incidents with highest 'Trust & Safety' risk. | |
| The 'summary' array MUST follow this order: | |
| 1. "Event: [Brief summary of what happened]" | |
| 2. "Violation: [Specific Community Guideline breached]" | |
| 3. "Risk: [Potential offline harm]" | |
| Return ONLY JSON: | |
| {"issues": [{"title": "Short Title", "score": 85, "summary": ["Event: ...", "Violation: ...", "Risk: ..."], "link": ".."}]} | |
| """ | |
| res = get_ai_response(system_msg, news_context) | |
| if res: | |
| try: | |
| st.session_state.hot_issues = json.loads(res.choices[0].message.content).get('issues', [])[:3] | |
| except: | |
| st.session_state.hot_issues = [] | |
| if st.session_state.hot_issues: | |
| cols = st.columns(3) | |
| for i, issue in enumerate(st.session_state.hot_issues): | |
| with cols[i]: | |
| with st.container(border=True): | |
| # UI: ๊ฒ์ด์ง ๋์ ํฐ ์ซ์ ์ฌ์ฉ (๋ชจ๋ฐ์ผ ๊ฐ๋ ์ฑ) | |
| st.markdown(f"<h1 style='text-align: center; color: #FF4B4B; margin: 0;'>{issue.get('score', 50)}</h1>", unsafe_allow_html=True) | |
| st.markdown("<p style='text-align: center; color: gray; font-size: 0.8em;'>Safety Risk Index</p>", unsafe_allow_html=True) | |
| st.error(f"**{issue.get('title')}**") | |
| for line in issue.get('summary', []): st.caption(f"โข {line}") | |
| st.markdown(f"[๐ Link]({issue.get('link')})") | |
| st.divider() | |
| # --- 6. Strategic Tabs --- | |
| tab1, tab2, tab3 = st.tabs(["๐ GLOBAL POLICY SCAN", "๐ NATIONAL T&S FORENSICS", "๐ RISK VELOCITY"]) | |
| # --- [Tab 1: Strategic Global Scan] --- | |
| with tab1: | |
| st.header("Strategic Policy & Impact Briefing") | |
| keyword = st.text_input("Risk Category", "Election Integrity") | |
| if st.button("Analyze Policy Impact", type="primary"): | |
| with st.status("Auditing Global Content Compliance (20+ Sources)...", expanded=True): | |
| all_news = [] | |
| for reg in REGION_MAP: | |
| for geo in REGION_MAP[reg][:2]: | |
| all_news.extend(fetch_extensive_news(keyword, geo=geo, limit=6, period="7d")) | |
| if not all_news: | |
| st.error("No news found for this keyword.") | |
| else: | |
| news_summary = "\n".join([n['title'] for n in all_news[:45]]) | |
| # PROMPT: ์ฌ๊ฑด ๊ฐ์(Summary) ํ์ ํฌํจ | |
| global_prompt = f""" | |
| Analyze '{keyword}' focusing on 'Community Guidelines' and 'Social Impact'. | |
| 1. Executive Summary: Start with a clear **Event Summary** of what happened. Then, analyze the systemic policy risks and societal harm. (300+ words). | |
| 2. Risk Landscape: Map findings to specific violations. | |
| Return ONLY JSON: | |
| {{ | |
| "executive_summary": "Start with [The Incident Details], then move to [Policy Analysis].", | |
| "risk_landscape": [ | |
| {{"Component": "Primary Violation", "Findings": "...", "Risk_Level": "High"}}, | |
| {{"Component": "Vulnerable Target", "Findings": "...", "Risk_Level": "Critical"}}, | |
| {{"Component": "Offline Harm", "Findings": "...", "Risk_Level": "High"}}, | |
| {{"Component": "Enforcement Gap", "Findings": "...", "Risk_Level": "Medium"}} | |
| ], | |
| "platform_intelligence": [ | |
| {{"Platform": "TikTok", "Assessment": "...", "Strategy": "..."}}, | |
| {{"Platform": "YouTube", "Assessment": "...", "Strategy": "..."}}, | |
| {{"Platform": "Meta", "Assessment": "...", "Strategy": "..."}}, | |
| {{"Platform": "X", "Assessment": "...", "Strategy": "..."}} | |
| ], | |
| "strategic_conclusion": "Final Trust & Safety recommendation." | |
| }} | |
| """ | |
| res = get_ai_response(global_prompt, news_summary) | |
| if res: | |
| g_data = json.loads(res.choices[0].message.content) | |
| with st.container(border=True): | |
| st.subheader("1. Policy Impact Executive Summary") | |
| # ๊ฐ๋ ์ฑ์ ์ํ ์ค๋ฐ๊ฟ ์ฒ๋ฆฌ | |
| st.markdown(g_data.get('executive_summary').replace(". ", ".\n\n")) | |
| st.subheader("2. Guideline Violation Matrix") | |
| st.dataframe(pd.DataFrame(g_data.get('risk_landscape')), hide_index=True, use_container_width=True) | |
| st.subheader("3. Platform Enforcement Strategy") | |
| st.table(pd.DataFrame(g_data.get('platform_intelligence'))) | |
| st.success(f"**T&S Recommendation:** {g_data.get('strategic_conclusion')}") | |
| # DOWNLOAD: Markdown Format (๋ชจ๋ฐ์ผ ํธํ) | |
| report_md = f"# OMNI-WATCH POLICY REPORT: {keyword.upper()}\n\n" | |
| report_md += f"## 1. EXECUTIVE SUMMARY\n{g_data.get('executive_summary')}\n\n" | |
| report_md += "## 2. VIOLATION LANDSCAPE\n" | |
| for item in g_data.get('risk_landscape', []): | |
| report_md += f"- **{item['Component']}**: {item['Findings']} ({item['Risk_Level']})\n" | |
| st.download_button("๐ฅ Download Policy Report (.md)", report_md, f"Policy_Intel_{keyword}.md") | |
| # --- [Tab 2: Tactical Forensics] --- | |
| with tab2: | |
| st.header("National T&S Forensics") | |
| target_geo = st.text_input("ISO Code", "US").upper() | |
| if st.button("Analyze Violations"): | |
| with st.status(f"Scanning {target_geo} for Policy Breaches (20+ Sources)...", expanded=True): | |
| # ๊ฒ์์ด ์ต์ ํ: ์ค์ ์ฌ๊ฑด/์ฌ๊ณ ์์ฃผ | |
| news = fetch_extensive_news(f"{target_geo} controversy OR scandal OR protest OR violence", geo=target_geo, limit=40, period="7d") | |
| if not news: | |
| st.error(f"No recent controversy news found for {target_geo}.") | |
| else: | |
| news_titles = "\n".join([n['title'] for n in news]) | |
| # PROMPT: Incident -> Policy Analysis ๊ตฌ์กฐ ๊ฐ์ | |
| system_prompt = f""" | |
| Analyze Top 5 Risks in {target_geo} strictly through a 'Community Guidelines' lens. | |
| Summary MUST start with **"The Incident:"** (What happened) followed by **"Policy Analysis:"** (Why it violates rules). | |
| Return ONLY JSON: | |
| {{ | |
| "risks": [ | |
| {{ | |
| "rank": 1, "title": "Event Title", "score": 90, | |
| "summary": "1. The Incident: ... \n2. Policy Analysis: ...", | |
| "forensic_grid": {{ | |
| "Guideline_Breached": "e.g. Dangerous Organizations Policy", | |
| "Victim_Demographics": "e.g. Teenagers / Ethnic Minorities", | |
| "Societal_Impact": "e.g. Incitement to Violence", | |
| "Enforcement_Action": "e.g. Geo-blocking / Account Ban" | |
| }} | |
| }} | |
| ] | |
| }} | |
| """ | |
| res = get_ai_response(system_prompt, news_titles) | |
| if res: | |
| report_data = json.loads(res.choices[0].message.content).get('risks', []) | |
| full_report_md = f"# NATIONAL T&S FORENSICS: {target_geo}\n\n" | |
| for i, r in enumerate(report_data): | |
| full_report_md += f"## {r.get('rank')}. {r.get('title')} (Risk: {r.get('score')})\n" | |
| full_report_md += f"{r.get('summary')}\n\n" | |
| with st.expander(f"๐ฉ RISK {r.get('rank')}: {r.get('title')} (Score: {r.get('score')})", expanded=True): | |
| c1, c2 = st.columns([1, 4]) | |
| with c1: | |
| # ์์ธ ๋ถ์ ํญ์์๋ ๊ฒ์ด์ง ์ฐจํธ ์ฌ์ฉ (Key ์ค๋ณต ๋ฐฉ์ง ์ ์ฉ) | |
| st.plotly_chart(render_gauge(r.get('score'), "Risk Index"), use_container_width=True, key=f"fg_{i}") | |
| with c2: | |
| st.markdown("**Incident & Policy Analysis:**") | |
| st.markdown(r.get('summary').replace(". ", ".\n\n")) | |
| st.table(pd.DataFrame(r.get('forensic_grid', {}).items(), columns=["T&S Component", "Assessment"])) | |
| st.download_button("๐ฅ Download Forensic Report (.md)", full_report_md, f"T&S_Forensics_{target_geo}.md") | |
| st.divider() | |
| st.caption(f"Evidence Base: {len(news)} articles") | |
| st.dataframe(pd.DataFrame(news)[['title', 'source']], use_container_width=True) | |
| # --- [Tab 3: Velocity] --- | |
| with tab3: | |
| st.header("Risk Velocity") | |
| trend_key = st.text_input("Violation Type", "Hate Speech") | |
| if st.button("Check Trend"): | |
| dates = [(datetime.now() - timedelta(days=i)).strftime("%m-%d") for i in range(6, -1, -1)] | |
| fig = px.area(x=dates, y=[15, 30, 50, 80, 95, 88, 92], title=f"Violation Surge: {trend_key}") | |
| st.plotly_chart(fig, use_container_width=True) |