mohammed777 commited on
Commit
ba639d1
·
verified ·
1 Parent(s): 935e910

Upload 4 files

Browse files
Files changed (4) hide show
  1. app.py +165 -0
  2. cookies.txt +26 -0
  3. main.py +297 -0
  4. requirements.txt +19 -0
app.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import json
3
+ import matplotlib.pyplot as plt # استيراد مكتبة Matplotlib لرسم المخططات
4
+ import numpy as np # قد نحتاجه لبعض العمليات الحسابية البسيطة
5
+ import matplotlib.font_manager as fm # استيراد font_manager للتحكم في الخطوط
6
+ import arabic_reshaper # لاستعادة شكل الحروف العربية
7
+ from bidi.algorithm import get_display # لضبط اتجاه النص RTL
8
+
9
+ # استيراد الدالة الرئيسية لتقييم قائمة تشغيل YouTube من ملف main.py
10
+ # تأكد أن ملف main.py موجود في نفس المجلد وأن النماذج (.pkl) متاحة له
11
+ from main import evaluate_youtube_playlist_individually_same_method2
12
+
13
+ # =======================================================
14
+ # إعداد الخطوط لـ Matplotlib لدعم اللغة العربية
15
+ # =======================================================
16
+ # يجب التأكد من تثبيت خط عربي في بيئة Docker الخاصة بـ Hugging Face Space.
17
+ # مثال على التثبيت في Dockerfile: RUN apt-get update && apt-get install -y fonts-noto-arabic
18
+ # سنبحث عن خط Noto Sans Arabic، وإذا لم نجده، سيظل Matplotlib يستخدم الخط الافتراضي
19
+ font_path = None
20
+ # البحث عن خط Noto Sans Arabic أو Tajawal (يمكنك تغيير هذا بناءً على الخط الذي تفضله وتثبته)
21
+ for font in fm.findSystemFonts(fontpaths=None):
22
+ if 'NotoSansArabic' in font or 'Tajawal' in font or 'Amiri' in font:
23
+ font_path = font
24
+ break
25
+
26
+ if font_path:
27
+ # إضافة الخط العربي إلى قائمة الخطوط الافتراضية لـ Matplotlib
28
+ plt.rcParams['font.sans-serif'] = [fm.FontProperties(fname=font_path).get_name()] + plt.rcParams['font.sans-serif']
29
+ # تعطيل عرض علامة السالب كـ "صندوق" في بعض الخطوط
30
+ plt.rcParams['axes.unicode_minus'] = False
31
+ # تعيين عائلة الخط الافتراضية
32
+ plt.rcParams['font.family'] = 'sans-serif'
33
+ print(f"تم تعيين الخط العربي لـ Matplotlib: {font_path}") # لغرض التصحيح في السجلات
34
+ else:
35
+ print("تحذير: لم يتم العثور على خط عربي مناسب. قد لا يظهر النص العربي بشكل صحيح في المخطط.")
36
+ gr.Warning("تحذير: لم يتم العثور على خط عربي مناسب. قد لا يظهر النص العربي بشكل صحيح في المخطط.")
37
+
38
+
39
+ async def evaluate_playlist_local(youtube_url: str, max_comments_per_video: int = 50, max_workers: int = 3):
40
+ """
41
+ تستدعي دالة تقييم قائمة تشغيل YouTube مباشرة من ملف main.py.
42
+ وتعيد النتائج النصية مجمعة في صندوق واحد، بالإضافة إلى مخطط Matplotlib يوضح نسب التعليقات.
43
+ """
44
+ error_message = ""
45
+
46
+ # تهيئة المخرجات الافتراضية في حالة وجود خطأ أو عدم وجود رابط
47
+ default_text_output = "الرجاء إدخال رابط صحيح وتشغيل التقييم."
48
+ # إنشاء شكل Matplotlib فارغ لتجنب الأخطاء في حالة عدم وجود بيانات للمخطط
49
+ default_plot = plt.figure()
50
+ plt.close(default_plot) # إغلاق الشكل الافتراضي لمنع عرضه غير الضروري
51
+
52
+ # التحقق من أن الرابط ليس فارغًا قبل المتابعة
53
+ if not youtube_url:
54
+ error_message = "الرجاء إدخال رابط قائمة تشغيل أو فيديو يوتيوب."
55
+ gr.Warning(error_message) # عرض تحذير للمستخدم في واجهة Gradio
56
+ return error_message, default_plot # إرجاع رسالة خطأ ومخطط فارغ
57
+
58
+ try:
59
+ # استدعاء الدالة الرئيسية لمعالجة قائمة التشغيل/الفيديو
60
+ result = await evaluate_youtube_playlist_individually_same_method2(
61
+ youtube_url=youtube_url,
62
+ max_comments_per_video=max_comments_per_video,
63
+ max_workers=max_workers
64
+ )
65
+
66
+ # التحقق مما إذا كانت الدالة قد أعادت خطأ
67
+ if "error" in result:
68
+ error_message = f"خطأ في المعالجة: {result['error']}"
69
+ gr.Error(error_message) # عرض الخطأ كرسالة خطأ حمراء في Gradio
70
+ return error_message, default_plot # إرجاع رسالة الخطأ ومخطط فارغ
71
+
72
+ # استخراج النتائج المطلوبة من كائن JSON المُعاد
73
+ overall_quality = result.get("overall_quality", "غير متوفر")
74
+ composite_quality = result.get("composite_quality", "غير متوفر")
75
+ composite_score = result.get("composite_score", "غير متوفر")
76
+ percent_good_videos = result.get("percent_good_videos", "غير متوفر")
77
+ # التأكد من أن النسب عددية، وإلا سيتم تعيينها إلى 0.0
78
+ positive_ratio = result.get("positive_ratio", 0.0)
79
+ negative_ratio = result.get("negative_ratio", 0.0)
80
+
81
+ # =======================================================
82
+ # 1. إعداد النص الموحد ليتم عرضه في صندوق واحد (باستخدام Markdown للتنسيق)
83
+ # =======================================================
84
+ formatted_output_text = f"""
85
+ ## 📊 نتائج تقييم قائمة التشغيل:
86
+
87
+ - **الجودة العامة:** **{overall_quality}**
88
+ - **الجودة المركبة:** **{composite_quality}**
89
+ - **النقاط المركبة:** **{composite_score} / 100**
90
+ - **نسبة الفيديوهات الجيدة:** **{percent_good_videos}%**
91
+ - **نسبة التعليقات الإيجابية:** **{positive_ratio}%**
92
+ - **نسبة التعليقات السلبية:** **{negative_ratio}%**
93
+ """
94
+
95
+ # =======================================================
96
+ # 2. رسم المخطط باستخدام Matplotlib (مخطط دائري)
97
+ # =======================================================
98
+ # إنشاء شكل ومحاور للمخطط بحجم مناسب
99
+ fig, ax = plt.subplots(figsize=(6, 4))
100
+
101
+ # النصوص العربية التي ستظهر على المخطط
102
+ labels_arabic = ['إيجابية', 'سلبية']
103
+
104
+ # تطبيق arabic_reshaper و get_display على التسميات العربية
105
+ # هذا هو الجزء الذي يحل مشكلة قلب النص وتقطيعه
106
+ reshaped_labels = [get_display(arabic_reshaper.reshape(label)) for label in labels_arabic]
107
+
108
+ sizes = [positive_ratio, negative_ratio] # قيم النسب المئوية
109
+ colors = ['#4CAF50', '#F44336'] # ألوان الشرائح (أخضر للإيجابي، أحمر للسلبي)
110
+ explode = (0.1, 0) # "تفجير" الشريحة الإيجابية قليلاً لإبرازها (اختياري)
111
+
112
+ # رسم المخطط الدائري باستخدام التسميات المصححة
113
+ ax.pie(sizes, explode=explode, labels=reshaped_labels, colors=colors,
114
+ autopct='%1.1f%%', shadow=True, startangle=90, textprops={'fontsize': 12})
115
+ ax.axis('equal') # يضمن أن يكون المخطط دائريًا متناسبًا.
116
+
117
+ # تطبيق arabic_reshaper و get_display على عنوان المخطط
118
+ reshaped_title = get_display(arabic_reshaper.reshape('توزيع التعليقات'))
119
+ ax.set_title(reshaped_title, fontsize=14, pad=20)
120
+
121
+ # إغلاق الشكل لمنع Matplotlib من عرضه تلقائيًا في الخلفية، Gradio سيتولى العرض.
122
+ plt.close(fig)
123
+
124
+ # إرجاع النص المنسق والمخطط كناتجين للدالة
125
+ return formatted_output_text, fig
126
+
127
+ except Exception as e:
128
+ # التقاط أي أخطاء غير متوقعة أثناء المعالجة
129
+ error_message = f"حدث خطأ غير متوقع أثناء المعالجة: {e}"
130
+ gr.Error(error_message) # عرض رسالة الخطأ للمستخدم
131
+ return error_message, default_plot # إرجاع رسالة الخطأ ومخطط فارغ في حالة الفشل
132
+
133
+
134
+ # بناء واجهة Gradio باستخدام gr.Blocks للتحكم في التخطيط
135
+ with gr.Blocks(title="تقييم قائمة تشغيل يوتيوب") as demo:
136
+ # عنوان ووصف الواجهة
137
+ gr.Markdown("# 📊 تقييم جودة قائمة تشغيل يوتيوب")
138
+ gr.Markdown("أدخل رابط قائمة تشغيل أو فيديو يوتيوب لتقييم جودتها بناءً على التحليلات والمشاعر.")
139
+
140
+ # تقسيم الواجهة إلى صفين (Row) لأعمدة المدخلات والمخرجات
141
+ with gr.Row():
142
+ # العمود الأول للمدخلات
143
+ with gr.Column(scale=1):
144
+ youtube_url_input = gr.Textbox(label="رابط قائمة تشغيل/فيديو يوتيوب", placeholder="الصق رابط يوتيوب هنا...", interactive=True)
145
+ max_comments_input = gr.Slider(minimum=10, maximum=200, value=50, step=10, label="الحد الأقصى للتعليقات لكل فيديو", interactive=True)
146
+ max_workers_input = gr.Slider(minimum=1, maximum=10, value=3, step=1, label="الحد الأقصى للعمال المتوازيين", interactive=True)
147
+ submit_button = gr.Button("🚀 تقييم قائمة التشغيل", variant="primary") # زر الإرسال الرئيسي
148
+
149
+ # العمود الثاني للمخرجات (بجعل scale=2 ليكون أكبر)
150
+ with gr.Column(scale=2):
151
+ # صندوق نصي واحد لعرض جميع النتائج النصية المنسقة
152
+ output_text_box = gr.Markdown(label="النتائج النهائية للتقييم")
153
+ # عنصر Gradio لعرض مخطط Matplotlib
154
+ output_plot = gr.Plot(label="توزيع التعليقات (إيجابية مقابل سلبية)")
155
+
156
+ # ربط زر الإرسال بالدالة evaluate_playlist_local
157
+ # المدخلات هي عناصر واجهة المستخدم التي توفر البيانات للدالة
158
+ # المخرجات هي عناصر واجهة المستخدم التي ستعرض النتائج من الدالة
159
+ submit_button.click(
160
+ evaluate_playlist_local,
161
+ inputs=[youtube_url_input, max_comments_input, max_workers_input],
162
+ outputs=[output_text_box, output_plot]
163
+ )
164
+ # تشغيل واجهة Gradio
165
+ demo.launch(debug=True)
cookies.txt ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Netscape HTTP Cookie File
2
+ # http://curl.haxx.se/rfc/cookie_spec.html
3
+ # This is a generated file! Do not edit.
4
+
5
+ .youtube.com TRUE / TRUE 1750605937 GPS 1
6
+ .youtube.com TRUE / FALSE 1785164314 HSID ARry8BUK29EDrBoaF
7
+ .youtube.com TRUE / TRUE 1785164314 SSID AjCzjJbJLISdFrbYI
8
+ .youtube.com TRUE / FALSE 1785164314 APISID 6dEVcNzrdyrSNj7H/AZnazb-bFpAgUEfQr
9
+ .youtube.com TRUE / TRUE 1785164314 SAPISID 2NjKZ3kQBu8bv0_e/AGFPUddDotSAN2vVx
10
+ .youtube.com TRUE / TRUE 1785164314 __Secure-1PAPISID 2NjKZ3kQBu8bv0_e/AGFPUddDotSAN2vVx
11
+ .youtube.com TRUE / TRUE 1785164314 __Secure-3PAPISID 2NjKZ3kQBu8bv0_e/AGFPUddDotSAN2vVx
12
+ .youtube.com TRUE / TRUE 1785164318 PREF f6=40000000&tz=Europe.Moscow&f7=100
13
+ .youtube.com TRUE / TRUE 1782140314 __Secure-1PSIDTS sidts-CjIB5H03PxuzW2Vf9c_jiy29lu5xxueOCAKa5jOwCuuVfnbo674ACoNTHkYWSlk4qT68_BAA
14
+ .youtube.com TRUE / TRUE 1782140314 __Secure-3PSIDTS sidts-CjIB5H03PxuzW2Vf9c_jiy29lu5xxueOCAKa5jOwCuuVfnbo674ACoNTHkYWSlk4qT68_BAA
15
+ .youtube.com TRUE / FALSE 1785164314 SID g.a000yQi382-L9qE4kH90yiuUmV1jpZ6Z6Rog3NDDErE979f6Xma13c5myG1NgMw2prvvipxaoQACgYKAVgSARASFQHGX2Mi6asQ6aBxFLddrOp89XzCrBoVAUF8yKrcaRQIUVME90wBCGnagMRV0076
16
+ .youtube.com TRUE / TRUE 1785164314 __Secure-1PSID g.a000yQi382-L9qE4kH90yiuUmV1jpZ6Z6Rog3NDDErE979f6Xma1z4fcO3r7X4O2xuSsFhhmBgACgYKAVoSARASFQHGX2MigD_g3CbWmUoEQC6BefdschoVAUF8yKqSIIugdIVfv1ws8wER5vEQ0076
17
+ .youtube.com TRUE / TRUE 1785164314 __Secure-3PSID g.a000yQi382-L9qE4kH90yiuUmV1jpZ6Z6Rog3NDDErE979f6Xma1OtS11r88WMywoxNuX4i1qgACgYKARYSARASFQHGX2MiWNoHv0DHjaZvOom_Qxa9-xoVAUF8yKqSWJtKrf0bKMuypA8yUvju0076
18
+ .youtube.com TRUE / TRUE 1785164314 LOGIN_INFO AFmmF2swRQIgMksh9mqUXdjZrxKDFDpzhoCiqISm12xIKttEPuVO960CIQDeKRqJ9GdwTBTsNZ7F3nn29aZhXrAxa3zRxcHaz2Ezvw:QUQ3MjNmeU9WdHNNSzJJOVg0eFFZc3ZqcS1UYmZnMEJmemFsUTlBbEduR2hMMFNFWm53VldzV2xCQmswckhubnNtVng0YVpiYVNSTHRORU41YTlaOVJxNmVpUElUVmt1WTJOUndBVnFpWkdMWnlDNFVhUV9qTTlGS0dFa0ItdFZVcGlqU2xLdVdFS1J3Vy1RcElmTXlyX3kyMk9TSE1tRzd3
19
+ .youtube.com TRUE / FALSE 1750604320 ST-1av3flt session_logininfo=AFmmF2swRQIhAMYEHjGtEiunO3YTi-LK_XI8qU5-yfXTbmAB7g4RAZhMAiBpBVw881T2bnA2AtulKo3Y8QCteWBDie1xlh1JApeMpg%3AQUQ3MjNmd1Z1SlZrckJzZXZmaU9BM2ZuMXRlMEo2MC1pbWlMSUxBSWJONXBxNDM1dUFlS1ExM05qUkhsaHRGSDhLem1rQ215S1hPOGNTa3RHbmVVVXdQbWlSd1RSRk80cHhmdjdMTFdHV254dExudVFEeGx0Nl9IYmgwWjJuWWNtSXBNZ0tFSU84Y1BWM1Y3NzJvQ05RbGNQbFNxWWtISkRn
20
+ .youtube.com TRUE / FALSE 1782140316 SIDCC AKEyXzU4Aq9nBSw7WT6Gn4zXKQJZ67dy8TtW3WWGGnhNLehzBAHV1j_p7Blb3P17lojScfzV2A
21
+ .youtube.com TRUE / TRUE 1782140316 __Secure-1PSIDCC AKEyXzWoeWe0DYmTnNS5wJjaWJKHtHY06IM5gRfGPvL0qcYhho08TeoFe7xZIDDPvcCFy_cQ
22
+ .youtube.com TRUE / TRUE 1782140316 __Secure-3PSIDCC AKEyXzVbdhRyt2HZyJly66JY8bUYKiA0Gqp0zg-AuUjPekDR1Wv2mA4c3Lhq0qIVymbsMTYUNA
23
+ .youtube.com TRUE / TRUE 0 YSC VAkAySvY4Wk
24
+ .youtube.com TRUE / TRUE 1766156316 VISITOR_INFO1_LIVE -gI4hoAZQFk
25
+ .youtube.com TRUE / TRUE 1766156316 VISITOR_PRIVACY_METADATA CgJZRRIEGgAgMw%3D%3D
26
+ .youtube.com TRUE / TRUE 1766156144 __Secure-ROLLOUT_TOKEN CKH-5NPA1ZGPxwEQmJHKzqSFjgMY64v50aSFjgM%3D
main.py ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import io
20
+ import base64
21
+ import math
22
+ import time
23
+
24
+ # --- إعدادات NLTK و Logging ---
25
+ nltk.data.path.append('/app/nltk_data')
26
+ try:
27
+ nltk.download('stopwords', quiet=True)
28
+ except Exception as e:
29
+ logging.error(f"Failed to download NLTK stopwords: {e}")
30
+
31
+ arabic_stopwords = set(stopwords.words('arabic'))
32
+
33
+ # --- إعداد التسجيل ---
34
+ logging.basicConfig(
35
+ filename='youtube_scraper.log',
36
+ level=logging.INFO,
37
+ format='%(asctime)s - %(levelname)s - %(message)s'
38
+ )
39
+
40
+ app = FastAPI()
41
+
42
+ # تحميل النماذج
43
+ loaded_quality_model = None
44
+ loaded_sentiment_pipeline = None
45
+
46
+ try:
47
+ with open('models/final_youtube_quality_model.pkl', 'rb') as f:
48
+ loaded_quality_model = pickle.load(f)
49
+ logging.info("تم تحميل نموذج جودة الفيديو بنجاح.")
50
+ except FileNotFoundError:
51
+ logging.error("خطأ: لم يتم العثور على ملف النموذج 'final_youtube_quality_model.pkl'.")
52
+ except Exception as e:
53
+ logging.error(f"خطأ غير متوقع أثناء تحميل نموذج جودة الفيديو: {e}")
54
+
55
+ try:
56
+ with open('models/best_sentiment_pipeline.pkl', 'rb') as f:
57
+ loaded_sentiment_pipeline = pickle.load(f)
58
+ logging.info("تم تحميل نموذج تصنيف المشاعر بنجاح.")
59
+ except FileNotFoundError:
60
+ logging.error("خطأ: لم يتم العثور على ملف النموذج 'best_sentiment_pipeline.pkl'.")
61
+ except Exception as e:
62
+ logging.error(f"خطأ غير متوقع أثناء تحميل نموذج تحليل المشاعر: {e}")
63
+
64
+ # --- إعدادات User-Agent لـ yt-dlp ---
65
+ ua = UserAgent()
66
+
67
+ def get_desktop_user_agent():
68
+ while True:
69
+ candidate = random.choice([ua.chrome, ua.firefox, ua.safari])
70
+ if all(x not in candidate for x in ['Mobile', 'Android', 'iPhone', 'iPad']):
71
+ return candidate
72
+
73
+ selected_user_agent = get_desktop_user_agent()
74
+
75
+ headers = {'User-Agent': selected_user_agent}
76
+
77
+ # تحديد مسار ملف الكوكيز داخل حاوية Docker
78
+ COOKIES_FILE_PATH = 'cookies.txt'
79
+
80
+ ydl_opts_video_info = {
81
+ 'quiet': True,
82
+ 'skip_download': True,
83
+ 'extract_flat': True,
84
+ 'ignoreerrors': True,
85
+ 'no_warnings': True,
86
+ 'age_limit': 18,
87
+ 'force_generic_extractor': False,
88
+ 'http_headers': headers,
89
+ 'cookiefile': COOKIES_FILE_PATH # سيبقى هذا الخيار لـ yt-dlp
90
+ }
91
+
92
+ # --- النموذج ---
93
+ class PlaylistRequest(BaseModel):
94
+ playlist_url: str
95
+
96
+ # --- دالة استخراج ID الفيديو ---
97
+ def extract_video_id(url):
98
+ if 'youtu.be/' in url:
99
+ return url.split('/')[-1].split('?')[0]
100
+ elif 'watch?v=' in url:
101
+ return parse_qs(urlparse(url).query).get('v', [None])[0]
102
+ return None
103
+
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
+ # تم حذف cookies_file من هنا
121
+ downloader = YoutubeCommentDownloader()
122
+
123
+ video_id = extract_video_id(video_url)
124
+ if not video_id:
125
+ logging.warning(f"رابط فيديو غير صالح: {video_url}. تم تجاهله.")
126
+ return None
127
+
128
+ try:
129
+ time.sleep(random.uniform(1, 3))
130
+
131
+ with yt_dlp.YoutubeDL(ydl_opts_video_info) as ydl:
132
+ info_dict = ydl.extract_info(video_url, download=False)
133
+
134
+ logging.info(f"[فيديو: {video_url}] تم استخراج البيانات: {info_dict.keys()}")
135
+
136
+ if not info_dict or info_dict.get('is_live', False) or info_dict.get('age_limit', 0) > 0:
137
+ logging.warning(f"لا يمكن معالجة الفيديو {video_id}: مباشر أو مقيد عمرًا. تم تجاهله.")
138
+ return None
139
+
140
+ views = info_dict.get('view_count', 0)
141
+ likes = info_dict.get('like_count', 0)
142
+
143
+ logging.info(f"[فيديو: {video_url}] المشاهدات: {views}, الإعجابات: {likes}")
144
+
145
+ upload_date = info_dict.get('upload_date', 'Unknown')
146
+ publish_year = int(upload_date[:4]) if upload_date != 'Unknown' else datetime.now().year
147
+
148
+ time.sleep(random.uniform(1, 3))
149
+
150
+ sampled_comments = []
151
+ try:
152
+ for comment in downloader.get_comments_from_url(video_url):
153
+ if 'text' in comment:
154
+ sampled_comments.append(comment['text'])
155
+ if len(sampled_comments) >= max_comments_per_video:
156
+ break
157
+ except Exception as e:
158
+ logging.warning(f"فشل في جلب التعليقات للفيديو {video_id}. السبب: {e}.")
159
+ sampled_comments = []
160
+
161
+ like_view_ratio = likes / views if views > 0 else 0.0
162
+ comment_view_ratio = len(sampled_comments) / views if views > 0 else 0.0
163
+ engagement_score = like_view_ratio + comment_view_ratio
164
+
165
+ positive_comments = 0
166
+ negative_comments = 0
167
+ overall_sentiment = "لا توجد تعليقات كافية"
168
+
169
+ if sampled_comments:
170
+ processed_comments = [preprocess_text(c) for c in sampled_comments]
171
+ sentiment_predictions = loaded_sentiment_pipeline.predict(processed_comments)
172
+ positive_comments = np.sum(sentiment_predictions == 1)
173
+ negative_comments = np.sum(sentiment_predictions == 0)
174
+
175
+ if positive_comments > negative_comments:
176
+ overall_sentiment = "إيجابي"
177
+ elif negative_comments > positive_comments:
178
+ overall_sentiment = "سلبي"
179
+ else:
180
+ overall_sentiment = "محايد"
181
+
182
+ input_df = pd.DataFrame([[views, likes, len(sampled_comments), 0, publish_year,
183
+ like_view_ratio, comment_view_ratio, engagement_score]],
184
+ columns=['views_count', 'likes_count', 'comments_count',
185
+ 'video_duration_seconds', 'publish_year',
186
+ 'like_view_ratio', 'comment_view_ratio', 'engagement_score'])
187
+
188
+ playlist_quality = "لم يتم التقييم"
189
+ try:
190
+ prediction_numeric = loaded_quality_model.predict(input_df)[0]
191
+ logging.info(f"[فيديو: {video_url}] نتيجة التنبؤ: {prediction_numeric}")
192
+ playlist_quality = "جيد" if prediction_numeric == 1 else "سيء"
193
+ except Exception as e:
194
+ playlist_quality = f"خطأ في التقييم: {e}"
195
+ logging.error(f"[فيديو: {video_url}] خطأ في تقييم الفيديو: {e}")
196
+
197
+ return {
198
+ "video_url": video_url,
199
+ "views": views,
200
+ "likes": likes,
201
+ "comments": len(sampled_comments),
202
+ "like_view_ratio": like_view_ratio,
203
+ "comment_view_ratio": comment_view_ratio,
204
+ "engagement_score": engagement_score,
205
+ "quality": playlist_quality,
206
+ "sentiment": overall_sentiment,
207
+ "positive_comments": positive_comments,
208
+ "negative_comments": negative_comments
209
+ }
210
+
211
+ except Exception as e:
212
+ logging.error(f"حدث خطأ في الفيديو {video_url}: {e}")
213
+ return None
214
+
215
+ @app.post("/evaluate_youtube_playlist_individually_same_method2/")
216
+ async def evaluate_youtube_playlist_individually_same_method2(youtube_url: str, max_comments_per_video: int = 50, max_workers: int = 3):
217
+ """
218
+ تقييم قائمة تشغيل يوتيوب باستخدام نظام مركب (نسخة 2)
219
+ """
220
+
221
+ if loaded_quality_model is None or loaded_sentiment_pipeline is None:
222
+ logging.error("لم يتم تحميل النماذج المطلوبة.")
223
+ return {"error": "لم يتم تحميل النماذج المطلوبة."}
224
+
225
+ video_links = []
226
+ try:
227
+ # خيار cookiefile لـ yt-dlp يبقى هنا
228
+ with yt_dlp.YoutubeDL({'extract_flat': True, 'quiet': True, 'playlist_items': '1:10', 'cookiefile': COOKIES_FILE_PATH}) as ydl:
229
+ playlist_info = ydl.extract_info(youtube_url, download=False)
230
+ if 'entries' in playlist_info:
231
+ for entry in playlist_info['entries'][:10]:
232
+ if entry and 'url' in entry:
233
+ video_links.append(entry['url'])
234
+ else:
235
+ logging.warning("لا توجد فيديوهات في هذه القائمة.")
236
+ return {"error": "لا توجد فيديوهات في هذه القائمة."}
237
+ except Exception as e:
238
+ logging.error(f"فشل في جلب روابط الفيديو: {e}")
239
+ return {"error": f"فشل في جلب روابط الفيديو: {e}"}
240
+
241
+ individual_results = []
242
+
243
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
244
+ futures = [
245
+ executor.submit(
246
+ process_single_video2,
247
+ video_url,
248
+ loaded_quality_model,
249
+ loaded_sentiment_pipeline,
250
+ max_comments_per_video
251
+ ) for video_url in video_links
252
+ ]
253
+
254
+ for future in futures:
255
+ result = future.result()
256
+ if result:
257
+ individual_results.append(result)
258
+ time.sleep(random.uniform(0.5, 1.5))
259
+
260
+ num_good_videos = sum(1 for r in individual_results if r and r.get('quality') == 'جيد')
261
+ total_positive_comments = sum(r.get('positive_comments', 0) for r in individual_results if r)
262
+ total_negative_comments = sum(r.get('negative_comments', 0) for r in individual_results if r)
263
+ total_classified_comments = total_positive_comments + total_negative_comments
264
+
265
+ total_videos = len(individual_results)
266
+ percent_good_videos = (num_good_videos / total_videos) * 100 if total_videos > 0 else 0
267
+
268
+ if percent_good_videos >= 70:
269
+ overall_quality = "جيد جداً"
270
+ elif percent_good_videos >= 50:
271
+ overall_quality = "جيد"
272
+ else:
273
+ overall_quality = "سيء"
274
+
275
+ WEIGHT_QUALITY = 0.6
276
+ WEIGHT_SENTIMENT = 0.4
277
+
278
+ positive_ratio = (total_positive_comments / total_classified_comments) * 100 if total_classified_comments > 0 else 0.0
279
+ composite_score = (WEIGHT_QUALITY * percent_good_videos) + (WEIGHT_SENTIMENT * positive_ratio)
280
+
281
+ if composite_score >= 75:
282
+ composite_quality = "جيد جداً"
283
+ elif composite_score >= 60:
284
+ composite_quality = "جيد"
285
+ elif composite_score >= 45:
286
+ composite_quality = "متوسط"
287
+ else:
288
+ composite_quality = "سيء"
289
+
290
+ return {
291
+ "overall_quality": overall_quality,
292
+ "composite_quality": composite_quality,
293
+ "composite_score": round(composite_score, 1),
294
+ "percent_good_videos": round(percent_good_videos, 1),
295
+ "positive_ratio": round(positive_ratio, 1),
296
+ "negative_ratio": round(100 - positive_ratio, 1),
297
+ }
requirements.txt ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
17
+ matplotlib
18
+ arabic-reshaper
19
+ python-bidi