""" OFP Bad Word Sentinel - Gradio Dashboard Real-time monitoring interface for content moderation """ import gradio as gr import os import logging from datetime import datetime, timezone from apscheduler.schedulers.background import BackgroundScheduler import yaml from fastapi import FastAPI from fastapi.responses import JSONResponse import uvicorn from threading import Thread # Import sentinel components from src.profanity_detector import ProfanityDetector from src.sentinel import BadWordSentinel from src.models import Envelope, DialogEvent # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # Load configuration CONFIG_FILE = 'config/config.yaml' try: with open(CONFIG_FILE, 'r') as f: config = yaml.safe_load(f) logger.info("Configuration loaded successfully") except FileNotFoundError: logger.warning("Config file not found, using defaults") config = { 'sentinel': { 'speaker_uri': 'tag:sentinel.service,2025:badword-01', 'service_url': 'https://sentinel-service.com/ofp', 'convener_uri': 'tag:convener.service,2025:default', 'convener_url': 'https://convener-service.com/ofp' }, 'profanity': { 'use_default': True, 'whitelist': ['scunthorpe', 'arsenal'] }, 'monitoring': { 'check_interval': 30, 'auto_start': True }, 'dashboard': { 'refresh_interval': 5, 'show_test_panel': True } } # Initialize profanity detector whitelist = config['profanity'].get('whitelist', []) custom_wordlist_path = config['profanity'].get('custom_wordlist') # Load custom words if specified custom_words = None if custom_wordlist_path and os.path.exists(custom_wordlist_path): custom_words = ProfanityDetector.load_wordlist_from_file(custom_wordlist_path) if custom_words: logger.info(f"Loaded {len(custom_words)} custom words") # Get Kaggle dataset configuration use_kaggle = config['profanity'].get('use_kaggle_dataset', False) kaggle_path = config['profanity'].get('kaggle_dataset_path') if use_kaggle else None languages = config['profanity'].get('languages', ['en']) if use_kaggle else None detector = ProfanityDetector( custom_words=custom_words, whitelist=whitelist, kaggle_dataset_path=kaggle_path, languages=languages ) # Initialize sentinel sentinel = BadWordSentinel( speaker_uri=config['sentinel']['speaker_uri'], service_url=config['sentinel']['service_url'], profanity_detector=detector, convener_uri=config['sentinel']['convener_uri'], convener_url=config['sentinel']['convener_url'] ) # Start monitoring if auto-start enabled if config['monitoring'].get('auto_start', True): sentinel.start_monitoring() # Background monitoring simulation def simulate_monitoring(): """Simulate OFP event processing (in production, replace with actual OFP listener)""" try: if sentinel.is_monitoring: # This is a simulation - in production, replace with actual OFP event stream # For demo purposes, we just update the status sentinel._log_activity("Monitoring check completed") logger.debug("Monitoring check completed") except Exception as e: logger.error(f"Monitoring error: {e}") sentinel._log_activity(f"ERROR: {str(e)}") # Setup scheduler for background tasks scheduler = BackgroundScheduler() check_interval = config['monitoring'].get('check_interval', 30) scheduler.add_job(func=simulate_monitoring, trigger="interval", seconds=check_interval) scheduler.start() logger.info(f"Background scheduler started (interval: {check_interval}s)") # Create FastAPI app app = FastAPI() @app.get("/manifest") async def get_manifest(): """Serve the assistant manifest (simple JSON response)""" manifest = sentinel.get_manifest() return JSONResponse(content=manifest) @app.post("/ofp") async def receive_ofp_envelope(envelope: dict): """Receive OFP envelopes from convener Handles all OFP events including: - getManifests: Returns publishManifest event with servicingManifests - utterance: Processes messages for profanity detection - Other events: Logged and acknowledged """ try: # Check if this is an OFP envelope if "openFloor" not in envelope: return JSONResponse( content={"status": "error", "message": "Invalid OFP envelope"}, status_code=400 ) openfloor_data = envelope["openFloor"] events = openfloor_data.get("events", []) # Process each event for event in events: event_type = event.get("eventType") # Handle getManifests event if event_type == "getManifests": manifest = sentinel.get_manifest() return JSONResponse(content={ "openFloor": { "schema": {"version": "1.0.0"}, "conversation": openfloor_data.get("conversation", {}), "sender": { "speakerUri": config['sentinel']['speaker_uri'], "serviceUrl": config['sentinel']['service_url'] }, "events": [{ "eventType": "publishManifest", "to": event.get("to", {}), "parameters": { "servicingManifests": [manifest] } }] } }) # Handle utterance events (main monitoring function) elif event_type == "utterance": from src.models import Envelope from datetime import datetime, timezone import uuid # Process for monitoring/statistics ofp_envelope = Envelope.from_dict(envelope) sentinel.process_envelope(ofp_envelope) # Extract text from the incoming utterance params = event.get('parameters', {}) dialog_event = params.get('dialogEvent', {}) features = dialog_event.get('features', {}) text_feature = features.get('text', {}) tokens = text_feature.get('tokens', []) text = ' '.join(token.get('value', '') for token in tokens) # Check for profanity violation = detector.detect_violations(text) if violation: # Profanity detected - send warning utterance back warning_message = f"โš ๏ธ Content moderation alert: {violation['violation_count']} violation(s) detected. Censored: {violation['censored_text']}" return JSONResponse(content={ "openFloor": { "schema": {"version": "1.0.0"}, "conversation": openfloor_data.get("conversation", {}), "sender": { "speakerUri": config['sentinel']['speaker_uri'], "serviceUrl": config['sentinel']['service_url'] }, "events": [{ "eventType": "utterance", "to": event.get("to", {}), "parameters": { "dialogEvent": { "id": f"de:{uuid.uuid4()}", "speakerUri": config['sentinel']['speaker_uri'], "span": { "startTime": datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z') }, "features": { "text": { "mimeType": "text/plain", "tokens": [{ "value": warning_message, "confidence": 1 }] } } } } }] } }) else: # Clean message - acknowledge silently return JSONResponse(content={ "openFloor": { "schema": {"version": "1.0.0"}, "conversation": openfloor_data.get("conversation", {}), "sender": { "speakerUri": config['sentinel']['speaker_uri'], "serviceUrl": config['sentinel']['service_url'] }, "events": [] } }) # Return acknowledgment for other event types return JSONResponse(content={ "openFloor": { "schema": {"version": "1.0.0"}, "conversation": openfloor_data.get("conversation", {}), "sender": { "speakerUri": config['sentinel']['speaker_uri'], "serviceUrl": config['sentinel']['service_url'] }, "events": [] } }) except Exception as e: logger.error(f"Error processing OFP envelope: {e}") return JSONResponse( content={"status": "error", "message": str(e)}, status_code=400 ) # Gradio Interface Functions def update_dashboard(): """Update dashboard with current status""" status = sentinel.get_status() recent_logs = '\n'.join(status['recent_logs']) if status['recent_logs'] else "No recent activity" current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") return ( status['connection_status'], current_time, status['violations_detected'], status['alerts_sent'], status['messages_processed'], recent_logs ) def test_detection(text: str): """Test profanity detection on input text""" if not text: return {"error": "No text provided"} violation = detector.detect_violations(text) if violation: return { "profane": True, "severity": violation['severity'], "violations_found": violation['violations'], "censored": violation['censored_text'], "count": violation['violation_count'] } else: return { "profane": False, "message": "No profanity detected" } def simulate_test_violation(): """Simulate a test violation for demonstration""" # Create mock envelope with profane content test_envelope = Envelope( schema={"version": "1.0.0"}, conversation={"id": "conv:test-123"}, sender={"speakerUri": "tag:test.user,2025:demo"}, events=[{ "eventType": "utterance", "parameters": { "dialogEvent": { "id": "de:test-456", "speakerUri": "tag:test.user,2025:demo", "span": {"startTime": datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')}, "features": { "text": { "mimeType": "text/plain", "tokens": [{"value": "This is a test with sh1t and damn"}] } } } } }] ) sentinel.process_envelope(test_envelope) return update_dashboard() def toggle_monitoring(current_status: str): """Toggle monitoring on/off""" if "Active" in current_status: sentinel.stop_monitoring() else: sentinel.start_monitoring() return update_dashboard() def reset_stats(): """Reset statistics""" sentinel.reset_statistics() return update_dashboard() # Build Gradio Interface with Custom CSS custom_css = """ /* Main container styling */ .gradio-container { font-family: 'Inter', system-ui, -apple-system, sans-serif !important; max-width: 1200px !important; margin: 0 auto !important; } /* Hero header */ .hero-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 3rem 2rem; border-radius: 16px; margin-bottom: 2rem; box-shadow: 0 10px 40px rgba(102, 126, 234, 0.3); text-align: center; } .hero-header h1 { color: white !important; font-size: 2.75rem !important; margin: 0 0 1rem 0 !important; font-weight: 700 !important; letter-spacing: -0.02em; } .hero-subtitle { color: rgba(255, 255, 255, 0.95) !important; font-size: 1.15rem !important; margin: 0.5rem 0 1rem 0 !important; line-height: 1.6; } .hero-stats { color: rgba(255, 255, 255, 0.9) !important; font-weight: 600 !important; font-size: 1.05rem !important; margin-top: 1rem !important; } /* Status dashboard styling */ .status-dashboard { background: white; border-radius: 16px; padding: 2rem; margin: 2rem 0; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); border: 1px solid #e2e8f0; } .status-title { font-size: 1.5rem !important; font-weight: 700 !important; color: #2d3748 !important; margin: 0 0 1.5rem 0 !important; display: flex; align-items: center; gap: 0.5rem; } .stat-card { background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%); border-radius: 12px; padding: 1.5rem; text-align: center; border: 2px solid #e2e8f0; transition: all 0.3s ease; height: 100%; } .stat-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); border-color: #667eea; } .stat-label { font-size: 0.875rem !important; font-weight: 600 !important; color: #718096 !important; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem !important; } .stat-value { font-size: 2rem !important; font-weight: 700 !important; color: #2d3748 !important; margin-top: 0.5rem !important; } .stat-icon { font-size: 1.5rem; margin-bottom: 0.5rem; } /* Section headers */ .section-wrapper { background: white; border-radius: 16px; padding: 2rem; margin: 2rem 0; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); border: 1px solid #e2e8f0; } .section-header { color: #2d3748 !important; font-size: 1.5rem !important; font-weight: 700 !important; margin: 0 0 0.5rem 0 !important; } .section-subtitle { color: #718096 !important; font-size: 0.95rem !important; margin: 0 0 1.5rem 0 !important; } /* Buttons */ .primary-button { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; border: none !important; color: white !important; font-weight: 600 !important; padding: 0.75rem 2rem !important; border-radius: 8px !important; font-size: 1rem !important; transition: all 0.2s ease !important; } .primary-button:hover { transform: translateY(-2px) !important; box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4) !important; } /* Input fields */ textarea, input { border: 2px solid #e2e8f0 !important; border-radius: 8px !important; padding: 0.75rem !important; font-size: 1rem !important; transition: all 0.2s ease !important; } textarea:focus, input:focus { border-color: #667eea !important; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important; outline: none !important; } /* JSON output styling */ .json-output { background: #f7fafc !important; border: 2px solid #e2e8f0 !important; border-radius: 8px !important; padding: 1rem !important; font-family: 'Monaco', 'Courier New', monospace !important; } /* Attribution section */ .attribution { background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%); padding: 2.5rem; border-radius: 16px; border-left: 6px solid #667eea; margin-top: 3rem; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); } .attribution h2 { color: #2d3748 !important; font-size: 1.5rem !important; font-weight: 700 !important; margin-top: 0 !important; } .attribution h3 { color: #4a5568 !important; font-size: 1.1rem !important; font-weight: 600 !important; margin-top: 1.5rem !important; } .attribution ul { color: #4a5568 !important; line-height: 1.8 !important; } .attribution a { color: #667eea !important; text-decoration: none !important; font-weight: 500 !important; transition: color 0.2s ease !important; } .attribution a:hover { color: #764ba2 !important; text-decoration: underline !important; } /* Accordion styling */ .accordion-item { border: 2px solid #e2e8f0 !important; border-radius: 8px !important; margin-top: 1rem !important; } /* Responsive adjustments */ @media (max-width: 768px) { .hero-header h1 { font-size: 2rem !important; } .stat-value { font-size: 1.5rem !important; } .status-dashboard, .section-wrapper { padding: 1.5rem !important; } } """ with gr.Blocks(title="OFP Bad Word Sentinel", theme=gr.themes.Soft(), css=custom_css) as demo: # Hero Header gr.HTML("""

๐Ÿ”ฅ OFP Bad Word Sentinel

Real-time profanity detection for Open Floor Protocol conversations

58 languages ยท 6,936+ profanity patterns

""") # Status Dashboard with Cards gr.HTML("""
๐Ÿ“Š Live Status
""") with gr.Row(equal_height=True): with gr.Column(scale=1): gr.HTML("""
โœ…
Status
Active
""") with gr.Column(scale=1): current_time = datetime.now().strftime('%H:%M:%S') gr.HTML(f"""
๐Ÿ•
Last Check
{current_time}
""") with gr.Column(scale=1): gr.HTML(f"""
โš ๏ธ
Violations
{sentinel.violations_detected}
""") with gr.Column(scale=1): gr.HTML(f"""
๐Ÿ“Š
Processed
{sentinel.messages_processed}
""") # Test Playground Section gr.HTML("""

๐Ÿงช Test Playground

Test profanity detection across 58 languages

""") test_input = gr.Textbox( label="Enter text to test", placeholder="Type any message to check for profanity...", lines=4, show_label=True ) with gr.Row(): test_detect_btn = gr.Button("๐Ÿ” Detect Profanity", variant="primary", scale=3, elem_classes="primary-button") clear_btn = gr.Button("Clear", variant="secondary", scale=1) test_output = gr.JSON(label="Detection Result", elem_classes="json-output") # OFP Testing Section (Optional) with gr.Accordion("๐Ÿ“ก OFP Protocol Testing", open=False, elem_classes="accordion-item"): gr.Markdown("Send a test message with profanity through the OFP pipeline to simulate real monitoring.") simulate_btn = gr.Button("Send Test Profanity Message", variant="secondary") # Attribution Section gr.HTML("""

๐Ÿ“š Attribution & Resources

Data Sources

Protocol

Technology

""") # Event handlers test_detect_btn.click( fn=test_detection, inputs=test_input, outputs=test_output ) clear_btn.click( fn=lambda: ("", None), inputs=None, outputs=[test_input, test_output] ) # Launch configuration if __name__ == "__main__": import argparse # Check if running on HuggingFace Spaces is_hf_space = os.getenv("SPACE_ID") is not None parser = argparse.ArgumentParser(description='OFP Bad Word Sentinel') parser.add_argument('--share', action='store_true', help='Create public share link (for OpenFloor Playground)') parser.add_argument('--port', type=int, default=7860, help='Port to run on') args = parser.parse_args() # On HuggingFace Spaces, mount Gradio at root if is_hf_space: print("\n" + "="*60) print("๐Ÿ”ฅ OFP Bad Word Sentinel - HuggingFace Spaces") print("="*60) print("\nDashboard: https:///") print("Manifest endpoint: https:///manifest") print("OFP endpoint: https:///ofp") print("="*60 + "\n") # Mount Gradio at root path app_with_gradio = gr.mount_gradio_app(app, demo, path="/") # Use Uvicorn to run the combined app (HF Spaces compatible) import uvicorn uvicorn.run( app_with_gradio, host="0.0.0.0", port=args.port, log_level="info" ) elif args.share: # Share mode for local testing with OpenFloor Playground print("\n" + "="*60) print("๐Ÿ”ฅ OFP Bad Word Sentinel - Share Mode") print("="*60) print("\nGenerating public share URL for Gradio...") print("This URL can be used in OpenFloor Playground!") print("\nDashboard: https:///") print("Manifest endpoint: https:///manifest") print("OFP endpoint: https:///ofp") print("="*60 + "\n") # Mount Gradio at root path app_with_gradio = gr.mount_gradio_app(app, demo, path="/") # For share mode, we need to use demo.launch() with share=True # But this doesn't work well with custom FastAPI routes # Workaround: Use queue() and launch with auth or public link import uvicorn uvicorn.run( app_with_gradio, host="0.0.0.0", port=args.port, log_level="info" ) else: # Local deployment with Uvicorn (full FastAPI control) # Mount Gradio at root path app = gr.mount_gradio_app(app, demo, path="/") print("\n" + "="*60) print("๐Ÿ”ฅ OFP Bad Word Sentinel - Local Mode") print("="*60) print(f"\nStarting server on http://localhost:{args.port}") print(f"Dashboard: http://localhost:{args.port}/") print(f"Manifest: http://localhost:{args.port}/manifest") print(f"OFP endpoint: http://localhost:{args.port}/ofp") print(f"API docs: http://localhost:{args.port}/docs") print("="*60 + "\n") uvicorn.run( app, host="0.0.0.0", port=args.port, log_level="info" )