Spaces:
Running
Running
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) |