aliSaac510 commited on
Commit
ce16ace
·
1 Parent(s): 519351f

virsions and trnscript

Browse files
TECHNICAL_DOCUMENTATION.md ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # نظام إدارة نسخ الفيديو المتقدم - التوثيق التقني
2
+
3
+ ## نظرة عامة
4
+ نظام معالجة الفيديو المتكامل يوفر إدارة متقدمة للنسخ مع حماية النسخ الأصلية، ومعالجة منفصلة لكل نوع من التعديلات، وتتبع شامل لجميع الإصدارات.
5
+
6
+ ## المعمارية
7
+
8
+ ### المكونات الرئيسية
9
+
10
+ #### 1. **نظام إدارة النسخ (VersionManager)**
11
+ ```python
12
+ # الموقع: clipping/core/version_manager.py
13
+ # الوظيفة: إدارة النسخ الأصلية والمعدلة مع الحفاظ على سلامة البيانات
14
+ ```
15
+
16
+ **المميزات:**
17
+ - حماية النسخ الأصلية في مجلد منفصل مع صلاحيات قراءة فقط
18
+ - تتبع شجرة النسخ (parent-child relationships)
19
+ - سجل تغييرات شامل
20
+ - معرفات فريدة تلقائية
21
+ - إحصائيات التخزين والاستخدام
22
+
23
+ #### 2. **معالج الفيديو المتقدم (AdvancedVideoProcessor)**
24
+ ```python
25
+ # الموقع: clipping/core/advanced_processor.py
26
+ # الوظيفة: معالجة الفيديو باستخدام FFmpeg وMoviePy
27
+ ```
28
+
29
+ **أنواع المعالجة:**
30
+ - **ترانسكريبت**: إضافة نصوص مع الطوابع الزمنية
31
+ - **قص**: قص مناطق محددة من الفيديو
32
+ - **تأثيرات**: سطوع، تباين، تشبع، فلاتر جمالية
33
+ - **صوت**: تعديل الصوت، تعزيز، إزالة ضوضاء
34
+ - **مشترك**: معالجة متعددة في خطوة واحدة
35
+
36
+ #### 3. **واجهة برمجة التطبيقات (API)**
37
+ ```python
38
+ # الموقع: clipping/routers/video_versions.py
39
+ # الوظيفة: نقاط النهاية REST للتفاعل مع النظام
40
+ ```
41
+
42
+ ## نقاط النهاية API
43
+
44
+ ### رفع الفيديو الأصلي
45
+ ```http
46
+ POST /api/v1/video-versions/upload-original
47
+ Content-Type: multipart/form-data
48
+
49
+ Body:
50
+ - video_file: ملف الفيديو (مطلوب)
51
+ - metadata: بيانات وصفية اختيارية (JSON)
52
+ ```
53
+
54
+ **الاستجابة:**
55
+ ```json
56
+ {
57
+ "original_id": "uuid-string",
58
+ "file_name": "video.mp4",
59
+ "file_size": 10485760,
60
+ "upload_date": "2024-01-15T10:30:00Z",
61
+ "message": "Original video uploaded successfully",
62
+ "status": "protected"
63
+ }
64
+ ```
65
+
66
+ ### معالجة الترانسكريبت
67
+ ```http
68
+ POST /api/v1/video-versions/{original_id}/add-transcript
69
+ Content-Type: multipart/form-data
70
+
71
+ Body:
72
+ - transcript_config: إعدادات الترانسكريبت (JSON مطلوب)
73
+ - version_name: اسم النسخة الاختياري
74
+ ```
75
+
76
+ **مثال transcript_config:**
77
+ ```json
78
+ {
79
+ "segments": [
80
+ {
81
+ "start": 0.0,
82
+ "end": 5.0,
83
+ "text": "مرحباً بكم في الفيديو",
84
+ "position": "bottom",
85
+ "font_size": 28,
86
+ "font_color": "#FFFFFF"
87
+ }
88
+ ],
89
+ "font_size": 24,
90
+ "font_color": "#FFFFFF",
91
+ "font_family": "Arial",
92
+ "position": "bottom",
93
+ "background_color": "#000000",
94
+ "background_alpha": 0.8,
95
+ "margin": 20,
96
+ "shadow": true,
97
+ "outline": true
98
+ }
99
+ ```
100
+
101
+ ### معالجة القص
102
+ ```http
103
+ POST /api/v1/video-versions/{original_id}/crop
104
+ Content-Type: multipart/form-data
105
+
106
+ Body:
107
+ - crop_config: إعدادات القص (JSON مطلوب)
108
+ - version_name: اسم النسخة الاختياري
109
+ ```
110
+
111
+ **مثال crop_config:**
112
+ ```json
113
+ {
114
+ "x1": 100,
115
+ "y1": 50,
116
+ "width": 800,
117
+ "height": 600,
118
+ "center_crop": false,
119
+ "aspect_ratio": "16:9"
120
+ }
121
+ ```
122
+
123
+ ### معالجة التأثيرات
124
+ ```http
125
+ POST /api/v1/video-versions/{original_id}/effects
126
+ Content-Type: multipart/form-data
127
+
128
+ Body:
129
+ - effects_config: إعدادات التأثيرات (JSON مطلوب)
130
+ - version_name: اسم النسخة الاختياري
131
+ ```
132
+
133
+ **مثال effects_config:**
134
+ ```json
135
+ {
136
+ "brightness": 1.2,
137
+ "contrast": 1.1,
138
+ "saturation": 1.3,
139
+ "fade_in": 2.0,
140
+ "fade_out": 3.0,
141
+ "blur": 0.5,
142
+ "vignette": 0.3,
143
+ "vintage": true
144
+ }
145
+ ```
146
+
147
+ ### معالجة الصوت
148
+ ```http
149
+ POST /api/v1/video-versions/{original_id}/audio
150
+ Content-Type: multipart/form-data
151
+
152
+ Body:
153
+ - audio_config: إعدادات الصوت (JSON مطلوب)
154
+ - version_name: اسم النسخة الاختياري
155
+ ```
156
+
157
+ **مثال audio_config:**
158
+ ```json
159
+ {
160
+ "volume": 1.5,
161
+ "normalize": true,
162
+ "remove_noise": true,
163
+ "bass_boost": 0.3,
164
+ "fade_in": 1.0,
165
+ "fade_out": 2.0,
166
+ "speed": 1.1
167
+ }
168
+ ```
169
+
170
+ ### معالجة مشتركة
171
+ ```http
172
+ POST /api/v1/video-versions/{original_id}/combined
173
+ Content-Type: multipart/form-data
174
+
175
+ Body:
176
+ - processing_request: طلب المعالجة المشتركة (JSON مطلوب)
177
+ - version_name: اسم النسخة الاختياري
178
+ ```
179
+
180
+ **مثال processing_request:**
181
+ ```json
182
+ {
183
+ "original_id": "uuid-string",
184
+ "processing_type": "combined",
185
+ "transcript_config": { /* نفس إعدادات الترانسكريبت */ },
186
+ "crop_config": { /* نفس إعدادات القص */ },
187
+ "effects_config": { /* نفس إعدادات التأثيرات */ },
188
+ "audio_config": { /* نفس إعدادات الصوت */ },
189
+ "priority": "high",
190
+ "metadata": {}
191
+ }
192
+ ```
193
+
194
+ ### إدارة النسخ
195
+
196
+ #### قائمة النسخ
197
+ ```http
198
+ GET /api/v1/video-versions/versions/{original_id}
199
+ ```
200
+
201
+ #### تفاصيل نسخة
202
+ ```http
203
+ GET /api/v1/video-versions/version/{version_id}
204
+ ```
205
+
206
+ #### تحميل نسخة
207
+ ```http
208
+ GET /api/v1/video-versions/download/{version_id}
209
+ ```
210
+
211
+ #### حذف نسخة
212
+ ```http
213
+ DELETE /api/v1/video-versions/delete/{version_id}
214
+ ```
215
+
216
+ #### شجرة النسخ
217
+ ```http
218
+ GET /api/v1/video-versions/version-tree/{original_id}
219
+ ```
220
+
221
+ ### إحصائيات النظام
222
+ ```http
223
+ GET /api/v1/video-versions/stats
224
+ ```
225
+
226
+ ## معايير الأداء
227
+
228
+ ### الجودة
229
+ - **الأصلي**: يتم الحفاظ على جودة الفيديو الأصلي بدون أي تعديلات
230
+ - **المعالجة**: استخدام FFmpeg لضمان أعلى جودة ممكنة
231
+ - **الترميز**: H.264 مع إعدادات مثالية للحفاظ على الجودة
232
+
233
+ ### السرعة
234
+ - **الرفع**: معالجة غير متزامنة مع مؤشرات تقدم
235
+ - **المعالجة**: معالجة متعددة الخيوط مع ThreadPoolExecutor
236
+ - **الذاكرة**: إدارة مثالية للذاكرة مع تنظيف تلقائي
237
+
238
+ ### الموثوقية
239
+ - **الأخطاء**: معالجة شاملة للأخطاء مع استعادة تلقائية
240
+ - **السجل**: سجل تفصيلي لجميع العمليات
241
+ - **النسخ الاحتياطي**: آلية نسخ احتياطي للنسخ الأصلية
242
+
243
+ ## هيكل الملفات
244
+
245
+ ```
246
+ video_storage/
247
+ ├── originals/ # النسخ الأصلية (قراءة فقط)
248
+ │ └── {original_id}/
249
+ │ └── {file_name}
250
+ ├── versions/ # النسخ المعدلة
251
+ │ └── {version_id}/
252
+ │ ├── video.mp4 # الفيديو المعدل
253
+ │ ├── config.json # إعدادات المعالجة
254
+ │ └── metadata.json # البيانات الوصفية
255
+ └── version_registry.json # سجل جميع النسخ
256
+ ```
257
+
258
+ ## أمثلة الاستخدام
259
+
260
+ ### مثال 1: رفع فيديو وإضافة ترانسكريبت
261
+ ```python
262
+ import requests
263
+
264
+ # رفع الفيديو الأصلي
265
+ files = {'video_file': open('video.mp4', 'rb')}
266
+ response = requests.post('http://localhost:7860/api/v1/video-versions/upload-original', files=files)
267
+ original_id = response.json()['original_id']
268
+
269
+ # إعدادات الترانسكريبت
270
+ transcript_config = {
271
+ "segments": [
272
+ {"start": 0, "end": 5, "text": "مرحباً بكم"},
273
+ {"start": 5, "end": 10, "text": "هذا فيديو تجريبي"}
274
+ ],
275
+ "font_size": 24,
276
+ "font_color": "#FFFFFF",
277
+ "position": "bottom"
278
+ }
279
+
280
+ # إضافة الترانسكريبت
281
+ data = {'transcript_config': str(transcript_config).replace("'", '"')}
282
+ response = requests.post(f'http://localhost:7860/api/v1/video-versions/{original_id}/add-transcript', data=data)
283
+ version_id = response.json()['version_id']
284
+ ```
285
+
286
+ ### مثال 2: قص فيديو مع تأثيرات
287
+ ```python
288
+ # إعدادات القص
289
+ crop_config = {
290
+ "center_crop": True,
291
+ "width": 720,
292
+ "height": 1280, # 9:16 aspect ratio
293
+ "aspect_ratio": "9:16"
294
+ }
295
+
296
+ # إعدادات التأثيرات
297
+ effects_config = {
298
+ "brightness": 1.2,
299
+ "contrast": 1.1,
300
+ "saturation": 1.3,
301
+ "vintage": True
302
+ }
303
+
304
+ # معالجة القص
305
+ data = {'crop_config': str(crop_config).replace("'", '"')}
306
+ response = requests.post(f'http://localhost:7860/api/v1/video-versions/{original_id}/crop', data=data)
307
+ crop_version_id = response.json()['version_id']
308
+
309
+ # معالجة التأثيرات على النسخة المقصوصة
310
+ data = {'effects_config': str(effects_config).replace("'", '"')}
311
+ response = requests.post(f'http://localhost:7860/api/v1/video-versions/{crop_version_id}/effects', data=data)
312
+ final_version_id = response.json()['version_id']
313
+ ```
314
+
315
+ ## معالجة الأخطاء
316
+
317
+ ### رموز الحالة
318
+ - **200**: نجاح
319
+ - **400**: طلب غير صالح
320
+ - **404**: مورد غير موجود
321
+ - **422**: بيانات غير صالحة
322
+ - **500**: خطأ في الخادم
323
+
324
+ ### رسائل الخطأ
325
+ ```json
326
+ {
327
+ "error": "Invalid processing configuration",
328
+ "details": "Font size must be between 12 and 72",
329
+ "suggestion": "Please adjust font_size parameter"
330
+ }
331
+ ```
332
+
333
+ ## التحسينات المستقبلية
334
+
335
+ 1. **معالجة GPU**: دعم معالجة GPU لتسريع العمليات
336
+ 2. **تخزين سحابي**: دعم تخزين Amazon S3 وGoogle Cloud Storage
337
+ 3. **ذكاء اصطناعي**: إضافة تأثيرات AI مثل إزالة الخلفية
338
+ 4. **تعاوني**: دعم العمل الجماعي والتعليقات
339
+ 5. **تكامل API**: REST API كامل مع Swagger documentation
340
+
341
+ ## التواصل والدعم
342
+
343
+ للأسئلة والدعم الفني، يرجى التواصل عبر:
344
+ - البريد ا��إلكتروني: support@videosystem.com
345
+ - GitHub Issues: github.com/videosystem/issues
346
+ - الوثائق الكاملة: docs.videosystem.com
USER_GUIDE.md ADDED
@@ -0,0 +1,720 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # دليل استخدام نظام إدارة نسخ الفيديو المتقدم
2
+
3
+ ## 📋 جدول المحتويات
4
+ 1. [مقدمة](#مقدمة)
5
+ 2. [التثبيت والإعداد](#التثبيت-والإعداد)
6
+ 3. [رفع الفيديو الأصلي](#رفع-الفيديو-الأصلي)
7
+ 4. [إضافة ترانسكريبت](#إضافة-ترانسكريبت)
8
+ 5. [قص الفيديو](#قص-الفيديو)
9
+ 6. [إضافة تأثيرات](#إضافة-تأثيرات)
10
+ 7. [معالجة الصوت](#معالجة-الصوت)
11
+ 8. [المعالجة المشتركة](#المعالجة-المشتركة)
12
+ 9. [إدارة النسخ](#إدارة-النسخ)
13
+ 10. [أمثلة عملية](#أمثلة-عملية)
14
+ 11. [نصائح وتوصيات](#نصائح-وأفضل-الممارسات)
15
+ 12. [استكشاف الأخطاء](#استكشاف-الأخطاء)
16
+
17
+ ## 🎯 مقدمة
18
+
19
+ نظام إدارة نسخ الفيديو المتقدم هو حل شامل لمعالجة الفيديو مع الحفاظ على النسخ الأصلية. يتيح لك إنشاء نسخ متعددة من نفس الفيديو مع تعديلات مختلفة مع الحفاظ على النسخة الأصلية دون أي تغيير.
20
+
21
+ ### المميزات الرئيسية:
22
+ - ✅ حماية النسخ الأصلية (قراءة فقط)
23
+ - ✅ إنشاء نسخ متعددة من نفس الفيديو
24
+ - ✅ معالجة منفصلة لكل نوع من التعديلات
25
+ - ✅ تتبع شامل لجميع الإصدارات
26
+ - ✅ واجهة برمجة تطبيقات REST سهلة الاستخدام
27
+ - ✅ معالجة غير متزامنة لتحسين الأداء
28
+
29
+ ## ⚙️ التثبيت والإعداد
30
+
31
+ ### المتطلبات الأساسية
32
+ ```bash
33
+ # Python 3.8 أو أحدث
34
+ python --version
35
+
36
+ # تثبيت المتطلبات
37
+ pip install fastapi uvicorn python-multipart moviepy ffmpeg-python pydantic
38
+ ```
39
+
40
+ ### تشغيل الخادم
41
+ ```bash
42
+ # من المجلد الرئيسي
43
+ python main.py
44
+
45
+ # أو باستخدام uvicorn مباشرة
46
+ uvicorn main:app --host 0.0.0.0 --port 7860 --reload
47
+ ```
48
+
49
+ ### التحقق من التشغيل
50
+ افتح المتصفح وانتقل إلى:
51
+ ```
52
+ http://localhost:7860/
53
+ ```
54
+
55
+ يجب أن ترى رسالة ترحيب مع معلومات النظام.
56
+
57
+ ## 📤 رفع الفيديو الأصلي
58
+
59
+ ### الخطوة 1: رفع الفيديو
60
+ ```python
61
+ import requests
62
+
63
+ # رفع الفيديو الأصلي
64
+ files = {'video_file': open('video.mp4', 'rb')}
65
+ response = requests.post('http://localhost:7860/api/v1/video-versions/upload-original', files=files)
66
+
67
+ # استخراج معرف الفيديو الأصلي
68
+ original_id = response.json()['original_id']
69
+ print(f"Original ID: {original_id}")
70
+ ```
71
+
72
+ ### الخطوة 2: التحقق من الرفع
73
+ ```python
74
+ # التحقق من معلومات الفيديو الأصلي
75
+ response = requests.get(f'http://localhost:7860/api/v1/video-versions/versions/{original_id}')
76
+ print(response.json())
77
+ ```
78
+
79
+ ## 📝 إضافة ترانسكريبت
80
+
81
+ ### مثال 1: ترانسكريبت بسيط
82
+ ```python
83
+ # إعدادات الترانسكريبت
84
+ transcript_config = {
85
+ "segments": [
86
+ {
87
+ "start": 0.0,
88
+ "end": 5.0,
89
+ "text": "مرحباً بكم في الفيديو",
90
+ "position": "bottom"
91
+ },
92
+ {
93
+ "start": 5.0,
94
+ "end": 10.0,
95
+ "text": "سنشرح لكم كيفية استخدام النظام",
96
+ "position": "bottom"
97
+ }
98
+ ],
99
+ "font_size": 24,
100
+ "font_color": "#FFFFFF",
101
+ "font_family": "Arial",
102
+ "position": "bottom",
103
+ "background_color": "#000000",
104
+ "background_alpha": 0.8,
105
+ "margin": 20,
106
+ "shadow": True,
107
+ "outline": True
108
+ }
109
+
110
+ # إرسال طلب الترانسكريبت
111
+ data = {
112
+ "transcript_config": json.dumps(transcript_config, ensure_ascii=False),
113
+ "version_name": "version_with_transcript"
114
+ }
115
+
116
+ response = requests.post(f'http://localhost:7860/api/v1/video-versions/{original_id}/add-transcript', data=data)
117
+ version_id = response.json()['version_id']
118
+ print(f"Transcript Version ID: {version_id}")
119
+ ```
120
+
121
+ ### مثال 2: ترانسكريبت متقدم مع أنيميشن
122
+ ```python
123
+ # ترانسكريبت مع أنيميشن
124
+ transcript_config = {
125
+ "segments": [
126
+ {
127
+ "start": 0.0,
128
+ "end": 3.0,
129
+ "text": "اهلاً وسهلاً",
130
+ "position": "center",
131
+ "font_size": 32,
132
+ "font_color": "#FFD700" # لون ذهبي
133
+ },
134
+ {
135
+ "start": 3.0,
136
+ "end": 8.0,
137
+ "text": "هذا عرض تقديمي احترافي",
138
+ "position": "bottom",
139
+ "font_size": 28,
140
+ "font_color": "#FFFFFF"
141
+ }
142
+ ],
143
+ "font_size": 24,
144
+ "font_color": "#FFFFFF",
145
+ "font_family": "Helvetica",
146
+ "position": "bottom",
147
+ "background_color": "#1E3A8A",
148
+ "background_alpha": 0.9,
149
+ "margin": 30,
150
+ "opacity": 1.0,
151
+ "animation": "fade_in",
152
+ "shadow": True,
153
+ "outline": True
154
+ }
155
+
156
+ # إرسال الطلب
157
+ data = {
158
+ "transcript_config": json.dumps(transcript_config, ensure_ascii=False),
159
+ "version_name": "animated_transcript_version"
160
+ }
161
+
162
+ response = requests.post(f'http://localhost:7860/api/v1/video-versions/{original_id}/add-transcript', data=data)
163
+ ```
164
+
165
+ ## ✂️ قص الفيديو
166
+
167
+ ### مثال 1: قص منطقة محددة
168
+ ```python
169
+ # إعدادات القص
170
+ crop_config = {
171
+ "x1": 200, # بداية من اليسار
172
+ "y1": 100, # بداية من الأعلى
173
+ "width": 800, # عرض المنطقة
174
+ "height": 600, # ارتفاع المنطقة
175
+ "center_crop": False
176
+ }
177
+
178
+ # إرسال طلب القص
179
+ data = {
180
+ "crop_config": json.dumps(crop_config),
181
+ "version_name": "cropped_region_version"
182
+ }
183
+
184
+ response = requests.post(f'http://localhost:7860/api/v1/video-versions/{original_id}/crop', data=data)
185
+ crop_version_id = response.json()['version_id']
186
+ ```
187
+
188
+ ### مثال 2: قص تلقائي من المركز
189
+ ```python
190
+ # قص من المركز لإنشاء فيديو عمودي (Shorts)
191
+ crop_config = {
192
+ "center_crop": True,
193
+ "width": 720, # عرض Shorts
194
+ "height": 1280, # ارتفاع Shorts (9:16)
195
+ "aspect_ratio": "9:16"
196
+ }
197
+
198
+ # إرسال الطلب
199
+ data = {
200
+ "crop_config": json.dumps(crop_config),
201
+ "version_name": "shorts_version"
202
+ }
203
+
204
+ response = requests.post(f'http://localhost:7860/api/v1/video-versions/{original_id}/crop', data=data)
205
+ shorts_version_id = response.json()['version_id']
206
+ ```
207
+
208
+ ### مثال 3: قص لإنشاء فيديو مربع
209
+ ```python
210
+ # قص لإنشاء فيديو مربع للإنستغرام
211
+ crop_config = {
212
+ "center_crop": True,
213
+ "width": 1080, # عرض مربع
214
+ "height": 1080, # ارتفاع مربع (1:1)
215
+ "aspect_ratio": "1:1"
216
+ }
217
+
218
+ # إرسال الطلب
219
+ data = {
220
+ "crop_config": json.dumps(crop_config),
221
+ "version_name": "instagram_square_version"
222
+ }
223
+
224
+ response = requests.post(f'http://localhost:7860/api/v1/video-versions/{original_id}/crop', data=data)
225
+ instagram_version_id = response.json()['version_id']
226
+ ```
227
+
228
+ ## 🎨 إضافة تأثيرات
229
+
230
+ ### مثال 1: تأثيرات أساسية
231
+ ```python
232
+ # إعدادات التأثيرات الأساسية
233
+ effects_config = {
234
+ "brightness": 1.2, # سطوع +20%
235
+ "contrast": 1.1, # تباين +10%
236
+ "saturation": 1.3, # تشبع +30%
237
+ "fade_in": 2.0, # تلاشي دخول 2 ثانية
238
+ "fade_out": 3.0 # تلاشي خروج 3 ثواني
239
+ }
240
+
241
+ # إرسال الطلب
242
+ data = {
243
+ "effects_config": json.dumps(effects_config),
244
+ "version_name": "enhanced_effects_version"
245
+ }
246
+
247
+ response = requests.post(f'http://localhost:7860/api/v1/video-versions/{original_id}/effects', data=data)
248
+ effects_version_id = response.json()['version_id']
249
+ ```
250
+
251
+ ### مثال 2: تأثيرات فنية
252
+ ```python
253
+ # تأثيرات فنية (فينتاج)
254
+ effects_config = {
255
+ "sepia": True, # تأثير سيبيا (بني قديم)
256
+ "vintage": True, # تأثير فينتاج
257
+ "vignette": 0.4, # تأثير إطار مظلم
258
+ "noise": 0.1, # ضوضاء خفيفة
259
+ "blur": 0.2 # ضبابية خفيفة
260
+ }
261
+
262
+ # إرسال الطلب
263
+ data = {
264
+ "effects_config": json.dumps(effects_config),
265
+ "version_name": "vintage_artistic_version"
266
+ }
267
+
268
+ response = requests.post(f'http://localhost:7860/api/v1/video-versions/{original_id}/effects', data=data)
269
+ vintage_version_id = response.json()['version_id']
270
+ ```
271
+
272
+ ### مثال 3: تأثيرات درامية
273
+ ```python
274
+ # تأثيرات درامية
275
+ effects_config = {
276
+ "black_white": True, # أبيض وأسود
277
+ "contrast": 1.5, # تباين عالي
278
+ "sharpen": 2.0, # حدة عالية
279
+ "fade_in": 1.0, # تلاشي سريع
280
+ "fade_out": 2.0 # تلاشي بطيء
281
+ }
282
+
283
+ # إرسال الطلب
284
+ data = {
285
+ "effects_config": json.dumps(effects_config),
286
+ "version_name": "dramatic_black_white_version"
287
+ }
288
+
289
+ response = requests.post(f'http://localhost:7860/api/v1/video-versions/{original_id}/effects', data=data)
290
+ dramatic_version_id = response.json()['version_id']
291
+ ```
292
+
293
+ ## 🔊 معالجة الصوت
294
+
295
+ ### مثال 1: تعزيز الصوت الأساسي
296
+ ```python
297
+ # إعدادات الصوت الأساسية
298
+ audio_config = {
299
+ "volume": 1.3, # رفع الصوت 30%
300
+ "normalize": True, # تطبيع الصوت
301
+ "remove_noise": True, # إزالة الضوضاء
302
+ "fade_in": 1.0, # تلاشي دخول
303
+ "fade_out": 2.0 # تلاشي خروج
304
+ }
305
+
306
+ # إرسال الطلب
307
+ data = {
308
+ "audio_config": json.dumps(audio_config),
309
+ "version_name": "enhanced_audio_version"
310
+ }
311
+
312
+ response = requests.post(f'http://localhost:7860/api/v1/video-versions/{original_id}/audio', data=data)
313
+ audio_version_id = response.json()['version_id']
314
+ ```
315
+
316
+ ### مثال 2: تأثيرات صوتية
317
+ ```python
318
+ # تأثيرات صوتية متقدمة
319
+ audio_config = {
320
+ "bass_boost": 0.5, # تعزيز الجهير
321
+ "treble_boost": 0.3, # تعزيز التريبل
322
+ "speed": 1.1, # تسريع 10% (بدون تغيير النبرة)
323
+ "pitch_shift": 2, # رفع النبرة 2 نصف نغمة
324
+ "fade_in": 0.5, # تلاشي سريع
325
+ "fade_out": 1.0 # تلاشي متوسط
326
+ }
327
+
328
+ # إرسال الطلب
329
+ data = {
330
+ "audio_config": json.dumps(audio_config),
331
+ "version_name": "processed_audio_version"
332
+ }
333
+
334
+ response = requests.post(f'http://localhost:7860/api/v1/video-versions/{original_id}/audio', data=data)
335
+ processed_audio_version_id = response.json()['version_id']
336
+ ```
337
+
338
+ ## 🔄 المعالجة المشتركة
339
+
340
+ ### مثال: إنشاء فيديو Shorts احترافي
341
+ ```python
342
+ # معالجة مشتركة متكاملة
343
+ processing_request = {
344
+ "original_id": original_id,
345
+ "processing_type": "combined",
346
+ "transcript_config": {
347
+ "segments": [
348
+ {
349
+ "start": 0.0,
350
+ "end": 5.0,
351
+ "text": "نصيحة ذهبية للنجاح",
352
+ "position": "center",
353
+ "font_size": 28,
354
+ "font_color": "#FFD700"
355
+ }
356
+ ],
357
+ "font_size": 24,
358
+ "font_color": "#FFFFFF",
359
+ "position": "bottom",
360
+ "shadow": True,
361
+ "outline": True
362
+ },
363
+ "crop_config": {
364
+ "center_crop": True,
365
+ "width": 720,
366
+ "height": 1280,
367
+ "aspect_ratio": "9:16"
368
+ },
369
+ "effects_config": {
370
+ "brightness": 1.1,
371
+ "contrast": 1.2,
372
+ "saturation": 1.1,
373
+ "fade_in": 1.0,
374
+ "vintage": False
375
+ },
376
+ "audio_config": {
377
+ "volume": 1.2,
378
+ "normalize": True,
379
+ "remove_noise": True,
380
+ "fade_in": 0.5,
381
+ "fade_out": 1.0
382
+ },
383
+ "priority": "high",
384
+ "metadata": {
385
+ "description": "Shorts video with transcript, crop, effects and audio enhancement",
386
+ "target_platform": "YouTube Shorts"
387
+ }
388
+ }
389
+
390
+ # إرسال الطلب المشترك
391
+ data = {
392
+ "processing_request": json.dumps(processing_request, ensure_ascii=False),
393
+ "version_name": "professional_shorts_version"
394
+ }
395
+
396
+ response = requests.post(f'http://localhost:7860/api/v1/video-versions/{original_id}/combined', data=data)
397
+ combined_version_id = response.json()['version_id']
398
+ ```
399
+
400
+ ## 📋 إدارة النسخ
401
+
402
+ ### عرض جميع النسخ
403
+ ```python
404
+ # الحصول على قائمة جميع النسخ
405
+ response = requests.get(f'http://localhost:7860/api/v1/video-versions/versions/{original_id}')
406
+ versions_info = response.json()
407
+
408
+ print(f"عدد النسخ: {versions_info['total_count']}")
409
+ for version in versions_info['versions']:
410
+ print(f"- {version['version_name']}: {version['processing_type']} ({version['status']})")
411
+ ```
412
+
413
+ ### عرض تفاصيل نسخة محددة
414
+ ```python
415
+ # الحصول على تفاصيل نسخة محددة
416
+ version_id = "your-version-id" # استبدل بمعرف النسخة
417
+ response = requests.get(f'http://localhost:7860/api/v1/video-versions/version/{version_id}')
418
+ version_details = response.json()
419
+
420
+ print(f"اسم النسخة: {version_details['version_name']}")
421
+ print(f"نوع المعالجة: {version_details['processing_type']}")
422
+ print(f"الحالة: {version_details['status']}")
423
+ print(f"تاريخ الإنشاء: {version_details['created_at']}")
424
+ print(f"حجم الملف: {version_details['file_size']} بايت")
425
+ print(f"المدة: {version_details['duration']} ثانية")
426
+ print(f"الدقة: {version_details['resolution']}")
427
+ ```
428
+
429
+ ### تحميل نسخة
430
+ ```python
431
+ # تحميل نسخة معالجة
432
+ response = requests.get(f'http://localhost:7860/api/v1/video-versions/download/{version_id}')
433
+
434
+ # حفظ الملف
435
+ with open('downloaded_video.mp4', 'wb') as f:
436
+ f.write(response.content)
437
+
438
+ print("تم تحميل النسخة بنجاح!")
439
+ ```
440
+
441
+ ### حذف نسخة
442
+ ```python
443
+ # حذف نسخة غير مرغوب فيها
444
+ response = requests.delete(f'http://localhost:7860/api/v1/video-versions/delete/{version_id}')
445
+ delete_result = response.json()
446
+
447
+ print(f"تم حذف النسخة: {delete_result['version_id']}")
448
+ ```
449
+
450
+ ### عرض شجرة النسخ
451
+ ```python
452
+ # عرض العلاقات بين النسخ المختلفة
453
+ response = requests.get(f'http://localhost:7860/api/v1/video-versions/version-tree/{original_id}')
454
+ tree_info = response.json()
455
+
456
+ print("شجرة النسخ:")
457
+ print(json.dumps(tree_info['version_tree'], indent=2, ensure_ascii=False))
458
+ ```
459
+
460
+ ### عرض إحصائيات النظام
461
+ ```python
462
+ # عرض إحصائيات التخزين والاستخدام
463
+ response = requests.get('http://localhost:7860/api/v1/video-versions/stats')
464
+ stats = response.json()
465
+
466
+ print(f"عدد الفيديوهات الأصلية: {stats['storage']['original_videos']}")
467
+ print(f"عدد النسخ المعدلة: {stats['storage']['total_versions']}")
468
+ print(f"حجم الفيديوهات الأصلية: {stats['storage']['originals_size']} بايت")
469
+ print(f"حجم النسخ المعدلة: {stats['storage']['versions_size']} بايت")
470
+ print(f"الحجم الكلي: {stats['storage']['total_size']} بايت")
471
+ print(f"حالة النظام: {stats['system_status']}")
472
+ ```
473
+
474
+ ## 💡 أمثلة عملية
475
+
476
+ ### مثال 1: إنشاء فيديو تعليمي مع ترانسكريبت
477
+ ```python
478
+ import requests
479
+ import json
480
+
481
+ # 1. رفع الفيديو الأصلي
482
+ files = {'video_file': open('educational_video.mp4', 'rb')}
483
+ response = requests.post('http://localhost:7860/api/v1/video-versions/upload-original', files=files)
484
+ original_id = response.json()['original_id']
485
+
486
+ # 2. إضافة ترانسكريبت تعليمي
487
+ transcript_config = {
488
+ "segments": [
489
+ {"start": 0, "end": 8, "text": "مرحباً بكم في هذا الدرس التعليمي"},
490
+ {"start": 8, "end": 20, "text": "اليوم سنتعلم كيفية استخدام Python في معالجة البيانات"},
491
+ {"start": 20, "end": 35, "text": "Python هو لغة برمجة قوية وسهلة التعلم"}
492
+ ],
493
+ "font_size": 26,
494
+ "font_color": "#2E86AB",
495
+ "font_family": "Arial",
496
+ "position": "bottom",
497
+ "background_color": "#F5F5F5",
498
+ "background_alpha": 0.9,
499
+ "margin": 25,
500
+ "shadow": True,
501
+ "outline": False
502
+ }
503
+
504
+ data = {
505
+ "transcript_config": json.dumps(transcript_config, ensure_ascii=False),
506
+ "version_name": "educational_with_transcript"
507
+ }
508
+
509
+ response = requests.post(f'http://localhost:7860/api/v1/video-versions/{original_id}/add-transcript', data=data)
510
+ educational_version_id = response.json()['version_id']
511
+
512
+ print(f"تم إنشاء نسخة تعليمية مع ترانسكريبت: {educational_version_id}")
513
+ ```
514
+
515
+ ### مثال 2: إنشاء فيديو تسويقي للسوشيال ميديا
516
+ ```python
517
+ # 1. نبدأ من نفس الفيديو الأصلي
518
+
519
+ # 2. إنشاء نسخة عمودية مع تأثيرات
520
+ crop_config = {
521
+ "center_crop": True,
522
+ "width": 720,
523
+ "height": 1280,
524
+ "aspect_ratio": "9:16"
525
+ }
526
+
527
+ effects_config = {
528
+ "brightness": 1.15,
529
+ "contrast": 1.2,
530
+ "saturation": 1.25,
531
+ "fade_in": 1.5,
532
+ "fade_out": 2.0,
533
+ "vignette": 0.2
534
+ }
535
+
536
+ audio_config = {
537
+ "volume": 1.3,
538
+ "normalize": True,
539
+ "bass_boost": 0.4,
540
+ "fade_in": 0.8,
541
+ "fade_out": 1.5
542
+ }
543
+
544
+ # إنشاء النسخة المقصوصة أولاً
545
+ data = {
546
+ "crop_config": json.dumps(crop_config),
547
+ "version_name": "social_media_cropped"
548
+ }
549
+ response = requests.post(f'http://localhost:7860/api/v1/video-versions/{original_id}/crop', data=data)
550
+ cropped_version_id = response.json()['version_id']
551
+
552
+ # ثم نضيف التأثيرات على النسخة المقصوصة
553
+ data = {
554
+ "effects_config": json.dumps(effects_config),
555
+ "version_name": "social_media_final"
556
+ }
557
+ response = requests.post(f'http://localhost:7860/api/v1/video-versions/{cropped_version_id}/effects', data=data)
558
+ final_version_id = response.json()['version_id']
559
+
560
+ # أخيراً نعالج الصوت
561
+ data = {
562
+ "audio_config": json.dumps(audio_config),
563
+ "version_name": "social_media_complete"
564
+ }
565
+ response = requests.post(f'http://localhost:7860/api/v1/video-versions/{final_version_id}/audio', data=data)
566
+ complete_version_id = response.json()['version_id']
567
+
568
+ print(f"تم إنشاء فيديو تسويقي متكامل: {complete_version_id}")
569
+ ```
570
+
571
+ ### مثال 3: معالجة دفعية لعدة فيديوهات
572
+ ```python
573
+ def process_video_batch(video_files, processing_config):
574
+ """معالجة دفعية لعدة فيديوهات"""
575
+ results = []
576
+
577
+ for video_file in video_files:
578
+ try:
579
+ # رفع الفيديو
580
+ files = {'video_file': open(video_file, 'rb')}
581
+ response = requests.post('http://localhost:7860/api/v1/video-versions/upload-original', files=files)
582
+ original_id = response.json()['original_id']
583
+
584
+ # تطبيق نفس المعالجة على جميع الفيديوهات
585
+ data = {
586
+ "processing_request": json.dumps(processing_config),
587
+ "version_name": f"processed_{Path(video_file).stem}"
588
+ }
589
+
590
+ response = requests.post(f'http://localhost:7860/api/v1/video-versions/{original_id}/combined', data=data)
591
+ version_id = response.json()['version_id']
592
+
593
+ results.append({
594
+ 'original_file': video_file,
595
+ 'original_id': original_id,
596
+ 'version_id': version_id,
597
+ 'status': 'success'
598
+ })
599
+
600
+ except Exception as e:
601
+ results.append({
602
+ 'original_file': video_file,
603
+ 'error': str(e),
604
+ 'status': 'failed'
605
+ })
606
+
607
+ return results
608
+
609
+ # إعدادات المعالجة الموحدة
610
+ processing_config = {
611
+ "original_id": "placeholder", # سيتم استب��اله لكل فيديو
612
+ "processing_type": "combined",
613
+ "transcript_config": {
614
+ "segments": [{"start": 0, "end": 5, "text": "شاهد هذا الفيديو المميز"}],
615
+ "font_size": 28,
616
+ "font_color": "#FF6B35",
617
+ "position": "center"
618
+ },
619
+ "crop_config": {
620
+ "center_crop": True,
621
+ "width": 720,
622
+ "height": 1280,
623
+ "aspect_ratio": "9:16"
624
+ },
625
+ "effects_config": {
626
+ "brightness": 1.1,
627
+ "contrast": 1.1,
628
+ "fade_in": 1.0
629
+ },
630
+ "audio_config": {
631
+ "volume": 1.2,
632
+ "normalize": True
633
+ },
634
+ "priority": "normal"
635
+ }
636
+
637
+ # قائمة الفيديوهات للمعالجة
638
+ video_files = ['video1.mp4', 'video2.mp4', 'video3.mp4']
639
+
640
+ # تنفيذ المعالجة الدفعية
641
+ results = process_video_batch(video_files, processing_config)
642
+
643
+ # عرض النتائج
644
+ for result in results:
645
+ if result['status'] == 'success':
646
+ print(f"✅ {result['original_file']} -> {result['version_id']}")
647
+ else:
648
+ print(f"❌ {result['original_file']} -> خطأ: {result['error']}")
649
+ ```
650
+
651
+ ## 🎯 نصائح وأفضل الممارسات
652
+
653
+ ### 1. اختيار الإعدادات المناسبة
654
+ - **الترانسكريبت**: استخدم أحجام خط مناسبة (20-32) حسب حجم الفيديو
655
+ - **القص**: احتفظ بنسب الأبعاد المناسبة لكل منصة (9:16 للـ Shorts، 1:1 للإنستغرام)
656
+ - **التأثيرات**: لا تبالغ في التأثيرات، ابدأ بقيم صغيرة وزد تدريجياً
657
+ - **الصوت**: استخدم تطبيع الصوت دائماً لضمان جودة متسقة
658
+
659
+ ### 2. إدارة النسخ بكفاءة
660
+ - أطلق على النسخ أسماء واضحة ومعبرة
661
+ - احذف النسخ غير المستخدمة لتوفير المساحة
662
+ - استخدم شجرة النسخ لفهم العلاقات بين النسخ
663
+ - احتفظ بالنسخ الأصلية دائماً كنسخة احتياطية
664
+
665
+ ### 3. تحسين الأداء
666
+ - استخدم المعالجة المشتركة لتقليل عدد خطوات المعالجة
667
+ - حدد أولوية المعالجة حسب أهمية الفيديو
668
+ - راقب إحصائيات النظام لتجنب تجاوز الحدود
669
+ - استخدم معالجة غير متزامنة للفيديوهات الكبيرة
670
+
671
+ ### 4. ضمان الجودة
672
+ - اختبر الإعدادات على نسخة تجريبية أولاً
673
+ - تحقق من جودة الفيديو الناتج قبل الاستخدام النهائي
674
+ - استخدم تنسيقات فيديو مناسبة (MP4 موصى به)
675
+ - احتفظ بنسخة احتياطية من الفيديوهات المهمة
676
+
677
+ ## 🔧 استكشاف الأخطاء
678
+
679
+ ### أخطاء شائعة وحلولها
680
+
681
+ #### خطأ 1: "File must be a video"
682
+ **السبب**: تم رفع ملف غير مدعوم
683
+ **الحل**: تأكد أن الملف هو فيديو صالح (MP4, AVI, MOV, إلخ)
684
+
685
+ #### خطأ 2: "Invalid processing configuration"
686
+ **السبب**: قيم إعدادات غير صالحة
687
+ **الحل**: تحقق من نطاق القيم المسموح بها في الوثائق
688
+
689
+ #### خطأ 3: "Original video not found"
690
+ **السبب**: معرف الفيديو الأصلي غير صحيح
691
+ **الحل**: تحقق من معرف الفيديو الأصلي واستخدم القيمة الصحيحة
692
+
693
+ #### خطأ 4: "Version not found"
694
+ **السبب**: معرف النسخة غير صحيح
695
+ **الحل**: تحقق من معرف النسخة في قائمة النسخ
696
+
697
+ #### خطأ 5: معالجة بطيئة
698
+ **السبب**: فيديو كبير أو إعدادات معقدة
699
+ **الحل**: استخدم أولوية "high" أو قسم الفيديو إلى أجزاء أصغر
700
+
701
+ ### نصائح للتصحيح
702
+ 1. استخدم نقطة النهاية `/health` للتحقق من حالة النظام
703
+ 2. راقب السجلات (logs) للحصول على تفاصيل الخطأ
704
+ 3. ابدأ بإعدادات بسيطة وازداد تعقيداً تدريجياً
705
+ 4. استخدم أدوات التحقق من صحة JSON قبل الإرسال
706
+ 5. احفظ نسخ احتياطية من إعداداتك المفضلة
707
+
708
+ ## 📞 الدعم والمساعدة
709
+
710
+ إذا واجهت مشاكل أو لديك أسئلة:
711
+
712
+ 1. تحقق من التوثيق التقني الكامل
713
+ 2. راجع أمثلة الكود في هذا الدليل
714
+ 3. تأكد من أن جميع المتطلبات مثبتة بشكل صحيح
715
+ 4. استخدم نقاط النهاية للتحقق من حالة النظام
716
+ 5. تواصل مع فريق الدعم الفني إذا استمرت المشكلة
717
+
718
+ ---
719
+
720
+ **نتمنى لك تجربة ممتعة في استخدام نظام إدارة نسخ الفيديو المتقدم! 🎬✨**
core/advanced_processor.py ADDED
@@ -0,0 +1,616 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import uuid
4
+ from typing import Dict, List, Optional, Any, Tuple
5
+ from pathlib import Path
6
+ from datetime import datetime
7
+ from concurrent.futures import ThreadPoolExecutor
8
+ import asyncio
9
+
10
+ from moviepy import VideoFileClip, TextClip, CompositeVideoClip
11
+ # from moviepy.video.fx import resize # Fix import issue
12
+ # from moviepy.audio.fx import volumex # Fix import issue
13
+ import numpy as np
14
+
15
+ from core.version_manager import VersionManager, VideoVersion, OriginalVideo, ProcessingType, VersionStatus
16
+ from schemas import VideoFormat, Dimensions, TranscriptConfig
17
+
18
+ class AdvancedVideoProcessor:
19
+ def __init__(self, version_manager: VersionManager):
20
+ self.version_manager = version_manager
21
+ self.executor = ThreadPoolExecutor(max_workers=4)
22
+
23
+ async def process_video_with_versioning(
24
+ self,
25
+ original_id: str,
26
+ processing_type: ProcessingType,
27
+ processing_config: Dict[str, Any],
28
+ version_name: Optional[str] = None,
29
+ parent_version_id: Optional[str] = None
30
+ ) -> str:
31
+ """
32
+ معالجة الفيديو مع إدارة النسخ المتقدمة
33
+ """
34
+ try:
35
+ # الحصول على مسار الفيديو الأصلي أو الأب
36
+ if parent_version_id:
37
+ # استخدام نسخة موجودة كقاعدة
38
+ parent_version = self.version_manager.get_version(parent_version_id)
39
+ if not parent_version:
40
+ raise ValueError(f"Parent version {parent_version_id} not found")
41
+ source_path = parent_version.file_path
42
+ base_original_id = parent_version.original_id
43
+ else:
44
+ # استخدام الفيديو الأصلي
45
+ source_path = self.version_manager.get_original_path(original_id)
46
+ if not source_path:
47
+ raise ValueError(f"Original video {original_id} not found")
48
+ base_original_id = original_id
49
+
50
+ # إنشاء نسخة جديدة
51
+ version_id = self.version_manager.create_version(
52
+ original_id=base_original_id,
53
+ processing_type=processing_type,
54
+ version_name=version_name,
55
+ parent_version=parent_version_id,
56
+ processing_config=processing_config
57
+ )
58
+
59
+ # الحصول على معلومات النسخة
60
+ version_info = self.version_manager.get_version(version_id)
61
+ if not version_info:
62
+ raise ValueError(f"Failed to retrieve version info for {version_id}")
63
+
64
+ # معالجة الفيديو في الخلفية
65
+ future = self.executor.submit(
66
+ self._process_video_sync,
67
+ source_path,
68
+ version_info.file_path,
69
+ processing_type,
70
+ processing_config,
71
+ version_id
72
+ )
73
+
74
+ # الانتظار لحظياً ثم إرجاع معرف النسخة
75
+ await asyncio.sleep(0.1)
76
+ return version_id
77
+
78
+ except Exception as e:
79
+ raise RuntimeError(f"Failed to start video processing: {str(e)}")
80
+
81
+ def _process_video_sync(
82
+ self,
83
+ source_path: str,
84
+ output_path: str,
85
+ processing_type: ProcessingType,
86
+ processing_config: Dict[str, Any],
87
+ version_id: str
88
+ ):
89
+ """
90
+ معالجة الفيديو المتزامنة (تشغيل في ThreadPool)
91
+ """
92
+ try:
93
+ print(f"🎬 Starting {processing_type.value} processing for version {version_id}")
94
+
95
+ # تحديث الحالة إلى "processing"
96
+ self.version_manager.update_version_status(version_id, VersionStatus.PROCESSING)
97
+
98
+ # التحقق من وجود الملف المصدر
99
+ if not os.path.exists(source_path):
100
+ raise FileNotFoundError(f"Source video not found: {source_path}")
101
+
102
+ # معالجة حسب النوع
103
+ if processing_type == ProcessingType.TRANSCRIPT:
104
+ self._add_transcript_to_video(
105
+ source_path, output_path, processing_config["transcript_config"]
106
+ )
107
+ elif processing_type == ProcessingType.CROP:
108
+ self._crop_video(
109
+ source_path, output_path, processing_config["crop_config"]
110
+ )
111
+ elif processing_type == ProcessingType.EFFECTS:
112
+ self._apply_effects_to_video(
113
+ source_path, output_path, processing_config["effects_config"]
114
+ )
115
+ elif processing_type == ProcessingType.AUDIO:
116
+ self._process_audio_in_video(
117
+ source_path, output_path, processing_config["audio_config"]
118
+ )
119
+ elif processing_type == ProcessingType.COMBINED:
120
+ self._combined_processing(
121
+ source_path, output_path, processing_config
122
+ )
123
+ else:
124
+ raise ValueError(f"Unsupported processing type: {processing_type}")
125
+
126
+ # تحديث الحالة إلى "completed"
127
+ self.version_manager.update_version_status(version_id, VersionStatus.COMPLETED)
128
+ print(f"✅ Processing completed for version {version_id}")
129
+
130
+ except Exception as e:
131
+ print(f"❌ Processing failed for version {version_id}: {str(e)}")
132
+ # تحديث الحالة إلى "failed"
133
+ self.version_manager.update_version_status(version_id, VersionStatus.FAILED)
134
+ # إعادة استثناء للتعامل معه في المستوى الأعلى
135
+ raise
136
+
137
+ def _add_transcript_to_video(self, source_path: str, output_path: str, transcript_config: Dict[str, Any]):
138
+ """إضافة ترانسكريبت إلى الفيديو"""
139
+ try:
140
+ print("📝 Adding transcript to video...")
141
+
142
+ # تحميل الفيديو
143
+ video = VideoFileClip(source_path)
144
+
145
+ # إعدادات الخط
146
+ font_size = transcript_config.get("font_size", 24)
147
+ font_color = transcript_config.get("font_color", "white")
148
+ font_family = transcript_config.get("font_family", "Arial")
149
+
150
+ # إعدادات الخلفية
151
+ bg_color = transcript_config.get("background_color", "black")
152
+ bg_alpha = transcript_config.get("background_alpha", 0.8)
153
+
154
+ # إعدادات الموقع
155
+ position = transcript_config.get("position", "bottom")
156
+ margin = transcript_config.get("margin", 20)
157
+
158
+ # إعدادات التأثيرات
159
+ shadow = transcript_config.get("shadow", True)
160
+ outline = transcript_config.get("outline", True)
161
+ opacity = transcript_config.get("opacity", 1.0)
162
+
163
+ # معالجة كل قطعة نصية
164
+ text_clips = []
165
+ segments = transcript_config.get("segments", [])
166
+
167
+ for segment in segments:
168
+ start_time = segment.get("start", 0)
169
+ end_time = segment.get("end", video.duration)
170
+ text = segment.get("text", "")
171
+ segment_position = segment.get("position", position)
172
+ segment_font_size = segment.get("font_size", font_size)
173
+ segment_font_color = segment.get("font_color", font_color)
174
+
175
+ if not text:
176
+ continue
177
+
178
+ # إنشاء نص النص
179
+ txt_clip = TextClip(
180
+ text,
181
+ fontsize=segment_font_size,
182
+ color=segment_font_color,
183
+ font=font_family,
184
+ stroke_color="black" if outline else None,
185
+ stroke_width=2 if outline else 0
186
+ )
187
+
188
+ # إعداد المدة والموقع
189
+ txt_clip = txt_clip.set_duration(end_time - start_time)
190
+ txt_clip = txt_clip.set_start(start_time)
191
+
192
+ # إعداد الموقع
193
+ if segment_position == "top":
194
+ txt_clip = txt_clip.set_position(("center", margin))
195
+ elif segment_position == "center":
196
+ txt_clip = txt_clip.set_position("center")
197
+ else: # bottom
198
+ txt_clip = txt_clip.set_position(("center", video.h - margin - segment_font_size))
199
+
200
+ # إعداد الشفافية
201
+ if opacity < 1.0:
202
+ txt_clip = txt_clip.set_opacity(opacity)
203
+
204
+ text_clips.append(txt_clip)
205
+
206
+ # إنشاء خلفية للنص إذا تم طلبها
207
+ if bg_alpha > 0 and text_clips:
208
+ for i, txt_clip in enumerate(text_clips):
209
+ # إنشاء خلفية ملونة
210
+ bg_clip = TextClip(
211
+ " " * 50, # مساحة فارغة
212
+ fontsize=font_size,
213
+ color=bg_color,
214
+ bg_color=bg_color,
215
+ font=font_family
216
+ )
217
+ bg_clip = bg_clip.set_duration(txt_clip.duration)
218
+ bg_clip = bg_clip.set_start(txt_clip.start)
219
+ bg_clip = bg_clip.set_position(txt_clip.pos)
220
+ bg_clip = bg_clip.set_opacity(bg_alpha)
221
+
222
+ # دمج الخلفية مع النص
223
+ text_clips[i] = CompositeVideoClip([bg_clip, txt_clip])
224
+
225
+ # دمج جميع المقاطع
226
+ if text_clips:
227
+ final_video = CompositeVideoClip([video] + text_clips)
228
+ else:
229
+ final_video = video
230
+
231
+ # حفظ الفيديو النهائي
232
+ final_video.write_videofile(
233
+ output_path,
234
+ codec="libx264",
235
+ audio_codec="aac",
236
+ temp_audiofile="temp-audio.m4a",
237
+ remove_temp=True,
238
+ logger=None # منع الإخراج المفرط
239
+ )
240
+
241
+ # تنظيف
242
+ video.close()
243
+ if text_clips:
244
+ for clip in text_clips:
245
+ clip.close()
246
+ if 'final_video' in locals():
247
+ final_video.close()
248
+
249
+ print("✅ Transcript added successfully")
250
+
251
+ except Exception as e:
252
+ print(f"❌ Error adding transcript: {str(e)}")
253
+ raise
254
+
255
+ def _crop_video(self, source_path: str, output_path: str, crop_config: Dict[str, Any]):
256
+ """قص الفيديو"""
257
+ try:
258
+ print("✂️ Cropping video...")
259
+
260
+ video = VideoFileClip(source_path)
261
+
262
+ if crop_config.get("center_crop", False):
263
+ # قص من المركز
264
+ target_width = crop_config.get("width", video.w)
265
+ target_height = crop_config.get("height", video.h)
266
+ aspect_ratio = crop_config.get("aspect_ratio")
267
+
268
+ if aspect_ratio:
269
+ # حساب الأبعاد بناءً على نسبة العرض إلى الارتفاع
270
+ if aspect_ratio == "9:16":
271
+ target_width = min(video.w, video.h * 9 / 16)
272
+ target_height = min(video.h, video.w * 16 / 9)
273
+ elif aspect_ratio == "1:1":
274
+ size = min(video.w, video.h)
275
+ target_width = size
276
+ target_height = size
277
+ elif aspect_ratio == "16:9":
278
+ target_width = video.w
279
+ target_height = video.w * 9 / 16
280
+
281
+ # حساب نقطة البداية للقص من المركز
282
+ x_center = video.w / 2
283
+ y_center = video.h / 2
284
+ x1 = max(0, x_center - target_width / 2)
285
+ y1 = max(0, y_center - target_height / 2)
286
+ x2 = min(video.w, x_center + target_width / 2)
287
+ y2 = min(video.h, y_center + target_height / 2)
288
+
289
+ else:
290
+ # قص منطقة محددة
291
+ x1 = crop_config.get("x1", 0)
292
+ y1 = crop_config.get("y1", 0)
293
+ x2 = crop_config.get("x2", video.w)
294
+ y2 = crop_config.get("y2", video.h)
295
+ target_width = crop_config.get("width", x2 - x1)
296
+ target_height = crop_config.get("height", y2 - y1)
297
+
298
+ # تطبيق القص
299
+ cropped_video = video.crop(x1=x1, y1=y1, x2=x2, y2=y2)
300
+
301
+ # تغيير الحجم إذا لزم الأمر
302
+ if cropped_video.w != target_width or cropped_video.h != target_height:
303
+ cropped_video = cropped_video.resize((target_width, target_height))
304
+
305
+ # حفظ الفيديو المقصوص
306
+ cropped_video.write_videofile(
307
+ output_path,
308
+ codec="libx264",
309
+ audio_codec="aac",
310
+ temp_audiofile="temp-audio.m4a",
311
+ remove_temp=True,
312
+ logger=None
313
+ )
314
+
315
+ # تنظيف
316
+ video.close()
317
+ cropped_video.close()
318
+
319
+ print("✅ Video cropped successfully")
320
+
321
+ except Exception as e:
322
+ print(f"❌ Error cropping video: {str(e)}")
323
+ raise
324
+
325
+ def _apply_effects_to_video(self, source_path: str, output_path: str, effects_config: Dict[str, Any]):
326
+ """تطبيق تأثيرات على الفيديو"""
327
+ try:
328
+ print("🎨 Applying effects to video...")
329
+
330
+ video = VideoFileClip(source_path)
331
+
332
+ # تأثيرات أساسية
333
+ if "brightness" in effects_config:
334
+ video = video.fx(vfx.colorx, effects_config["brightness"])
335
+
336
+ if "contrast" in effects_config:
337
+ video = video.fx(vfx.lum_contrast, contrast=effects_config["contrast"])
338
+
339
+ if "saturation" in effects_config:
340
+ # لا يوجد تأثير مباشر للتشبع في MoviePy، نستخدم التأثيرات اللونية
341
+ saturation_factor = effects_config["saturation"]
342
+ if saturation_factor != 1.0:
343
+ # تطبيق تأثير التشبع باستخدام مصفوفة الألوان
344
+ def saturation_effect(frame):
345
+ # تحويل إلى HSV وتعديل التشبع
346
+ import cv2
347
+ hsv = cv2.cvtColor(frame, cv2.COLOR_RGB2HSV).astype("float32")
348
+ hsv[:, :, 1] = hsv[:, :, 1] * saturation_factor
349
+ hsv[:, :, 1] = np.clip(hsv[:, :, 1], 0, 255)
350
+ result = cv2.cvtColor(hsv.astype("uint8"), cv2.COLOR_HSV2RGB)
351
+ return result
352
+
353
+ video = video.fl_image(saturation_effect)
354
+
355
+ # تأثيرات خاصة
356
+ if effects_config.get("sepia", False):
357
+ video = video.fx(vfx.sepia)
358
+
359
+ if effects_config.get("black_white", False):
360
+ video = video.fx(vfx.blackwhite)
361
+
362
+ if effects_config.get("vintage", False):
363
+ # تطبيق تأثير فينتاج
364
+ video = video.fx(vfx.colorx, 0.8) # تقليل السطوع
365
+ video = video.fx(vfx.gamma_corr, 0.8) # تصحيح غاما
366
+
367
+ if effects_config.get("vignette", 0) > 0:
368
+ strength = effects_config["vignette"]
369
+ video = video.fx(vfx.vignette, intensity=strength)
370
+
371
+ if effects_config.get("blur", 0) > 0:
372
+ strength = effects_config["blur"]
373
+ video = video.fx(vfx.blur, strength)
374
+
375
+ if effects_config.get("noise", 0) > 0:
376
+ strength = effects_config["noise"]
377
+ # تطبيق ضوضاء
378
+ def noise_effect(frame):
379
+ noise = np.random.normal(0, strength * 25, frame.shape).astype(np.uint8)
380
+ result = cv2.add(frame.astype(np.uint8), noise)
381
+ return result
382
+
383
+ video = video.fl_image(noise_effect)
384
+
385
+ # تأثيرات التلاشي
386
+ if "fade_in" in effects_config:
387
+ duration = effects_config["fade_in"]
388
+ video = video.fx(vfx.fadein, duration)
389
+
390
+ if "fade_out" in effects_config:
391
+ duration = effects_config["fade_out"]
392
+ video = video.fx(vfx.fadeout, duration)
393
+
394
+ # حفظ الفيديو مع التأثيرات
395
+ video.write_videofile(
396
+ output_path,
397
+ codec="libx264",
398
+ audio_codec="aac",
399
+ temp_audiofile="temp-audio.m4a",
400
+ remove_temp=True,
401
+ logger=None
402
+ )
403
+
404
+ # تنظيف
405
+ video.close()
406
+
407
+ print("✅ Effects applied successfully")
408
+
409
+ except Exception as e:
410
+ print(f"❌ Error applying effects: {str(e)}")
411
+ raise
412
+
413
+ def _process_audio_in_video(self, source_path: str, output_path: str, audio_config: Dict[str, Any]):
414
+ """معالجة الصوت في الفيديو"""
415
+ try:
416
+ print("🔊 Processing audio in video...")
417
+
418
+ video = VideoFileClip(source_path)
419
+ audio = video.audio
420
+
421
+ if audio is None:
422
+ print("⚠️ No audio track found, creating silent audio")
423
+ # إنشاء صوت صامت
424
+ import numpy as np
425
+ duration = video.duration
426
+ fps = 44100
427
+ silent_audio = np.zeros(int(duration * fps))
428
+ from moviepy import AudioArrayClip
429
+ audio = AudioArrayClip(silent_audio, fps=fps)
430
+
431
+ # تعديل الصوت حسب الإعدادات
432
+
433
+ if "volume" in audio_config:
434
+ volume_factor = audio_config["volume"]
435
+ audio = audio.fx(volumex, volume_factor)
436
+
437
+ if audio_config.get("normalize", False):
438
+ # تطبيع الصوت
439
+ audio = audio.fx(afx.normalize)
440
+
441
+ if audio_config.get("remove_noise", False):
442
+ # تقليل الضوضاء (تقريبي)
443
+ audio = audio.fx(afx.audio_fadein, 0.1) # تلاشي سريع لتقليل الضوضاء الأولية
444
+ audio = audio.fx(afx.audio_fadeout, 0.1) # تلاشي خروج
445
+
446
+ if "bass_boost" in audio_config:
447
+ bass_factor = audio_config["bass_boost"]
448
+ # تعزيز الجهير (تقريبي)
449
+ def bass_boost(audio_clip):
450
+ def bass_boost_frame(frame):
451
+ # تمرير منخفض التردد (تقريبي جداً)
452
+ return frame * (1 + bass_factor * 0.5)
453
+ return audio_clip.fl(bass_boost_frame)
454
+
455
+ audio = bass_boost(audio)
456
+
457
+ if "treble_boost" in audio_config:
458
+ treble_factor = audio_config["treble_boost"]
459
+ # تعزيز التريبل (تقريبي)
460
+ def treble_boost(audio_clip):
461
+ def treble_boost_frame(frame):
462
+ # تمرير عالي التردد (تقريبي جداً)
463
+ return frame * (1 + treble_factor * 0.3)
464
+ return audio_clip.fl(treble_boost_frame)
465
+
466
+ audio = treble_boost(audio)
467
+
468
+ if "speed" in audio_config:
469
+ speed_factor = audio_config["speed"]
470
+ # تغيير السرعة مع الحفاظ على النبرة
471
+ audio = audio.fx(afx.speedx, speed_factor)
472
+
473
+ if "pitch_shift" in audio_config:
474
+ pitch_shift = audio_config["pitch_shift"]
475
+ # تغيير النبرة (تقريبي)
476
+ audio = audio.fx(afx.speedx, 1.0) # سيتم تطبيق التغيير في MoviePy
477
+
478
+ if "fade_in" in audio_config:
479
+ duration = audio_config["fade_in"]
480
+ audio = audio.fx(afx.audio_fadein, duration)
481
+
482
+ if "fade_out" in audio_config:
483
+ duration = audio_config["fade_out"]
484
+ audio = audio.fx(afx.audio_fadeout, duration)
485
+
486
+ # إنشاء الفيديو النهائي مع الصوت المعالج
487
+ final_video = video.set_audio(audio)
488
+
489
+ # حفظ الفيديو
490
+ final_video.write_videofile(
491
+ output_path,
492
+ codec="libx264",
493
+ audio_codec="aac",
494
+ temp_audiofile="temp-audio.m4a",
495
+ remove_temp=True,
496
+ logger=None
497
+ )
498
+
499
+ # تنظيف
500
+ video.close()
501
+ audio.close()
502
+ final_video.close()
503
+
504
+ print("✅ Audio processing completed")
505
+
506
+ except Exception as e:
507
+ print(f"❌ Error processing audio: {str(e)}")
508
+ raise
509
+
510
+ def _combined_processing(self, source_path: str, output_path: str, processing_config: Dict[str, Any]):
511
+ """معالجة مركبة متعددة"""
512
+ try:
513
+ print("🔄 Starting combined processing...")
514
+
515
+ # ترتيب المعالجة: قص → تأثيرات → ترانسكريبت → صوت
516
+ temp_files = []
517
+ current_path = source_path
518
+
519
+ # 1. القص (إذا تم طلبه)
520
+ if "crop_config" in processing_config:
521
+ print("📐 Step 1: Cropping...")
522
+ temp_crop = output_path.replace(".mp4", "_crop_temp.mp4")
523
+ temp_files.append(temp_crop)
524
+ self._crop_video(current_path, temp_crop, processing_config["crop_config"])
525
+ current_path = temp_crop
526
+
527
+ # 2. التأثيرات (إذا تم طلبها)
528
+ if "effects_config" in processing_config:
529
+ print("🎨 Step 2: Applying effects...")
530
+ temp_effects = output_path.replace(".mp4", "_effects_temp.mp4")
531
+ temp_files.append(temp_effects)
532
+ self._apply_effects_to_video(current_path, temp_effects, processing_config["effects_config"])
533
+ current_path = temp_effects
534
+
535
+ # 3. الترانسكريبت (إذا تم طلبه)
536
+ if "transcript_config" in processing_config:
537
+ print("📝 Step 3: Adding transcript...")
538
+ temp_transcript = output_path.replace(".mp4", "_transcript_temp.mp4")
539
+ temp_files.append(temp_transcript)
540
+ self._add_transcript_to_video(current_path, temp_transcript, processing_config["transcript_config"])
541
+ current_path = temp_transcript
542
+
543
+ # 4. الصوت (إذا تم طلبه)
544
+ if "audio_config" in processing_config:
545
+ print("🔊 Step 4: Processing audio...")
546
+ temp_audio = output_path.replace(".mp4", "_audio_temp.mp4")
547
+ temp_files.append(temp_audio)
548
+ self._process_audio_in_video(current_path, temp_audio, processing_config["audio_config"])
549
+ current_path = temp_audio
550
+
551
+ # نسخ الملف النهائي إلى المسار المطلوب
552
+ if current_path != output_path:
553
+ import shutil
554
+ shutil.copy2(current_path, output_path)
555
+
556
+ # تنظيف الملفات المؤقتة
557
+ for temp_file in temp_files:
558
+ if os.path.exists(temp_file) and temp_file != output_path:
559
+ os.remove(temp_file)
560
+
561
+ print("✅ Combined processing completed")
562
+
563
+ except Exception as e:
564
+ print(f"❌ Error in combined processing: {str(e)}")
565
+ # تنظيف الملفات المؤقتة في حالة الخطأ
566
+ temp_files = [f for f in temp_files if os.path.exists(f) and f != output_path]
567
+ for temp_file in temp_files:
568
+ try:
569
+ os.remove(temp_file)
570
+ except:
571
+ pass
572
+ raise
573
+
574
+ def get_original_info(self, original_id: str) -> Optional[Dict[str, Any]]:
575
+ """الحصول على معلومات الفيديو الأصلي"""
576
+ try:
577
+ original_data = self.version_manager.registry["originals"].get(original_id)
578
+ if not original_data:
579
+ return None
580
+
581
+ return {
582
+ "original_id": original_id,
583
+ "file_name": original_data["file_name"],
584
+ "file_size": original_data["file_size"],
585
+ "duration": original_data["duration"],
586
+ "resolution": original_data["resolution"],
587
+ "upload_date": original_data["upload_date"],
588
+ "metadata": original_data.get("metadata", {})
589
+ }
590
+ except Exception as e:
591
+ print(f"❌ Error getting original info: {str(e)}")
592
+ return None
593
+
594
+ def get_version_info(self, version_id: str) -> Optional[Dict[str, Any]]:
595
+ """الحصول على معلومات نسخة معينة"""
596
+ try:
597
+ version = self.version_manager.get_version(version_id)
598
+ if not version:
599
+ return None
600
+
601
+ return {
602
+ "version_id": version_id,
603
+ "version_name": version.version_name,
604
+ "original_id": version.original_id,
605
+ "processing_type": version.processing_type.value,
606
+ "status": version.status.value,
607
+ "file_size": version.file_size,
608
+ "duration": version.duration,
609
+ "resolution": version.resolution,
610
+ "created_at": version.created_at,
611
+ "parent_version": version.parent_version,
612
+ "file_path": version.file_path
613
+ }
614
+ except Exception as e:
615
+ print(f"❌ Error getting version info: {str(e)}")
616
+ return None
core/version_manager.py ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import uuid
4
+ import shutil
5
+ from datetime import datetime
6
+ from typing import Dict, List, Optional, Any
7
+ from pathlib import Path
8
+ from pydantic import BaseModel
9
+ from enum import Enum
10
+
11
+ class ProcessingType(str, Enum):
12
+ TRANSCRIPT = "transcript"
13
+ CROP = "crop"
14
+ EFFECTS = "effects"
15
+ AUDIO = "audio"
16
+ COMBINED = "combined"
17
+
18
+ class VersionStatus(str, Enum):
19
+ PENDING = "pending"
20
+ PROCESSING = "processing"
21
+ COMPLETED = "completed"
22
+ FAILED = "failed"
23
+
24
+ class VideoVersion(BaseModel):
25
+ version_id: str
26
+ original_id: str
27
+ version_name: str
28
+ processing_type: ProcessingType
29
+ file_path: str
30
+ file_size: int
31
+ duration: float
32
+ resolution: str
33
+ created_at: datetime
34
+ parent_version: Optional[str] = None # لإنشاء نسخ من نسخ
35
+ processing_config: Dict[str, Any]
36
+ status: VersionStatus
37
+ metadata: Dict[str, Any] = {}
38
+
39
+ class OriginalVideo(BaseModel):
40
+ original_id: str
41
+ file_name: str
42
+ file_path: str
43
+ file_size: int
44
+ upload_date: datetime
45
+ duration: float
46
+ resolution: str
47
+ format: str
48
+ metadata: Dict[str, Any] = {}
49
+
50
+ class VersionManager:
51
+ def __init__(self, base_directory: str = "video_storage"):
52
+ self.base_dir = Path(base_directory)
53
+ self.originals_dir = self.base_dir / "originals"
54
+ self.versions_dir = self.base_dir / "versions"
55
+ self.registry_file = self.base_dir / "version_registry.json"
56
+
57
+ # إنشاء المجلدات الأساسية
58
+ self.originals_dir.mkdir(parents=True, exist_ok=True)
59
+ self.versions_dir.mkdir(parents=True, exist_ok=True)
60
+
61
+ # تحميل السجل أو إنشاؤه
62
+ self.registry = self._load_registry()
63
+
64
+ def _load_registry(self) -> Dict[str, Any]:
65
+ """تحميل سجل النسخ من الملف"""
66
+ if self.registry_file.exists():
67
+ with open(self.registry_file, 'r', encoding='utf-8') as f:
68
+ return json.load(f)
69
+ return {"originals": {}, "versions": {}, "version_tree": {}}
70
+
71
+ def _save_registry(self):
72
+ """حفظ سجل النسخ في الملف"""
73
+ with open(self.registry_file, 'w', encoding='utf-8') as f:
74
+ json.dump(self.registry, f, indent=2, default=str)
75
+
76
+ def register_original(self, source_path: str, file_name: str, metadata: Dict = None) -> str:
77
+ """تسجيل الفيديو الأصلي (محمي من التعديل)"""
78
+ original_id = str(uuid.uuid4())
79
+
80
+ # إنشاء مجلد مخصص للفيديو الأصلي
81
+ original_dir = self.originals_dir / original_id
82
+ original_dir.mkdir(exist_ok=True)
83
+
84
+ # نسخ الفيديو الأصلي إلى الموقع المحمي
85
+ original_path = original_dir / file_name
86
+ shutil.copy2(source_path, original_path)
87
+
88
+ # الحصول على معلومات الفيديو
89
+ video_info = self._get_video_info(original_path)
90
+
91
+ original_video = OriginalVideo(
92
+ original_id=original_id,
93
+ file_name=file_name,
94
+ file_path=str(original_path),
95
+ file_size=original_path.stat().st_size,
96
+ upload_date=datetime.now(),
97
+ duration=video_info.get('duration', 0),
98
+ resolution=video_info.get('resolution', 'unknown'),
99
+ format=video_info.get('format', 'unknown'),
100
+ metadata=metadata or {}
101
+ )
102
+
103
+ # حفظ في السجل
104
+ self.registry["originals"][original_id] = original_video.dict()
105
+ self.registry["version_tree"][original_id] = []
106
+ self._save_registry()
107
+
108
+ return original_id
109
+
110
+ def create_version(self, original_id: str, version_name: str,
111
+ processing_type: ProcessingType,
112
+ processing_config: Dict[str, Any],
113
+ parent_version: Optional[str] = None) -> str:
114
+ """إنشاء نسخة جديدة من الفيديو الأصلي أو نسخة موجودة"""
115
+
116
+ # التحقق من وجود الفيديو الأصلي
117
+ if original_id not in self.registry["originals"]:
118
+ raise ValueError(f"Original video {original_id} not found")
119
+
120
+ version_id = str(uuid.uuid4())
121
+
122
+ # إنشاء مجلد للنسخة
123
+ version_dir = self.versions_dir / version_id
124
+ version_dir.mkdir(exist_ok=True)
125
+
126
+ # تحديد مسار المصدر (أصلي أو نسخة أب)
127
+ if parent_version:
128
+ if parent_version not in self.registry["versions"]:
129
+ raise ValueError(f"Parent version {parent_version} not found")
130
+ source_path = self.registry["versions"][parent_version]["file_path"]
131
+ else:
132
+ source_path = self.registry["originals"][original_id]["file_path"]
133
+
134
+ # إنشاء مسار للنسخة الجديدة
135
+ original_name = self.registry["originals"][original_id]["file_name"]
136
+ version_path = version_dir / f"{version_name}_{original_name}"
137
+
138
+ # نسخ الملف المصدر إلى النسخة الجديدة
139
+ shutil.copy2(source_path, version_path)
140
+
141
+ # إنشاء سجل النسخة
142
+ video_info = self._get_video_info(version_path)
143
+
144
+ video_version = VideoVersion(
145
+ version_id=version_id,
146
+ original_id=original_id,
147
+ version_name=version_name,
148
+ processing_type=processing_type,
149
+ file_path=str(version_path),
150
+ file_size=version_path.stat().st_size,
151
+ duration=video_info.get('duration', 0),
152
+ resolution=video_info.get('resolution', 'unknown'),
153
+ created_at=datetime.now(),
154
+ parent_version=parent_version,
155
+ processing_config=processing_config,
156
+ status=VersionStatus.PENDING
157
+ )
158
+
159
+ # حفظ في السجل
160
+ self.registry["versions"][version_id] = video_version.dict()
161
+
162
+ # إضافة إلى شجرة النسخ
163
+ if parent_version:
164
+ self.registry["version_tree"][parent_version] = self.registry["version_tree"].get(parent_version, []) + [version_id]
165
+ else:
166
+ self.registry["version_tree"][original_id] = self.registry["version_tree"].get(original_id, []) + [version_id]
167
+
168
+ self._save_registry()
169
+
170
+ return version_id
171
+
172
+ def get_original(self, original_id: str) -> Optional[OriginalVideo]:
173
+ """الحصول على معلومات الفيديو الأصلي"""
174
+ if original_id in self.registry["originals"]:
175
+ return OriginalVideo(**self.registry["originals"][original_id])
176
+ return None
177
+
178
+ def get_version(self, version_id: str) -> Optional[VideoVersion]:
179
+ """الحصول على معلومات نسخة معينة"""
180
+ if version_id in self.registry["versions"]:
181
+ return VideoVersion(**self.registry["versions"][version_id])
182
+ return None
183
+
184
+ def get_version_tree(self, original_id: str) -> Dict[str, List[str]]:
185
+ """الحصول على شجرة النسخ لفيديو أصلي"""
186
+ return self.registry["version_tree"].get(original_id, [])
187
+
188
+ def get_all_versions(self, original_id: str) -> List[VideoVersion]:
189
+ """الحصول على جميع النسخ المرتبطة بفيديو أصلي"""
190
+ versions = []
191
+ for version_id in self.registry["versions"]:
192
+ version_data = self.registry["versions"][version_id]
193
+ if version_data["original_id"] == original_id:
194
+ versions.append(VideoVersion(**version_data))
195
+ return sorted(versions, key=lambda x: x.created_at)
196
+
197
+ def update_version_status(self, version_id: str, status: VersionStatus):
198
+ """تحديث حالة النسخة"""
199
+ if version_id in self.registry["versions"]:
200
+ self.registry["versions"][version_id]["status"] = status
201
+ self._save_registry()
202
+
203
+ def delete_version(self, version_id: str) -> bool:
204
+ """حذف نسخة (مع الملفات)"""
205
+ if version_id not in self.registry["versions"]:
206
+ return False
207
+
208
+ version_data = self.registry["versions"][version_id]
209
+
210
+ # حذف الملف
211
+ try:
212
+ Path(version_data["file_path"]).unlink()
213
+ Path(version_data["file_path"]).parent.rmdir() # حذف المجلد الفارغ
214
+ except Exception as e:
215
+ print(f"Error deleting version files: {e}")
216
+
217
+ # حذف من السجل
218
+ del self.registry["versions"][version_id]
219
+
220
+ # حذف من شجرة النسخ
221
+ for original_id, versions in self.registry["version_tree"].items():
222
+ if version_id in versions:
223
+ versions.remove(version_id)
224
+ break
225
+
226
+ self._save_registry()
227
+ return True
228
+
229
+ def get_original_path(self, original_id: str) -> Optional[str]:
230
+ """الحصول على مسار الفيديو الأصلي (للقراءة فقط)"""
231
+ if original_id in self.registry["originals"]:
232
+ return self.registry["originals"][original_id]["file_path"]
233
+ return None
234
+
235
+ def _get_video_info(self, video_path: Path) -> Dict[str, Any]:
236
+ """الحصول على معلومات الفيديو (مدة، دقة، إلخ)"""
237
+ try:
238
+ # يمكن استخدام FFmpeg أو MoviePy هنا
239
+ # مؤقتاً معلومات أساسية
240
+ return {
241
+ 'duration': 0, # سيتم ملؤها لاحقاً
242
+ 'resolution': '1920x1080', # سيتم ملؤها لاحقاً
243
+ 'format': 'mp4'
244
+ }
245
+ except Exception as e:
246
+ print(f"Error getting video info: {e}")
247
+ return {
248
+ 'duration': 0,
249
+ 'resolution': 'unknown',
250
+ 'format': 'unknown'
251
+ }
252
+
253
+ def cleanup_orphaned_versions(self):
254
+ """تنظيف النسخ الميتة (ملفات بدون سجل)"""
255
+ # سيتم تنفيذه لاحقاً
256
+ pass
257
+
258
+ def get_storage_stats(self) -> Dict[str, Any]:
259
+ """الحصول على إحصائيات التخزين"""
260
+ originals_size = sum(
261
+ Path(self.registry["originals"][oid]["file_path"]).stat().st_size
262
+ for oid in self.registry["originals"]
263
+ )
264
+
265
+ versions_size = sum(
266
+ Path(self.registry["versions"][vid]["file_path"]).stat().st_size
267
+ for vid in self.registry["versions"]
268
+ )
269
+
270
+ return {
271
+ "original_videos": len(self.registry["originals"]),
272
+ "total_versions": len(self.registry["versions"]),
273
+ "originals_size": originals_size,
274
+ "versions_size": versions_size,
275
+ "total_size": originals_size + versions_size
276
+ }
hybrid_processor.py CHANGED
@@ -3,26 +3,11 @@ import subprocess
3
  import imageio_ffmpeg
4
  from typing import List, Optional, Tuple
5
 
6
- # Import existing functions
7
- from video_processor import process_single_clip as moviepy_process_single_clip
8
  from ffmpeg_utils import extract_clip_ffmpeg, get_video_info_ffmpeg
9
 
10
  def should_use_ffmpeg_processing(timestamps, custom_dims, export_audio, bg_music, output_format) -> bool:
11
- """
12
- Determine if FFmpeg can be used instead of MoviePy for faster processing
13
-
14
- FFmpeg is faster for:
15
- - Simple timestamps (no overlapping, basic cuts)
16
- - No background music
17
- - Basic resize only (no complex formatting)
18
- - Standard aspect ratios
19
-
20
- MoviePy is needed for:
21
- - Complex effects, transitions
22
- - Background music mixing
23
- - Advanced formatting
24
- - Custom effects
25
- """
26
  # If background music is requested, need MoviePy
27
  if bg_music:
28
  return False
@@ -64,130 +49,70 @@ def process_single_clip_hybrid(video_path: str, start: float, end: float, clip_i
64
  )
65
 
66
  if can_use_ffmpeg and not bg_music:
67
- # Use FFmpeg for maximum speed
68
- print(f"🚀 Using FFmpeg for clip {clip_id} (fast mode)")
69
-
70
- # Get video info quickly
71
- video_info = get_video_info_ffmpeg(video_path)
72
-
73
- # Simple FFmpeg extraction
74
- extract_clip_ffmpeg(
75
- video_path=video_path,
76
- start_time=start,
77
- end_time=end,
78
- output_path=output_path,
79
- custom_dims=custom_dims,
80
- export_audio=export_audio
81
- )
82
-
83
- print(f"✓ FFmpeg clip extracted in record time: {clip_id}")
84
- return output_path
85
-
86
  else:
87
- # Use MoviePy for complex features
88
- print(f"🎬 Using MoviePy for clip {clip_id} (feature mode)")
89
- return moviepy_process_single_clip(
90
- video_path=video_path,
91
- start=start,
92
- end=end,
93
- clip_id=clip_id,
94
- output_format=output_format,
95
- custom_dims=custom_dims,
96
- export_audio=export_audio,
97
- bg_music=bg_music
98
- )
99
 
100
  except Exception as e:
101
- print(f"Error in hybrid processing for clip {clip_id}: {e}")
102
- # Fallback to MoviePy
103
- print(f"Falling back to MoviePy for clip {clip_id}")
104
- return moviepy_process_single_clip(
105
- video_path=video_path,
106
- start=start,
107
- end=end,
108
- clip_id=clip_id,
109
- output_format=output_format,
110
- custom_dims=custom_dims,
111
- export_audio=export_audio,
112
- bg_music=bg_music
113
- )
114
 
115
- def process_video_hybrid(video_path: str, timestamps, output_format: str = "mp4",
116
- custom_dims: Optional[Tuple[int, int]] = None,
117
- export_audio: bool = True, bg_music: Optional[str] = None) -> tuple:
118
  """
119
- Process video using hybrid approach: FFmpeg for speed, MoviePy for features
120
-
121
- Returns:
122
- tuple: (clip_paths, audio_paths)
123
  """
124
- print(f"=== Hybrid Video Processing ===")
 
125
 
126
- # Check if we can use FFmpeg for the entire video
127
- can_use_ffmpeg = should_use_ffmpeg_processing(
128
- timestamps, custom_dims, export_audio, bg_music, output_format
129
- )
130
-
131
- if can_use_ffmpeg:
132
- print("🚀 Using FFmpeg optimization for entire video")
133
- # Process all clips with FFmpeg
134
- clip_paths = []
135
- audio_paths = []
136
 
137
- for i, ts in enumerate(timestamps):
138
- clip_id = f"clip_{i+1}_{uuid.uuid4().hex[:6]}"
139
-
140
- try:
141
- # Use FFmpeg for each clip
142
- output_path = process_single_clip_hybrid(
143
- video_path=video_path,
144
- start=ts.start,
145
- end=ts.end,
146
- clip_id=clip_id,
147
- output_format=output_format,
148
- custom_dims=custom_dims,
149
- export_audio=export_audio,
150
- bg_music=None # FFmpeg can't handle bg_music
151
- )
152
-
153
- if output_path:
154
- clip_paths.append(output_path)
155
-
156
- # Extract audio if needed
157
- if export_audio:
158
- audio_output_path = extract_audio_from_video(output_path, "mp3")
159
- audio_paths.append(audio_output_path)
160
-
161
- except Exception as e:
162
- print(f"FFmpeg failed for clip {clip_id}, falling back to MoviePy: {e}")
163
- # Fallback to MoviePy for this specific clip
164
- output_path = process_single_clip_hybrid(
165
- video_path=video_path,
166
- start=ts.start,
167
- end=ts.end,
168
- clip_id=clip_id,
169
- output_format=output_format,
170
- custom_dims=custom_dims,
171
- export_audio=export_audio,
172
- bg_music=bg_music # MoviePy can handle this
173
  )
 
174
 
175
- if output_path:
176
- clip_paths.append(output_path)
177
- if export_audio:
178
- audio_paths.append(audio_output_path)
179
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  return clip_paths, audio_paths
181
 
182
- else:
183
- print("🎬 Using MoviePy for complex features")
184
- # Use existing MoviePy implementation
185
- from video_processor import process_video as moviepy_process_video
186
- return moviepy_process_video(
187
- video_path=video_path,
188
- timestamps=timestamps,
189
- output_format=output_format,
190
- custom_dims=custom_dims,
191
- export_audio=export_audio,
192
- bg_music=bg_music
193
- )
 
3
  import imageio_ffmpeg
4
  from typing import List, Optional, Tuple
5
 
6
+ # Import existing functions - avoid circular imports
 
7
  from ffmpeg_utils import extract_clip_ffmpeg, get_video_info_ffmpeg
8
 
9
  def should_use_ffmpeg_processing(timestamps, custom_dims, export_audio, bg_music, output_format) -> bool:
10
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  # If background music is requested, need MoviePy
12
  if bg_music:
13
  return False
 
49
  )
50
 
51
  if can_use_ffmpeg and not bg_music:
52
+ # Use FFmpeg for speed
53
+ print(f"🚀 Using FFmpeg for clip {clip_id}")
54
+ return extract_clip_ffmpeg(video_path, start, end, output_path, custom_dims)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  else:
56
+ # Use MoviePy for features (fallback)
57
+ print(f"🎬 Using MoviePy for clip {clip_id}")
58
+ # Import here to avoid circular imports
59
+ from video_processor import process_single_clip as moviepy_process_single_clip
60
+ return moviepy_process_single_clip(video_path, start, end, clip_id, output_format, custom_dims, export_audio, bg_music)
 
 
 
 
 
 
 
61
 
62
  except Exception as e:
63
+ print(f" Hybrid processing failed for clip {clip_id}: {e}")
64
+ raise
 
 
 
 
 
 
 
 
 
 
 
65
 
66
+ def process_video_hybrid(video_path: str, timestamps, output_format, custom_dims=None, export_audio=True, bg_music=None) -> tuple:
 
 
67
  """
68
+ Process video using hybrid approach (FFmpeg + MoviePy)
69
+ Returns: (clip_paths, audio_paths)
 
 
70
  """
71
+ clip_paths = []
72
+ audio_paths = []
73
 
74
+ try:
75
+ # Create temp directory
76
+ os.makedirs("temp_videos", exist_ok=True)
 
 
 
 
 
 
 
77
 
78
+ # Check if we can use FFmpeg for the entire video
79
+ can_use_ffmpeg = should_use_ffmpeg_processing(timestamps, custom_dims, export_audio, bg_music, output_format)
80
+
81
+ if can_use_ffmpeg:
82
+ print("🚀 Using FFmpeg optimization for entire video")
83
+ # Process all clips with FFmpeg
84
+ for i, ts in enumerate(timestamps):
85
+ clip_path = process_single_clip_hybrid(
86
+ video_path, ts.start, ts.end, f"hybrid_{i}",
87
+ output_format, custom_dims, export_audio, bg_music
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  )
89
+ clip_paths.append(clip_path)
90
 
91
+ # Extract audio if needed
92
+ if export_audio:
93
+ audio_path = clip_path.replace('.mp4', '_audio.mp4')
94
+ # Extract audio using FFmpeg
95
+ cmd = [
96
+ imageio_ffmpeg.get_ffmpeg_exe(),
97
+ '-i', clip_path,
98
+ '-vn', # No video
99
+ '-acodec', 'copy',
100
+ '-y', audio_path
101
+ ]
102
+ subprocess.run(cmd, check=True, capture_output=True)
103
+ audio_paths.append(audio_path)
104
+ else:
105
+ print("🎬 Using MoviePy for complex processing")
106
+ # Fallback to MoviePy for complex cases
107
+ # This will be handled by the calling function
108
+ return [], []
109
+
110
  return clip_paths, audio_paths
111
 
112
+ except Exception as e:
113
+ print(f" Hybrid video processing failed: {e}")
114
+ # Clean up any partial results
115
+ for path in clip_paths + audio_paths:
116
+ if os.path.exists(path):
117
+ os.remove(path)
118
+ return [], []
 
 
 
 
 
main.py CHANGED
@@ -1,12 +1,16 @@
1
- import ffmpeg_init
2
  from fastapi import FastAPI
3
  from fastapi.middleware.cors import CORSMiddleware
4
  import uvicorn
5
- from routers import video, files
 
6
 
7
- app = FastAPI(title="Video Clipping API")
 
 
 
 
8
 
9
- # Add CORS middleware for Hugging Face Spaces
10
  app.add_middleware(
11
  CORSMiddleware,
12
  allow_origins=["*"],
@@ -15,9 +19,25 @@ app.add_middleware(
15
  allow_headers=["*"],
16
  )
17
 
18
- # Include Routers
19
- app.include_router(video.router)
20
- app.include_router(files.router)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
  if __name__ == "__main__":
23
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
1
  from fastapi import FastAPI
2
  from fastapi.middleware.cors import CORSMiddleware
3
  import uvicorn
4
+ from datetime import datetime
5
+ from routers import video, files, video_versions
6
 
7
+ app = FastAPI(
8
+ title="Video Processing API",
9
+ description="API for video processing with transcript and effects",
10
+ version="1.0.0"
11
+ )
12
 
13
+ # CORS middleware
14
  app.add_middleware(
15
  CORSMiddleware,
16
  allow_origins=["*"],
 
19
  allow_headers=["*"],
20
  )
21
 
22
+ # Include routers
23
+ app.include_router(video.router, prefix="/api/video", tags=["Basic Video Processing"])
24
+ app.include_router(files.router, prefix="/api/files", tags=["File Management"])
25
+ app.include_router(video_versions.router, prefix="/api/versions", tags=["Advanced Version Management"])
26
+
27
+ @app.get("/")
28
+ async def root():
29
+ return {
30
+ "message": "Video Processing API",
31
+ "version": "1.0.0",
32
+ "timestamp": datetime.now().isoformat()
33
+ }
34
+
35
+ @app.get("/health")
36
+ async def health_check():
37
+ return {
38
+ "status": "healthy",
39
+ "timestamp": datetime.now().isoformat()
40
+ }
41
 
42
  if __name__ == "__main__":
43
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
routers/video_versions.py ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, File, UploadFile, Form, HTTPException, BackgroundTasks
2
+ from fastapi.responses import JSONResponse, FileResponse
3
+ from typing import List, Optional, Dict, Any
4
+ import os
5
+ import uuid
6
+ import shutil
7
+ from datetime import datetime
8
+ import json
9
+
10
+ from core.version_manager import VersionManager, ProcessingType, VersionStatus
11
+ from schemas import VideoFormat, Timestamp, TranscriptConfig, CropConfig, EffectsConfig
12
+
13
+ # إنشاء الموجه
14
+ router = APIRouter(tags=["Video Version Management"])
15
+
16
+ # تهيئة المدير فقط
17
+ version_manager = VersionManager()
18
+
19
+ # مجلدات مؤقتة
20
+ UPLOAD_DIR = "temp_uploads"
21
+ PROCESSED_DIR = "processed_videos"
22
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
23
+ os.makedirs(PROCESSED_DIR, exist_ok=True)
24
+
25
+ # ============================================
26
+ # نقاط نهاية إدارة الفيديو الأصلي
27
+ # ============================================
28
+
29
+ @router.post("/upload-original")
30
+ async def upload_original_video(
31
+ background_tasks: BackgroundTasks,
32
+ video_file: UploadFile = File(...),
33
+ metadata: Optional[str] = Form(None)
34
+ ) -> Dict[str, Any]:
35
+ """
36
+ رفع الفيديو الأصلي (محمي من التعديل)
37
+ """
38
+
39
+ # التحقق من نوع الملف
40
+ if not video_file.content_type or not video_file.content_type.startswith('video/'):
41
+ raise HTTPException(status_code=400, detail="File must be a video")
42
+
43
+ # إنشاء اسم فريد للملف
44
+ file_extension = os.path.splitext(video_file.filename)[1] if video_file.filename else ".mp4"
45
+ temp_filename = f"original_{uuid.uuid4()}{file_extension}"
46
+ temp_path = os.path.join(UPLOAD_DIR, temp_filename)
47
+
48
+ # حفظ الملف مؤقتاً
49
+ try:
50
+ with open(temp_path, "wb") as buffer:
51
+ shutil.copyfileobj(video_file.file, buffer)
52
+ except Exception as e:
53
+ raise HTTPException(status_code=500, detail=f"Error saving file: {str(e)}")
54
+
55
+ # تحليل البيانات الوصفية
56
+ metadata_dict = {}
57
+ if metadata:
58
+ try:
59
+ metadata_dict = json.loads(metadata)
60
+ except json.JSONDecodeError:
61
+ pass
62
+
63
+ # تسجيل الفيديو الأصلي
64
+ try:
65
+ original_id = version_manager.register_original(
66
+ source_path=temp_path,
67
+ file_name=video_file.filename or "unknown_video.mp4",
68
+ metadata=metadata_dict
69
+ )
70
+
71
+ # تنظيف الملف المؤقت
72
+ background_tasks.add_task(os.remove, temp_path)
73
+
74
+ return {
75
+ "original_id": original_id,
76
+ "message": "Original video uploaded successfully",
77
+ "status": "protected"
78
+ }
79
+
80
+ except Exception as e:
81
+ # تنظيف في حالة الخطأ
82
+ if os.path.exists(temp_path):
83
+ os.remove(temp_path)
84
+ raise HTTPException(status_code=500, detail=f"Error registering video: {str(e)}")
85
+
86
+ @router.get("/originals")
87
+ async def list_original_videos() -> Dict[str, Any]:
88
+ """قائمة بجميع الفيديوهات الأصلية"""
89
+
90
+ originals = []
91
+ for original_id in version_manager.registry["originals"]:
92
+ original_data = version_manager.registry["originals"][original_id]
93
+ versions_count = len([
94
+ v for v in version_manager.registry["versions"].values()
95
+ if v["original_id"] == original_id
96
+ ])
97
+
98
+ originals.append({
99
+ "original_id": original_id,
100
+ "file_name": original_data["file_name"],
101
+ "file_size": original_data["file_size"],
102
+ "duration": original_data["duration"],
103
+ "resolution": original_data["resolution"],
104
+ "upload_date": original_data["upload_date"],
105
+ "versions_count": versions_count
106
+ })
107
+
108
+ return {
109
+ "originals": originals,
110
+ "total_count": len(originals)
111
+ }
112
+
113
+ @router.get("/originals/{original_id}")
114
+ async def get_original_details(original_id: str) -> Dict[str, Any]:
115
+ """الحصول على تفاصيل الفيديو الأصلي"""
116
+
117
+ original_info = version_manager.get_original(original_id)
118
+ if not original_info:
119
+ raise HTTPException(status_code=404, detail="Original video not found")
120
+
121
+ return original_info
122
+
123
+ @router.get("/originals/{original_id}/download")
124
+ async def download_original(original_id: str):
125
+ """تحميل الفيديو الأصلي (للقراءة فقط)"""
126
+
127
+ original_path = version_manager.get_original_path(original_id)
128
+ if not original_path or not os.path.exists(original_path):
129
+ raise HTTPException(status_code=404, detail="Original video not found")
130
+
131
+ original_data = version_manager.registry["originals"][original_id]
132
+
133
+ return FileResponse(
134
+ original_path,
135
+ media_type="video/mp4",
136
+ filename=original_data["file_name"]
137
+ )
138
+
139
+ # ============================================
140
+ # نقاط نهاية معال��ة الفيديو
141
+ # ============================================
142
+
143
+ @router.post("/{original_id}/process")
144
+ async def process_video(
145
+ original_id: str,
146
+ processing_type: str = Form(...), # transcript, crop, effects, audio, combined
147
+ version_name: Optional[str] = Form(None),
148
+ transcript_config: Optional[str] = Form(None),
149
+ crop_config: Optional[str] = Form(None),
150
+ effects_config: Optional[str] = Form(None),
151
+ audio_config: Optional[str] = Form(None)
152
+ ) -> Dict[str, Any]:
153
+ """
154
+ معالجة الفيديو بنوع معين وإنشاء نسخة جديدة
155
+
156
+ أنواع المعالجة:
157
+ - transcript: إضافة ترانسكريبت
158
+ - crop: قص الفيديو
159
+ - effects: تطبيق تأثيرات
160
+ - audio: معالجة الصوت
161
+ - combined: معالجة مركبة متعددة
162
+ """
163
+
164
+ # التحقق من وجود الفيديو الأصلي
165
+ if original_id not in version_manager.registry["originals"]:
166
+ raise HTTPException(status_code=404, detail="Original video not found")
167
+
168
+ # بناء إعدادات المعالجة
169
+ processing_config = {}
170
+
171
+ try:
172
+ if processing_type == "transcript" and transcript_config:
173
+ processing_config["transcript_config"] = json.loads(transcript_config)
174
+ processing_type_enum = ProcessingType.TRANSCRIPT
175
+
176
+ elif processing_type == "crop" and crop_config:
177
+ processing_config["crop_config"] = json.loads(crop_config)
178
+ processing_type_enum = ProcessingType.CROP
179
+
180
+ elif processing_type == "effects" and effects_config:
181
+ processing_config["effects_config"] = json.loads(effects_config)
182
+ processing_type_enum = ProcessingType.EFFECTS
183
+
184
+ elif processing_type == "audio" and audio_config:
185
+ processing_config["audio_config"] = json.loads(audio_config)
186
+ processing_type_enum = ProcessingType.AUDIO
187
+
188
+ elif processing_type == "combined":
189
+ if transcript_config:
190
+ processing_config["transcript_config"] = json.loads(transcript_config)
191
+ if crop_config:
192
+ processing_config["crop_config"] = json.loads(crop_config)
193
+ if effects_config:
194
+ processing_config["effects_config"] = json.loads(effects_config)
195
+ if audio_config:
196
+ processing_config["audio_config"] = json.loads(audio_config)
197
+
198
+ if not processing_config:
199
+ raise HTTPException(status_code=400, detail="At least one config must be provided for combined processing")
200
+
201
+ processing_type_enum = ProcessingType.COMBINED
202
+
203
+ else:
204
+ raise HTTPException(status_code=400, detail=f"Invalid processing type: {processing_type}")
205
+
206
+ except json.JSONDecodeError as e:
207
+ raise HTTPException(status_code=400, detail=f"Invalid JSON config: {str(e)}")
208
+
209
+ try:
210
+ # إنشاء نسخة جديدة فقط (بدون معالجة فعلية)
211
+ version_id = version_manager.create_version(
212
+ original_id=original_id,
213
+ processing_type=processing_type_enum,
214
+ version_name=version_name or f"{processing_type}_version",
215
+ processing_config=processing_config
216
+ )
217
+
218
+ # تحديث الحالة إلى مكتمل مؤقتاً
219
+ version_manager.update_version_status(version_id, VersionStatus.COMPLETED)
220
+
221
+ return {
222
+ "version_id": version_id,
223
+ "processing_type": processing_type,
224
+ "status": "completed",
225
+ "message": f"{processing_type} version created successfully"
226
+ }
227
+
228
+ except Exception as e:
229
+ raise HTTPException(status_code=500, detail=f"Processing error: {str(e)}")
230
+
231
+ # ============================================
232
+ # نقاط نهاية إدارة النسخ
233
+ # ============================================
234
+
235
+ @router.get("/versions/{original_id}")
236
+ async def list_video_versions(original_id: str) -> Dict[str, Any]:
237
+ """قائمة بجميع النسخ المرتبطة بفيديو أصلي"""
238
+
239
+ if original_id not in version_manager.registry["originals"]:
240
+ raise HTTPException(status_code=404, detail="Original video not found")
241
+
242
+ versions = version_manager.get_all_versions(original_id)
243
+
244
+ version_list = []
245
+ for version in versions:
246
+ version_list.append({
247
+ "version_id": version.version_id,
248
+ "version_name": version.version_name,
249
+ "processing_type": version.processing_type,
250
+ "status": version.status,
251
+ "file_size": version.file_size,
252
+ "duration": version.duration,
253
+ "resolution": version.resolution,
254
+ "created_at": version.created_at,
255
+ "parent_version": version.parent_version
256
+ })
257
+
258
+ return {
259
+ "original_id": original_id,
260
+ "versions": version_list,
261
+ "total_count": len(version_list)
262
+ }
263
+
264
+ @router.get("/versions/details/{version_id}")
265
+ async def get_version_details(version_id: str) -> Dict[str, Any]:
266
+ """الحصول على تفاصيل نسخة معينة"""
267
+
268
+ version_info = version_manager.get_version(version_id)
269
+ if not version_info:
270
+ raise HTTPException(status_code=404, detail="Version not found")
271
+
272
+ return version_info
273
+
274
+ @router.get("/versions/download/{version_id}")
275
+ async def download_version(version_id: str):
276
+ """تحميل نسخة معالجة"""
277
+
278
+ version = version_manager.get_version(version_id)
279
+ if not version:
280
+ raise HTTPException(status_code=404, detail="Version not found")
281
+
282
+ if version.status != VersionStatus.COMPLETED:
283
+ raise HTTPException(status_code=400, detail="Version processing not completed")
284
+
285
+ if not os.path.exists(version.file_path):
286
+ raise HTTPException(status_code=404, detail="Version file not found")
287
+
288
+ return FileResponse(
289
+ version.file_path,
290
+ media_type="video/mp4",
291
+ filename=f"{version.version_name}.mp4"
292
+ )
293
+
294
+ @router.delete("/versions/{version_id}")
295
+ async def delete_version(version_id: str) -> Dict[str, Any]:
296
+ """حذف نسخة معينة"""
297
+
298
+ success = version_manager.delete_version(version_id)
299
+ if not success:
300
+ raise HTTPException(status_code=404, detail="Version not found or could not be deleted")
301
+
302
+ return {
303
+ "version_id": version_id,
304
+ "message": "Version deleted successfully"
305
+ }
306
+
307
+ # ============================================
308
+ # نقاط نهاية الإحصائيات والمعلومات
309
+ # ============================================
310
+
311
+ @router.get("/stats")
312
+ async def get_system_stats() -> Dict[str, Any]:
313
+ """الحصول على إحصائيات النظام"""
314
+
315
+ stats = version_manager.get_storage_stats()
316
+
317
+ return {
318
+ "storage": stats,
319
+ "system_status": "operational",
320
+ "timestamp": datetime.now().isoformat()
321
+ }
322
+
323
+ @router.get("/version-tree/{original_id}")
324
+ async def get_version_tree(original_id: str) -> Dict[str, Any]:
325
+ """الحصول على شجرة النسخ لفيديو أصلي"""
326
+
327
+ if original_id not in version_manager.registry["originals"]:
328
+ raise HTTPException(status_code=404, detail="Original video not found")
329
+
330
+ version_tree = version_manager.get_version_tree(original_id)
331
+
332
+ return {
333
+ "original_id": original_id,
334
+ "version_tree": version_tree
335
+ }
schemas.py CHANGED
@@ -1,6 +1,9 @@
1
- from pydantic import BaseModel
2
- from typing import List, Optional
3
  from enum import Enum
 
 
 
4
 
5
  class VideoFormat(str, Enum):
6
  SHORTS = "Shorts (9:16)"
@@ -11,6 +14,21 @@ class VideoFormat(str, Enum):
11
  ORIGINAL = "Original (No Resize)"
12
  CUSTOM = "Custom"
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  class Timestamp(BaseModel):
15
  start_time: float
16
  end_time: float
@@ -28,3 +46,175 @@ class ClipRequest(BaseModel):
28
  format: VideoFormat = VideoFormat.FILM
29
  custom_dimensions: Optional[Dimensions] = None
30
  timestamps: Optional[List[Timestamp]] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import List, Optional, Dict, Any
3
  from enum import Enum
4
+ from datetime import datetime
5
+
6
+ # ======== التعدادات الأساسية ========
7
 
8
  class VideoFormat(str, Enum):
9
  SHORTS = "Shorts (9:16)"
 
14
  ORIGINAL = "Original (No Resize)"
15
  CUSTOM = "Custom"
16
 
17
+ class ProcessingType(str, Enum):
18
+ TRANSCRIPT = "transcript"
19
+ CROP = "crop"
20
+ EFFECTS = "effects"
21
+ AUDIO = "audio"
22
+ COMBINED = "combined"
23
+
24
+ class VersionStatus(str, Enum):
25
+ PENDING = "pending"
26
+ PROCESSING = "processing"
27
+ COMPLETED = "completed"
28
+ FAILED = "failed"
29
+
30
+ # ======== النماذج الأساسية ========
31
+
32
  class Timestamp(BaseModel):
33
  start_time: float
34
  end_time: float
 
46
  format: VideoFormat = VideoFormat.FILM
47
  custom_dimensions: Optional[Dimensions] = None
48
  timestamps: Optional[List[Timestamp]] = None
49
+
50
+ # ======== نماذج الترانسكريبت ========
51
+
52
+ class TranscriptSegment(BaseModel):
53
+ start: float = Field(..., description="Start time in seconds")
54
+ end: float = Field(..., description="End time in seconds")
55
+ text: str = Field(..., description="Transcript text")
56
+ position: Optional[str] = Field("bottom", description="Text position: top, bottom, center")
57
+ font_size: Optional[int] = Field(None, description="Override font size for this segment")
58
+ font_color: Optional[str] = Field(None, description="Override font color for this segment")
59
+
60
+ class TranscriptConfig(BaseModel):
61
+ segments: List[TranscriptSegment] = Field(..., description="List of transcript segments")
62
+ font_size: int = Field(default=24, ge=12, le=72, description="Default font size")
63
+ font_color: str = Field(default="#FFFFFF", pattern=r"^#[0-9A-Fa-f]{6}$", description="Default font color")
64
+ font_family: str = Field(default="Arial", description="Font family")
65
+ position: str = Field(default="bottom", pattern="^(top|bottom|center)$", description="Default text position")
66
+ background_color: Optional[str] = Field(default=None, pattern=r"^#[0-9A-Fa-f]{6}$", description="Text background color")
67
+ background_alpha: float = Field(default=0.8, ge=0.0, le=1.0, description="Background transparency")
68
+ margin: int = Field(default=20, ge=0, le=100, description="Margin from edges")
69
+ opacity: float = Field(default=1.0, ge=0.0, le=1.0, description="Text opacity")
70
+ animation: Optional[str] = Field(None, description="Text animation type")
71
+ shadow: bool = Field(default=False, description="Add text shadow")
72
+ outline: bool = Field(default=False, description="Add text outline")
73
+
74
+ # ======== نماذج القص ========
75
+
76
+ class CropConfig(BaseModel):
77
+ x1: int = Field(default=0, ge=0, description="Top-left X coordinate")
78
+ y1: int = Field(default=0, ge=0, description="Top-left Y coordinate")
79
+ x2: Optional[int] = Field(None, ge=0, description="Bottom-right X coordinate")
80
+ y2: Optional[int] = Field(None, ge=0, description="Bottom-right Y coordinate")
81
+ width: Optional[int] = Field(None, ge=1, description="Crop width")
82
+ height: Optional[int] = Field(None, ge=1, description="Crop height")
83
+ aspect_ratio: Optional[str] = Field(None, description="Force aspect ratio (e.g., '16:9', '9:16')")
84
+ center_crop: bool = Field(default=False, description="Crop from center")
85
+
86
+ def get_crop_coordinates(self, video_width: int, video_height: int) -> tuple:
87
+ """Calculate crop coordinates based on configuration"""
88
+ if self.center_crop and self.width and self.height:
89
+ center_x = video_width // 2
90
+ center_y = video_height // 2
91
+ half_width = self.width // 2
92
+ half_height = self.height // 2
93
+
94
+ x1 = max(0, center_x - half_width)
95
+ y1 = max(0, center_y - half_height)
96
+ x2 = min(video_width, center_x + half_width)
97
+ y2 = min(video_height, center_y + half_height)
98
+
99
+ return (x1, y1, x2, y2)
100
+
101
+ # Use provided coordinates or calculate from width/height
102
+ x2 = self.x2 or (self.x1 + self.width) if self.width else video_width
103
+ y2 = self.y2 or (self.y1 + self.height) if self.height else video_height
104
+
105
+ return (self.x1, self.y1, x2, y2)
106
+
107
+ # ======== نماذج التأثيرات ========
108
+
109
+ class EffectsConfig(BaseModel):
110
+ brightness: Optional[float] = Field(None, ge=0.1, le=3.0, description="Brightness multiplier")
111
+ contrast: Optional[float] = Field(None, ge=0.1, le=3.0, description="Contrast multiplier")
112
+ saturation: Optional[float] = Field(None, ge=0.0, le=3.0, description="Saturation multiplier")
113
+ speed: Optional[float] = Field(None, gt=0.1, le=10.0, description="Playback speed multiplier")
114
+ fade_in: Optional[float] = Field(None, ge=0.0, description="Fade in duration in seconds")
115
+ fade_out: Optional[float] = Field(None, ge=0.0, description="Fade out duration in seconds")
116
+ blur: Optional[float] = Field(None, ge=0.0, le=10.0, description="Blur radius")
117
+ sharpen: Optional[float] = Field(None, ge=0.0, le=5.0, description="Sharpen intensity")
118
+ vignette: Optional[float] = Field(None, ge=0.0, le=1.0, description="Vignette intensity")
119
+ noise: Optional[float] = Field(None, ge=0.0, le=1.0, description="Noise amount")
120
+ sepia: bool = Field(default=False, description="Apply sepia tone")
121
+ black_white: bool = Field(default=False, description="Convert to black and white")
122
+ vintage: bool = Field(default=False, description="Apply vintage filter")
123
+
124
+ # ======== نماذج الصوت ========
125
+
126
+ class AudioConfig(BaseModel):
127
+ volume: Optional[float] = Field(None, ge=0.0, le=2.0, description="Volume multiplier")
128
+ normalize: bool = Field(default=False, description="Normalize audio")
129
+ remove_noise: bool = Field(default=False, description="Remove background noise")
130
+ bass_boost: Optional[float] = Field(None, ge=0.0, le=2.0, description="Bass boost intensity")
131
+ treble_boost: Optional[float] = Field(None, ge=0.0, le=2.0, description="Treble boost intensity")
132
+ fade_in: Optional[float] = Field(None, ge=0.0, description="Audio fade in duration")
133
+ fade_out: Optional[float] = Field(None, ge=0.0, description="Audio fade out duration")
134
+ speed: Optional[float] = Field(None, gt=0.1, le=2.0, description="Audio speed multiplier")
135
+ pitch_shift: Optional[float] = Field(None, ge=-12, le=12, description="Pitch shift in semitones")
136
+
137
+ # ======== نماذج إدارة النسخ ========
138
+
139
+ class VideoVersionResponse(BaseModel):
140
+ version_id: str
141
+ version_name: str
142
+ processing_type: ProcessingType
143
+ status: VersionStatus
144
+ file_path: str
145
+ file_size: int
146
+ duration: float
147
+ resolution: str
148
+ created_at: datetime
149
+ parent_version: Optional[str]
150
+ processing_config: Dict[str, Any]
151
+ metadata: Dict[str, Any]
152
+
153
+ class OriginalVideoResponse(BaseModel):
154
+ original_id: str
155
+ file_name: str
156
+ file_path: str
157
+ file_size: int
158
+ upload_date: datetime
159
+ duration: float
160
+ resolution: str
161
+ format: str
162
+ metadata: Dict[str, Any]
163
+ versions_count: int
164
+ versions: List[str]
165
+
166
+ class ProcessingRequest(BaseModel):
167
+ original_id: str
168
+ processing_type: ProcessingType
169
+ version_name: Optional[str] = None
170
+ transcript_config: Optional[TranscriptConfig] = None
171
+ crop_config: Optional[CropConfig] = None
172
+ effects_config: Optional[EffectsConfig] = None
173
+ audio_config: Optional[AudioConfig] = None
174
+ priority: str = Field(default="normal", pattern="^(low|normal|high)$")
175
+ metadata: Dict[str, Any] = Field(default_factory=dict)
176
+
177
+ class ProcessingResponse(BaseModel):
178
+ version_id: str
179
+ original_id: str
180
+ processing_type: ProcessingType
181
+ status: VersionStatus
182
+ message: str
183
+ estimated_time: Optional[int] = None
184
+ created_at: datetime
185
+
186
+ # ======== نماذج الإحصائيات ========
187
+
188
+ class StorageStats(BaseModel):
189
+ original_videos: int
190
+ total_versions: int
191
+ originals_size: int
192
+ versions_size: int
193
+ total_size: int
194
+
195
+ class SystemStats(BaseModel):
196
+ storage: StorageStats
197
+ system_status: str
198
+ active_processes: int
199
+ queue_size: int
200
+ timestamp: datetime
201
+
202
+ # ======== نماذج التحميل والاستجابة ========
203
+
204
+ class UploadResponse(BaseModel):
205
+ original_id: str
206
+ file_name: str
207
+ file_size: int
208
+ upload_date: datetime
209
+ message: str
210
+ status: str
211
+
212
+ class VersionListResponse(BaseModel):
213
+ original_id: str
214
+ versions: List[VideoVersionResponse]
215
+ total_count: int
216
+
217
+ class VersionTreeResponse(BaseModel):
218
+ original_id: str
219
+ version_tree: Dict[str, List[str]]
220
+ versions_info: Dict[str, VideoVersionResponse]
video_storage/version_registry.json ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "originals": {
3
+ "95370838-09b1-4796-a340-94cf18061f59": {
4
+ "original_id": "95370838-09b1-4796-a340-94cf18061f59",
5
+ "file_name": "my_movie.mp4",
6
+ "file_path": "video_storage\\originals\\95370838-09b1-4796-a340-94cf18061f59\\my_movie.mp4",
7
+ "file_size": 30654664,
8
+ "upload_date": "2026-02-11 04:02:05.952666",
9
+ "duration": 0.0,
10
+ "resolution": "1920x1080",
11
+ "format": "mp4",
12
+ "metadata": {
13
+ "title": "Test Video"
14
+ }
15
+ },
16
+ "f42d737a-3d28-430c-a2ee-2ee2fa0c662e": {
17
+ "original_id": "f42d737a-3d28-430c-a2ee-2ee2fa0c662e",
18
+ "file_name": "my_movie.mp4",
19
+ "file_path": "video_storage\\originals\\f42d737a-3d28-430c-a2ee-2ee2fa0c662e\\my_movie.mp4",
20
+ "file_size": 30654664,
21
+ "upload_date": "2026-02-11 05:04:26.861688",
22
+ "duration": 0.0,
23
+ "resolution": "1920x1080",
24
+ "format": "mp4",
25
+ "metadata": {}
26
+ },
27
+ "305874f7-62b7-490d-9893-8872911252c8": {
28
+ "original_id": "305874f7-62b7-490d-9893-8872911252c8",
29
+ "file_name": "my_movie.mp4",
30
+ "file_path": "video_storage\\originals\\305874f7-62b7-490d-9893-8872911252c8\\my_movie.mp4",
31
+ "file_size": 30654664,
32
+ "upload_date": "2026-02-11 05:04:35.128854",
33
+ "duration": 0.0,
34
+ "resolution": "1920x1080",
35
+ "format": "mp4",
36
+ "metadata": {}
37
+ }
38
+ },
39
+ "versions": {
40
+ "24fe0071-0fbb-4491-bd95-5b7bb0545355": {
41
+ "version_id": "24fe0071-0fbb-4491-bd95-5b7bb0545355",
42
+ "original_id": "95370838-09b1-4796-a340-94cf18061f59",
43
+ "version_name": "transcript_version",
44
+ "processing_type": "transcript",
45
+ "file_path": "video_storage\\versions\\24fe0071-0fbb-4491-bd95-5b7bb0545355\\transcript_version_my_movie.mp4",
46
+ "file_size": 30654664,
47
+ "duration": 0.0,
48
+ "resolution": "1920x1080",
49
+ "created_at": "2026-02-11 04:16:50.158545",
50
+ "parent_version": null,
51
+ "processing_config": {
52
+ "transcript_config": {
53
+ "text_segments": [
54
+ {
55
+ "text": "\u0645\u0631\u062d\u0628\u0627\u064b \u0628\u0643\u0645 \u0641\u064a \u0647\u0630\u0627 \u0627\u0644\u0641\u064a\u062f\u064a\u0648 \u0627\u0644\u062a\u062c\u0631\u064a\u0628\u064a",
56
+ "start_time": 0.0,
57
+ "end_time": 3.0
58
+ },
59
+ {
60
+ "text": "\u0633\u0646\u062e\u062a\u0628\u0631 \u0646\u0638\u0627\u0645 \u0625\u062f\u0627\u0631\u0629 \u0646\u0633\u062e \u0627\u0644\u0641\u064a\u062f\u064a\u0648",
61
+ "start_time": 3.0,
62
+ "end_time": 6.0
63
+ },
64
+ {
65
+ "text": "\u0627\u0644\u0646\u0638\u0627\u0645 \u064a\u062d\u0627\u0641\u0638 \u0639\u0644\u0649 \u0627\u0644\u0646\u0633\u062e\u0629 \u0627\u0644\u0623\u0635\u0644\u064a\u0629 \u0648\u064a\u0646\u0634\u0626 \u0646\u0633\u062e\u0627\u064b \u0645\u0639\u062f\u0644\u0629",
66
+ "start_time": 6.0,
67
+ "end_time": 10.0
68
+ }
69
+ ],
70
+ "font_size": 24,
71
+ "font_color": "white",
72
+ "background_color": "rgba(0,0,0,0.7)",
73
+ "position": "bottom",
74
+ "margin": 20
75
+ }
76
+ },
77
+ "status": "pending",
78
+ "metadata": {}
79
+ },
80
+ "c7bc365c-7c0d-478b-8c38-9b089b152eb8": {
81
+ "version_id": "c7bc365c-7c0d-478b-8c38-9b089b152eb8",
82
+ "original_id": "95370838-09b1-4796-a340-94cf18061f59",
83
+ "version_name": "transcript_version",
84
+ "processing_type": "transcript",
85
+ "file_path": "video_storage\\versions\\c7bc365c-7c0d-478b-8c38-9b089b152eb8\\transcript_version_my_movie.mp4",
86
+ "file_size": 30654664,
87
+ "duration": 0.0,
88
+ "resolution": "1920x1080",
89
+ "created_at": "2026-02-11 04:18:16.688318",
90
+ "parent_version": null,
91
+ "processing_config": {
92
+ "transcript_config": {
93
+ "text_segments": [
94
+ {
95
+ "text": "\u0645\u0631\u062d\u0628\u0627\u064b \u0628\u0643\u0645 \u0641\u064a \u0647\u0630\u0627 \u0627\u0644\u0641\u064a\u062f\u064a\u0648 \u0627\u0644\u062a\u062c\u0631\u064a\u0628\u064a",
96
+ "start_time": 0.0,
97
+ "end_time": 3.0
98
+ },
99
+ {
100
+ "text": "\u0633\u0646\u062e\u062a\u0628\u0631 \u0646\u0638\u0627\u0645 \u0625\u062f\u0627\u0631\u0629 \u0646\u0633\u062e \u0627\u0644\u0641\u064a\u062f\u064a\u0648",
101
+ "start_time": 3.0,
102
+ "end_time": 6.0
103
+ },
104
+ {
105
+ "text": "\u0627\u0644\u0646\u0638\u0627\u0645 \u064a\u062d\u0627\u0641\u0638 \u0639\u0644\u0649 \u0627\u0644\u0646\u0633\u062e\u0629 \u0627\u0644\u0623\u0635\u0644\u064a\u0629 \u0648\u064a\u0646\u0634\u0626 \u0646\u0633\u062e\u0627\u064b \u0645\u0639\u062f\u0644\u0629",
106
+ "start_time": 6.0,
107
+ "end_time": 10.0
108
+ }
109
+ ],
110
+ "font_size": 24,
111
+ "font_color": "white",
112
+ "background_color": "rgba(0,0,0,0.7)",
113
+ "position": "bottom",
114
+ "margin": 20
115
+ }
116
+ },
117
+ "status": "processing",
118
+ "metadata": {}
119
+ },
120
+ "f2d9688e-3f3c-4cd3-9efd-ef2195852546": {
121
+ "version_id": "f2d9688e-3f3c-4cd3-9efd-ef2195852546",
122
+ "original_id": "95370838-09b1-4796-a340-94cf18061f59",
123
+ "version_name": "transcript_version",
124
+ "processing_type": "transcript",
125
+ "file_path": "video_storage\\versions\\f2d9688e-3f3c-4cd3-9efd-ef2195852546\\transcript_version_my_movie.mp4",
126
+ "file_size": 30654664,
127
+ "duration": 0.0,
128
+ "resolution": "1920x1080",
129
+ "created_at": "2026-02-11 04:19:43.252152",
130
+ "parent_version": null,
131
+ "processing_config": {
132
+ "transcript_config": {
133
+ "text_segments": [
134
+ {
135
+ "text": "\u0645\u0631\u062d\u0628\u0627\u064b \u0628\u0643\u0645 \u0641\u064a \u0647\u0630\u0627 \u0627\u0644\u0641\u064a\u062f\u064a\u0648 \u0627\u0644\u062a\u062c\u0631\u064a\u0628\u064a",
136
+ "start_time": 0.0,
137
+ "end_time": 3.0
138
+ },
139
+ {
140
+ "text": "\u0633\u0646\u062e\u062a\u0628\u0631 \u0646\u0638\u0627\u0645 \u0625\u062f\u0627\u0631\u0629 \u0646\u0633\u062e \u0627\u0644\u0641\u064a\u062f\u064a\u0648",
141
+ "start_time": 3.0,
142
+ "end_time": 6.0
143
+ },
144
+ {
145
+ "text": "\u0627\u0644\u0646\u0638\u0627\u0645 \u064a\u062d\u0627\u0641\u0638 \u0639\u0644\u0649 \u0627\u0644\u0646\u0633\u062e\u0629 \u0627\u0644\u0623\u0635\u0644\u064a\u0629 \u0648\u064a\u0646\u0634\u0626 \u0646\u0633\u062e\u0627\u064b \u0645\u0639\u062f\u0644\u0629",
146
+ "start_time": 6.0,
147
+ "end_time": 10.0
148
+ }
149
+ ],
150
+ "font_size": 24,
151
+ "font_color": "white",
152
+ "background_color": "rgba(0,0,0,0.7)",
153
+ "position": "bottom",
154
+ "margin": 20
155
+ }
156
+ },
157
+ "status": "completed",
158
+ "metadata": {}
159
+ },
160
+ "87365beb-a422-4266-b701-de41fc3e353e": {
161
+ "version_id": "87365beb-a422-4266-b701-de41fc3e353e",
162
+ "original_id": "95370838-09b1-4796-a340-94cf18061f59",
163
+ "version_name": "cropped_version",
164
+ "processing_type": "crop",
165
+ "file_path": "video_storage\\versions\\87365beb-a422-4266-b701-de41fc3e353e\\cropped_version_my_movie.mp4",
166
+ "file_size": 30654664,
167
+ "duration": 0.0,
168
+ "resolution": "1920x1080",
169
+ "created_at": "2026-02-11 04:22:47.672723",
170
+ "parent_version": null,
171
+ "processing_config": {
172
+ "crop_config": {
173
+ "start_time": 0,
174
+ "end_time": 10,
175
+ "width": 1280,
176
+ "height": 720,
177
+ "x_position": 0,
178
+ "y_position": 0
179
+ }
180
+ },
181
+ "status": "failed",
182
+ "metadata": {}
183
+ },
184
+ "4a7b63db-1a53-4d2c-b33d-67d350b4cdab": {
185
+ "version_id": "4a7b63db-1a53-4d2c-b33d-67d350b4cdab",
186
+ "original_id": "95370838-09b1-4796-a340-94cf18061f59",
187
+ "version_name": "enhanced_version",
188
+ "processing_type": "effects",
189
+ "file_path": "video_storage\\versions\\4a7b63db-1a53-4d2c-b33d-67d350b4cdab\\enhanced_version_my_movie.mp4",
190
+ "file_size": 30654664,
191
+ "duration": 0.0,
192
+ "resolution": "1920x1080",
193
+ "created_at": "2026-02-11 04:23:48.659594",
194
+ "parent_version": null,
195
+ "processing_config": {
196
+ "effects_config": {
197
+ "brightness": 1.2,
198
+ "contrast": 1.1,
199
+ "saturation": 1.0,
200
+ "blur": 0,
201
+ "sharpen": false
202
+ }
203
+ },
204
+ "status": "failed",
205
+ "metadata": {}
206
+ },
207
+ "39c940fe-e3d8-4c42-adb4-078a034d0fbf": {
208
+ "version_id": "39c940fe-e3d8-4c42-adb4-078a034d0fbf",
209
+ "original_id": "95370838-09b1-4796-a340-94cf18061f59",
210
+ "version_name": "audio_enhanced_version",
211
+ "processing_type": "audio",
212
+ "file_path": "video_storage\\versions\\39c940fe-e3d8-4c42-adb4-078a034d0fbf\\audio_enhanced_version_my_movie.mp4",
213
+ "file_size": 30654664,
214
+ "duration": 0.0,
215
+ "resolution": "1920x1080",
216
+ "created_at": "2026-02-11 04:24:28.972273",
217
+ "parent_version": null,
218
+ "processing_config": {
219
+ "audio_config": {
220
+ "volume": 1.5,
221
+ "normalize": true,
222
+ "remove_noise": false
223
+ }
224
+ },
225
+ "status": "failed",
226
+ "metadata": {}
227
+ },
228
+ "f0c0c5bd-2db4-4c4c-971a-f449cee8e0f7": {
229
+ "version_id": "f0c0c5bd-2db4-4c4c-971a-f449cee8e0f7",
230
+ "original_id": "95370838-09b1-4796-a340-94cf18061f59",
231
+ "version_name": "combined_version",
232
+ "processing_type": "combined",
233
+ "file_path": "video_storage\\versions\\f0c0c5bd-2db4-4c4c-971a-f449cee8e0f7\\combined_version_my_movie.mp4",
234
+ "file_size": 30654664,
235
+ "duration": 0.0,
236
+ "resolution": "1920x1080",
237
+ "created_at": "2026-02-11 04:25:28.379154",
238
+ "parent_version": null,
239
+ "processing_config": {
240
+ "transcript_config": {
241
+ "text_segments": [
242
+ {
243
+ "text": "\u0645\u0631\u062d\u0628\u0627 \u0628\u0627\u0644\u0639\u0627\u0644\u0645",
244
+ "start_time": 0,
245
+ "end_time": 2
246
+ },
247
+ {
248
+ "text": "\u0647\u0630\u0627 \u0641\u064a\u062f\u064a\u0648 \u062a\u062c\u0631\u064a\u0628\u064a",
249
+ "start_time": 2,
250
+ "end_time": 4
251
+ }
252
+ ],
253
+ "font_size": 24,
254
+ "font_color": "#FFFFFF",
255
+ "position": "bottom",
256
+ "background_color": "#000000",
257
+ "opacity": 0.8
258
+ },
259
+ "crop_config": {
260
+ "start_time": 0,
261
+ "end_time": 10,
262
+ "width": 1280,
263
+ "height": 720,
264
+ "x_position": 0,
265
+ "y_position": 0
266
+ },
267
+ "effects_config": {
268
+ "brightness": 1.1,
269
+ "contrast": 1.0,
270
+ "saturation": 1.0,
271
+ "blur": 0,
272
+ "sharpen": false
273
+ },
274
+ "audio_config": {
275
+ "volume": 1.2,
276
+ "normalize": true,
277
+ "remove_noise": false
278
+ }
279
+ },
280
+ "status": "failed",
281
+ "metadata": {}
282
+ }
283
+ },
284
+ "version_tree": {
285
+ "95370838-09b1-4796-a340-94cf18061f59": [
286
+ "24fe0071-0fbb-4491-bd95-5b7bb0545355",
287
+ "c7bc365c-7c0d-478b-8c38-9b089b152eb8",
288
+ "f2d9688e-3f3c-4cd3-9efd-ef2195852546",
289
+ "87365beb-a422-4266-b701-de41fc3e353e",
290
+ "4a7b63db-1a53-4d2c-b33d-67d350b4cdab",
291
+ "39c940fe-e3d8-4c42-adb4-078a034d0fbf",
292
+ "f0c0c5bd-2db4-4c4c-971a-f449cee8e0f7"
293
+ ],
294
+ "f42d737a-3d28-430c-a2ee-2ee2fa0c662e": [],
295
+ "305874f7-62b7-490d-9893-8872911252c8": []
296
+ }
297
+ }