batool0 commited on
Commit
eea45a6
·
verified ·
1 Parent(s): ccaf68b

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +382 -0
app.py ADDED
@@ -0,0 +1,382 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import joblib
3
+ import torch
4
+ import torch.nn as nn
5
+ import numpy as np
6
+ import pandas as pd
7
+ from transformers import AutoTokenizer, AutoModel, AutoModelForSequenceClassification
8
+ from sklearn.preprocessing import LabelEncoder
9
+ from huggingface_hub import hf_hub_download
10
+ import re
11
+ import pyarabic.araby as araby
12
+
13
+ # Constants
14
+ MODEL_NAME = "aubmindlab/bert-base-arabertv02"
15
+ MODEL_HUB = "batool0/arabic-speech-act-models"
16
+ MAX_LEN = 64
17
+ CLASSES = ['Assertion', 'Expression', 'Question', 'Recommendation', 'Request']
18
+ CLASS_AR = {
19
+ 'Assertion': 'تأكيد',
20
+ 'Expression': 'تعبير',
21
+ 'Question': 'سؤال',
22
+ 'Recommendation': 'توصية',
23
+ 'Request': 'طلب'
24
+ }
25
+ ERROR_PATTERNS = {
26
+ ('Expression', 'Assertion'): "Expression/Assertion boundary: tweets describing events with emotional tone are often misclassified as Assertion.",
27
+ ('Assertion', 'Expression'): "Expression/Assertion boundary: factual tweets with emotional vocabulary are sometimes misclassified as Expression.",
28
+ ('Question', 'Expression'): "Implicit question: this question lacks an explicit interrogative particle (هل، ماذا), causing it to resemble an Expression.",
29
+ ('Expression', 'Question'): "Implicit question: emotionally phrased tweet contains question-like vocabulary.",
30
+ ('Request', 'Assertion'): "Analytical request: the request is framed as a logical argument, resembling an Assertion.",
31
+ ('Request', 'Recommendation'): "Request vs Recommendation: the boundary between requesting and recommending is thin in Arabic.",
32
+ ('Recommendation', 'Expression'): "Sarcastic recommendation misread as emotional Expression.",
33
+ ('Assertion', 'Question'): "Assertion/Question boundary: the tweet may contain an implicit question structure without explicit particles.",
34
+ ('Question', 'Assertion'): "Question/Assertion boundary: the question is phrased as a statement, common in Arabic rhetorical questions.",
35
+ }
36
+
37
+ CLASS_DESCRIPTIONS = {
38
+ 'Assertion': "states a fact or conveys information objectively.",
39
+ 'Expression': "expresses an opinion, emotion, or personal feeling.",
40
+ 'Question': "asks for information or seeks clarification.",
41
+ 'Recommendation': "suggests or advises a course of action.",
42
+ 'Request': "asks someone to do something or take action.",
43
+ }
44
+
45
+ # Text Cleaning
46
+ def clean_text(text):
47
+ text = re.sub(r'http\S+|www\S+', '', text)
48
+ text = re.sub(r'@\w+', '', text)
49
+ text = re.sub(r'#\w+', '', text)
50
+ text = re.sub(r'\d+', '', text)
51
+ text = re.sub(r'[^\w\s\u0600-\u06FF]', '', text)
52
+ text = araby.strip_tashkeel(text)
53
+ text = araby.strip_tatweel(text)
54
+ text = re.sub(r'[إأآا]', 'ا', text)
55
+ text = re.sub(r'ة', 'ه', text)
56
+ text = re.sub(r'ى', 'ي', text)
57
+ text = re.sub(r'\s+', ' ', text).strip()
58
+ return text
59
+
60
+ # BiLSTM Architecture
61
+ class AraBERTBiLSTM(nn.Module):
62
+ def __init__(self, bert_model_name, hidden_dim, num_layers, num_classes, dropout=0.3):
63
+ super(AraBERTBiLSTM, self).__init__()
64
+ self.bert = AutoModel.from_pretrained(bert_model_name)
65
+ for param in self.bert.parameters():
66
+ param.requires_grad = False
67
+ bert_hidden_size = self.bert.config.hidden_size
68
+ self.bilstm = nn.LSTM(
69
+ input_size=bert_hidden_size,
70
+ hidden_size=hidden_dim,
71
+ num_layers=num_layers,
72
+ batch_first=True,
73
+ bidirectional=True,
74
+ dropout=dropout if num_layers > 1 else 0.0
75
+ )
76
+ self.dropout = nn.Dropout(dropout)
77
+ self.classifier = nn.Linear(hidden_dim * 2, num_classes)
78
+
79
+ def forward(self, input_ids, attention_mask):
80
+ with torch.no_grad():
81
+ bert_output = self.bert(input_ids=input_ids, attention_mask=attention_mask)
82
+ token_embeddings = bert_output.last_hidden_state
83
+ lstm_output, _ = self.bilstm(token_embeddings)
84
+ mask = attention_mask.unsqueeze(-1).float()
85
+ pooled = (lstm_output * mask).sum(dim=1) / mask.sum(dim=1).clamp(min=1e-9)
86
+ pooled = self.dropout(pooled)
87
+ return self.classifier(pooled)
88
+
89
+ # AraBERTClassifier Architecture
90
+ class AraBERTClassifier(nn.Module):
91
+ def __init__(self, model_name, num_classes, dropout=0.3):
92
+ super(AraBERTClassifier, self).__init__()
93
+ self.bert = AutoModel.from_pretrained(model_name)
94
+ self.dropout = nn.Dropout(dropout)
95
+ self.classifier = nn.Linear(self.bert.config.hidden_size, num_classes)
96
+
97
+ def forward(self, input_ids, attention_mask):
98
+ output = self.bert(input_ids=input_ids, attention_mask=attention_mask)
99
+ cls_output = output.last_hidden_state[:, 0, :]
100
+ cls_output = self.dropout(cls_output)
101
+ logits = self.classifier(cls_output)
102
+ return logits
103
+
104
+ # Load Everything
105
+ print("Loading models...")
106
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
107
+
108
+ le = LabelEncoder()
109
+ le.fit(CLASSES)
110
+
111
+ svm_model = joblib.load('svm_model.pkl')
112
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
113
+
114
+ bilstm_path = hf_hub_download(repo_id=MODEL_HUB, filename='best_bilstm_arabert.pt')
115
+ bilstm_model = AraBERTBiLSTM(MODEL_NAME, hidden_dim=128, num_layers=2, num_classes=5)
116
+ bilstm_model.load_state_dict(torch.load(bilstm_path, map_location=device))
117
+ bilstm_model.to(device)
118
+ bilstm_model.eval()
119
+
120
+ arabert_path = hf_hub_download(repo_id=MODEL_HUB, filename='best_arabert.pt')
121
+ arabert_model = AraBERTClassifier(MODEL_NAME, num_classes=len(CLASSES), dropout=0.1).to(device)
122
+ arabert_model.load_state_dict(torch.load(arabert_path, map_location=device))
123
+ arabert_model.eval()
124
+
125
+ test_df = pd.read_csv('test_with_labels.csv')
126
+ print("All models loaded")
127
+
128
+ # Predict Functions
129
+ def predict_svm(text):
130
+ scores = svm_model.decision_function([text])[0]
131
+ scores = (scores - scores.min()) / (scores.max() - scores.min() + 1e-9)
132
+ pred_idx = scores.argmax()
133
+ pred_class = svm_model.classes_[pred_idx]
134
+ return pred_class, dict(zip(svm_model.classes_, scores.tolist()))
135
+
136
+ def predict_bilstm(text):
137
+ enc = tokenizer(text, max_length=MAX_LEN, padding='max_length',
138
+ truncation=True, return_tensors='pt')
139
+ with torch.no_grad():
140
+ logits = bilstm_model(enc['input_ids'].to(device), enc['attention_mask'].to(device))
141
+ probs = torch.softmax(logits, dim=1).cpu().numpy()[0]
142
+ pred_class = le.classes_[probs.argmax()]
143
+ return pred_class, dict(zip(le.classes_, probs.tolist()))
144
+
145
+ def predict_arabert(text):
146
+ enc = tokenizer(text, max_length=MAX_LEN, padding='max_length',
147
+ truncation=True, return_tensors='pt')
148
+ with torch.no_grad():
149
+ logits = arabert_model(input_ids=enc['input_ids'].to(device),
150
+ attention_mask=enc['attention_mask'].to(device))
151
+ probs = torch.softmax(logits, dim=1).cpu().numpy()[0]
152
+ pred_class = le.classes_[probs.argmax()]
153
+ return pred_class, dict(zip(le.classes_, probs.tolist()))
154
+
155
+ # Ground Truth Lookup
156
+ def get_ground_truth(text):
157
+ cleaned = clean_text(text)
158
+ match = test_df[test_df['text'] == cleaned]
159
+ if len(match) > 0:
160
+ return match.iloc[0]['label']
161
+ return None
162
+
163
+ # Error Analysis for known tweets
164
+ def get_error_analysis(true_label, pred_label):
165
+ key = (true_label, pred_label)
166
+ return ERROR_PATTERNS.get(key, f"The model predicted {pred_label} instead of {true_label}. This may reflect lexical overlap between these classes in Arabic social media text.")
167
+
168
+ # Smart Analysis for new tweets
169
+ def get_smart_analysis(svm_pred, bilstm_pred, ara_pred, svm_probs, bilstm_probs, ara_probs):
170
+ predictions = [svm_pred, bilstm_pred, ara_pred]
171
+ unique_preds = set(predictions)
172
+
173
+ if len(unique_preds) == 1:
174
+ pred = ara_pred
175
+ ara_conf = round(ara_probs.get(pred, 0) * 100)
176
+ if ara_conf >= 80:
177
+ return {'type': 'agree_high', 'message': f"All 3 models confidently agree: this tweet <b style='color:inherit;'>{CLASS_DESCRIPTIONS.get(pred, '')}</b> ({ara_conf}% confidence by AraBERT). This is an unambiguous case."}
178
+ else:
179
+ return {'type': 'agree_low', 'message': f"All 3 models agree on <b style='color:inherit;'>{pred}</b>, but with moderate confidence ({ara_conf}%). The tweet may have overlapping features with other classes."}
180
+
181
+ if len(unique_preds) == 2:
182
+ majority = max(set(predictions), key=predictions.count)
183
+ minority_model = None
184
+ minority_pred = None
185
+ for model_name, pred in [("SVM", svm_pred), ("BiLSTM", bilstm_pred), ("AraBERT", ara_pred)]:
186
+ if pred != majority:
187
+ minority_model = model_name
188
+ minority_pred = pred
189
+ pattern_key = (majority, minority_pred)
190
+ pattern_explanation = ERROR_PATTERNS.get(pattern_key, f"The boundary between {majority} and {minority_pred} can be ambiguous in Arabic social media text.")
191
+ return {'type': 'partial_disagree', 'message': f"2 models agree on <b style='color:inherit;'>{majority}</b> while {minority_model} predicts <b style='color:inherit;'>{minority_pred}</b>. {pattern_explanation}"}
192
+
193
+ return {'type': 'full_disagree', 'message': f"All 3 models disagree — SVM: <b style='color:inherit;'>{svm_pred}</b>, BiLSTM: <b style='color:inherit;'>{bilstm_pred}</b>, AraBERT: <b style='color:inherit;'>{ara_pred}</b>. This tweet is inherently ambiguous, likely due to mixed communicative intent, sarcasm, or dialectal phrasing."}
194
+
195
+ # SVM Top Features
196
+ def get_top_features(text, pred_class):
197
+ vec = svm_model.named_steps['tfidf']
198
+ clf = svm_model.named_steps['svm']
199
+ feature_names = vec.get_feature_names_out()
200
+ transformed = vec.transform([text])
201
+ class_idx = list(svm_model.classes_).index(pred_class)
202
+ scores = transformed.toarray()[0] * clf.coef_[class_idx]
203
+ top_idx = scores.argsort()[-5:][::-1]
204
+ return [feature_names[i] for i in top_idx if scores[i] > 0]
205
+
206
+ # Main Classify Function
207
+ def classify(text):
208
+ if not text.strip():
209
+ return "<p style='color:#555;font-family:sans-serif;'>Please enter an Arabic tweet.</p>"
210
+
211
+ cleaned = clean_text(text)
212
+
213
+ svm_pred, svm_probs = predict_svm(cleaned)
214
+ bilstm_pred, bilstm_probs = predict_bilstm(cleaned)
215
+ ara_pred, ara_probs = predict_arabert(cleaned)
216
+ ground_truth = get_ground_truth(cleaned)
217
+ top_features = get_top_features(cleaned, svm_pred)
218
+
219
+ def conf(probs, cls):
220
+ return round(probs.get(cls, 0) * 100)
221
+
222
+ def verdict(pred, gt):
223
+ if gt is None: return ""
224
+ return "Correct" if pred == gt else f"Wrong — True: {gt}"
225
+
226
+ svm_v = verdict(svm_pred, ground_truth)
227
+ bilstm_v = verdict(bilstm_pred, ground_truth)
228
+ ara_v = verdict(ara_pred, ground_truth)
229
+
230
+ # ── Colors: hard-coded to work on both light & dark themes ──
231
+ # Cards use white background with dark text always
232
+ card_bg = "#ffffff"
233
+ card_text = "#111111"
234
+ card_sub = "#555555"
235
+ card_conf = "#333333"
236
+
237
+ features_bg = "#d4edda"
238
+ features_text = "#155724"
239
+ features_title= "#0a4520"
240
+
241
+ bar_bg = "#dddddd"
242
+ bar_fill = "#2563eb"
243
+ bar_label = "#222222"
244
+ bar_pct = "#444444"
245
+
246
+ breakdown_title = "#333333"
247
+
248
+ correct_color = "#15803d"
249
+ wrong_color = "#dc2626"
250
+
251
+ features_html = " ".join([
252
+ f"<span style='background:#d4edda;color:#155724;padding:3px 10px;border-radius:20px;font-size:13px;font-weight:500;'>{w}</span>"
253
+ for w in top_features
254
+ ]) or "<span style='color:#555;'>—</span>"
255
+
256
+ # Analysis Section
257
+ if ground_truth:
258
+ errors = []
259
+ for model_name, pred in [("SVM", svm_pred), ("BiLSTM", bilstm_pred), ("AraBERT", ara_pred)]:
260
+ if pred != ground_truth:
261
+ analysis = get_error_analysis(ground_truth, pred)
262
+ errors.append(f"<b style='color:#111111;'>{model_name}:</b> <span style='color:#111111;'>{analysis}</span>")
263
+ if errors:
264
+ error_html = "<br>".join(errors)
265
+ analysis_section = f"""
266
+ <div style='margin-top:16px;padding:14px;background:#fde8e8;border-left:4px solid #dc2626;border-radius:8px;'>
267
+ <b style='color:#991b1b;font-size:13px;'>Error analysis — ground truth: <span style='color:#111111;'>{ground_truth}</span></b><br>
268
+ <span style='font-size:13px;color:#111111;line-height:1.6;'>{error_html}</span>
269
+ </div>"""
270
+ else:
271
+ analysis_section = f"""
272
+ <div style='margin-top:16px;padding:14px;background:#dcfce7;border-left:4px solid #16a34a;border-radius:8px;'>
273
+ <b style='color:#15803d;font-size:13px;'>All models correct</b>
274
+ <span style='color:#111111;font-size:13px;'> — ground truth: <b style='color:#111111;'>{ground_truth}</b>. Unambiguous tweet, all 3 models agree.</span>
275
+ </div>"""
276
+ else:
277
+ smart = get_smart_analysis(svm_pred, bilstm_pred, ara_pred, svm_probs, bilstm_probs, ara_probs)
278
+ styles = {
279
+ 'agree_high': ('#dcfce7', '#15803d', '#16a34a'),
280
+ 'agree_low': ('#fef9c3', '#854d0e', '#a16207'),
281
+ 'partial_disagree': ('#ffedd5', '#9a3412', '#c2410c'),
282
+ 'full_disagree': ('#fde8e8', '#991b1b', '#dc2626'),
283
+ }
284
+ bg, text_col, title_col = styles.get(smart['type'], ('#f5f5f5', '#333', '#555'))
285
+ border_colors = {
286
+ 'agree_high': '#16a34a',
287
+ 'agree_low': '#a16207',
288
+ 'partial_disagree': '#c2410c',
289
+ 'full_disagree': '#dc2626',
290
+ }
291
+ border_col = border_colors.get(smart['type'], '#888')
292
+ analysis_section = f"""
293
+ <div style='margin-top:16px;padding:14px;background:{bg};border-left:4px solid {border_col};border-radius:8px;'>
294
+ <b style='color:{title_col};font-size:13px;'>Model analysis — new tweet</b><br>
295
+ <span style='font-size:13px;color:{text_col};line-height:1.6;'>{smart['message']}</span>
296
+ </div>"""
297
+
298
+ bars_html = ""
299
+ for cls in CLASSES:
300
+ pct = conf(ara_probs, cls)
301
+ bars_html += f"""
302
+ <div style='display:flex;align-items:center;gap:10px;margin-bottom:7px;'>
303
+ <span style='font-size:12px;width:130px;color:{bar_label};font-weight:500;'>{cls}</span>
304
+ <div style='flex:1;background:{bar_bg};border-radius:4px;height:7px;'>
305
+ <div style='background:{bar_fill};width:{pct}%;height:7px;border-radius:4px;'></div>
306
+ </div>
307
+ <span style='font-size:12px;width:36px;text-align:right;color:{bar_pct};font-weight:600;'>{pct}%</span>
308
+ </div>"""
309
+
310
+ html = f"""
311
+ <div style='font-family:sans-serif;max-width:660px;'>
312
+
313
+ <div style='display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:16px;'>
314
+
315
+ <div style='background:{card_bg};border:1.5px solid #6ee7b7;border-radius:10px;padding:14px;box-shadow:0 1px 4px rgba(0,0,0,0.08);'>
316
+ <p style='font-size:11px;color:#059669;margin:0 0 6px;font-weight:700;letter-spacing:0.3px;'>SVM + TF-IDF</p>
317
+ <p style='font-size:17px;font-weight:700;margin:0;color:{card_text};'>{svm_pred}</p>
318
+ <p style='font-size:11px;color:{card_sub};margin:2px 0 6px;'>{CLASS_AR.get(svm_pred,'')}</p>
319
+ <p style='font-size:12px;margin:0;color:{card_conf};'>{conf(svm_probs, svm_pred)}% confidence</p>
320
+ <p style='font-size:11px;margin:5px 0 0;font-weight:600;color:{"" + correct_color if "Correct" in svm_v else wrong_color};'>{svm_v}</p>
321
+ </div>
322
+
323
+ <div style='background:{card_bg};border:2px solid #a5b4fc;border-radius:10px;padding:14px;box-shadow:0 1px 4px rgba(0,0,0,0.08);'>
324
+ <p style='font-size:11px;color:#4f46e5;margin:0 0 6px;font-weight:700;letter-spacing:0.3px;'>BiLSTM</p>
325
+ <p style='font-size:17px;font-weight:700;margin:0;color:{card_text};'>{bilstm_pred}</p>
326
+ <p style='font-size:11px;color:{card_sub};margin:2px 0 6px;'>{CLASS_AR.get(bilstm_pred,'')}</p>
327
+ <p style='font-size:12px;margin:0;color:{card_conf};'>{conf(bilstm_probs, bilstm_pred)}% confidence</p>
328
+ <p style='font-size:11px;margin:5px 0 0;font-weight:600;color:{"" + correct_color if "Correct" in bilstm_v else wrong_color};'>{bilstm_v}</p>
329
+ </div>
330
+
331
+ <div style='background:{card_bg};border:1.5px solid #93c5fd;border-radius:10px;padding:14px;box-shadow:0 1px 4px rgba(0,0,0,0.08);'>
332
+ <p style='font-size:11px;color:#1d4ed8;margin:0 0 6px;font-weight:700;letter-spacing:0.3px;'>AraBERT</p>
333
+ <p style='font-size:17px;font-weight:700;margin:0;color:{card_text};'>{ara_pred}</p>
334
+ <p style='font-size:11px;color:{card_sub};margin:2px 0 6px;'>{CLASS_AR.get(ara_pred,'')}</p>
335
+ <p style='font-size:12px;margin:0;color:{card_conf};'>{conf(ara_probs, ara_pred)}% confidence</p>
336
+ <p style='font-size:11px;margin:5px 0 0;font-weight:600;color:{"" + correct_color if "Correct" in ara_v else wrong_color};'>{ara_v}</p>
337
+ </div>
338
+
339
+ </div>
340
+
341
+ <div style='padding:14px;background:{features_bg};border-radius:8px;margin-bottom:14px;'>
342
+ <p style='font-size:12px;color:{features_title};margin:0 0 8px;font-weight:700;'>Top signals </p>
343
+ {features_html}
344
+ </div>
345
+
346
+ <div style='margin-bottom:14px;background:#ffffff;padding:12px;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,0.06);'>
347
+ <p style='font-size:12px;color:{breakdown_title};margin:0 0 10px;font-weight:700;'>All classes — confidence breakdown </p>
348
+ {bars_html}
349
+ </div>
350
+
351
+ {analysis_section}
352
+ </div>
353
+ """
354
+ return html
355
+
356
+ # Launch
357
+ demo = gr.Interface(
358
+ fn=classify,
359
+ inputs=gr.Textbox(
360
+ label="Enter an Arabic tweet",
361
+ placeholder="مثال: الـ AraBERT فهم التغريدات العربية احسن مني انا ههههههههه",
362
+ rtl=True,
363
+ lines=3
364
+ ),
365
+ outputs=gr.HTML(label="Results"),
366
+ title="Arabic Dialogue/Speech Act Classifier",
367
+ description="AI 445 — NLP Project | Jordan University of Science and Technology",
368
+ examples=[
369
+ [""],
370
+ ["الأكل كان رائع جداً!"],
371
+ ["مين المسؤول ان الصوت بيقطع و مش ماشي مع كلام الرئيس اودام العالم كله"],
372
+ ["رئيس الجمهوريه التونسيه حاضرا مباراه بلاده في تصفيات كاس العالم"],
373
+ ["المشروع سينتهي غداً"],
374
+ ["ليش ال Recommendation غالبا بفشل؟ لانه مثلي ماحدا بسمعه"],
375
+ ["ماذا قال محمد صلاح عن اداء وتاهل تونس والمغرب الي المونديال"],
376
+ ["عندي اقتراح للشيخ عزمي بشاره بما ان رايه صائب الي هذه الدرجه ان يجلس مع الشيخ تميم ويوضعوا خطه محكمه لاعاده فلسطين او اعاده القدس ويتركوا الربيع العربي مؤقتا"],
377
+ ["رياضه محمد صلاح ينافس نجوم علي جائزه BBC للافضل في افريقيا"]
378
+ ],
379
+ flagging_mode="never"
380
+ )
381
+
382
+ demo.launch()