aliSaac510 commited on
Commit
caaa62c
·
1 Parent(s): 4f83369

add emproved proccess parameter

Browse files
hybrid_processor.py CHANGED
@@ -7,14 +7,19 @@ from typing import List, Optional, Tuple
7
  from ffmpeg_utils import extract_clip_ffmpeg, get_video_info_ffmpeg
8
 
9
  def should_use_ffmpeg_processing(timestamps, custom_dims, export_audio, bg_music, output_format) -> bool:
10
- from schemas import LayoutType
11
 
12
  # If background music is requested, need MoviePy
13
  if bg_music:
14
  return False
15
 
 
 
 
 
 
16
  # If complex layout is requested, need MoviePy
17
- if custom_dims and hasattr(custom_dims, 'layout_type') and custom_dims.layout_type != LayoutType.CROP:
18
  return False
19
 
20
  # If complex custom dimensions, need MoviePy
 
7
  from ffmpeg_utils import extract_clip_ffmpeg, get_video_info_ffmpeg
8
 
9
  def should_use_ffmpeg_processing(timestamps, custom_dims, export_audio, bg_music, output_format) -> bool:
10
+ from schemas import LayoutType, ShortsStyle
11
 
12
  # If background music is requested, need MoviePy
13
  if bg_music:
14
  return False
15
 
16
+ # If a specific ShortsStyle is requested that requires MoviePy
17
+ if output_format and isinstance(output_format, ShortsStyle):
18
+ if output_format in [ShortsStyle.CINEMATIC, ShortsStyle.SPLIT_SCREEN, ShortsStyle.FIT_BARS]:
19
+ return False
20
+
21
  # If complex layout is requested, need MoviePy
22
+ if custom_dims and hasattr(custom_dims, 'layout_type') and custom_dims.layout_type != LayoutType.CROP_CENTER:
23
  return False
24
 
25
  # If complex custom dimensions, need MoviePy
requirements.txt CHANGED
@@ -6,4 +6,5 @@ python-multipart
6
  yt-dlp
7
  pydantic
8
  imageio-ffmpeg
9
- requests
 
 
6
  yt-dlp
7
  pydantic
8
  imageio-ffmpeg
9
+ requests
10
+ scipy
routers/video.py CHANGED
@@ -9,7 +9,7 @@ import requests
9
  import subprocess
10
  import imageio_ffmpeg
11
  from typing import List, Optional
12
- from schemas import VideoFormat, Timestamp, ClipRequest, Dimensions
13
  from video_processor import process_video_clips, safe_remove, create_zip_archive
14
  router = APIRouter()
15
 
@@ -40,7 +40,8 @@ def background_processing(
40
  task_id: str,
41
  temp_path: str,
42
  timestamps: list,
43
- format: VideoFormat,
 
44
  dims: Dimensions,
45
  export_audio: bool,
46
  audio_path: str = None,
@@ -50,15 +51,22 @@ def background_processing(
50
  ):
51
  """
52
  Executes the video processing logic.
53
- If webhook_url is provided, sends the result via POST.
54
  """
55
  response_data = {}
56
  files_to_cleanup = [temp_path]
57
  if audio_path: files_to_cleanup.append(audio_path)
58
 
59
  try:
60
- # Process clips with FFmpeg optimization enabled
61
- clip_paths, audio_clip_paths = process_video_clips(temp_path, timestamps, format, custom_dims=dims, export_audio=export_audio, use_ffmpeg_optimization=True)
 
 
 
 
 
 
 
 
62
 
63
  # Prepare result
64
  clips_filenames = [os.path.basename(p) for p in clip_paths]
@@ -210,19 +218,20 @@ async def process_video(
210
  request: Request,
211
  background_tasks: BackgroundTasks,
212
  video: UploadFile = File(...),
213
- format: VideoFormat = Form(VideoFormat.FILM, description="Select the output video format"),
214
- background_music: UploadFile = File(None, description="Upload an audio file for background music (files only, no URLs)"),
 
215
  video_volume: float = Form(1.0, description="Volume of original video (0.0 to 1.0+)"),
216
  music_volume: float = Form(0.2, description="Volume of background music (0.0 to 1.0+)"),
217
  loop_music: bool = Form(True, description="Loop background music if shorter than video"),
218
  timestamps_json: str = Form(
219
  None,
220
  alias="timestamps",
221
- description='Optional JSON list of timestamps. If not provided, will process entire video. Recommended duration for weak devices: < 60 seconds per clip.'
222
  ),
223
- export_audio: bool = Form(False, description="Export separate audio files for each clip (original audio). Returns a ZIP if True."),
224
- webhook_url: str = Form(None, description="Optional URL to receive processing results via POST."),
225
- return_files: bool = Form(False, description="Return processed files directly instead of URLs (for n8n automation)")
226
  ):
227
  task_id = uuid.uuid4().hex[:8]
228
  temp_path = None
@@ -271,8 +280,8 @@ async def process_video(
271
 
272
  # Prepare dimensions
273
  dims = Dimensions(
274
- width=0,
275
- height=0,
276
  audio_path=audio_path,
277
  video_volume=video_volume,
278
  music_volume=music_volume,
@@ -283,13 +292,13 @@ async def process_video(
283
  if webhook_url:
284
  background_tasks.add_task(
285
  background_processing,
286
- task_id, temp_file_for_processing, timestamps, format, dims, export_audio, audio_path, webhook_url, str(request.base_url)
287
  )
288
- return {"status": "processing", "task_id": task_id, "message": "Processing started. Results will be sent to webhook."}
289
  else:
290
  # Synchronous execution
291
  return background_processing(
292
- task_id, temp_file_for_processing, timestamps, format, dims, export_audio, audio_path, None, str(request.base_url)
293
  )
294
 
295
  except Exception as e:
@@ -370,60 +379,6 @@ async def list_saved_files():
370
  }
371
  except Exception as e:
372
  return JSONResponse(status_code=500, content={"error": f"Failed to list files: {str(e)}"})
373
- try:
374
- # Get disk usage
375
- total, used, free = shutil.disk_usage("/")
376
-
377
- # Count files in all subfolders
378
- total_files_count = 0
379
- total_files_size = 0
380
- folder_stats = {}
381
-
382
- folders = {
383
- "originals": ORIGINALS_DIR,
384
- "processed": PROCESSED_DIR,
385
- "audio": AUDIO_DIR,
386
- "bgmusic": BG_MUSIC_DIR,
387
- "temp": TEMP_DIR
388
- }
389
-
390
- for name, path in folders.items():
391
- count = 0
392
- size = 0
393
- if os.path.exists(path):
394
- for filename in os.listdir(path):
395
- f_path = os.path.join(path, filename)
396
- if os.path.isfile(f_path):
397
- count += 1
398
- size += os.path.getsize(f_path)
399
-
400
- folder_stats[name] = {
401
- "count": count,
402
- "size_mb": round(size / (1024**2), 2)
403
- }
404
- total_files_count += count
405
- total_files_size += size
406
-
407
- return {
408
- "status": "healthy",
409
- "disk_usage": {
410
- "total_gb": round(total / (1024**3), 2),
411
- "used_gb": round(used / (1024**3), 2),
412
- "free_gb": round(free / (1024**3), 2),
413
- "usage_percent": round((used / total) * 100, 1)
414
- },
415
- "temp_files": {
416
- "total_count": total_files_count,
417
- "total_size_mb": round(total_files_size / (1024**2), 2),
418
- "by_folder": folder_stats
419
- },
420
- "base_directory": BASE_DIR
421
- }
422
- except Exception as e:
423
- return JSONResponse(
424
- status_code=500,
425
- content={"error": f"Failed to get status: {str(e)}"}
426
- )
427
 
428
  @router.api_route("/clear", methods=["GET", "POST"], tags=["Video"])
429
  async def clear_temp_files(
@@ -632,10 +587,11 @@ async def process_saved_video(
632
  request: Request,
633
  background_tasks: BackgroundTasks,
634
  original_filename: str = Form(..., description="اسم الفيديو الأصلي المحفوظ"),
635
- format: VideoFormat = Form(VideoFormat.FILM, description="Select the output video format"),
636
- timestamps_json: str = Form(None, description='JSON list of timestamps. If not provided, will process entire video.'),
637
- export_audio: bool = Form(False, description="Export separate audio files for each clip"),
638
- webhook_url: str = Form(None, description="Optional URL to receive processing results via POST.")
 
639
  ):
640
  """
641
  معالجة فيديو محفوظ أصلي بدون رفعه تاني - توفير وقت ومساحة
@@ -674,20 +630,27 @@ async def process_saved_video(
674
  with VideoFileClip(temp_path) as clip:
675
  timestamps = [Timestamp(start_time=0, end_time=clip.duration)]
676
 
677
- # Prepare dimensions (بدون موسيقى خلفية)
678
- dims = Dimensions(width=0, height=0, audio_path=None, video_volume=1.0, music_volume=0, loop_music=False)
 
 
 
 
 
 
 
679
 
680
  # Dispatch
681
  if webhook_url:
682
  background_tasks.add_task(
683
  background_processing,
684
- task_id, temp_path, timestamps, format, dims, export_audio, None, webhook_url, host_url
685
  )
686
- return {"status": "processing", "task_id": task_id, "message": "Processing started using saved video. Results will be sent to webhook.", "original_used": original_filename}
687
  else:
688
  # Synchronous execution
689
  return background_processing(
690
- task_id, temp_path, timestamps, format, dims, export_audio, None, None, host_url
691
  )
692
 
693
  except Exception as e:
@@ -696,30 +659,4 @@ async def process_saved_video(
696
  if isinstance(e, HTTPException): raise e
697
  return JSONResponse(status_code=500, content={"error": str(e)})
698
 
699
- @router.get("/list-saved", tags=["Video"])
700
- async def list_saved_videos():
701
- """
702
- عرض قائمة الفيديوهات الأصلية المحفوظة
703
- """
704
- try:
705
- saved_videos = []
706
- if os.path.exists(ORIGINALS_DIR):
707
- for filename in os.listdir(ORIGINALS_DIR):
708
- if filename.endswith(('.mp4', '.avi', '.mov', '.mkv')):
709
- file_path = os.path.join(ORIGINALS_DIR, filename)
710
- file_stats = os.stat(file_path)
711
- saved_videos.append({
712
- "filename": filename,
713
- "size_mb": round(file_stats.st_size / (1024*1024), 2),
714
- "created": file_stats.st_ctime,
715
- "full_path": file_path
716
- })
717
-
718
- return {
719
- "status": "success",
720
- "count": len(saved_videos),
721
- "videos": saved_videos,
722
- "originals_directory": ORIGINALS_DIR
723
- }
724
- except Exception as e:
725
- return JSONResponse(status_code=500, content={"error": f"Failed to list videos: {str(e)}"})
 
9
  import subprocess
10
  import imageio_ffmpeg
11
  from typing import List, Optional
12
+ from schemas import AspectRatio, ShortsStyle, Timestamp, ClipRequest, Dimensions
13
  from video_processor import process_video_clips, safe_remove, create_zip_archive
14
  router = APIRouter()
15
 
 
40
  task_id: str,
41
  temp_path: str,
42
  timestamps: list,
43
+ aspect_ratio: AspectRatio,
44
+ style: ShortsStyle,
45
  dims: Dimensions,
46
  export_audio: bool,
47
  audio_path: str = None,
 
51
  ):
52
  """
53
  Executes the video processing logic.
 
54
  """
55
  response_data = {}
56
  files_to_cleanup = [temp_path]
57
  if audio_path: files_to_cleanup.append(audio_path)
58
 
59
  try:
60
+ # Pass both aspect_ratio and style to the processor
61
+ clip_paths, audio_clip_paths = process_video_clips(
62
+ temp_path,
63
+ timestamps,
64
+ aspect_ratio,
65
+ style,
66
+ custom_dims=dims,
67
+ export_audio=export_audio,
68
+ use_ffmpeg_optimization=True
69
+ )
70
 
71
  # Prepare result
72
  clips_filenames = [os.path.basename(p) for p in clip_paths]
 
218
  request: Request,
219
  background_tasks: BackgroundTasks,
220
  video: UploadFile = File(...),
221
+ aspect_ratio: AspectRatio = Form(AspectRatio.RATIO_9_16, description="Select target aspect ratio"),
222
+ style: ShortsStyle = Form(ShortsStyle.CINEMATIC, description="Select the visual style"),
223
+ background_music: UploadFile = File(None, description="Upload an audio file for background music"),
224
  video_volume: float = Form(1.0, description="Volume of original video (0.0 to 1.0+)"),
225
  music_volume: float = Form(0.2, description="Volume of background music (0.0 to 1.0+)"),
226
  loop_music: bool = Form(True, description="Loop background music if shorter than video"),
227
  timestamps_json: str = Form(
228
  None,
229
  alias="timestamps",
230
+ description='Optional JSON list of timestamps.'
231
  ),
232
+ export_audio: bool = Form(False, description="Export separate audio files"),
233
+ webhook_url: str = Form(None, description="Optional URL for results"),
234
+ return_files: bool = Form(False, description="Return processed files directly")
235
  ):
236
  task_id = uuid.uuid4().hex[:8]
237
  temp_path = None
 
280
 
281
  # Prepare dimensions
282
  dims = Dimensions(
283
+ target_ratio=aspect_ratio,
284
+ style=style,
285
  audio_path=audio_path,
286
  video_volume=video_volume,
287
  music_volume=music_volume,
 
292
  if webhook_url:
293
  background_tasks.add_task(
294
  background_processing,
295
+ task_id, temp_file_for_processing, timestamps, aspect_ratio, style, dims, export_audio, audio_path, webhook_url, str(request.base_url)
296
  )
297
+ return {"status": "processing", "task_id": task_id, "message": "Processing started."}
298
  else:
299
  # Synchronous execution
300
  return background_processing(
301
+ task_id, temp_file_for_processing, timestamps, aspect_ratio, style, dims, export_audio, audio_path, None, str(request.base_url)
302
  )
303
 
304
  except Exception as e:
 
379
  }
380
  except Exception as e:
381
  return JSONResponse(status_code=500, content={"error": f"Failed to list files: {str(e)}"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
 
383
  @router.api_route("/clear", methods=["GET", "POST"], tags=["Video"])
384
  async def clear_temp_files(
 
587
  request: Request,
588
  background_tasks: BackgroundTasks,
589
  original_filename: str = Form(..., description="اسم الفيديو الأصلي المحفوظ"),
590
+ aspect_ratio: AspectRatio = Form(AspectRatio.RATIO_9_16, description="Select target aspect ratio"),
591
+ style: ShortsStyle = Form(ShortsStyle.CINEMATIC, description="Select visual style"),
592
+ timestamps_json: str = Form(None, description='JSON list of timestamps.'),
593
+ export_audio: bool = Form(False, description="Export separate audio files"),
594
+ webhook_url: str = Form(None, description="Optional URL for results")
595
  ):
596
  """
597
  معالجة فيديو محفوظ أصلي بدون رفعه تاني - توفير وقت ومساحة
 
630
  with VideoFileClip(temp_path) as clip:
631
  timestamps = [Timestamp(start_time=0, end_time=clip.duration)]
632
 
633
+ # Prepare dimensions
634
+ dims = Dimensions(
635
+ target_ratio=aspect_ratio,
636
+ style=style,
637
+ audio_path=None,
638
+ video_volume=1.0,
639
+ music_volume=0,
640
+ loop_music=False
641
+ )
642
 
643
  # Dispatch
644
  if webhook_url:
645
  background_tasks.add_task(
646
  background_processing,
647
+ task_id, temp_path, timestamps, aspect_ratio, style, dims, export_audio, None, webhook_url, host_url
648
  )
649
+ return {"status": "processing", "task_id": task_id, "message": "Processing started using saved video.", "original_used": original_filename}
650
  else:
651
  # Synchronous execution
652
  return background_processing(
653
+ task_id, temp_path, timestamps, aspect_ratio, style, dims, export_audio, None, None, host_url
654
  )
655
 
656
  except Exception as e:
 
659
  if isinstance(e, HTTPException): raise e
660
  return JSONResponse(status_code=500, content={"error": str(e)})
661
 
662
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
schemas.py CHANGED
@@ -5,20 +5,25 @@ from datetime import datetime
5
 
6
  # ======== التعدادات الأساسية ========
7
 
8
- class VideoFormat(str, Enum):
9
- SHORTS = "Shorts (9:16)"
10
- VIDEO = "Video (16:9)"
11
- SQUARE = "Square (1:1)"
12
- CINEMA = "Cinema (21:9)"
13
- FILM = "Film (2.35:1)"
14
- ORIGINAL = "Original (No Resize)"
15
- CUSTOM = "Custom"
16
-
17
  class LayoutType(str, Enum):
18
- CROP = "crop" # Current behavior: Crop to fill
19
- CINEMATIC = "cinematic" # Blurred background + Original video
20
- SPLIT_SCREEN = "split" # Two videos stacked
21
- FIT = "fit" # Fit within canvas with black bars
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  class ProcessingType(str, Enum):
24
  TRANSCRIPT = "transcript"
@@ -40,19 +45,24 @@ class Timestamp(BaseModel):
40
  end_time: float
41
 
42
  class Dimensions(BaseModel):
43
- width: int
44
- height: int
 
 
 
 
 
 
45
  audio_path: Optional[str] = None
46
  video_volume: float = 1.0
47
  music_volume: float = 0.2
48
  loop_music: bool = True
49
- layout_type: LayoutType = LayoutType.CROP
50
- blur_intensity: int = 20 # For cinematic layout
51
- background_video_url: Optional[str] = None # For split screen
52
 
53
  class ClipRequest(BaseModel):
54
  video_url: Optional[str] = None
55
- format: VideoFormat = VideoFormat.FILM
 
56
  custom_dimensions: Optional[Dimensions] = None
57
  timestamps: Optional[List[Timestamp]] = None
58
 
 
5
 
6
  # ======== التعدادات الأساسية ========
7
 
 
 
 
 
 
 
 
 
 
8
  class LayoutType(str, Enum):
9
+ CINEMATIC_BLUR = "cinematic_blur"
10
+ SPLIT_SCREEN = "split_screen"
11
+ CROP_CENTER = "crop_center"
12
+ FIT_CENTER = "fit_center"
13
+
14
+ class AspectRatio(str, Enum):
15
+ RATIO_9_16 = "9:16" # Shorts, TikTok, Reels
16
+ RATIO_1_1 = "1:1" # Instagram Square
17
+ RATIO_16_9 = "16:9" # Standard YouTube
18
+ RATIO_4_5 = "4:5" # Facebook Portrait
19
+ ORIGINAL = "original" # Keep source ratio
20
+
21
+ class ShortsStyle(str, Enum):
22
+ CINEMATIC = "cinematic" # Blurred background
23
+ CROP_FILL = "crop_fill" # Crop to fill target ratio
24
+ SPLIT_SCREEN = "split" # Split screen
25
+ FIT_BARS = "fit_bars" # Letterboxing
26
+ ORIGINAL = "original" # No changes
27
 
28
  class ProcessingType(str, Enum):
29
  TRANSCRIPT = "transcript"
 
45
  end_time: float
46
 
47
  class Dimensions(BaseModel):
48
+ width: Optional[int] = 0
49
+ height: Optional[int] = 0
50
+ target_ratio: AspectRatio = AspectRatio.RATIO_9_16
51
+ style: ShortsStyle = ShortsStyle.CINEMATIC
52
+ video_scale: float = 1.0
53
+ vertical_shift: float = 0.0
54
+ blur_intensity: int = 20
55
+ background_image_url: Optional[str] = None
56
  audio_path: Optional[str] = None
57
  video_volume: float = 1.0
58
  music_volume: float = 0.2
59
  loop_music: bool = True
60
+ background_video_url: Optional[str] = None
 
 
61
 
62
  class ClipRequest(BaseModel):
63
  video_url: Optional[str] = None
64
+ aspect_ratio: AspectRatio = AspectRatio.RATIO_9_16
65
+ style: ShortsStyle = ShortsStyle.CINEMATIC
66
  custom_dimensions: Optional[Dimensions] = None
67
  timestamps: Optional[List[Timestamp]] = None
68
 
temp-audio-b8d8aab9.mp3 ADDED
File without changes
video_processor.py CHANGED
@@ -1,13 +1,27 @@
1
  import os
2
  import uuid
3
  from concurrent.futures import ThreadPoolExecutor
4
- from moviepy import VideoFileClip, vfx, CompositeVideoClip, ColorClip
5
- from schemas import VideoFormat, Dimensions, LayoutType
 
 
6
  from hybrid_processor import process_video_hybrid
7
 
8
- def process_video_clips(video_path: str, timestamps, output_format: VideoFormat, custom_dims: Dimensions = None, export_audio: bool = False, use_parallel: bool = True, use_ffmpeg_optimization: bool = True):
 
 
 
 
 
 
 
 
 
 
 
 
9
  """
10
- Processes a video file into multiple clips based on timestamps and format.
11
  If export_audio is True, also saves the original audio track of each clip.
12
  use_parallel: Enable parallel processing for better performance (default: True)
13
  use_ffmpeg_optimization: Use FFmpeg for simple operations (much faster) (default: True)
@@ -22,7 +36,7 @@ def process_video_clips(video_path: str, timestamps, output_format: VideoFormat,
22
  clip_paths, audio_paths = process_video_hybrid(
23
  video_path=video_path,
24
  timestamps=timestamps,
25
- output_format=output_format,
26
  custom_dims=custom_dims,
27
  export_audio=export_audio
28
  )
@@ -32,15 +46,6 @@ def process_video_clips(video_path: str, timestamps, output_format: VideoFormat,
32
  except Exception as e:
33
  print(f"⚠️ FFmpeg optimization failed: {e}")
34
  print("🎬 Falling back to MoviePy...")
35
-
36
- # Target aspect ratios
37
- ratios = {
38
- VideoFormat.SHORTS: 9/16,
39
- VideoFormat.VIDEO: 16/9,
40
- VideoFormat.SQUARE: 1/1,
41
- VideoFormat.CINEMA: 21/9,
42
- VideoFormat.FILM: 2.35/1
43
- }
44
 
45
  try:
46
  # Load background music if provided
@@ -62,8 +67,8 @@ def process_video_clips(video_path: str, timestamps, output_format: VideoFormat,
62
  clip_id = uuid.uuid4().hex[:8]
63
  future = executor.submit(
64
  process_single_clip,
65
- ts, video_path, output_format, custom_dims,
66
- export_audio, bg_music, ratios, clip_id
67
  )
68
  futures.append((future, clip_id))
69
 
@@ -84,6 +89,10 @@ def process_video_clips(video_path: str, timestamps, output_format: VideoFormat,
84
  from moviepy import VideoFileClip, CompositeAudioClip
85
  import moviepy.audio.fx as afx
86
 
 
 
 
 
87
  with VideoFileClip(video_path) as video:
88
  print(f"DEBUG: Video loaded. Duration: {video.duration}")
89
 
@@ -100,7 +109,7 @@ def process_video_clips(video_path: str, timestamps, output_format: VideoFormat,
100
  # Generate unique ID for this clip operation
101
  clip_id = uuid.uuid4().hex[:8]
102
  output_filename = f"clip_{clip_id}.mp4"
103
- output_path = os.path.join(os.path.dirname(video_path), output_filename)
104
  audio_output_path = None
105
 
106
  # Extract subclip and process it
@@ -136,29 +145,34 @@ def process_video_clips(video_path: str, timestamps, output_format: VideoFormat,
136
  subclip = subclip.with_audio(music_clip)
137
 
138
  # Apply formatting
139
- if output_format == VideoFormat.ORIGINAL:
140
  pass # Skip resizing, keep original dimensions
141
  else:
142
- # Standardize on 1080x1920 for Shorts if format is Shorts
143
- target_w, target_h = 1080, 1920
144
- if output_format == VideoFormat.VIDEO:
145
- target_w, target_h = 1920, 1080
146
- elif output_format == VideoFormat.SQUARE:
147
- target_w, target_h = 1080, 1080
 
 
 
 
 
148
 
149
- layout = custom_dims.layout_type if custom_dims else LayoutType.CROP
150
- subclip = apply_layout_factory(subclip, layout, target_w, target_h, custom_dims)
151
 
152
- # Write file - optimized settings for speed
153
  subclip.write_videofile(
154
  output_path,
155
  codec="libx264",
156
  audio_codec="aac",
157
- temp_audiofile=f"temp-audio-{clip_id}.m4a",
158
  remove_temp=True,
159
  fps=24,
160
  threads=4,
161
- preset="ultrafast", # Fastest encoding
162
  logger=None
163
  )
164
 
@@ -167,8 +181,8 @@ def process_video_clips(video_path: str, timestamps, output_format: VideoFormat,
167
  if export_audio:
168
  audio_paths.append(audio_output_path)
169
 
170
- if bg_music: bg_music.close()
171
- return clip_paths, audio_paths
172
 
173
  except Exception as e:
174
  print(f"Error processing video: {str(e)}")
@@ -178,28 +192,57 @@ def apply_layout_factory(clip, layout_type, target_w, target_h, config=None):
178
  """
179
  Factory to apply different video layouts.
180
  """
181
- if layout_type == LayoutType.CINEMATIC:
182
- return apply_cinematic_blur(clip, target_w, target_h, config.blur_intensity if config else 20)
183
- elif layout_type == LayoutType.FIT:
184
  return apply_fit_layout(clip, target_w, target_h)
185
- else:
186
  # Default to Crop/Fill
187
  target_ratio = target_w / target_h
188
  formatted = format_clip(clip, target_ratio)
189
  return formatted.resized(width=target_w, height=target_h)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
  def apply_cinematic_blur(clip, target_w, target_h, blur_intensity=20):
192
  """
193
  Creates a cinematic blurred background with the original video on top.
 
194
  """
 
 
 
 
 
 
195
  # 1. Background: Scale to fill and blur
196
- # First, crop/resize to fill target dimensions
197
  bg = format_clip(clip, target_w / target_h)
198
  bg = bg.resized(width=target_w, height=target_h)
199
- bg = bg.with_effects([vfx.GaussianBlur(blur_intensity)])
 
 
200
 
201
  # 2. Foreground: Resize original to fit width while keeping aspect ratio
202
- # If original is 16:9, it will have bars top/bottom over the blur
203
  fg = clip.resized(width=target_w)
204
 
205
  # Center the foreground on the background
@@ -295,7 +338,7 @@ def create_zip_archive(file_paths: list, output_filename: str):
295
 
296
  return zip_path
297
 
298
- def process_single_clip(ts, video_path, output_format, custom_dims, export_audio, bg_music, ratios, clip_id):
299
  """
300
  Process a single clip - for parallel processing.
301
  """
@@ -354,15 +397,11 @@ def process_single_clip(ts, video_path, output_format, custom_dims, export_audio
354
  subclip = subclip.with_audio(music_clip)
355
 
356
  # Apply formatting
357
- if output_format == VideoFormat.ORIGINAL:
358
  pass # Skip resizing, keep original dimensions
359
  else:
360
- # Standardize on 1080x1920 for Shorts if format is Shorts
361
  target_w, target_h = 1080, 1920
362
- if output_format == VideoFormat.VIDEO:
363
- target_w, target_h = 1920, 1080
364
- elif output_format == VideoFormat.SQUARE:
365
- target_w, target_h = 1080, 1080
366
 
367
  layout = custom_dims.layout_type if custom_dims else LayoutType.CROP
368
  subclip = apply_layout_factory(subclip, layout, target_w, target_h, custom_dims)
@@ -376,7 +415,7 @@ def process_single_clip(ts, video_path, output_format, custom_dims, export_audio
376
  remove_temp=True,
377
  fps=24,
378
  threads=4,
379
- preset="ultrafast", # Fastest encoding
380
  logger=None
381
  )
382
 
 
1
  import os
2
  import uuid
3
  from concurrent.futures import ThreadPoolExecutor
4
+ from moviepy import VideoFileClip, CompositeVideoClip, ColorClip
5
+ import numpy as np
6
+ from scipy.ndimage import gaussian_filter
7
+ from schemas import ShortsStyle, Dimensions, LayoutType, AspectRatio
8
  from hybrid_processor import process_video_hybrid
9
 
10
+ def get_canvas_dimensions(ratio: AspectRatio) -> tuple:
11
+ """Returns (width, height) for a given aspect ratio."""
12
+ if ratio == AspectRatio.RATIO_9_16:
13
+ return 1080, 1920
14
+ elif ratio == AspectRatio.RATIO_1_1:
15
+ return 1080, 1080
16
+ elif ratio == AspectRatio.RATIO_16_9:
17
+ return 1920, 1080
18
+ elif ratio == AspectRatio.RATIO_4_5:
19
+ return 1080, 1350
20
+ return None, None # For ORIGINAL
21
+
22
+ def process_video_clips(video_path: str, timestamps, aspect_ratio: AspectRatio = AspectRatio.RATIO_9_16, style: ShortsStyle = ShortsStyle.CINEMATIC, custom_dims: Dimensions = None, export_audio: bool = False, use_parallel: bool = True, use_ffmpeg_optimization: bool = True):
23
  """
24
+ Processes a video file into multiple clips based on timestamps and style.
25
  If export_audio is True, also saves the original audio track of each clip.
26
  use_parallel: Enable parallel processing for better performance (default: True)
27
  use_ffmpeg_optimization: Use FFmpeg for simple operations (much faster) (default: True)
 
36
  clip_paths, audio_paths = process_video_hybrid(
37
  video_path=video_path,
38
  timestamps=timestamps,
39
+ output_format=style,
40
  custom_dims=custom_dims,
41
  export_audio=export_audio
42
  )
 
46
  except Exception as e:
47
  print(f"⚠️ FFmpeg optimization failed: {e}")
48
  print("🎬 Falling back to MoviePy...")
 
 
 
 
 
 
 
 
 
49
 
50
  try:
51
  # Load background music if provided
 
67
  clip_id = uuid.uuid4().hex[:8]
68
  future = executor.submit(
69
  process_single_clip,
70
+ ts, video_path, aspect_ratio, style, custom_dims,
71
+ export_audio, bg_music, clip_id
72
  )
73
  futures.append((future, clip_id))
74
 
 
89
  from moviepy import VideoFileClip, CompositeAudioClip
90
  import moviepy.audio.fx as afx
91
 
92
+ # Define processed directory
93
+ processed_dir = os.path.join("temp_videos", "processed")
94
+ os.makedirs(processed_dir, exist_ok=True)
95
+
96
  with VideoFileClip(video_path) as video:
97
  print(f"DEBUG: Video loaded. Duration: {video.duration}")
98
 
 
109
  # Generate unique ID for this clip operation
110
  clip_id = uuid.uuid4().hex[:8]
111
  output_filename = f"clip_{clip_id}.mp4"
112
+ output_path = os.path.join(processed_dir, output_filename)
113
  audio_output_path = None
114
 
115
  # Extract subclip and process it
 
145
  subclip = subclip.with_audio(music_clip)
146
 
147
  # Apply formatting
148
+ if style == ShortsStyle.ORIGINAL:
149
  pass # Skip resizing, keep original dimensions
150
  else:
151
+ # Map style to layout
152
+ layout_map = {
153
+ ShortsStyle.CINEMATIC: LayoutType.CINEMATIC_BLUR,
154
+ ShortsStyle.CROP_FILL: LayoutType.CROP_CENTER,
155
+ ShortsStyle.FIT_BARS: LayoutType.FIT_CENTER,
156
+ ShortsStyle.SPLIT_SCREEN: LayoutType.SPLIT_SCREEN
157
+ }
158
+ layout = layout_map.get(style, LayoutType.CROP_CENTER)
159
+
160
+ # Get target dimensions
161
+ target_w, target_h = get_canvas_dimensions(aspect_ratio)
162
 
163
+ if target_w and target_h:
164
+ subclip = apply_layout_factory(subclip, layout, target_w, target_h, custom_dims)
165
 
166
+ # Write file - optimized settings for compatibility and speed
167
  subclip.write_videofile(
168
  output_path,
169
  codec="libx264",
170
  audio_codec="aac",
171
+ temp_audiofile=f"temp-audio-{clip_id}.m4a", # m4a is native for aac
172
  remove_temp=True,
173
  fps=24,
174
  threads=4,
175
+ preset="superfast", # Faster than medium, safer than ultrafast
176
  logger=None
177
  )
178
 
 
181
  if export_audio:
182
  audio_paths.append(audio_output_path)
183
 
184
+ if bg_music: bg_music.close()
185
+ return clip_paths, audio_paths
186
 
187
  except Exception as e:
188
  print(f"Error processing video: {str(e)}")
 
192
  """
193
  Factory to apply different video layouts.
194
  """
195
+ if layout_type == LayoutType.CINEMATIC_BLUR:
196
+ return apply_cinematic_blur(clip, target_w, target_h, config.blur_intensity if config and hasattr(config, 'blur_intensity') else 20)
197
+ elif layout_type == LayoutType.FIT_CENTER:
198
  return apply_fit_layout(clip, target_w, target_h)
199
+ elif layout_type == LayoutType.CROP_CENTER:
200
  # Default to Crop/Fill
201
  target_ratio = target_w / target_h
202
  formatted = format_clip(clip, target_ratio)
203
  return formatted.resized(width=target_w, height=target_h)
204
+ elif layout_type == LayoutType.SPLIT_SCREEN:
205
+ return apply_split_screen(clip, target_w, target_h)
206
+ else:
207
+ # Fallback
208
+ return clip.resized(width=target_w) if clip.w > target_w else clip
209
+
210
+ def apply_split_screen(clip, target_w, target_h):
211
+ """
212
+ Splits the screen into two halves (top and bottom) with the same video.
213
+ """
214
+ half_h = target_h // 2
215
+
216
+ # Top half: resize to fill
217
+ top = format_clip(clip, target_w / half_h).resized(width=target_w, height=half_h)
218
+
219
+ # Bottom half: same video
220
+ bottom = format_clip(clip, target_w / half_h).resized(width=target_w, height=half_h)
221
+
222
+ return CompositeVideoClip([
223
+ top.with_position(("center", "top")),
224
+ bottom.with_position(("center", "bottom"))
225
+ ], size=(target_w, target_h))
226
 
227
  def apply_cinematic_blur(clip, target_w, target_h, blur_intensity=20):
228
  """
229
  Creates a cinematic blurred background with the original video on top.
230
+ Uses custom fl_image filter for maximum compatibility.
231
  """
232
+ def blur_filter(image):
233
+ # Apply gaussian filter to the RGB channels (axis 0 and 1)
234
+ # Sigma is the blur intensity.
235
+ # We use (blur_intensity, blur_intensity, 0) to not blur the color channel axis.
236
+ return gaussian_filter(image, sigma=(blur_intensity, blur_intensity, 0))
237
+
238
  # 1. Background: Scale to fill and blur
 
239
  bg = format_clip(clip, target_w / target_h)
240
  bg = bg.resized(width=target_w, height=target_h)
241
+
242
+ # Apply our custom blur filter
243
+ bg = bg.image_transform(blur_filter)
244
 
245
  # 2. Foreground: Resize original to fit width while keeping aspect ratio
 
246
  fg = clip.resized(width=target_w)
247
 
248
  # Center the foreground on the background
 
338
 
339
  return zip_path
340
 
341
+ def process_single_clip(ts, video_path, style, custom_dims, export_audio, bg_music, clip_id):
342
  """
343
  Process a single clip - for parallel processing.
344
  """
 
397
  subclip = subclip.with_audio(music_clip)
398
 
399
  # Apply formatting
400
+ if style == ShortsStyle.ORIGINAL:
401
  pass # Skip resizing, keep original dimensions
402
  else:
403
+ # Standardize on 1080x1920 for Shorts styles
404
  target_w, target_h = 1080, 1920
 
 
 
 
405
 
406
  layout = custom_dims.layout_type if custom_dims else LayoutType.CROP
407
  subclip = apply_layout_factory(subclip, layout, target_w, target_h, custom_dims)
 
415
  remove_temp=True,
416
  fps=24,
417
  threads=4,
418
+ preset="superfast", # Safer than ultrafast
419
  logger=None
420
  )
421