File size: 7,716 Bytes
92557ee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# -*- coding: utf-8 -*-
"""Untitled18.ipynb

Automatically generated by Colab.

Original file is located at
    https://colab.research.google.com/drive/1ebBGzEo4wbwwvReea_n0PRHdfYescKcs
"""

import os
import torch
from transformers import EncoderDecoderModel, AutoTokenizer
import re

# تعريف الثوابت
HF_REPO_ID = "bayan10/PuncAra-v1"

# متغيرات عامة
device = None
test_model = None
test_tokenizer = None

def initialize_model(repo_id=HF_REPO_ID):
    """
    تهيئة وإعداد كرت الشاشة وتحميل النموذج والـ Tokenizer من Hugging Face Hub.
    يتم استدعاء هذه الدالة مرة واحدة فقط في بداية تشغيل المشروع.
    """
    global device, test_model, test_tokenizer
    print(f"Loading test model directly from Hugging Face Hub: {repo_id}")
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print(f"Loading test model to: {device}")

    if device.type == "cuda" and not torch.cuda.is_available():
        print("Warning: CUDA device requested, but torch.cuda.is_available() is False. Model will be loaded to CPU.")
        device = torch.device("cpu")

    test_model = EncoderDecoderModel.from_pretrained(repo_id).to(device)
    test_tokenizer = AutoTokenizer.from_pretrained(repo_id)

    # إعداد الـ Special tokens للـ Decoder والـ Encoder
    test_model.config.decoder_start_token_id = test_tokenizer.cls_token_id
    test_model.config.bos_token_id = test_tokenizer.cls_token_id
    test_model.config.eos_token_id = test_tokenizer.sep_token_id
    test_model.config.pad_token_id = test_tokenizer.pad_token_id
    print("Model and Tokenizer loaded successfully!")

def predict_chunk(text_chunk):
    """توليد التوقعات لعلامات الترقيم لقطعة نصية صغيرة لا تتعدى الـ 128 Token."""
    global device, test_model, test_tokenizer
    if test_model is None or test_tokenizer is None:
        raise RuntimeError("الموديل لم يتم تهيئته بعد. يرجى استدعاء initialize_model() أولاً.")

    # تطبيق الـ Preprocessing لتنظيف التشكيل قبل دخول النص للموديل
    text_chunk = arabic_preprocessing(text_chunk)

    inputs = test_tokenizer(text_chunk, return_tensors="pt", padding=True, truncation=True, max_length=128).to(device)

    outputs = test_model.generate(
          inputs.input_ids,
          attention_mask=inputs.attention_mask,
          decoder_start_token_id=test_tokenizer.cls_token_id,
          bos_token_id=test_tokenizer.cls_token_id,
          eos_token_id=test_tokenizer.sep_token_id,
          pad_token_id=test_tokenizer.pad_token_id,
          max_length=128,
          num_beams=3,
          repetition_penalty=1.2,
          length_penalty=1.0,
          early_stopping=True,
          do_sample=False
      )
    return test_tokenizer.decode(outputs[0], skip_special_tokens=True)

def arabic_preprocessing(text):
    """حذف الحركات التشكيلية لتوحيد المدخلات وتسهيل عمل الموديل."""
    arabic_diacritics = re.compile(r'[\u064B-\u0652]')
    return re.sub(arabic_diacritics, '', text).strip()

def arabic_postprocessing(text):
    """
    التنظيف والتحسين المطبعي وعلاج مشاكل دمج النصوص وعلامات الترقيم الزائدة.
    """
    if not text:
        return text

    # 1. حماية الأرقام والكسور والتوقيت من التحويل الخاطئ
    text = re.sub(r'(?<=\d),(?=\d)', '٪TEMP_COMMA٪', text)
    text = re.sub(r'(?<=\d):(?=\d)', '٪TEMP_COLON٪', text)

    # 2. التوحيد والتعريب المطبعي للعلامات
    text = text.replace(',', '،').replace(';', '؛').replace('?', '؟')

    # 3. ضبط المسافات الداخلية للأقواس وعلامات الاقتباس العربي
    text = re.sub(r'\(\s+', '(', text)
    text = re.sub(r'\s+\)', ')', text)
    text = re.sub(r'\[\s+', '[', text)
    text = re.sub(r'\s+\]', ']', text)
    text = re.sub(r'«\s+', '«', text)
    text = re.sub(r'\s+»', '»', text)

    # 4. منع تكرار العلامات الانفعالية عدا النقاط الثلاثية للحذف
    text = re.sub(r'([،؛:!؟])\1+', r'\1', text)
    text = re.sub(r'\.{4,}', '...', text)

    # 5. معالجة التناقضات المباشرة الناتجة عن تجميع الـ Chunks
    text = re.sub(r'[،؛:]+([.!؟])', r'\1', text)
    text = re.sub(r'،؛|؛،', '؛', text)
    text = re.sub(r'([!؟])\.', r'\1', text)

    # 6. مسح علامات الترقيم العشوائية إذا ظهرت أول النص
    text = re.sub(r'^[،؛:!؟. \t]+', '', text)

    # 7. ضمان مسافة فارغة واحدة بعد علامة الترقيم إذا تبعها كلام
    text = re.sub(r'([،؛:!؟.])(?=\S)', r'\1 ', text)

    # 8. إعادة الأرقام والكسور والتوقيت المحمية إلى أصلها
    text = text.replace('٪TEMP_COMMA٪', ',').replace('٪TEMP_COLON٪', ':')

    # 9. إلصاق علامات الترقيم بالكلمة السابقة لها مباشرة
    text = re.sub(r'\s+([،؛:!؟.])', r'\1', text)

    # 10. إزالة المسافات المتكررة الأفقية فقط (بدون لمس السطور الجديدة)
    text = re.sub(r'[ \t]+', ' ', text).strip()
    return text

def fix_punctuation(text):
    """معالجة الفقرة الواحدة الطويلة عبر تقسيمها لقطع غير متداخلة لمنع التكرار."""
    words = text.split()
    total_words = len(words)

    # جعل حجم الخطوة مساوياً لحجم النافذة يمنع تكرار الكلمات تماماً
    window_size = 50
    stride = 50

    if total_words <= window_size:
        result = predict_chunk(text)
    else:
        segments_output = []

        for i in range(0, total_words, stride):
            chunk_words = words[i : i + window_size]
            chunk_text = " ".join(chunk_words)

            if not chunk_text.strip():
                continue

            processed_segment = predict_chunk(chunk_text).strip()

            # مسح علامات الترقيم الناتجة عن القص الإجباري بين القطع
            is_last_segment = (i + window_size) >= total_words
            if not is_last_segment:
                punctuation_marks = ".?!،؛:؟!"
                if processed_segment and processed_segment[-1] in punctuation_marks:
                    # نمسح العلامة تماماً لأن السياق مستمر في القطعة اللي بعدها
                    processed_segment = processed_segment[:-1]

            segments_output.append(processed_segment)

        result = " ".join(segments_output)

    # تنظيف المسافات الزائدة والتكرار إن وجد
    result = re.sub(r'\s+', ' ', result).strip()
    return result

def process_full_document(text):
    if not text:
        return text

    # تقسيم بناءً على السطور الجديدة وتنظيف الأسطر الفارغة
    paragraphs = [p.strip() for p in text.split('\n') if p.strip()]
    processed_paragraphs = []

    for paragraph in paragraphs:
        # معالجة الفقرة المستقلة
        punctuated_paragraph = fix_punctuation(paragraph)
        cleaned_paragraph = arabic_postprocessing(punctuated_paragraph)
        processed_paragraphs.append(cleaned_paragraph)

    # الدمج بسطرين متباعدين لضمان الفصل البصري التام بين الفقرات
    return "\n\n".join(processed_paragraphs)