mohammed777 commited on
Commit
5ea3020
·
verified ·
1 Parent(s): d7678ca

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +111 -0
  2. main.py +302 -0
  3. requirements.txt +16 -0
app.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ import json
4
+
5
+ # عنوان الـ API الخاص بـ FastAPI
6
+ # يجب استبدال هذا بعنوان الـ API الفعلي الخاص بك على Hugging Face
7
+ # مثال: "https://<your-space-name>.hf.space" إذا كان هو الـ Space الرئيسي
8
+ # أو "https://<your-space-name>.hf.space/evaluate_youtube_playlist_individually_same_method2/"
9
+ # إذا كنت تستخدم مسارًا فرعيًا. تأكد من المسار الصحيح.
10
+ FASTAPI_API_URL = "http://localhost:8000/evaluate_youtube_playlist_individually_same_method2/"
11
+ # إذا كان تطبيق FastAPI الخاص بك يعمل على Hugging Face، فاستبدل localhost بعنوان URL الخاص بـ Hugging Face Space.
12
+ # مثال: "https://YOUR_HF_SPACE_NAME.hf.space/evaluate_youtube_playlist_individually_same_method2/"
13
+ # تذكر أن Hugging Face قد توفر عنوان URL للقاعدة (root) فقط، وتحتاج إلى إضافة المسار إلى نقطة النهاية.
14
+
15
+ def evaluate_playlist_with_fastapi(youtube_url: str, max_comments_per_video: int = 50, max_workers: int = 3):
16
+ """
17
+ تستدعي نقطة النهاية FastAPI API لتقييم قائمة تشغيل YouTube.
18
+ """
19
+ # تهيئة رسالة الخطأ الافتراضية
20
+ error_message = ""
21
+
22
+ # التحقق من أن الرابط ليس فارغًا
23
+ if not youtube_url:
24
+ error_message = "الرجاء إدخال رابط قائمة تشغيل أو فيديو يوتيوب."
25
+ gr.Warning(error_message) # عرض تحذير في واجهة Gradio
26
+ return error_message, error_message, error_message, error_message, error_message, error_message # إرجاع قيم فارغة أو رسالة خطأ لجميع المخرجات
27
+
28
+ payload = {
29
+ "youtube_url": youtube_url,
30
+ "max_comments_per_video": max_comments_per_video,
31
+ "max_workers": max_workers
32
+ }
33
+ headers = {"Content-Type": "application/json"}
34
+
35
+ try:
36
+ # إرسال طلب POST إلى API
37
+ response = requests.post(FASTAPI_API_URL, data=json.dumps(payload), headers=headers)
38
+ response.raise_for_status() # إثارة استثناء لأكواد حالة HTTP 4xx/5xx
39
+
40
+ result = response.json()
41
+
42
+ # التحقق من وجود مفتاح "error" في الاستجابة
43
+ if "error" in result:
44
+ error_message = f"خطأ من API: {result['error']}"
45
+ gr.Warning(error_message) # عرض تحذير في واجهة Gradio
46
+ return error_message, error_message, error_message, error_message, error_message, error_message
47
+
48
+ # استخراج النتائج المطلوبة
49
+ overall_quality = result.get("overall_quality", "غير متوفر")
50
+ composite_quality = result.get("composite_quality", "غير متوفر")
51
+ composite_score = result.get("composite_score", "غير متوفر")
52
+ percent_good_videos = result.get("percent_good_videos", "غير متوفر")
53
+ positive_ratio = result.get("positive_ratio", "غير متوفر")
54
+ negative_ratio = result.get("negative_ratio", "غير متوفر")
55
+
56
+ # إرجاع النتائج للعرض في Gradio
57
+ return (f"جودة عامة: {overall_quality}",
58
+ f"جودة مركبة: {composite_quality}",
59
+ f"النقاط المركبة: {composite_score}",
60
+ f"نسبة الفيديوهات الجيدة: {percent_good_videos}%",
61
+ f"نسبة التعليقات الإيجابية: {positive_ratio}%",
62
+ f"نسبة التعليقات السلبية: {negative_ratio}%")
63
+
64
+ except requests.exceptions.ConnectionError as e:
65
+ error_message = f"خطأ في الاتصال بـ API: {e}. تأكد من أن API يعمل والـ URL صحيح."
66
+ gr.Error(error_message) # عرض خطأ في واجهة Gradio
67
+ except requests.exceptions.Timeout as e:
68
+ error_message = f"انتهت مهلة طلب API: {e}. قد يكون API مشغولًا."
69
+ gr.Error(error_message)
70
+ except requests.exceptions.RequestException as e:
71
+ error_message = f"خطأ عام في الطلب: {e}. الاستجابة: {response.text if 'response' in locals() else 'لا توجد استجابة'}"
72
+ gr.Error(error_message)
73
+ except json.JSONDecodeError as e:
74
+ error_message = f"خطأ في تحليل استجابة JSON من API: {e}. الاستجابة الخام: {response.text if 'response' in locals() else 'لا توجد استجابة'}"
75
+ gr.Error(error_message)
76
+ except Exception as e:
77
+ error_message = f"حدث خطأ غير متوقع: {e}"
78
+ gr.Error(error_message)
79
+
80
+ # في حالة أي خطأ، أرجع رسالة الخطأ لجميع المخرجات
81
+ return error_message, error_message, error_message, error_message, error_message, error_message
82
+
83
+
84
+ # بناء واجهة Gradio
85
+ with gr.Blocks(title="تقييم قائمة تشغيل يوتيوب") as demo:
86
+ gr.Markdown("# تقييم جودة قائمة تشغيل يوتيوب")
87
+ gr.Markdown("هذه الواجهة تستخدم نموذج تحليل المشاعر والجودة لتقييم الفيديوهات.")
88
+
89
+ with gr.Row():
90
+ with gr.Column():
91
+ youtube_url_input = gr.Textbox(label="رابط قائمة تشغيل/فيديو يوتيوب", placeholder="الصق رابط يوتيوب هنا...")
92
+ max_comments_input = gr.Slider(minimum=10, maximum=200, value=50, step=10, label="الحد الأقصى للتعليقات لكل فيديو")
93
+ max_workers_input = gr.Slider(minimum=1, maximum=10, value=3, step=1, label="الحد الأقصى للعمال المتوازيين")
94
+ submit_button = gr.Button("تقييم قائمة التشغيل")
95
+ with gr.Column():
96
+ overall_quality_output = gr.Textbox(label="جودة قائمة التشغيل الإجمالية")
97
+ composite_quality_output = gr.Textbox(label="الجودة المركبة")
98
+ composite_score_output = gr.Textbox(label="النقاط المركبة")
99
+ percent_good_videos_output = gr.Textbox(label="نسبة الفيديوهات الجيدة")
100
+ positive_ratio_output = gr.Textbox(label="نسبة التعليقات الإيجابية")
101
+ negative_ratio_output = gr.Textbox(label="نسبة التعليقات السلبية")
102
+
103
+ # ربط الزر بالدالة وتحديد المدخلات والمخرجات
104
+ submit_button.click(
105
+ evaluate_playlist_with_fastapi,
106
+ inputs=[youtube_url_input, max_comments_input, max_workers_input],
107
+ outputs=[overall_quality_output, composite_quality_output, composite_score_output,
108
+ percent_good_videos_output, positive_ratio_output, negative_ratio_output]
109
+ )
110
+
111
+ demo.launch(debug=True)
main.py ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from pydantic import BaseModel
3
+ import pickle
4
+ import pandas as pd
5
+ import numpy as np
6
+ import yt_dlp
7
+ from youtube_comment_downloader import YoutubeCommentDownloader
8
+ from datetime import datetime
9
+ import re
10
+ import string
11
+ import nltk
12
+ import emoji
13
+ from urllib.parse import urlparse, parse_qs
14
+ from nltk.corpus import stopwords
15
+ import logging # جديد
16
+ from concurrent.futures import ThreadPoolExecutor # جديد
17
+ from fake_useragent import UserAgent # جديد
18
+ import random # جديد
19
+
20
+ # --- إعدادات NLTK و Logging ---
21
+ # ضبط مسار بيانات NLTK ليطابق Dockerfile
22
+ nltk.data.path.append('/app/nltk_data')
23
+ try:
24
+ # سيتم تنزيلها مرة واحدة في Dockerfile، ولكن هذا يضمن أنها متاحة
25
+ nltk.download('stopwords', quiet=True)
26
+ except Exception as e:
27
+ logging.error(f"Failed to download NLTK stopwords: {e}")
28
+ # التعامل مع الخطأ إذا لم يتمكن من التنزيل (مثلاً إذا لم يكن المسار صحيحاً أو صلاحيات)
29
+
30
+ arabic_stopwords = set(stopwords.words('arabic'))
31
+
32
+ # --- إعداد التسجيل ---
33
+ logging.basicConfig(
34
+ filename='youtube_scraper.log',
35
+ level=logging.INFO,
36
+ format='%(asctime)s - %(levelname)s - %(message)s'
37
+ )
38
+
39
+ app = FastAPI()
40
+
41
+ # تحميل النماذج
42
+ loaded_quality_model = None
43
+ loaded_sentiment_pipeline = None
44
+
45
+ try:
46
+ with open('final_youtube_quality_model.pkl', 'rb') as f:
47
+ loaded_quality_model = pickle.load(f)
48
+ logging.info("تم تحميل نموذج جودة الفيديو بنجاح.")
49
+ except FileNotFoundError:
50
+ logging.error("خطأ: لم يتم العثور على ملف النموذج 'final_youtube_quality_model.pkl'.")
51
+ except Exception as e:
52
+ logging.error(f"خطأ غير متوقع أثناء تحميل نموذج جودة الفيديو: {e}")
53
+
54
+ try:
55
+ with open('best_sentiment_pipeline.pkl', 'rb') as f:
56
+ loaded_sentiment_pipeline = pickle.load(f)
57
+ logging.info("تم تحميل نموذج تصنيف المشاعر بنجاح.")
58
+ except FileNotFoundError:
59
+ logging.error("خطأ: لم يتم العثور على ملف النموذج 'best_sentiment_pipeline.pkl'.")
60
+ except Exception as e:
61
+ logging.error(f"خطأ غير متوقع أثناء تحميل نموذج تحليل المشاعر: {e}")
62
+
63
+ # --- إعدادات User-Agent لـ yt-dlp (جديد) ---
64
+ ua = UserAgent()
65
+
66
+ def get_desktop_user_agent():
67
+ while True:
68
+ candidate = random.choice([ua.chrome, ua.firefox, ua.safari])
69
+ if all(x not in candidate for x in ['Mobile', 'Android', 'iPhone', 'iPad']):
70
+ return candidate
71
+
72
+ selected_user_agent = get_desktop_user_agent()
73
+
74
+ headers = {'User-Agent': selected_user_agent}
75
+
76
+
77
+
78
+ ydl_opts_video_info = {
79
+ 'quiet': True,
80
+ 'skip_download': True,
81
+ 'extract_flat': True,
82
+ 'ignoreerrors': True,
83
+ 'no_warnings': True,
84
+ 'age_limit': 18,
85
+ 'force_generic_extractor': False,
86
+ 'http_headers': headers
87
+ }
88
+
89
+ # --- النموذج ---
90
+ class PlaylistRequest(BaseModel):
91
+ playlist_url: str
92
+
93
+ # --- دالة استخراج ID الفيديو ---
94
+ def extract_video_id(url):
95
+ if 'youtu.be/' in url:
96
+ return url.split('/')[-1].split('?')[0]
97
+ elif 'watch?v=' in url:
98
+ return parse_qs(urlparse(url).query).get('v', [None])[0]
99
+ return None
100
+
101
+
102
+ # # --- تنظيف التعليقات ---
103
+ # arabic_stopwords = set(stopwords.words('arabic'))
104
+
105
+ def preprocess_text(text):
106
+ if not isinstance(text, str):
107
+ return ""
108
+ text = emoji.demojize(text)
109
+ text = re.sub(r'http\S+', '', text)
110
+ text = text.translate(str.maketrans('', '', string.punctuation + string.digits))
111
+ text = text.lower()
112
+ text = re.sub(r'\s+', ' ', text).strip()
113
+ text_tokens = text.split()
114
+ filtered_text = [word for word in text_tokens if word not in arabic_stopwords]
115
+ return ' '.join(filtered_text)
116
+
117
+
118
+ # --- معالجة فيديو واحد فقط (نسخة 2) ---
119
+ def process_single_video2(video_url, loaded_quality_model, loaded_sentiment_pipeline, max_comments_per_video=50):
120
+ downloader = YoutubeCommentDownloader()
121
+
122
+ video_id = extract_video_id(video_url)
123
+ if not video_id:
124
+ logging.warning(f"رابط فيديو غير صالح: {video_url}. تم تجاهله.")
125
+ return None
126
+
127
+ try:
128
+ with yt_dlp.YoutubeDL(ydl_opts_video_info) as ydl:
129
+ info_dict = ydl.extract_info(video_url, download=False)
130
+
131
+ # --- تسجيل البيانات المستخلصة ---
132
+ logging.info(f"[فيديو: {video_url}] تم استخراج البيانات: {info_dict.keys()}")
133
+
134
+ if not info_dict or info_dict.get('is_live', False) or info_dict.get('age_limit', 0) > 0:
135
+ logging.warning(f"لا يمكن معالجة الفيديو {video_id}: مباشر أو مقيد عمرًا. تم تجاهله.")
136
+ return None
137
+
138
+ views = info_dict.get('view_count', 0)
139
+ likes = info_dict.get('like_count', 0)
140
+
141
+ # --- تسجيل المشاهدات والإعجابات ---
142
+ logging.info(f"[فيديو: {video_url}] المشاهدات: {views}, الإعجابات: {likes}")
143
+
144
+ upload_date = info_dict.get('upload_date', 'Unknown')
145
+ publish_year = int(upload_date[:4]) if upload_date != 'Unknown' else datetime.datetime.now().year
146
+
147
+ # --- جلب التعليقات ---
148
+ sampled_comments = []
149
+ try:
150
+ for comment in downloader.get_comments_from_url(video_url):
151
+ if 'text' in comment:
152
+ sampled_comments.append(comment['text'])
153
+ if len(sampled_comments) >= max_comments_per_video:
154
+ break
155
+ except Exception as e:
156
+ logging.warning(f"فشل في جلب التعليقات للفيديو {video_id}. السبب: {e}.")
157
+ sampled_comments = []
158
+
159
+ like_view_ratio = likes / views if views > 0 else 0.0
160
+ comment_view_ratio = len(sampled_comments) / views if views > 0 else 0.0
161
+ engagement_score = like_view_ratio + comment_view_ratio
162
+
163
+ # --- تحليل التعليقات ---
164
+ positive_comments = 0
165
+ negative_comments = 0
166
+ overall_sentiment = "لا توجد تعليقات كافية"
167
+
168
+ if sampled_comments:
169
+ processed_comments = [preprocess_text(c) for c in sampled_comments]
170
+ sentiment_predictions = loaded_sentiment_pipeline.predict(processed_comments)
171
+ positive_comments = np.sum(sentiment_predictions == 1)
172
+ negative_comments = np.sum(sentiment_predictions == 0)
173
+
174
+ if positive_comments > negative_comments:
175
+ overall_sentiment = "إيجابي"
176
+ elif negative_comments > positive_comments:
177
+ overall_sentiment = "سلبي"
178
+ else:
179
+ overall_sentiment = "محايد"
180
+
181
+ input_df = pd.DataFrame([[views, likes, len(sampled_comments), 0, publish_year,
182
+ like_view_ratio, comment_view_ratio, engagement_score]],
183
+ columns=['views_count', 'likes_count', 'comments_count',
184
+ 'video_duration_seconds', 'publish_year',
185
+ 'like_view_ratio', 'comment_view_ratio', 'engagement_score'])
186
+
187
+ playlist_quality = "لم يتم التقييم"
188
+ try:
189
+ prediction_numeric = loaded_quality_model.predict(input_df)[0]
190
+ logging.info(f"[فيديو: {video_url}] نتيجة التنبؤ: {prediction_numeric}") # تسجيل النتيجة
191
+ playlist_quality = "جيد" if prediction_numeric == 1 else "سيء"
192
+ except Exception as e:
193
+ playlist_quality = f"خطأ في التقييم: {e}"
194
+ logging.error(f"[فيديو: {video_url}] خطأ في تقييم الفيديو: {e}")
195
+
196
+ return {
197
+ "video_url": video_url,
198
+ "views": views,
199
+ "likes": likes,
200
+ "comments": len(sampled_comments),
201
+ "like_view_ratio": like_view_ratio,
202
+ "comment_view_ratio": comment_view_ratio,
203
+ "engagement_score": engagement_score,
204
+ "quality": playlist_quality,
205
+ "sentiment": overall_sentiment,
206
+ "positive_comments": positive_comments,
207
+ "negative_comments": negative_comments
208
+ }
209
+
210
+ except Exception as e:
211
+ logging.error(f"حدث خطأ في الفيديو {video_url}: {e}")
212
+ return None
213
+
214
+
215
+
216
+
217
+ # --- نفس الدالة ولكن صحيحة مع Pydantic (بعد التعديلات) ---
218
+ @app.post("/evaluate_youtube_playlist_individually_same_method2/")
219
+ async def evaluate_youtube_playlist_individually_same_method2(youtube_url, max_comments_per_video=50, max_workers=3):
220
+ """
221
+ تقييم قائمة تشغيل يوتيوب باستخدام نظام مركب (نسخة 2)
222
+ """
223
+
224
+ if loaded_quality_model is None or loaded_sentiment_pipeline is None:
225
+ logging.error("لم يتم تحميل النماذج المطلوبة.")
226
+ return {"error": "لم يتم تحميل النماذج المطلوبة."}
227
+
228
+ video_links = []
229
+ try:
230
+ with yt_dlp.YoutubeDL({'extract_flat': True, 'quiet': True, 'playlist_items': '1:10'}) as ydl:
231
+ playlist_info = ydl.extract_info(youtube_url, download=False)
232
+ if 'entries' in playlist_info:
233
+ for entry in playlist_info['entries'][:10]:
234
+ if entry and 'url' in entry:
235
+ video_links.append(entry['url'])
236
+ else:
237
+ logging.warning("لا توجد فيديوهات في هذه القائمة.")
238
+ return {"error": "لا توجد فيديوهات في هذه القائمة."}
239
+ except Exception as e:
240
+ logging.error(f"فشل في جلب روابط الفيديو: {e}")
241
+ return {"error": f"فشل في جلب روابط الفيديو: {e}"}
242
+
243
+ individual_results = []
244
+
245
+ # --- معالجة الفيديوهات بالتوازي ---
246
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
247
+ futures = [
248
+ executor.submit(
249
+ process_single_video2,
250
+ video_url,
251
+ loaded_quality_model,
252
+ loaded_sentiment_pipeline,
253
+ max_comments_per_video
254
+ ) for video_url in video_links
255
+ ]
256
+
257
+ for future in tqdm(futures, total=len(video_links), desc="معالجة الفيديوهات"):
258
+ result = future.result()
259
+ if result:
260
+ individual_results.append(result)
261
+
262
+ # --- الإحصاء النهائي ---
263
+ num_good_videos = sum(1 for r in individual_results if r['quality'] == 'جيد')
264
+ total_positive_comments = sum(r['positive_comments'] for r in individual_results)
265
+ total_negative_comments = sum(r['negative_comments'] for r in individual_results)
266
+ total_classified_comments = total_positive_comments + total_negative_comments
267
+
268
+ total_videos = len(individual_results)
269
+ percent_good_videos = (num_good_videos / total_videos) * 100 if total_videos > 0 else 0
270
+
271
+ # --- حساب التقييم العام ---
272
+ if percent_good_videos >= 70:
273
+ overall_quality = "جيد جداً"
274
+ elif percent_good_videos >= 50:
275
+ overall_quality = "جيد"
276
+ else:
277
+ overall_quality = "سيء"
278
+
279
+ # --- النظام المركب ---
280
+ WEIGHT_QUALITY = 0.6
281
+ WEIGHT_SENTIMENT = 0.4
282
+
283
+ positive_ratio = (total_positive_comments / total_classified_comments) * 100 if total_classified_comments > 0 else 0.0
284
+ composite_score = (WEIGHT_QUALITY * percent_good_videos) + (WEIGHT_SENTIMENT * positive_ratio)
285
+
286
+ if composite_score >= 75:
287
+ composite_quality = "جيد جداً"
288
+ elif composite_score >= 60:
289
+ composite_quality = "جيد"
290
+ elif composite_score >= 45:
291
+ composite_quality = "متوسط"
292
+ else:
293
+ composite_quality = "سيء"
294
+
295
+ return {
296
+ "overall_quality": overall_quality,
297
+ "composite_quality": composite_quality,
298
+ "composite_score": round(composite_score, 1),
299
+ "percent_good_videos": round(percent_good_videos, 1),
300
+ "positive_ratio": round(positive_ratio, 1),
301
+ "negative_ratio": round(100 - positive_ratio, 1),
302
+ }
requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ python-multipart
4
+ yt-dlp
5
+ youtube-comment-downloader
6
+ nltk
7
+ emoji
8
+ pandas
9
+ numpy
10
+ # استخدم الإصدار الدقيق الذي دربت عليه النماذج
11
+ scikit-learn==1.6.1
12
+ gunicorn
13
+ fake-useragent # <--- جديد
14
+ tqdm
15
+ gradio
16
+ requests