Commit ·
00db48c
1
Parent(s): d1c8604
Add Cinematic Blur layout and fix clear endpoint
Browse files- TECHNICAL_DOCUMENTATION.md +0 -346
- USER_GUIDE.md +0 -720
- hybrid_processor.py +5 -3
- main.py +19 -6
- routers/files.py +82 -8
- routers/video.py +425 -125
- schemas.py +9 -0
- video_processor.py +91 -20
- video_storage/version_registry.json +0 -4
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
app = FastAPI(
|
| 8 |
title="Video Processing API",
|
| 9 |
-
description="API for video processing
|
| 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=["
|
| 24 |
-
app.include_router(files.router, prefix="/api/files", tags=["
|
| 25 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
| 11 |
"""
|
| 12 |
-
|
|
|
|
| 13 |
"""
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 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 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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"{
|
| 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 |
-
|
| 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"{
|
| 65 |
|
| 66 |
audio_urls = []
|
| 67 |
if export_audio:
|
| 68 |
-
audio_urls = [f"{
|
| 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 |
-
|
|
|
|
|
|
|
| 98 |
|
| 99 |
-
@router.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 138 |
-
|
| 139 |
-
|
|
|
|
| 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
|
| 147 |
-
timestamps = [Timestamp(start_time=0, end_time=
|
| 148 |
|
| 149 |
-
# Handle Background Music
|
| 150 |
if background_music:
|
| 151 |
-
audio_path = os.path.join(
|
| 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,
|
| 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,
|
| 188 |
)
|
| 189 |
|
| 190 |
except Exception as e:
|
| 191 |
-
|
| 192 |
-
if
|
|
|
|
|
|
|
|
|
|
| 193 |
if isinstance(e, HTTPException): raise e
|
| 194 |
return JSONResponse(status_code=500, content={"error": str(e)})
|
| 195 |
|
| 196 |
-
@router.get("/status", tags=["
|
| 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
|
| 209 |
-
|
| 210 |
-
|
|
|
|
| 211 |
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
| 229 |
-
"
|
|
|
|
| 230 |
},
|
| 231 |
-
"
|
| 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.
|
| 240 |
-
async def clear_temp_files(
|
|
|
|
|
|
|
|
|
|
| 241 |
"""
|
| 242 |
-
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
| 244 |
"""
|
| 245 |
try:
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
|
| 261 |
-
|
| 262 |
-
|
| 263 |
|
| 264 |
return {
|
| 265 |
-
"status": "success",
|
| 266 |
-
"
|
| 267 |
-
"
|
| 268 |
-
"
|
|
|
|
|
|
|
| 269 |
}
|
|
|
|
| 270 |
except Exception as e:
|
| 271 |
return JSONResponse(
|
| 272 |
-
status_code=500,
|
| 273 |
-
content={"error": f"
|
| 274 |
)
|
| 275 |
|
| 276 |
-
@router.post("/extract-audio", tags=["
|
| 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
|
| 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 |
-
#
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
shutil.copyfileobj(video.file, buffer)
|
| 312 |
|
| 313 |
-
|
| 314 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
|
| 316 |
-
#
|
| 317 |
-
|
|
|
|
| 318 |
|
| 319 |
-
#
|
| 320 |
-
|
| 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 |
-
#
|
| 334 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
|
| 336 |
-
#
|
| 337 |
-
|
| 338 |
-
safe_remove(audio_path)
|
| 339 |
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 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 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 212 |
-
|
| 213 |
-
return
|
| 214 |
-
except PermissionError:
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|