File size: 8,505 Bytes
66aa4ff
 
 
77811fe
66aa4ff
265ec44
7d0356e
1a80857
 
66aa4ff
 
 
 
 
 
 
 
 
 
265ec44
669181c
 
 
 
 
 
 
 
 
 
265ec44
 
669181c
 
 
 
 
 
66aa4ff
265ec44
77811fe
 
265ec44
669181c
 
265ec44
669181c
 
 
77811fe
 
669181c
77811fe
 
669181c
 
 
 
 
 
77811fe
 
669181c
1a80857
14f39e7
669181c
 
 
265ec44
 
669181c
 
 
 
265ec44
669181c
 
 
 
265ec44
669181c
 
 
 
 
 
 
 
 
 
 
77811fe
 
 
 
1a80857
 
 
14f39e7
669181c
1a80857
66aa4ff
 
 
 
 
 
1a80857
14f39e7
669181c
77811fe
669181c
14f39e7
669181c
77811fe
 
669181c
 
 
77811fe
669181c
77811fe
 
669181c
 
 
 
 
77811fe
 
 
669181c
77811fe
 
14f39e7
77811fe
 
 
669181c
b0b2248
77811fe
669181c
 
 
 
 
66aa4ff
 
 
14f39e7
669181c
 
b0b2248
 
66aa4ff
 
b0b2248
 
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
import gradio as gr
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
import re


model_path = "." 
tokenizer = AutoTokenizer.from_pretrained(model_path, local_files_only=True)
model = AutoModelForSequenceClassification.from_pretrained(model_path, local_files_only=True)

id2label = {
    0: 'Fluency Disorder (تأتأة)',
    1: 'Language Disorder (تأخر لغة)',
    2: 'Motor Speech Disorder (اضطراب حركي)',
    3: 'Other/Medical (أخرى/طبية)',
    4: 'Social Communication/ASD (تواصل واجتماعي)',
    5: 'Speech Sound Disorder (مخارج الحروف)'
}

#  (Data Source: Amayreh & Dyson 1998 + ASHA) ---

SOUND_NORMS = {
    # المجموعة الأولى (تكتمل حوالي 3 سنوات)
    'ب': 3, 'م': 3, 'ن': 3, 'و': 3, 'ه': 3, 'د': 3, 'ت': 3, 'ك': 3, 'ف': 3, 'ل': 3, 'ي': 3,
    # المجموعة الثانية (تكتمل بين 4 - 5 سنوات)
    'س': 5, 'ز': 5, 'ش': 5, 'ج': 5,
    # المجموعة الثالثة (تكتمل حوالي 6 سنوات)
    'ر': 6, 'ث': 6, 'ذ': 6, 'ص': 6, 'ض': 6, 'ط': 6, 'ظ': 6, 'ع': 6, 'غ': 6, 'خ': 6
}

# (Phonological Processes)

PHONO_PROCESSES = {
    'fronting': {'age': 3.5, 'keywords': ['كاف', 'قاف', 'غين', 'خاء'], 'desc': 'التقديم (Fronting): استبدال الأصوات الخلفية بأمامية (مثل: كتاب -> تتاب) طبيعي حتى 3.5 سنوات.'},
    'gliding': {'age': 6.0, 'keywords': ['راء', 'لام', 'واو', 'ياء'], 'desc': 'الانزلاق (Gliding): إبدال الراء أو اللام بـ (واو/ياء) عملية طبيعية قد تستمر حتى 6 سنوات.'},
    'final_deletion': {'age': 3.0, 'keywords': ['اخر الكلمة', 'يحذف', 'نهاية'], 'desc': 'حذف الحرف الأخير (Final Consonant Deletion) طبيعي حتى عمر 3 سنوات فقط.'},
    'stopping': {'age': 4.0, 'keywords': ['فاء', 'سين', 'شين', 'صاد'], 'desc': 'الوقف (Stopping): استبدال الأصوات الاحتكاكية بأصوات انفجارية طبيعي حتى 3-4 سنوات.'}
}

# (Logic Layer) 

def extract_age(text):

    text = text.replace('١', '1').replace('٢', '2').replace('٣', '3').replace('٤', '4').replace('٥', '5').replace('٦', '6')
    

    match_half = re.search(r'(\d+)\s*(ونصف|و نص)', text)
    if match_half:
        return float(match_half.group(1)) + 0.5

    numbers = re.findall(r'\d+', text)
    if numbers: return float(numbers[0])
    
    text = text.lower()
    if 'سنتين' in text or 'عامين' in text: return 2.0
    if 'سنة' in text or 'عام' in text: return 1.0
    if 'ثلاث' in text: return 3.0
    if 'أربع' in text: return 4.0
    if 'خمس' in text: return 5.0
    if 'ست' in text: return 6.0
    return None

def check_clinical_logic(age, text, predicted_label):
    text = text.lower()
    
    if age is None:
        return predicted_label, None


    # Gliding 
    if any(k in text for k in PHONO_PROCESSES['gliding']['keywords']) and ('يبدل' in text or 'يقلب' in text or 'ما يقول' in text):
        if age < PHONO_PROCESSES['gliding']['age']:
            return "Phonological Process (عملية نطقية)", PHONO_PROCESSES['gliding']['desc']

    # Fronting
    if any(k in text for k in PHONO_PROCESSES['fronting']['keywords']) and ('يبدل' in text or 'تاء' in text or 'دال' in text):
        if age <= PHONO_PROCESSES['fronting']['age']:
            return "Phonological Process (عملية نطقية)", PHONO_PROCESSES['fronting']['desc']

    # 2. Sound Norms
    for sound, norm_age in SOUND_NORMS.items():
        patterns = [f"حرف ال{sound}", f"حرف {sound}", f"صوت ال{sound}", f"ال{sound}"]
        if any(p in text for p in patterns):
            if age < norm_age:
                return f"Age Appropriate Error (خطأ تطوري طبيعي)", f"حسب معايير (Amayreh & Dyson)، اكتساب صوت ({sound}) يكتمل عند {norm_age} سنوات. وعمر الطفل ({age}) سنوات، فالأمر طبيعي."

    if age < 4 and any(w in text for w in ['قصة', 'سالفة', 'يسرد']):
        return "Normal Development (تطور طبيعي)", "مهارة السرد القصصي تكتمل عادة بعد عمر 4 سنوات."

    if (2 <= age <= 4) and any(w in text for w in ['تأتأة', 'يعلق', 'يكرر']):
        return "Developmental Disfluency (تأتأة نمائية)", "التأتأة الطبيعية شائعة بين 2-4 سنوات وتختفي غالباً تلقائياً."

    return predicted_label, None

# -----------------------------------------

def predict_speech_issue(text):
    if not text.strip(): return "الرجاء كتابة وصف الحالة"
    
    # AI Prediction
    inputs = tokenizer(text, return_tensors="pt", truncation=True, padding=True)
    with torch.no_grad():
        outputs = model(**inputs)
        probs = torch.nn.functional.softmax(outputs.logits, dim=-1)
        pred_id = torch.argmax(probs).item()
        confidence = probs[0][pred_id].item()

    ai_label = id2label[pred_id]
    
    # Clinical Logic Check
    age = extract_age(text)
    final_label, logic_reason = check_clinical_logic(age, text, ai_label)
    
    # Formatting
    if logic_reason:
        advice = logic_reason
        color = "#27ae60" # Green
        badge = "✅ تحليل نمائي (Evidence-Based)"
        confidence_display = "استناداً للمعايير العلمية"
    else:
        # Default AI Recommendations
        recommendations = {
            'Speech Sound Disorder (مخارج الحروف)': "ينصح بتقييم مخارج الحروف وفحص أعضاء النطق (Oral Motor Exam).",
            'Language Disorder (تأخر لغة)': "ينصح بعمل اختبار لغة شامل (مثل PLS-4 أو معرب).",
            'Social Communication/ASD (تواصل واجتماعي)': "ينصح بمراجعة طبيب نمو وسلوك وعمل مقياس كارز أو ADOS.",
            'Fluency Disorder (تأتأة)': "ينصح بتقييم الطلاقة ومراقبة البيئة التواصلية.",
            'Motor Speech Disorder (اضطراب حركي)': "يحتاج تقييم دقيق (Apraxia/Dysarthria) من قبل مختص.",
            'Other/Medical (أخرى/طبية)': "⛔ يفضل استشارة طبيب أنف وأذن وحنجرة (ENT) لفحص السمع أو الحبال الصوتية."
        }
        advice = recommendations.get(final_label, "يرجى استشارة مختص.")
        color = "#c0392b" if "Medical" in final_label else "#2980b9"
        badge = "🤖 تصنيف الذكاء الاصطناعي"
        confidence_display = f"{confidence:.1%}"

    return f"""
    <div style="direction: rtl; text-align: right; padding: 20px; background-color: #fff; border-radius: 12px; border-right: 6px solid {color}; box-shadow: 0 4px 15px rgba(0,0,0,0.05);">
        <div style="display: flex; justify-content: space-between; align-items: center;">
            <h2 style="color: {color}; margin: 0;">{final_label}</h2>
            <span style="background-color: #f1f2f6; padding: 6px 12px; border-radius: 20px; font-size: 0.8em; color: #555;">{badge}</span>
        </div>
        <p style="color: #7f8c8d; margin-top: 5px;"><b>الثقة:</b> {confidence_display}</p>
        <hr style="border-top: 1px dashed #ddd; margin: 15px 0;">
        <div style="background-color: #fcfcfc; padding: 10px; border-radius: 8px;">
            <p style="font-size: 1.1em; color: #2c3e50; line-height: 1.6;"><b>💡 الرأي المختص:</b><br>{advice}</p>
        </div>
        <p style="font-size: 0.7em; color: #95a5a6; margin-top: 15px;">* المصادر: Amayreh & Dyson (1998), ASHA Guidelines.</p>
    </div>
    """

with gr.Blocks(theme=gr.themes.Soft()) as demo:
    gr.Markdown("<h1 style='text-align: center'>🇸🇦 أداة الفرز الذكية لاضطرابات النطق - المراعية للهجات السعودية</h1>")
    input_txt = gr.Textbox(label="وصف الحالة (يرجى ذكر العمر)", placeholder="مثال: ولدي عمره 3 سنوات ونص ويبدل الكاف تاء")
    btn = gr.Button("تحليل الحالة", variant="primary")
    output = gr.HTML(label="التقرير")
    btn.click(fn=predict_speech_issue, inputs=input_txt, outputs=output)

if __name__ == "__main__":
    demo.launch()