aliSaac510 commited on
Commit
00db48c
·
1 Parent(s): d1c8604

Add Cinematic Blur layout and fix clear endpoint

Browse files
TECHNICAL_DOCUMENTATION.md DELETED
@@ -1,346 +0,0 @@
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 DELETED
@@ -1,720 +0,0 @@
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
- **نتمنى لك تجربة ممتعة في استخدام نظام إدارة نسخ الفيديو المتقدم! 🎬✨**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
hybrid_processor.py CHANGED
@@ -7,15 +7,17 @@ from typing import List, Optional, Tuple
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
14
 
 
 
 
 
15
  # If complex custom dimensions, need MoviePy
16
- if custom_dims and hasattr(custom_dims, 'width') and hasattr(custom_dims, 'height'):
17
- # Simple resize is OK for FFmpeg
18
- pass
19
 
20
  # If complex output format, need MoviePy
21
  if output_format and output_format not in ['mp4', 'original']:
 
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
+ from schemas import LayoutType
11
 
12
  # If background music is requested, need MoviePy
13
  if bg_music:
14
  return False
15
 
16
+ # If complex layout is requested, need MoviePy
17
+ if custom_dims and hasattr(custom_dims, 'layout_type') and custom_dims.layout_type != LayoutType.CROP:
18
+ return False
19
+
20
  # If complex custom dimensions, need MoviePy
 
 
 
21
 
22
  # If complex output format, need MoviePy
23
  if output_format and output_format not in ['mp4', 'original']:
main.py CHANGED
@@ -2,11 +2,23 @@ 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
 
@@ -19,10 +31,11 @@ app.add_middleware(
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")
 
26
 
27
  @app.get("/")
28
  async def root():
 
2
  from fastapi.middleware.cors import CORSMiddleware
3
  import uvicorn
4
  from datetime import datetime
5
+ from routers import video, files
6
+ import os
7
+
8
+ # إنشاء فولدرات منظمة
9
+ UPLOAD_BASE_DIR = "temp_videos"
10
+ ORIGINALS_DIR = os.path.join(UPLOAD_BASE_DIR, "originals")
11
+ PROCESSED_DIR = os.path.join(UPLOAD_BASE_DIR, "processed")
12
+ AUDIO_DIR = os.path.join(UPLOAD_BASE_DIR, "audio")
13
+ TEMP_DIR = os.path.join(UPLOAD_BASE_DIR, "temp")
14
+
15
+ # إنشاء الفولدرات
16
+ for directory in [UPLOAD_BASE_DIR, ORIGINALS_DIR, PROCESSED_DIR, AUDIO_DIR, TEMP_DIR]:
17
+ os.makedirs(directory, exist_ok=True)
18
 
19
  app = FastAPI(
20
  title="Video Processing API",
21
+ description="Simple API for video processing - optimized for n8n automation",
22
  version="1.0.0"
23
  )
24
 
 
31
  allow_headers=["*"],
32
  )
33
 
34
+ # Include routers - توحيد الـ tags
35
+ app.include_router(video.router, prefix="/api/video", tags=["Video"])
36
+ app.include_router(files.router, prefix="/api/files", tags=["Files"])
37
+
38
+ # نحذف نظام إدارة النسخ لحد ما نحتاجه
39
 
40
  @app.get("/")
41
  async def root():
routers/files.py CHANGED
@@ -4,22 +4,96 @@ import os
4
 
5
  router = APIRouter()
6
 
7
- UPLOAD_DIR = "temp_videos"
 
 
 
 
 
8
 
9
- @router.get("/get-clip/{filename}", summary="Download/Play Video Clip", tags=["File Retrieval"])
10
- async def get_clip(filename: str):
 
 
11
  """
12
- Retrieves a processed video clip, audio file, or zip by its filename.
 
13
  """
14
- path = os.path.join(UPLOAD_DIR, filename)
 
 
 
 
 
 
 
 
 
 
 
 
15
  if os.path.exists(path):
 
16
  media_type = "application/octet-stream"
17
  if filename.endswith(".mp4"):
18
  media_type = "video/mp4"
 
 
19
  elif filename.endswith(".mp3"):
20
  media_type = "audio/mpeg"
 
 
21
  elif filename.endswith(".zip"):
22
  media_type = "application/zip"
23
-
24
- return FileResponse(path, media_type=media_type, filename=filename)
25
- raise HTTPException(status_code=404, detail="File not found")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  router = APIRouter()
6
 
7
+ # الفولدرات الجديدة
8
+ UPLOAD_BASE_DIR = "temp_videos"
9
+ ORIGINALS_DIR = os.path.join(UPLOAD_BASE_DIR, "originals")
10
+ PROCESSED_DIR = os.path.join(UPLOAD_BASE_DIR, "processed")
11
+ AUDIO_DIR = os.path.join(UPLOAD_BASE_DIR, "audio")
12
+ TEMP_DIR = os.path.join(UPLOAD_BASE_DIR, "temp")
13
 
14
+ # تم حذف الـ get_clip endpoint القديمة - نستخدم /download بدلاً منه
15
+
16
+ @router.get("/{filename}", summary="Download File by Name", tags=["Files"])
17
+ async def download_file(filename: str, file_type: str = "processed"):
18
  """
19
+ Download any file by its filename - مناسب لـ n8n وغيره
20
+ file_type: "originals", "processed", "audio", or "temp"
21
  """
22
+ # نحدد الفولدر حسب النوع
23
+ if file_type == "originals":
24
+ path = os.path.join(ORIGINALS_DIR, filename)
25
+ elif file_type == "audio":
26
+ path = os.path.join(AUDIO_DIR, filename)
27
+ elif file_type == "temp":
28
+ path = os.path.join(TEMP_DIR, filename)
29
+ else: # processed (default)
30
+ path = os.path.join(PROCESSED_DIR, filename)
31
+
32
+ print(f"[DOWNLOAD DEBUG] Requested file: {filename}, type: {file_type}, full path: {path}")
33
+ print(f"[DOWNLOAD DEBUG] File exists: {os.path.exists(path)}")
34
+
35
  if os.path.exists(path):
36
+ # نحدد الـ media type حسب الامتداد
37
  media_type = "application/octet-stream"
38
  if filename.endswith(".mp4"):
39
  media_type = "video/mp4"
40
+ elif filename.endswith(".webm"):
41
+ media_type = "video/webm"
42
  elif filename.endswith(".mp3"):
43
  media_type = "audio/mpeg"
44
+ elif filename.endswith(".wav"):
45
+ media_type = "audio/wav"
46
  elif filename.endswith(".zip"):
47
  media_type = "application/zip"
48
+
49
+ # نرجع الملف مباشرة مع header للتحميل
50
+ return FileResponse(
51
+ path,
52
+ media_type=media_type,
53
+ filename=filename,
54
+ headers={"Content-Disposition": f"attachment; filename={filename}"}
55
+ )
56
+
57
+ # لو الملف مش موجود، نرجع error واضح
58
+ raise HTTPException(
59
+ status_code=404,
60
+ detail=f"File not found: {filename} in {file_type} folder. Available folders: originals, processed, audio, temp"
61
+ )
62
+
63
+ @router.get("/list-files", summary="List All Available Files", tags=["Files"])
64
+ async def list_files():
65
+ """
66
+ List all available files in all folders - مفيد للتأكد من الملفات الموجودة
67
+ """
68
+ all_files = {}
69
+
70
+ folders = {
71
+ "originals": ORIGINALS_DIR,
72
+ "processed": PROCESSED_DIR,
73
+ "audio": AUDIO_DIR,
74
+ "temp": TEMP_DIR
75
+ }
76
+
77
+ for folder_name, folder_path in folders.items():
78
+ if os.path.exists(folder_path):
79
+ files = []
80
+ for filename in os.listdir(folder_path):
81
+ file_path = os.path.join(folder_path, filename)
82
+ if os.path.isfile(file_path):
83
+ file_info = {
84
+ "filename": filename,
85
+ "size_bytes": os.path.getsize(file_path),
86
+ "size_mb": round(os.path.getsize(file_path) / (1024 * 1024), 2),
87
+ "created": os.path.getctime(file_path)
88
+ }
89
+ files.append(file_info)
90
+ all_files[folder_name] = files
91
+ else:
92
+ all_files[folder_name] = []
93
+
94
+ return {
95
+ "status": "success",
96
+ "total_files": sum(len(files) for files in all_files.values()),
97
+ "folders": all_files,
98
+ "message": "Use /files/{filename}?file_type={type} to download any file"
99
+ }
routers/video.py CHANGED
@@ -1,17 +1,39 @@
1
- from fastapi import APIRouter, File, UploadFile, Form, HTTPException, BackgroundTasks, Request
2
  from fastapi.responses import JSONResponse, FileResponse
 
3
  import shutil
4
  import os
5
  import json
6
  import uuid
7
  import requests
 
 
 
8
  from schemas import VideoFormat, Timestamp, ClipRequest, Dimensions
9
  from video_processor import process_video_clips, safe_remove, create_zip_archive
10
-
11
  router = APIRouter()
12
 
13
- UPLOAD_DIR = "temp_videos"
14
- os.makedirs(UPLOAD_DIR, exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  def background_processing(
17
  task_id: str,
@@ -22,7 +44,8 @@ def background_processing(
22
  export_audio: bool,
23
  audio_path: str = None,
24
  webhook_url: str = None,
25
- host_url: str = ""
 
26
  ):
27
  """
28
  Executes the video processing logic.
@@ -39,6 +62,7 @@ def background_processing(
39
  # Prepare result
40
  clips_filenames = [os.path.basename(p) for p in clip_paths]
41
  full_clip_paths = clip_paths
 
42
 
43
  if export_audio and not webhook_url:
44
  # Create ZIP ONLY if it's a direct synchronous request (User waiting in browser)
@@ -48,7 +72,7 @@ def background_processing(
48
  zip_path = create_zip_archive(all_files, zip_filename)
49
 
50
  if zip_path:
51
- download_url = f"{host_url}get-clip/{os.path.basename(zip_path)}"
52
  response_data = {
53
  "status": "success",
54
  "task_id": task_id,
@@ -57,15 +81,15 @@ def background_processing(
57
  "archive_filename": os.path.basename(zip_path)
58
  }
59
  else:
60
- response_data = {"status": "error", "message": "Failed to create zip archive"}
61
  else:
62
  # For Webhooks OR Normal JSON response without export_audio
63
  # We return the LIST of URLs so n8n can loop over them
64
- clip_urls = [f"{host_url}get-clip/{name}" for name in clips_filenames]
65
 
66
  audio_urls = []
67
  if export_audio:
68
- audio_urls = [f"{host_url}get-clip/{os.path.basename(p)}" if p else None for p in audio_clip_paths]
69
 
70
  response_data = {
71
  "status": "success",
@@ -77,6 +101,12 @@ def background_processing(
77
 
78
  # Send Webhook if requested
79
  if webhook_url:
 
 
 
 
 
 
80
  try:
81
  requests.post(webhook_url, json=response_data)
82
  except Exception as e:
@@ -93,17 +123,94 @@ def background_processing(
93
  if not webhook_url: raise e # Re-raise if synchronous
94
 
95
  finally:
 
96
  for f in files_to_cleanup:
97
- safe_remove(f)
 
 
98
 
99
- @router.post("/process", tags=["Video Processing"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  async def process_video(
101
  request: Request,
102
  background_tasks: BackgroundTasks,
103
  video: UploadFile = File(...),
104
  format: VideoFormat = Form(VideoFormat.FILM, description="Select the output video format"),
105
- background_music: UploadFile = File(None, description="Upload an audio file for background music"),
106
- music_url: str = Form(None, description="URL of an audio file for background music"),
107
  video_volume: float = Form(1.0, description="Volume of original video (0.0 to 1.0+)"),
108
  music_volume: float = Form(0.2, description="Volume of background music (0.0 to 1.0+)"),
109
  loop_music: bool = Form(True, description="Loop background music if shorter than video"),
@@ -113,7 +220,8 @@ async def process_video(
113
  description='Optional JSON list of timestamps. If not provided, will process entire video. Recommended duration for weak devices: < 60 seconds per clip.'
114
  ),
115
  export_audio: bool = Form(False, description="Export separate audio files for each clip (original audio). Returns a ZIP if True."),
116
- webhook_url: str = Form(None, description="Optional URL to receive processing results via POST.")
 
117
  ):
118
  task_id = uuid.uuid4().hex[:8]
119
  temp_path = None
@@ -134,35 +242,31 @@ async def process_video(
134
  except Exception as e:
135
  raise HTTPException(status_code=400, detail=f"Invalid timestamps format: {str(e)}")
136
 
137
- # Save uploaded video first
138
- temp_path = os.path.join(UPLOAD_DIR, f"{task_id}_{video.filename}")
139
- with open(temp_path, "wb") as buffer:
 
140
  shutil.copyfileobj(video.file, buffer)
 
 
 
 
 
 
 
141
 
142
  # If no timestamps provided, process entire video
143
  if not timestamps:
144
  # Get video duration to create single timestamp for entire video
145
  from moviepy import VideoFileClip
146
- with VideoFileClip(temp_path) as video:
147
- timestamps = [Timestamp(start_time=0, end_time=video.duration)]
148
 
149
- # Handle Background Music
150
  if background_music:
151
- audio_path = os.path.join(UPLOAD_DIR, f"bg_{task_id}_{background_music.filename}")
152
  with open(audio_path, "wb") as buffer:
153
  shutil.copyfileobj(background_music.file, buffer)
154
- elif music_url:
155
- # Download audio from URL
156
- try:
157
- response = requests.get(music_url, stream=True)
158
- response.raise_for_status()
159
- audio_filename = f"bg_url_{task_id}.mp3"
160
- audio_path = os.path.join(UPLOAD_DIR, audio_filename)
161
- with open(audio_path, "wb") as buffer:
162
- for chunk in response.iter_content(chunk_size=8192):
163
- buffer.write(chunk)
164
- except Exception as e:
165
- print(f"Failed to download music: {e}")
166
 
167
  # Prepare dimensions
168
  dims = Dimensions(
@@ -178,43 +282,61 @@ async def process_video(
178
  if webhook_url:
179
  background_tasks.add_task(
180
  background_processing,
181
- task_id, temp_path, timestamps, format, dims, export_audio, audio_path, webhook_url, host_url
182
  )
183
  return {"status": "processing", "task_id": task_id, "message": "Processing started. Results will be sent to webhook."}
184
  else:
185
  # Synchronous execution
186
  return background_processing(
187
- task_id, temp_path, timestamps, format, dims, export_audio, audio_path, None, host_url
188
  )
189
 
190
  except Exception as e:
191
- if temp_path: safe_remove(temp_path)
192
- if audio_path: safe_remove(audio_path)
 
 
 
193
  if isinstance(e, HTTPException): raise e
194
  return JSONResponse(status_code=500, content={"error": str(e)})
195
 
196
- @router.get("/status", tags=["System"])
197
  async def get_system_status():
198
  """
199
- Get system status including disk usage and temp files count.
200
  """
201
  try:
202
- import os
203
- import shutil
204
-
205
  # Get disk usage
206
  total, used, free = shutil.disk_usage("/")
207
 
208
- # Count temp files
209
- temp_files_count = 0
210
- temp_files_size = 0
 
211
 
212
- if os.path.exists(UPLOAD_DIR):
213
- for filename in os.listdir(UPLOAD_DIR):
214
- file_path = os.path.join(UPLOAD_DIR, filename)
215
- if os.path.isfile(file_path):
216
- temp_files_count += 1
217
- temp_files_size += os.path.getsize(file_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
  return {
220
  "status": "healthy",
@@ -225,10 +347,11 @@ async def get_system_status():
225
  "usage_percent": round((used / total) * 100, 1)
226
  },
227
  "temp_files": {
228
- "count": temp_files_count,
229
- "size_mb": round(temp_files_size / (1024**2), 2)
 
230
  },
231
- "upload_directory": UPLOAD_DIR
232
  }
233
  except Exception as e:
234
  return JSONResponse(
@@ -236,64 +359,117 @@ async def get_system_status():
236
  content={"error": f"Failed to get status: {str(e)}"}
237
  )
238
 
239
- @router.post("/clear", tags=["Cleanup"])
240
- async def clear_temp_files():
 
 
 
241
  """
242
- Clean up all temporary files in the upload directory.
243
- Useful for freeing up disk space.
 
 
 
244
  """
245
  try:
246
- import shutil
247
- import os
248
-
249
- # Count files before cleanup
250
- files_count = 0
251
- total_size = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
- if os.path.exists(UPLOAD_DIR):
254
- for filename in os.listdir(UPLOAD_DIR):
255
- file_path = os.path.join(UPLOAD_DIR, filename)
256
- if os.path.isfile(file_path):
257
- files_count += 1
258
- total_size += os.path.getsize(file_path)
259
- safe_remove(file_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
 
261
- # Convert size to MB
262
- size_mb = total_size / (1024 * 1024)
263
 
264
  return {
265
- "status": "success",
266
- "message": f"Cleaned up {files_count} files ({size_mb:.2f} MB)",
267
- "files_removed": files_count,
268
- "space_freed_mb": round(size_mb, 2)
 
 
269
  }
 
270
  except Exception as e:
271
  return JSONResponse(
272
- status_code=500,
273
- content={"error": f"Cleanup failed: {str(e)}"}
274
  )
275
 
276
- @router.post("/extract-audio", tags=["Audio Processing"])
277
  async def extract_audio(
 
278
  video: UploadFile = File(...),
279
  output_format: str = Form("mp3"),
280
  host_url: str = Form("")
281
  ):
282
  """
283
- Extract audio from a video file and return the audio file.
284
-
285
- Args:
286
- video: The video file to extract audio from
287
- output_format: Output audio format (mp3, wav, etc.)
288
- host_url: Base URL for the download link (optional)
289
-
290
- Returns:
291
- Audio file directly if no host_url provided, otherwise JSON with download link
292
  """
 
 
 
293
  try:
294
- from video_processor import extract_audio_from_video
295
- import uuid
296
-
297
  # Validate output format
298
  allowed_formats = ["mp3", "wav", "m4a", "aac", "flac"]
299
  if output_format.lower() not in allowed_formats:
@@ -305,45 +481,68 @@ async def extract_audio(
305
  # Generate task ID
306
  task_id = uuid.uuid4().hex[:8]
307
 
308
- # Save uploaded video
309
- temp_path = os.path.join(UPLOAD_DIR, f"{task_id}_{video.filename}")
310
- with open(temp_path, "wb") as buffer:
311
- shutil.copyfileobj(video.file, buffer)
312
 
313
- # Extract audio
314
- audio_path = extract_audio_from_video(temp_path, output_format)
 
 
 
 
 
 
 
 
 
 
 
315
 
316
- # Clean up video file
317
- safe_remove(temp_path)
 
318
 
319
- # If host_url is provided, return JSON with download link
320
- if host_url:
321
- audio_filename = os.path.basename(audio_path)
322
- download_url = f"{host_url}get-clip/{audio_filename}"
323
-
324
- return {
325
- "status": "success",
326
- "task_id": task_id,
327
- "message": "Audio extracted successfully",
328
- "audio_url": download_url,
329
- "audio_filename": audio_filename,
330
- "format": output_format.lower()
331
- }
332
 
333
- # If no host_url, return the audio file directly
334
- audio_filename = os.path.basename(audio_path)
 
 
 
 
 
 
335
 
336
- # Clean up audio file after response
337
- def cleanup_audio():
338
- safe_remove(audio_path)
339
 
340
- # Return the audio file directly
341
- return FileResponse(
342
- audio_path,
343
- media_type=f"audio/{output_format.lower()}",
344
- filename=audio_filename,
345
- background=cleanup_audio
346
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
 
348
  except ValueError as e:
349
  return JSONResponse(
@@ -351,7 +550,108 @@ async def extract_audio(
351
  content={"error": str(e)}
352
  )
353
  except Exception as e:
 
 
 
 
354
  return JSONResponse(
355
  status_code=500,
356
  content={"error": f"Audio extraction failed: {str(e)}"}
357
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, File, Form, UploadFile, HTTPException, BackgroundTasks, Request, Query
2
  from fastapi.responses import JSONResponse, FileResponse
3
+ from urllib.parse import urljoin
4
  import shutil
5
  import os
6
  import json
7
  import uuid
8
  import requests
9
+ import subprocess
10
+ import imageio_ffmpeg
11
+ from typing import List, Optional
12
  from schemas import VideoFormat, Timestamp, ClipRequest, Dimensions
13
  from video_processor import process_video_clips, safe_remove, create_zip_archive
 
14
  router = APIRouter()
15
 
16
+ # فولدرات منظمة
17
+ BASE_DIR = "temp_videos"
18
+ ORIGINALS_DIR = os.path.join(BASE_DIR, "originals")
19
+ PROCESSED_DIR = os.path.join(BASE_DIR, "processed")
20
+ AUDIO_DIR = os.path.join(BASE_DIR, "audio")
21
+ TEMP_DIR = os.path.join(BASE_DIR, "temp")
22
+
23
+ def build_file_url(request: Request, filename: str, file_type: str = "processed") -> str:
24
+ """
25
+ دالة ذكية لبناء URL للملفات تعمل في كل البيئات
26
+ """
27
+ # نجيب الـ base URL من الـ request
28
+ base_url = str(request.base_url).rstrip('/')
29
+
30
+ # نبني الـ URL كامل
31
+ url_path = f"/api/files/get-clip/{filename}?file_type={file_type}"
32
+ return urljoin(base_url, url_path)
33
+
34
+ # إنشاء الفولدرات
35
+ for directory in [BASE_DIR, ORIGINALS_DIR, PROCESSED_DIR, AUDIO_DIR, TEMP_DIR]:
36
+ os.makedirs(directory, exist_ok=True)
37
 
38
  def background_processing(
39
  task_id: str,
 
44
  export_audio: bool,
45
  audio_path: str = None,
46
  webhook_url: str = None,
47
+ base_url: str = "",
48
+ return_files: bool = False
49
  ):
50
  """
51
  Executes the video processing logic.
 
62
  # Prepare result
63
  clips_filenames = [os.path.basename(p) for p in clip_paths]
64
  full_clip_paths = clip_paths
65
+ zip_path = None
66
 
67
  if export_audio and not webhook_url:
68
  # Create ZIP ONLY if it's a direct synchronous request (User waiting in browser)
 
72
  zip_path = create_zip_archive(all_files, zip_filename)
73
 
74
  if zip_path:
75
+ download_url = f"{base_url.rstrip('/')}/api/files/get-clip/{os.path.basename(zip_path)}"
76
  response_data = {
77
  "status": "success",
78
  "task_id": task_id,
 
81
  "archive_filename": os.path.basename(zip_path)
82
  }
83
  else:
84
+ response_data = {"status": "error", "message": "Failed to create zip archive"}
85
  else:
86
  # For Webhooks OR Normal JSON response without export_audio
87
  # We return the LIST of URLs so n8n can loop over them
88
+ clip_urls = [f"{base_url.rstrip('/')}/api/files/get-clip/{name}" for name in clips_filenames]
89
 
90
  audio_urls = []
91
  if export_audio:
92
+ audio_urls = [f"{base_url.rstrip('/')}/api/files/get-clip/{os.path.basename(p)}" if p else None for p in audio_clip_paths]
93
 
94
  response_data = {
95
  "status": "success",
 
101
 
102
  # Send Webhook if requested
103
  if webhook_url:
104
+ # enrich response with audio file info
105
+ if export_audio and not zip_path:
106
+ response_data["audio_files"] = [
107
+ {"filename": os.path.basename(p), "url": f"{base_url.rstrip('/')}/api/files/get-clip/{os.path.basename(p)}"}
108
+ for p in audio_clip_paths if p
109
+ ]
110
  try:
111
  requests.post(webhook_url, json=response_data)
112
  except Exception as e:
 
123
  if not webhook_url: raise e # Re-raise if synchronous
124
 
125
  finally:
126
+ # نمسح بس الملفات المؤقتة، مش الأصلي
127
  for f in files_to_cleanup:
128
+ # نتأكد إن الملف في فولدر التيمب قبل المسح
129
+ if f and os.path.exists(f) and TEMP_DIR in f:
130
+ safe_remove(f)
131
 
132
+ @router.get("/clear-preview", tags=["Video"])
133
+ async def get_clear_preview(folder: str = "temp"):
134
+ """
135
+ Preview files that will be deleted before actually clearing them.
136
+ Returns list of files and total size for each folder.
137
+
138
+ Parameters:
139
+ - folder: Which folder to preview (temp, processed, audio, originals, all)
140
+ """
141
+ try:
142
+ # نحدد الفولدرات اللي هنعرض منها
143
+ target_folders = []
144
+ if folder == "all":
145
+ target_folders = [TEMP_DIR, PROCESSED_DIR, AUDIO_DIR] # مش هنعرض originals
146
+ elif folder == "temp":
147
+ target_folders = [TEMP_DIR]
148
+ elif folder == "processed":
149
+ target_folders = [PROCESSED_DIR]
150
+ elif folder == "audio":
151
+ target_folders = [AUDIO_DIR]
152
+ elif folder == "originals":
153
+ target_folders = [ORIGINALS_DIR]
154
+ else:
155
+ return JSONResponse(
156
+ status_code=400,
157
+ content={"error": f"Invalid folder: {folder}. Use: temp, processed, audio, originals, or all"}
158
+ )
159
+
160
+ preview_data = {}
161
+ total_files = 0
162
+ total_size = 0
163
+
164
+ for target_folder in target_folders:
165
+ folder_name = os.path.basename(target_folder)
166
+ files_info = []
167
+ folder_size = 0
168
+
169
+ if os.path.exists(target_folder):
170
+ for filename in os.listdir(target_folder):
171
+ file_path = os.path.join(target_folder, filename)
172
+ if os.path.isfile(file_path):
173
+ file_size = os.path.getsize(file_path)
174
+ files_info.append({
175
+ "filename": filename,
176
+ "size_bytes": file_size,
177
+ "size_mb": round(file_size / (1024 * 1024), 2),
178
+ "created": os.path.getctime(file_path)
179
+ })
180
+ folder_size += file_size
181
+
182
+ preview_data[folder_name] = {
183
+ "files": files_info,
184
+ "file_count": len(files_info),
185
+ "total_size_bytes": folder_size,
186
+ "total_size_mb": round(folder_size / (1024 * 1024), 2)
187
+ }
188
+
189
+ total_files += len(files_info)
190
+ total_size += folder_size
191
+
192
+ return {
193
+ "status": "success",
194
+ "folder": folder,
195
+ "preview": preview_data,
196
+ "total_files": total_files,
197
+ "total_size_mb": round(total_size / (1024 * 1024), 2),
198
+ "message": f"Found {total_files} files ready for cleanup ({round(total_size / (1024 * 1024), 2)} MB)"
199
+ }
200
+
201
+ except Exception as e:
202
+ return JSONResponse(
203
+ status_code=500,
204
+ content={"error": f"Preview failed: {str(e)}"}
205
+ )
206
+
207
+ @router.post("/process", tags=["Video"])
208
  async def process_video(
209
  request: Request,
210
  background_tasks: BackgroundTasks,
211
  video: UploadFile = File(...),
212
  format: VideoFormat = Form(VideoFormat.FILM, description="Select the output video format"),
213
+ background_music: UploadFile = File(None, description="Upload an audio file for background music (files only, no URLs)"),
 
214
  video_volume: float = Form(1.0, description="Volume of original video (0.0 to 1.0+)"),
215
  music_volume: float = Form(0.2, description="Volume of background music (0.0 to 1.0+)"),
216
  loop_music: bool = Form(True, description="Loop background music if shorter than video"),
 
220
  description='Optional JSON list of timestamps. If not provided, will process entire video. Recommended duration for weak devices: < 60 seconds per clip.'
221
  ),
222
  export_audio: bool = Form(False, description="Export separate audio files for each clip (original audio). Returns a ZIP if True."),
223
+ webhook_url: str = Form(None, description="Optional URL to receive processing results via POST."),
224
+ return_files: bool = Form(False, description="Return processed files directly instead of URLs (for n8n automation)")
225
  ):
226
  task_id = uuid.uuid4().hex[:8]
227
  temp_path = None
 
242
  except Exception as e:
243
  raise HTTPException(status_code=400, detail=f"Invalid timestamps format: {str(e)}")
244
 
245
+ # نحفظ الفيديو الأصلي في فولدر الأصلي
246
+ original_filename = f"{task_id}_original_{video.filename}"
247
+ original_path = os.path.join(ORIGINALS_DIR, original_filename)
248
+ with open(original_path, "wb") as buffer:
249
  shutil.copyfileobj(video.file, buffer)
250
+
251
+ # ننسخ للـ temp عشان المعالجة (دي نسخة مؤقتة هنمسحها بعدين)
252
+ temp_path = os.path.join(TEMP_DIR, f"{task_id}_temp_{video.filename}")
253
+ shutil.copy2(original_path, temp_path)
254
+
255
+ # متغير عشان نعرف نمسح بس المؤقت مش الأصلي
256
+ temp_file_for_processing = temp_path
257
 
258
  # If no timestamps provided, process entire video
259
  if not timestamps:
260
  # Get video duration to create single timestamp for entire video
261
  from moviepy import VideoFileClip
262
+ with VideoFileClip(temp_path) as clip:
263
+ timestamps = [Timestamp(start_time=0, end_time=clip.duration)]
264
 
265
+ # Handle Background Music (n8n compatible - files only)
266
  if background_music:
267
+ audio_path = os.path.join(BASE_DIR, f"bg_{task_id}_{background_music.filename}")
268
  with open(audio_path, "wb") as buffer:
269
  shutil.copyfileobj(background_music.file, buffer)
 
 
 
 
 
 
 
 
 
 
 
 
270
 
271
  # Prepare dimensions
272
  dims = Dimensions(
 
282
  if webhook_url:
283
  background_tasks.add_task(
284
  background_processing,
285
+ task_id, temp_file_for_processing, timestamps, format, dims, export_audio, audio_path, webhook_url, str(request.base_url)
286
  )
287
  return {"status": "processing", "task_id": task_id, "message": "Processing started. Results will be sent to webhook."}
288
  else:
289
  # Synchronous execution
290
  return background_processing(
291
+ task_id, temp_file_for_processing, timestamps, format, dims, export_audio, audio_path, None, str(request.base_url)
292
  )
293
 
294
  except Exception as e:
295
+ # نمسح بس الملف المؤقت، الأصلي يفضل محفوظ
296
+ if 'temp_file_for_processing' in locals():
297
+ safe_remove(temp_file_for_processing)
298
+ if 'audio_path' in locals() and audio_path:
299
+ safe_remove(audio_path)
300
  if isinstance(e, HTTPException): raise e
301
  return JSONResponse(status_code=500, content={"error": str(e)})
302
 
303
+ @router.get("/status", tags=["Video"])
304
  async def get_system_status():
305
  """
306
+ Get system status including disk usage and temp files count across all subfolders.
307
  """
308
  try:
 
 
 
309
  # Get disk usage
310
  total, used, free = shutil.disk_usage("/")
311
 
312
+ # Count files in all subfolders
313
+ total_files_count = 0
314
+ total_files_size = 0
315
+ folder_stats = {}
316
 
317
+ folders = {
318
+ "originals": ORIGINALS_DIR,
319
+ "processed": PROCESSED_DIR,
320
+ "audio": AUDIO_DIR,
321
+ "temp": TEMP_DIR
322
+ }
323
+
324
+ for name, path in folders.items():
325
+ count = 0
326
+ size = 0
327
+ if os.path.exists(path):
328
+ for filename in os.listdir(path):
329
+ f_path = os.path.join(path, filename)
330
+ if os.path.isfile(f_path):
331
+ count += 1
332
+ size += os.path.getsize(f_path)
333
+
334
+ folder_stats[name] = {
335
+ "count": count,
336
+ "size_mb": round(size / (1024**2), 2)
337
+ }
338
+ total_files_count += count
339
+ total_files_size += size
340
 
341
  return {
342
  "status": "healthy",
 
347
  "usage_percent": round((used / total) * 100, 1)
348
  },
349
  "temp_files": {
350
+ "total_count": total_files_count,
351
+ "total_size_mb": round(total_files_size / (1024**2), 2),
352
+ "by_folder": folder_stats
353
  },
354
+ "base_directory": BASE_DIR
355
  }
356
  except Exception as e:
357
  return JSONResponse(
 
359
  content={"error": f"Failed to get status: {str(e)}"}
360
  )
361
 
362
+ @router.api_route("/clear", methods=["GET", "POST"], tags=["Video"])
363
+ async def clear_temp_files(
364
+ folder: str = Query("temp", description="Folder to clear", enum=["temp", "processed", "audio", "originals", "all"]),
365
+ specific_files: str = Query(None, description="Comma-separated list of specific files to delete (optional). Use /clear-preview to see available files.")
366
+ ):
367
  """
368
+ Clear files from specified folder(s). Works with GET and POST.
369
+
370
+ Parameters:
371
+ - folder: Which folder to clear (temp, processed, audio, originals, all)
372
+ - specific_files: Optional comma-separated list of specific files to delete
373
  """
374
  try:
375
+ # نحدد الفولدرات المستهدفة
376
+ target_folders = []
377
+ if folder == "all":
378
+ target_folders = [TEMP_DIR, PROCESSED_DIR, AUDIO_DIR] # مش هنمسح originals
379
+ elif folder == "temp":
380
+ target_folders = [TEMP_DIR]
381
+ elif folder == "processed":
382
+ target_folders = [PROCESSED_DIR]
383
+ elif folder == "audio":
384
+ target_folders = [AUDIO_DIR]
385
+ elif folder == "originals":
386
+ target_folders = [ORIGINALS_DIR]
387
+ else:
388
+ return JSONResponse(
389
+ status_code=400,
390
+ content={"error": f"Invalid folder: {folder}. Use: temp, processed, audio, originals, or all"}
391
+ )
392
+
393
+ deleted_files = []
394
+ total_size_freed = 0
395
+
396
+ # تسجيل مفصل للتأكد من المسارات
397
+ print(f"[CLEAR DEBUG] Target folders: {target_folders}")
398
+ print(f"[CLEAR DEBUG] Specific files requested: {specific_files}")
399
 
400
+ # لو فيه ملفات محددة، نمسحها بس
401
+ if specific_files:
402
+ files_to_delete = [f.strip() for f in specific_files.split(',') if f.strip()]
403
+ print(f"[CLEAR DEBUG] Files to delete: {files_to_delete}")
404
+
405
+ for target_folder in target_folders:
406
+ print(f"[CLEAR DEBUG] Checking folder: {target_folder}")
407
+ for filename in files_to_delete:
408
+ file_path = os.path.join(target_folder, filename)
409
+ print(f"[CLEAR DEBUG] Checking file: {file_path} - exists: {os.path.exists(file_path)}")
410
+ if os.path.exists(file_path) and os.path.isfile(file_path):
411
+ file_size = os.path.getsize(file_path)
412
+ print(f"[CLEAR DEBUG] Deleting file: {file_path}")
413
+ if safe_remove(file_path):
414
+ deleted_files.append(filename)
415
+ total_size_freed += file_size
416
+ print(f"[CLEAR DEBUG] Successfully deleted: {filename}")
417
+ else:
418
+ # نمسح كل الملفات في الفولدرات المستهدفة
419
+ for target_folder in target_folders:
420
+ print(f"[CLEAR DEBUG] Processing folder: {target_folder}")
421
+ if os.path.exists(target_folder):
422
+ folder_files = os.listdir(target_folder)
423
+ print(f"[CLEAR DEBUG] Files found in {target_folder}: {folder_files}")
424
+ for filename in os.listdir(target_folder):
425
+ file_path = os.path.join(target_folder, filename)
426
+ if os.path.isfile(file_path):
427
+ file_size = os.path.getsize(file_path)
428
+ print(f"[CLEAR DEBUG] Deleting file: {file_path}")
429
+ if safe_remove(file_path):
430
+ deleted_files.append(filename)
431
+ total_size_freed += file_size
432
+ print(f"[CLEAR DEBUG] Successfully deleted: {filename}")
433
+ else:
434
+ print(f"[CLEAR DEBUG] Folder does not exist: {target_folder}")
435
+
436
+ print(f"[CLEAR DEBUG] Total deleted: {len(deleted_files)} files, freed: {total_size_freed} bytes")
437
+
438
+ size_mb = total_size_freed / (1024 * 1024)
439
 
440
+ print(f"[CLEAR RESULT] Deleted {len(deleted_files)} files, freed {size_mb:.2f} MB")
441
+ print(f"[CLEAR RESULT] Deleted files: {deleted_files}")
442
 
443
  return {
444
+ "status": "success",
445
+ "folder": folder,
446
+ "deleted_files": deleted_files,
447
+ "files_count": len(deleted_files),
448
+ "size_freed_mb": round(size_mb, 2),
449
+ "message": f"Successfully deleted {len(deleted_files)} files ({round(size_mb, 2)} MB freed)"
450
  }
451
+
452
  except Exception as e:
453
  return JSONResponse(
454
+ status_code=500,
455
+ content={"error": f"Clear failed: {str(e)}"}
456
  )
457
 
458
+ @router.post("/extract-audio", tags=["Video"])
459
  async def extract_audio(
460
+ request: Request,
461
  video: UploadFile = File(...),
462
  output_format: str = Form("mp3"),
463
  host_url: str = Form("")
464
  ):
465
  """
466
+ Extract audio from a video file using FFmpeg (optimized for speed).
467
+ أسرع بكثير من استخدام MoviePy.
 
 
 
 
 
 
 
468
  """
469
+ print(f"[EXTRACT-AUDIO DEBUG] Starting extract-audio with file: {video.filename}")
470
+ print(f"[EXTRACT-AUDIO DEBUG] Request base_url: {request.base_url}")
471
+ print(f"[EXTRACT-AUDIO DEBUG] Output format: {output_format}")
472
  try:
 
 
 
473
  # Validate output format
474
  allowed_formats = ["mp3", "wav", "m4a", "aac", "flac"]
475
  if output_format.lower() not in allowed_formats:
 
481
  # Generate task ID
482
  task_id = uuid.uuid4().hex[:8]
483
 
484
+ # نحفظ الفيديو الأصلي دايماً في فولدر الأصلي
485
+ original_filename = f"{task_id}_original_{video.filename}"
486
+ original_path = os.path.join(ORIGINALS_DIR, original_filename)
 
487
 
488
+ if os.path.exists(original_path):
489
+ # لو لاقينا الأصلي، نستخدمه وما نحفظش تاني
490
+ temp_path = original_path
491
+ used_existing_original = True
492
+ else:
493
+ # لو مش موجود، نحفظ الفيديو الجديد في الأصلي
494
+ with open(original_path, "wb") as buffer:
495
+ shutil.copyfileobj(video.file, buffer)
496
+
497
+ # ننسخ للـ temp عشان المعالجة
498
+ temp_path = os.path.join(TEMP_DIR, f"{task_id}_temp_{video.filename}")
499
+ shutil.copy2(original_path, temp_path)
500
+ used_existing_original = False
501
 
502
+ # Extract audio using FFmpeg if available, otherwise use MoviePy
503
+ audio_filename = f"audio_{task_id}.{output_format.lower()}"
504
+ audio_path = os.path.join(AUDIO_DIR, audio_filename)
505
 
506
+ # Use FFmpeg from imageio-ffmpeg (أسرع بكثير)
507
+ ffmpeg_exe = imageio_ffmpeg.get_ffmpeg_exe()
 
 
 
 
 
 
 
 
 
 
 
508
 
509
+ # FFmpeg command optimized for speed
510
+ cmd = [
511
+ ffmpeg_exe, '-i', temp_path,
512
+ '-vn', # لا فيديو
513
+ '-acodec', 'copy' if output_format.lower() == 'aac' else 'libmp3lame',
514
+ '-y', # overwrite output file
515
+ audio_path
516
+ ]
517
 
518
+ # Run FFmpeg
519
+ result = subprocess.run(cmd, capture_output=True, text=True)
 
520
 
521
+ if result.returncode != 0:
522
+ raise ValueError(f"FFmpeg error: {result.stderr}")
523
+
524
+ # نمسح الملف الملف المؤقت، مش الأصلي
525
+ if temp_path and TEMP_DIR in temp_path:
526
+ safe_remove(temp_path)
527
+
528
+ # دايماً نرجع JSON مع الـ filename عشان نقدر نستخدمه لاحقاً
529
+ audio_filename = os.path.basename(audio_path)
530
+
531
+ # Use build_file_url for consistent URL construction
532
+ return {
533
+ "status": "success",
534
+ "task_id": task_id,
535
+ "message": "Audio extracted successfully (optimized)",
536
+ "audio_files": [
537
+ {
538
+ "filename": audio_filename,
539
+ "url": build_file_url(request, audio_filename, "audio")
540
+ }
541
+ ],
542
+ "format": output_format.lower(),
543
+ "method": "ffmpeg_direct",
544
+ "file_path": audio_path # مهم عشان نعرف نستخدمه لاحقاً
545
+ }
546
 
547
  except ValueError as e:
548
  return JSONResponse(
 
550
  content={"error": str(e)}
551
  )
552
  except Exception as e:
553
+ # نمسح بس الملف المؤقت، مش الأصلي
554
+ if 'temp_path' in locals() and 'temp_path' in locals() and temp_path and not used_existing_original and TEMP_DIR in temp_path:
555
+ safe_remove(temp_path)
556
+ if isinstance(e, HTTPException): raise e
557
  return JSONResponse(
558
  status_code=500,
559
  content={"error": f"Audio extraction failed: {str(e)}"}
560
  )
561
+
562
+ @router.post("/process-saved", tags=["Video"])
563
+ async def process_saved_video(
564
+ request: Request,
565
+ background_tasks: BackgroundTasks,
566
+ original_filename: str = Form(..., description="اسم الفيديو الأصلي المحفوظ"),
567
+ format: VideoFormat = Form(VideoFormat.FILM, description="Select the output video format"),
568
+ timestamps_json: str = Form(None, description='JSON list of timestamps. If not provided, will process entire video.'),
569
+ export_audio: bool = Form(False, description="Export separate audio files for each clip"),
570
+ webhook_url: str = Form(None, description="Optional URL to receive processing results via POST.")
571
+ ):
572
+ """
573
+ معالجة فيديو محفوظ أصلي بدون رفعه تاني - توفير وقت ومساحة
574
+ """
575
+ task_id = uuid.uuid4().hex[:8]
576
+ host_url = str(request.base_url)
577
+
578
+ try:
579
+ # ندور على الفيديو الأصلي
580
+ original_path = os.path.join(ORIGINALS_DIR, original_filename)
581
+
582
+ if not os.path.exists(original_path):
583
+ return JSONResponse(
584
+ status_code=404,
585
+ content={"error": f"Original video not found: {original_filename}"}
586
+ )
587
+
588
+ # ننسخ للـ temp عشان المعالجة
589
+ temp_path = os.path.join(TEMP_DIR, f"{task_id}_temp_{original_filename}")
590
+ shutil.copy2(original_path, temp_path)
591
+
592
+ # Parse timestamps
593
+ timestamps = []
594
+ if timestamps_json:
595
+ try:
596
+ timestamps_data = json.loads(timestamps_json)
597
+ if not isinstance(timestamps_data, list):
598
+ raise ValueError("Timestamps must be a list")
599
+ timestamps = [Timestamp(**ts) for ts in timestamps_data]
600
+ except Exception as e:
601
+ raise HTTPException(status_code=400, detail=f"Invalid timestamps format: {str(e)}")
602
+
603
+ # If no timestamps, process entire video
604
+ if not timestamps:
605
+ from moviepy import VideoFileClip
606
+ with VideoFileClip(temp_path) as clip:
607
+ timestamps = [Timestamp(start_time=0, end_time=clip.duration)]
608
+
609
+ # Prepare dimensions (بدون موسيقى خلفية)
610
+ dims = Dimensions(width=0, height=0, audio_path=None, video_volume=1.0, music_volume=0, loop_music=False)
611
+
612
+ # Dispatch
613
+ if webhook_url:
614
+ background_tasks.add_task(
615
+ background_processing,
616
+ task_id, temp_path, timestamps, format, dims, export_audio, None, webhook_url, host_url
617
+ )
618
+ return {"status": "processing", "task_id": task_id, "message": "Processing started using saved video. Results will be sent to webhook.", "original_used": original_filename}
619
+ else:
620
+ # Synchronous execution
621
+ return background_processing(
622
+ task_id, temp_path, timestamps, format, dims, export_audio, None, None, host_url
623
+ )
624
+
625
+ except Exception as e:
626
+ if 'temp_path' in locals():
627
+ safe_remove(temp_path)
628
+ if isinstance(e, HTTPException): raise e
629
+ return JSONResponse(status_code=500, content={"error": str(e)})
630
+
631
+ @router.get("/list-saved", tags=["Video"])
632
+ async def list_saved_videos():
633
+ """
634
+ عرض قائمة الفيديوهات الأصلية المحفوظة
635
+ """
636
+ try:
637
+ saved_videos = []
638
+ if os.path.exists(ORIGINALS_DIR):
639
+ for filename in os.listdir(ORIGINALS_DIR):
640
+ if filename.endswith(('.mp4', '.avi', '.mov', '.mkv')):
641
+ file_path = os.path.join(ORIGINALS_DIR, filename)
642
+ file_stats = os.stat(file_path)
643
+ saved_videos.append({
644
+ "filename": filename,
645
+ "size_mb": round(file_stats.st_size / (1024*1024), 2),
646
+ "created": file_stats.st_ctime,
647
+ "full_path": file_path
648
+ })
649
+
650
+ return {
651
+ "status": "success",
652
+ "count": len(saved_videos),
653
+ "videos": saved_videos,
654
+ "originals_directory": ORIGINALS_DIR
655
+ }
656
+ except Exception as e:
657
+ return JSONResponse(status_code=500, content={"error": f"Failed to list videos: {str(e)}"})
schemas.py CHANGED
@@ -14,6 +14,12 @@ class VideoFormat(str, Enum):
14
  ORIGINAL = "Original (No Resize)"
15
  CUSTOM = "Custom"
16
 
 
 
 
 
 
 
17
  class ProcessingType(str, Enum):
18
  TRANSCRIPT = "transcript"
19
  CROP = "crop"
@@ -40,6 +46,9 @@ class Dimensions(BaseModel):
40
  video_volume: float = 1.0
41
  music_volume: float = 0.2
42
  loop_music: bool = True
 
 
 
43
 
44
  class ClipRequest(BaseModel):
45
  video_url: Optional[str] = None
 
14
  ORIGINAL = "Original (No Resize)"
15
  CUSTOM = "Custom"
16
 
17
+ class LayoutType(str, Enum):
18
+ CROP = "crop" # Current behavior: Crop to fill
19
+ CINEMATIC = "cinematic" # Blurred background + Original video
20
+ SPLIT_SCREEN = "split" # Two videos stacked
21
+ FIT = "fit" # Fit within canvas with black bars
22
+
23
  class ProcessingType(str, Enum):
24
  TRANSCRIPT = "transcript"
25
  CROP = "crop"
 
46
  video_volume: float = 1.0
47
  music_volume: float = 0.2
48
  loop_music: bool = True
49
+ layout_type: LayoutType = LayoutType.CROP
50
+ blur_intensity: int = 20 # For cinematic layout
51
+ background_video_url: Optional[str] = None # For split screen
52
 
53
  class ClipRequest(BaseModel):
54
  video_url: Optional[str] = None
video_processor.py CHANGED
@@ -1,8 +1,8 @@
1
  import os
2
  import uuid
3
  from concurrent.futures import ThreadPoolExecutor
4
- from moviepy import VideoFileClip, vfx
5
- from schemas import VideoFormat, Dimensions
6
  from hybrid_processor import process_video_hybrid
7
 
8
  def process_video_clips(video_path: str, timestamps, output_format: VideoFormat, custom_dims: Dimensions = None, export_audio: bool = False, use_parallel: bool = True, use_ffmpeg_optimization: bool = True):
@@ -145,12 +145,16 @@ def process_video_clips(video_path: str, timestamps, output_format: VideoFormat,
145
  # Apply formatting
146
  if output_format == VideoFormat.ORIGINAL:
147
  pass # Skip resizing, keep original dimensions
148
- elif output_format != VideoFormat.CUSTOM:
149
- target_ratio = ratios.get(output_format, 16/9)
150
- subclip = format_clip(subclip, target_ratio)
151
- elif custom_dims:
152
- if hasattr(custom_dims, 'width') and hasattr(custom_dims, 'height'):
153
- subclip = subclip.resized(width=custom_dims.width, height=custom_dims.height)
 
 
 
 
154
 
155
  # Write file - optimized settings for speed
156
  subclip.write_videofile(
@@ -177,6 +181,54 @@ def process_video_clips(video_path: str, timestamps, output_format: VideoFormat,
177
  print(f"Error processing video: {str(e)}")
178
  raise e
179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  def format_clip(clip, target_ratio):
181
  """
182
  Crops and resizes a clip to a target aspect ratio.
@@ -206,14 +258,29 @@ def format_clip(clip, target_ratio):
206
  def safe_remove(path: str, max_retries: int = 3):
207
  """Attempt to remove a file with retries for Windows file locking."""
208
  import time
 
 
 
 
 
 
209
  for i in range(max_retries):
210
  try:
211
- if os.path.exists(path):
212
- os.remove(path)
213
- return
214
- except PermissionError:
215
- time.sleep(1) # Wait for handles to clear
216
- print(f"Warning: Could not delete {path} after {max_retries} attempts.")
 
 
 
 
 
 
 
 
 
217
 
218
  def create_zip_archive(file_paths: list, output_filename: str):
219
  """
@@ -303,12 +370,16 @@ def process_single_clip(ts, video_path, output_format, custom_dims, export_audio
303
  # Apply formatting
304
  if output_format == VideoFormat.ORIGINAL:
305
  pass # Skip resizing, keep original dimensions
306
- elif output_format != VideoFormat.CUSTOM:
307
- target_ratio = ratios.get(output_format, 16/9)
308
- subclip = format_clip(subclip, target_ratio)
309
- elif custom_dims:
310
- if hasattr(custom_dims, 'width') and hasattr(custom_dims, 'height'):
311
- subclip = subclip.resized(width=custom_dims.width, height=custom_dims.height)
 
 
 
 
312
 
313
  # Write file - optimized settings for speed
314
  subclip.write_videofile(
 
1
  import os
2
  import uuid
3
  from concurrent.futures import ThreadPoolExecutor
4
+ from moviepy import VideoFileClip, vfx, CompositeVideoClip, ColorClip
5
+ from schemas import VideoFormat, Dimensions, LayoutType
6
  from hybrid_processor import process_video_hybrid
7
 
8
  def process_video_clips(video_path: str, timestamps, output_format: VideoFormat, custom_dims: Dimensions = None, export_audio: bool = False, use_parallel: bool = True, use_ffmpeg_optimization: bool = True):
 
145
  # Apply formatting
146
  if output_format == VideoFormat.ORIGINAL:
147
  pass # Skip resizing, keep original dimensions
148
+ else:
149
+ # Standardize on 1080x1920 for Shorts if format is Shorts
150
+ target_w, target_h = 1080, 1920
151
+ if output_format == VideoFormat.VIDEO:
152
+ target_w, target_h = 1920, 1080
153
+ elif output_format == VideoFormat.SQUARE:
154
+ target_w, target_h = 1080, 1080
155
+
156
+ layout = custom_dims.layout_type if custom_dims else LayoutType.CROP
157
+ subclip = apply_layout_factory(subclip, layout, target_w, target_h, custom_dims)
158
 
159
  # Write file - optimized settings for speed
160
  subclip.write_videofile(
 
181
  print(f"Error processing video: {str(e)}")
182
  raise e
183
 
184
+ def apply_layout_factory(clip, layout_type, target_w, target_h, config=None):
185
+ """
186
+ Factory to apply different video layouts.
187
+ """
188
+ if layout_type == LayoutType.CINEMATIC:
189
+ return apply_cinematic_blur(clip, target_w, target_h, config.blur_intensity if config else 20)
190
+ elif layout_type == LayoutType.FIT:
191
+ return apply_fit_layout(clip, target_w, target_h)
192
+ else:
193
+ # Default to Crop/Fill
194
+ target_ratio = target_w / target_h
195
+ formatted = format_clip(clip, target_ratio)
196
+ return formatted.resized(width=target_w, height=target_h)
197
+
198
+ def apply_cinematic_blur(clip, target_w, target_h, blur_intensity=20):
199
+ """
200
+ Creates a cinematic blurred background with the original video on top.
201
+ """
202
+ # 1. Background: Scale to fill and blur
203
+ # First, crop/resize to fill target dimensions
204
+ bg = format_clip(clip, target_w / target_h)
205
+ bg = bg.resized(width=target_w, height=target_h)
206
+ bg = bg.with_effects([vfx.GaussianBlur(blur_intensity)])
207
+
208
+ # 2. Foreground: Resize original to fit width while keeping aspect ratio
209
+ # If original is 16:9, it will have bars top/bottom over the blur
210
+ fg = clip.resized(width=target_w)
211
+
212
+ # Center the foreground on the background
213
+ final_clip = CompositeVideoClip([
214
+ bg,
215
+ fg.with_position("center")
216
+ ], size=(target_w, target_h))
217
+
218
+ return final_clip
219
+
220
+ def apply_fit_layout(clip, target_w, target_h):
221
+ """
222
+ Fits the video inside the target dimensions with black bars (Letterboxing).
223
+ """
224
+ # Resize to fit within target dims
225
+ fg = clip.resized(width=target_w) if (clip.w / clip.h) > (target_w / target_h) else clip.resized(height=target_h)
226
+
227
+ # Create black background
228
+ bg = ColorClip(size=(target_w, target_h), color=(0,0,0), duration=clip.duration)
229
+
230
+ return CompositeVideoClip([bg, fg.with_position("center")], size=(target_w, target_h))
231
+
232
  def format_clip(clip, target_ratio):
233
  """
234
  Crops and resizes a clip to a target aspect ratio.
 
258
  def safe_remove(path: str, max_retries: int = 3):
259
  """Attempt to remove a file with retries for Windows file locking."""
260
  import time
261
+ import os
262
+
263
+ if not os.path.exists(path):
264
+ print(f"[SAFE_REMOVE] File does not exist: {path}")
265
+ return
266
+
267
  for i in range(max_retries):
268
  try:
269
+ os.remove(path)
270
+ print(f"[SAFE_REMOVE] Successfully deleted: {path}")
271
+ return True
272
+ except PermissionError as e:
273
+ print(f"[SAFE_REMOVE] Permission error on attempt {i+1}: {e}")
274
+ if i < max_retries - 1:
275
+ time.sleep(1) # Wait for handles to clear
276
+ else:
277
+ print(f"[SAFE_REMOVE] Warning: Could not delete {path} after {max_retries} attempts.")
278
+ return False
279
+ except Exception as e:
280
+ print(f"[SAFE_REMOVE] Error deleting {path}: {e}")
281
+ return False
282
+
283
+ return False
284
 
285
  def create_zip_archive(file_paths: list, output_filename: str):
286
  """
 
370
  # Apply formatting
371
  if output_format == VideoFormat.ORIGINAL:
372
  pass # Skip resizing, keep original dimensions
373
+ else:
374
+ # Standardize on 1080x1920 for Shorts if format is Shorts
375
+ target_w, target_h = 1080, 1920
376
+ if output_format == VideoFormat.VIDEO:
377
+ target_w, target_h = 1920, 1080
378
+ elif output_format == VideoFormat.SQUARE:
379
+ target_w, target_h = 1080, 1080
380
+
381
+ layout = custom_dims.layout_type if custom_dims else LayoutType.CROP
382
+ subclip = apply_layout_factory(subclip, layout, target_w, target_h, custom_dims)
383
 
384
  # Write file - optimized settings for speed
385
  subclip.write_videofile(
video_storage/version_registry.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
-
3
-
4
- }