| """ |
| Video Renderer |
| المحرك المشترك بين كل التمبلتس |
| بياخد التمبلت + البيانات → بيعمل الفيديو |
| """ |
|
|
| import os |
| import subprocess |
| import tempfile |
| import shutil |
| from PIL import Image |
| from templates.base import RenderRequest |
|
|
|
|
| def download_font(): |
| if not os.path.exists('/tmp/arabic.ttf'): |
| print('📥 تحميل الخط العربي...') |
| result = subprocess.run([ |
| 'wget', '-q', '-O', '/tmp/arabic.ttf', |
| 'https://github.com/googlefonts/noto-fonts/raw/main/hinted/ttf/NotoNaskhArabic/NotoNaskhArabic-Bold.ttf' |
| ], capture_output=True) |
| if result.returncode == 0: |
| print('✅ الخط جاهز') |
|
|
|
|
| def render_video(template, req: RenderRequest) -> str: |
| """ |
| بياخد التمبلت + الـ request |
| بيرجع مسار الفيديو النهائي |
| """ |
| download_font() |
|
|
| W, H = req.width, req.height |
| fps = req.fps |
| total = req.duration * fps |
| tmp = tempfile.mkdtemp() |
|
|
| |
| product_img = None |
| if req.image_path and os.path.exists(req.image_path): |
| product_img = Image.open(req.image_path).convert('RGBA') |
|
|
| logo_img = None |
|
|
| |
| print(f'🎬 [{template.NAME}] بيرسم {total} frame...') |
| for i in range(total): |
| t = i / fps |
| frame = template.make_frame(t, req, product_img, logo_img) |
| frame.save(os.path.join(tmp, f'frame_{i:05d}.png')) |
| if i % fps == 0: |
| print(f' {i//fps}/{req.duration} ثانية') |
|
|
| |
| video_raw = os.path.join(tmp, 'raw.mp4') |
| print('🎞️ FFmpeg...') |
| subprocess.run([ |
| 'ffmpeg', '-y', |
| '-framerate', str(fps), |
| '-i', os.path.join(tmp, 'frame_%05d.png'), |
| '-c:v', 'libx264', |
| '-pix_fmt', 'yuv420p', |
| '-crf', '18', |
| '-movflags', '+faststart', |
| video_raw |
| ], capture_output=True) |
|
|
| |
| final = req.output_path |
| if req.music_path and os.path.exists(req.music_path): |
| print('🎵 بيضيف الموسيقى...') |
| result = subprocess.run([ |
| 'ffmpeg', '-y', |
| '-i', video_raw, |
| '-i', req.music_path, |
| '-filter_complex', |
| f'[1:a]volume={req.music_volume},' |
| f'afade=t=in:st=0:d=1,' |
| f'afade=t=out:st=4:d=2[m];[m]apad[audio]', |
| '-map', '0:v', |
| '-map', '[audio]', |
| '-shortest', |
| '-c:v', 'copy', |
| '-c:a', 'aac', |
| '-b:a', '192k', |
| final |
| ], capture_output=True, text=True) |
|
|
| if result.returncode != 0: |
| shutil.copy(video_raw, final) |
| else: |
| shutil.copy(video_raw, final) |
|
|
| shutil.rmtree(tmp) |
| return final |
|
|