File size: 20,625 Bytes
930b9b6
 
 
32893cc
930b9b6
 
 
 
 
 
 
 
 
32893cc
930b9b6
d8baaa6
 
930b9b6
 
 
 
32893cc
930b9b6
 
32893cc
930b9b6
 
 
32893cc
930b9b6
 
 
 
 
 
 
15a42d4
930b9b6
15a42d4
ee989b2
15a42d4
ee989b2
15a42d4
 
ee989b2
15a42d4
 
 
 
 
 
 
ee989b2
15a42d4
 
 
 
 
 
 
ee989b2
15a42d4
 
 
 
 
 
 
ee989b2
15a42d4
 
 
 
 
 
 
ee989b2
15a42d4
 
 
 
 
 
 
ee989b2
15a42d4
 
 
 
 
 
 
ee989b2
15a42d4
 
 
 
 
 
 
ee989b2
15a42d4
 
 
 
 
 
 
ee989b2
15a42d4
 
 
 
 
 
 
 
 
 
ee989b2
15a42d4
 
 
 
 
 
 
 
ee989b2
15a42d4
 
 
 
 
 
 
 
ee989b2
15a42d4
ee989b2
15a42d4
 
ee989b2
15a42d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32893cc
930b9b6
 
 
32893cc
930b9b6
 
 
 
 
 
 
 
 
 
 
 
 
15a42d4
930b9b6
 
d8baaa6
930b9b6
 
 
33c1a0e
15a42d4
930b9b6
 
 
 
d8baaa6
 
930b9b6
 
 
 
 
 
 
 
 
 
d8baaa6
 
 
 
15a42d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d8baaa6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15a42d4
 
 
 
d8baaa6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15a42d4
d8baaa6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15a42d4
d8baaa6
 
 
 
 
 
930b9b6
 
 
 
 
4109a53
930b9b6
32893cc
12fb818
 
2593a57
32893cc
930b9b6
 
2593a57
930b9b6
32893cc
d8baaa6
930b9b6
 
 
 
 
 
32893cc
930b9b6
15a42d4
 
 
 
 
930b9b6
 
 
d8baaa6
32893cc
 
15a42d4
 
 
 
 
2593a57
15a42d4
 
 
 
 
 
 
 
 
 
ee989b2
32893cc
 
 
 
d8baaa6
32893cc
930b9b6
 
 
 
 
 
 
 
 
15a42d4
930b9b6
d8baaa6
930b9b6
 
 
 
32893cc
15a42d4
32893cc
 
 
 
 
 
 
 
 
 
d8baaa6
32893cc
 
 
930b9b6
 
32893cc
 
930b9b6
2593a57
930b9b6
 
 
 
 
 
 
32893cc
 
930b9b6
2593a57
930b9b6
 
 
 
 
 
 
 
 
 
32893cc
33c1a0e
930b9b6
 
12fb818
 
 
930b9b6
 
32893cc
d8baaa6
32893cc
 
 
 
 
 
930b9b6
 
32893cc
2593a57
930b9b6
 
 
 
 
 
 
32893cc
2593a57
930b9b6
 
 
 
 
32893cc
2593a57
930b9b6
 
 
33c1a0e
 
930b9b6
 
 
33c1a0e
 
 
 
 
 
930b9b6
 
 
33c1a0e
 
 
 
d8baaa6
33c1a0e
930b9b6
 
 
 
 
 
32893cc
 
930b9b6
 
 
 
 
d9bd647
d8baaa6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
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

@app.route('/', methods=['GET'])
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"
    })

@app.route('/health', methods=['GET'])
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
    })

@app.route('/moderate', methods=['POST'])
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

@app.route('/moderate/batch', methods=['POST'])
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)