Spaces:
Sleeping
Sleeping
| import os | |
| import json | |
| import logging | |
| from typing import Dict, Any, List | |
| import requests | |
| from datetime import datetime | |
| import re | |
| from flask import Flask, request, jsonify | |
| # Configure logging | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| class ArabicContentModerator: | |
| """ | |
| Arabic Story Content Moderation Model using Deepseek API | |
| Checks for cultural violations and inappropriate content | |
| """ | |
| def __init__(self, deepseek_api_key: str = None): | |
| """ | |
| Initialize the content moderator | |
| Args: | |
| deepseek_api_key: Deepseek API key | |
| """ | |
| self.api_key = deepseek_api_key or os.getenv('DEEPSEEK_API_KEY') | |
| if not self.api_key: | |
| raise ValueError("Deepseek API key is required") | |
| self.api_url = "https://api.deepseek.com/chat/completions" | |
| self.headers = { | |
| "Authorization": f"Bearer {self.api_key}", | |
| "Content-Type": "application/json" | |
| } | |
| # Enhanced Arabic Content Moderation with News Detection | |
| self.moderation_prompt = """ | |
| أنت مراجع محتوى عربي محترف متخصص في التمييز بين القصص الأدبية والمحتوى الإخباري. مهمتك مراجعة النصوص العربية ورفض أي محتوى غير أدبي. | |
| ## معايير الرفض الصارمة: | |
| ### 1. المحتوى الإخباري والصحفي - رفض فوري: | |
| **يجب رفض النصوص التي تحتوي على:** | |
| **أ) التقارير الرياضية:** | |
| - "بعد المباراة خرج وقال" | |
| - "اللاعب تألق ومنع أهداف" | |
| - "فاز بجائزة رجل المباراة" | |
| - "المباراة انتهت بنتيجة" | |
| - "في الشوط الأول" | |
| - "المدرب صرح" | |
| **ب) المؤتمرات الصحفية:** | |
| - "في مؤتمر صحفي" | |
| - "صرح الوزير" | |
| - "أعلن المسؤول" | |
| - "في تصريحات خاصة" | |
| - "قال النائب" | |
| - "أكد الخبير" | |
| **ج) الاجتماعات والفعاليات:** | |
| - "في اجتماع اليوم" | |
| - "خلال الجلسة" | |
| - "في المنتدى" | |
| - "أثناء المؤتمر" | |
| - "في الورشة" | |
| - "خلال اللقاء" | |
| **د) الأخبار السياسية:** | |
| - "الرئيس التقى" | |
| - "الوزير أعلن" | |
| - "البرلمان ناقش" | |
| - "الحكومة قررت" | |
| - "السفير وصل" | |
| - "الوزارة أصدرت" | |
| **هـ) الأخبار الاقتصادية:** | |
| - "البورصة ارتفعت" | |
| - "أسعار النفط" | |
| - "الدولار سجل" | |
| - "الشركة حققت" | |
| - "الاستثمارات بلغت" | |
| - "التضخم وصل" | |
| **و) التقارير التقنية:** | |
| - "التطبيق الجديد" | |
| - "الهاتف يتميز" | |
| - "الخاصية الجديدة" | |
| - "التحديث يتضمن" | |
| - "النظام يدعم" | |
| - "البرنامج أضاف" | |
| **ز) الأخبار المحلية:** | |
| - "في محافظة" | |
| - "بلدية المدينة" | |
| - "المحافظ افتتح" | |
| - "المجلس المحلي" | |
| - "الأهالي طالبوا" | |
| - "الخدمات تحسنت" | |
| ### 2. العلامات المميزة للمحتوى الإخباري: | |
| - استخدام أسماء حقيقية لأشخاص مشهورين | |
| - ذكر مباريات وأحداث رياضية محددة | |
| - استخدام مصطلحات إخبارية ("صرح"، "أعلن"، "أكد") | |
| - التواريخ والأرقام الإحصائية | |
| - ذكر مؤسسات وشركات حقيقية | |
| - النبرة الرسمية والتقريرية | |
| ### 3. المحتوى الأدبي المقبول: | |
| **يجب قبول النصوص التي تحتوي على:** | |
| - شخصيات خيالية أو مجهولة الهوية | |
| - أحداث متخيلة أو درامية | |
| - حوار إبداعي وعاطفي | |
| - وصف الشخصيات والأماكن | |
| - صراع نفسي أو اجتماعي | |
| - نهاية مفتوحة أو رسالة أدبية | |
| - استخدام التشبيهات والمجازات | |
| - الأسلوب السردي الإبداعي | |
| ### 4. الانتهاكات الدينية - فحص صارم: | |
| **رفض فوري للمحتوى الذي يحتوي على:** | |
| - أي استهزاء أو تهكم على الله أو الأنبياء | |
| - انتقاد الآيات القرآنية أو الأحاديث | |
| - السخرية من الشعائر الدينية | |
| - التطاول على الصحابة | |
| - التجديف أو الكفر الصريح | |
| - السب بالدين | |
| ### 5. السب والشتم - فحص صارم: | |
| **رفض فوري للمحتوى الذي يحتوي على:** | |
| - الألفاظ الجنسية الصريحة | |
| - السب بالأعضاء التناسلية | |
| - الألفاظ الإخراجية | |
| - إهانة الأم أو العرض | |
| - السب العرقي بألفاظ قبيحة | |
| - الكلمات المبتذلة الخادشة | |
| ## أمثلة للرفض: | |
| **مثال إخباري رياضي (يجب رفضه):** | |
| "لويس سواريز بعد المباراة خرج قال كنا نستطيع الفوز... الشناوي تألق ومنع 3 أهداف مؤكدة... فاز بجائزة رجل المباراة" | |
| **مثال مؤتمر صحفي (يجب رفضه):** | |
| "في مؤتمر صحفي اليوم، صرح الوزير بأن الحكومة ستتخذ إجراءات..." | |
| **مثال اجتماع (يجب رفضه):** | |
| "خلال اجتماع مجلس الإدارة أمس، تم الاتفاق على..." | |
| ## أمثلة للقبول: | |
| **قصة أدبية (يجب قبولها):** | |
| "كان يجلس في المقهى كل مساء، يراقب الناس ويحلم بحياة أخرى. في ذلك المساء، دخلت امرأة غريبة غيرت كل شيء..." | |
| **حوار درامي (يجب قبوله):** | |
| "قالت له بصوت مرتجف: لماذا تركتني؟ أجاب وهو يتجنب نظراتها: بعض الأشياء لا يمكن إصلاحها..." | |
| ## الاستجابة المطلوبة: | |
| بعد المراجعة، أجب بكلمة واحدة فقط: | |
| - "true" - إذا كان النص قصة أدبية إبداعية خالية من الانتهاكات | |
| - "no" - إذا كان النص إخبارياً أو يحتوي على انتهاكات دينية أو سب فاحش | |
| النص المطلوب مراجعته: | |
| """ | |
| def _call_deepseek_api(self, story_content: str) -> Dict[str, Any]: | |
| """ | |
| Call Deepseek API for content moderation | |
| Args: | |
| story_content: The Arabic story content to moderate | |
| Returns: | |
| API response dictionary | |
| """ | |
| try: | |
| payload = { | |
| "model": "deepseek-chat", | |
| "messages": [ | |
| { | |
| "role": "system", | |
| "content": "أنت مراجع محتوى عربي محترف متخصص في التمييز بين القصص الأدبية والمحتوى الإخباري. يجب عليك رفض أي محتوى إخباري أو صحفي بصرامة." | |
| }, | |
| { | |
| "role": "user", | |
| "content": f"{self.moderation_prompt}\n\n{story_content}" | |
| } | |
| ], | |
| "max_tokens": 10, | |
| "temperature": 0.0, | |
| "stream": False | |
| } | |
| response = requests.post( | |
| self.api_url, | |
| headers=self.headers, | |
| json=payload, | |
| timeout=30 | |
| ) | |
| if response.status_code == 200: | |
| return response.json() | |
| else: | |
| logger.error(f"API Error: {response.status_code} - {response.text}") | |
| return {"error": f"API Error: {response.status_code}"} | |
| except Exception as e: | |
| logger.error(f"Exception calling Deepseek API: {str(e)}") | |
| return {"error": str(e)} | |
| def _pre_check_news_content(self, story_content: str) -> bool: | |
| """ | |
| Pre-check for obvious news content patterns | |
| Args: | |
| story_content: Content to check | |
| Returns: | |
| True if appears to be news content, False otherwise | |
| """ | |
| # News indicators in Arabic | |
| news_patterns = [ | |
| r'بعد المباراة.*قال', | |
| r'في مؤتمر صحفي', | |
| r'صرح.*الوزير', | |
| r'أعلن.*المسؤول', | |
| r'فاز.*بجائزة.*رجل المباراة', | |
| r'تألق.*ومنع.*أهداف', | |
| r'خلال.*الاجتماع', | |
| r'في.*الجلسة', | |
| r'الرئيس.*التقى', | |
| r'البرلمان.*ناقش', | |
| r'الحكومة.*قررت', | |
| r'البورصة.*ارتفعت', | |
| r'أسعار.*النفط', | |
| r'الشركة.*حققت', | |
| r'المحافظ.*افتتح', | |
| r'بلدية.*المدينة', | |
| r'التطبيق.*الجديد', | |
| r'الهاتف.*يتميز', | |
| r'في.*محافظة' | |
| ] | |
| # Check for news patterns | |
| for pattern in news_patterns: | |
| if re.search(pattern, story_content, re.IGNORECASE): | |
| return True | |
| # Check for sports-specific terms | |
| sports_terms = ['المباراة', 'اللاعب', 'المدرب', 'الفريق', 'الهدف', 'الشوط'] | |
| news_verbs = ['صرح', 'أعلن', 'أكد', 'قال', 'فاز', 'تألق'] | |
| has_sports = any(term in story_content for term in sports_terms) | |
| has_news_verbs = any(verb in story_content for verb in news_verbs) | |
| if has_sports and has_news_verbs: | |
| return True | |
| return False | |
| def _validate_story_format(self, story_content: str) -> bool: | |
| """ | |
| Enhanced validation of story format and content | |
| Args: | |
| story_content: Story content to validate | |
| Returns: | |
| Boolean indicating if format is valid | |
| """ | |
| if not story_content or not isinstance(story_content, str): | |
| return False | |
| # Check minimum length (at least 50 characters for a meaningful story) | |
| if len(story_content.strip()) < 50: | |
| return False | |
| # Check for Arabic characters (must have substantial Arabic content) | |
| arabic_pattern = re.compile(r'[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]') | |
| arabic_chars = len(arabic_pattern.findall(story_content)) | |
| # Arabic characters should be at least 30% of total characters | |
| if arabic_chars < len(story_content.strip()) * 0.3: | |
| return False | |
| # Pre-check for obvious news content | |
| if self._pre_check_news_content(story_content): | |
| return False | |
| return True | |
| def moderate_story(self, story_content: str) -> Dict[str, Any]: | |
| """ | |
| Main method to moderate Arabic story content with enhanced validation | |
| Args: | |
| story_content: The Arabic story to moderate | |
| Returns: | |
| Dictionary with moderation result | |
| """ | |
| # Enhanced validation | |
| if not self._validate_story_format(story_content): | |
| return { | |
| "approved": False, | |
| "response": "no", | |
| "reason": "المحتوى يبدو أنه تقرير إخباري أو صحفي وليس قصة أدبية، أو فشل في التحقق من صحة التنسيق", | |
| "timestamp": datetime.now().isoformat() | |
| } | |
| # Clean and prepare content | |
| cleaned_content = story_content.strip() | |
| # Call Deepseek API | |
| api_response = self._call_deepseek_api(cleaned_content) | |
| if "error" in api_response: | |
| logger.error(f"Moderation failed: {api_response['error']}") | |
| return { | |
| "approved": False, | |
| "response": "no", | |
| "reason": "خطأ في خدمة المراجعة", | |
| "error": api_response["error"], | |
| "timestamp": datetime.now().isoformat() | |
| } | |
| try: | |
| # Extract the moderation decision | |
| ai_response = api_response.get("choices", [{}])[0].get("message", {}).get("content", "").strip().lower() | |
| # Clean the response (remove any extra whitespace or characters) | |
| ai_response = re.sub(r'[^\w]', '', ai_response) | |
| # Determine if content is approved (be more strict) | |
| approved = ai_response == "true" | |
| response_value = "true" if approved else "no" | |
| result = { | |
| "approved": approved, | |
| "response": response_value, | |
| "ai_decision": ai_response, | |
| "timestamp": datetime.now().isoformat(), | |
| "content_length": len(cleaned_content) | |
| } | |
| if not approved: | |
| result["reason"] = "المحتوى ينتهك القواعد المجتمعية أو الثقافية أو الدينية، أو أنه ليس قصة أدبية حقيقية بل محتوى إخباري" | |
| else: | |
| result["reason"] = "المحتوى مقبول ويلتزم بالمعايير المطلوبة" | |
| logger.info(f"Moderation completed: {response_value} for content of length {len(cleaned_content)}") | |
| return result | |
| except Exception as e: | |
| logger.error(f"Error processing API response: {str(e)}") | |
| return { | |
| "approved": False, | |
| "response": "no", | |
| "reason": "خطأ في معالجة نتيجة المراجعة", | |
| "error": str(e), | |
| "timestamp": datetime.now().isoformat() | |
| } | |
| # Flask application | |
| app = Flask(__name__) | |
| # Initialize the moderator (API key will be set via environment variable) | |
| try: | |
| moderator = ArabicContentModerator() | |
| logger.info("Arabic Content Moderator initialized successfully") | |
| except ValueError as e: | |
| logger.error(f"Failed to initialize moderator: {e}") | |
| moderator = None | |
| def home(): | |
| """Home endpoint with API documentation""" | |
| return jsonify({ | |
| "service": "مراجع المحتوى الأدبي العربي المحسن مع كشف الأخبار", | |
| "service_en": "Enhanced Arabic Literary Content Moderator with News Detection", | |
| "version": "3.0.0", | |
| "description": "AI-powered professional literary critic for Arabic short stories with enhanced news content detection", | |
| "description_ar": "ناقد أدبي محترف مدعوم بالذكاء الاصطناعي للقصص العربية القصيرة مع كشف محسن للمحتوى الإخباري", | |
| "endpoints": { | |
| "/health": "Health check", | |
| "/moderate": "POST - Moderate single story", | |
| "/moderate/batch": "POST - Moderate multiple stories" | |
| }, | |
| "features": [ | |
| "Enhanced news content detection and rejection", | |
| "Sports reporting detection", | |
| "Press conference content filtering", | |
| "Meeting and event content filtering", | |
| "Religious and cultural compliance checking", | |
| "Professional literary criticism standards", | |
| "Comprehensive profanity detection" | |
| ], | |
| "rejected_content_types": [ | |
| "Sports reports and match analysis", | |
| "Press conferences and official statements", | |
| "Meeting minutes and proceedings", | |
| "Political news and announcements", | |
| "Economic reports and market updates", | |
| "Technical reviews and product launches", | |
| "Local news and municipal updates" | |
| ], | |
| "usage": { | |
| "moderate": { | |
| "method": "POST", | |
| "payload": {"story_content": "Arabic story text"}, | |
| "response": {"approved": "boolean", "response": "true/no"} | |
| } | |
| }, | |
| "status": "healthy" if moderator else "service unavailable" | |
| }) | |
| def health_check(): | |
| """Health check endpoint""" | |
| return jsonify({ | |
| "status": "healthy" if moderator else "unhealthy", | |
| "service": "Enhanced Arabic Content Moderator with News Detection", | |
| "timestamp": datetime.now().isoformat(), | |
| "api_available": moderator is not None | |
| }) | |
| def moderate_content(): | |
| """ | |
| Enhanced moderation endpoint with news detection | |
| Expected JSON payload: | |
| { | |
| "story_content": "Arabic story text here" | |
| } | |
| Returns: | |
| { | |
| "approved": true/false, | |
| "response": "true"/"no", | |
| "reason": "reason in Arabic", | |
| "timestamp": "ISO timestamp" | |
| } | |
| """ | |
| if not moderator: | |
| return jsonify({ | |
| "error": "خدمة المراجعة غير متوفرة - لم يتم تكوين مفتاح API", | |
| "error_en": "Moderation service not available - API key not configured", | |
| "approved": False, | |
| "response": "no" | |
| }), 500 | |
| try: | |
| data = request.get_json() | |
| if not data or 'story_content' not in data: | |
| return jsonify({ | |
| "error": "محتوى القصة مفقود في الطلب", | |
| "error_en": "Missing story_content in request", | |
| "approved": False, | |
| "response": "no" | |
| }), 400 | |
| story_content = data['story_content'] | |
| result = moderator.moderate_story(story_content) | |
| return jsonify(result) | |
| except Exception as e: | |
| logger.error(f"Error in moderate_content: {str(e)}") | |
| return jsonify({ | |
| "error": "خطأ داخلي في الخادم", | |
| "error_en": "Internal server error", | |
| "approved": False, | |
| "response": "no", | |
| "details": str(e) | |
| }), 500 | |
| def moderate_batch(): | |
| """ | |
| Enhanced batch moderation endpoint | |
| Expected JSON payload: | |
| { | |
| "stories": ["story1", "story2", "story3"] | |
| } | |
| """ | |
| if not moderator: | |
| return jsonify({ | |
| "error": "خدمة المراجعة غير متوفرة - لم يتم تكوين مفتاح API", | |
| "error_en": "Moderation service not available - API key not configured" | |
| }), 500 | |
| try: | |
| data = request.get_json() | |
| if not data or 'stories' not in data: | |
| return jsonify({ | |
| "error": "مصفوفة القصص مفقودة في الطلب", | |
| "error_en": "Missing stories array in request" | |
| }), 400 | |
| stories = data['stories'] | |
| if not isinstance(stories, list): | |
| return jsonify({ | |
| "error": "القصص يجب أن تكون في شكل مصفوفة", | |
| "error_en": "Stories must be an array" | |
| }), 400 | |
| results = [] | |
| approved_count = 0 | |
| for i, story in enumerate(stories): | |
| logger.info(f"Moderating story {i+1}/{len(stories)}") | |
| result = moderator.moderate_story(story) | |
| results.append({ | |
| "story_index": i, | |
| "result": result | |
| }) | |
| if result.get("approved", False): | |
| approved_count += 1 | |
| return jsonify({ | |
| "results": results, | |
| "summary": { | |
| "total_processed": len(results), | |
| "approved_count": approved_count, | |
| "rejected_count": len(results) - approved_count, | |
| "approval_rate": f"{(approved_count/len(results)*100):.1f}%" if results else "0%" | |
| }, | |
| "timestamp": datetime.now().isoformat() | |
| }) | |
| except Exception as e: | |
| logger.error(f"Error in moderate_batch: {str(e)}") | |
| return jsonify({ | |
| "error": "خطأ داخلي في الخادم", | |
| "error_en": "Internal server error", | |
| "details": str(e) | |
| }), 500 | |
| if __name__ == '__main__': | |
| # For local testing | |
| port = int(os.environ.get('PORT', 7860)) | |
| app.run(host='0.0.0.0', port=port, debug=False) |