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