diff --git a/core/__pycache__/video_engine.cpython-311.pyc b/core/__pycache__/video_engine.cpython-311.pyc index 037abd2..aeaba09 100644 Binary files a/core/__pycache__/video_engine.cpython-311.pyc and b/core/__pycache__/video_engine.cpython-311.pyc differ diff --git a/core/video_engine.py b/core/video_engine.py index fe5a756..96d7cca 100644 --- a/core/video_engine.py +++ b/core/video_engine.py @@ -11,11 +11,26 @@ TEMP_AUDIO_DIR = OUTPUTS_DIR / "temp_audio" AUDIO_DIR = OUTPUTS_DIR / "audio" BACKGROUNDS_DIR = WORKSPACE_ROOT / "backgrounds" +# Droid Sans Fallback often supports Arabic/CJK +FONT_ARABIC = "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf" +# Fallback for English text +FONT_SANS = "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf" + def escape_ffmpeg_text(text): - # Escape for ffmpeg drawtext filter - # Single quotes need special handling: ' becomes '\'' - # Also colons and other special chars might need escaping depending on context - return text.replace("'", "'\\''").replace(":", "\\:") + if not text: + return "" + # Use ord() to check for backslash to avoid syntax errors with literals + result = "" + for char in text: + if ord(char) == 92: # Backslash + result += "\\" + elif char == "'": + result += "'\\''" + elif char == ":": + result += ":" + else: + result += char + return result def ensure_bg_video(bg_path): """Ensures a background video exists and is not empty. Creates a placeholder if needed.""" @@ -33,6 +48,13 @@ def ensure_bg_video(bg_path): '-pix_fmt', 'yuv420p', str(bg_path) ], check=True) +def get_audio_duration(file_path): + result = subprocess.run([ + 'ffprobe', '-v', 'error', '-show_entries', 'format=duration', + '-of', 'default=noprint_wrappers=1:nokey=1', str(file_path) + ], capture_output=True, text=True) + return float(result.stdout.strip()) + def generate_video(task_id): from core.models import VideoTask task = VideoTask.objects.get(id=task_id) @@ -56,7 +78,7 @@ def generate_video(task_id): all_ayahs = data['ayahs'] selected_ayahs = [a for a in all_ayahs if task.verse_start <= a['numberInSurah'] <= task.verse_end] - # Download audio files + # Download audio files and collect text for i, ayah in enumerate(selected_ayahs): audio_url = ayah['audio'] audio_path = TEMP_AUDIO_DIR / f"task_{task.id}_ayah_{i}.mp3" @@ -68,35 +90,64 @@ def generate_video(task_id): audio_files.append(str(audio_path)) verses_data.append({ 'text': ayah['text'], - 'audio': str(audio_path) + 'duration': get_audio_duration(audio_path) }) # 2. Combine Audio combined_audio = AUDIO_DIR / f"task_{task.id}_full.mp3" - # We use a filter_complex to concat audio as it's more reliable than concat protocol for different mp3s - filter_complex = "".join([f"[{i}:a]" for i in range(len(audio_files))]) + f"concat=n={len(audio_files)}:v=0:a=1[a]" + filter_complex_audio = "".join([f"[{i}:a]" for i in range(len(audio_files))]) + f"concat=n={len(audio_files)}:v=0:a=1[a]" cmd = ['ffmpeg', '-y'] for f in audio_files: cmd.extend(['-i', f]) - cmd.extend(['-filter_complex', filter_complex, '-map', '[a]', str(combined_audio)]) + cmd.extend(['-filter_complex', filter_complex_audio, '-map', '[a]', str(combined_audio)]) subprocess.run(cmd, check=True) + total_duration = get_audio_duration(combined_audio) + # 3. Generate Video with FFmpeg bg_video = BACKGROUNDS_DIR / (task.background_video or "nature.mp4") ensure_bg_video(bg_video) output_video = FINAL_VIDEO_DIR / f"reels_{task.id}.mp4" - # Escape text for drawtext - escaped_surah = escape_ffmpeg_text(task.surah_name) + # Build drawtext filters + drawtext_filters = [] - # Simple FFmpeg command: loop background, add audio, trim to audio length - # Using a vertical aspect ratio 720x1280 (common for Reels) + # 1. Surah Name at top + escaped_surah = escape_ffmpeg_text(task.surah_name) + drawtext_filters.append( + f"drawtext=text='{escaped_surah}':fontfile={FONT_SANS}:fontcolor={task.text_color}:fontsize=48:x=(w-text_w)/2:y=100:shadowcolor=black:shadowx=2:shadowy=2" + ) + + # 2. Verse text in middle (sequential) + current_time = 0 + for verse in verses_data: + escaped_text = escape_ffmpeg_text(verse['text']) + start = current_time + end = current_time + verse['duration'] + + font_to_use = FONT_ARABIC if os.path.exists(FONT_ARABIC) else FONT_SANS + + drawtext_filters.append( + f"drawtext=text='{escaped_text}':fontfile={font_to_use}:fontcolor={task.text_color}:fontsize=42:x=(w-text_w)/2:y=(h-text_h)/2:shadowcolor=black:shadowx=2:shadowy=2:enable='between(t,{start},{end})'" + ) + current_time += verse['duration'] + + vf_chain = [ + "scale=720:1280:force_original_aspect_ratio=increase", + "crop=720:1280", + *drawtext_filters + ] + vf_string = ",".join(vf_chain) + subprocess.run([ - 'ffmpeg', '-y', '-stream_loop', '-1', '-i', str(bg_video), - '-i', str(combined_audio), '-shortest', '-map', '0:v:0', '-map', '1:a:0', + 'ffmpeg', '-y', + '-stream_loop', '-1', '-i', str(bg_video), + '-i', str(combined_audio), + '-t', str(total_duration), + '-map', '0:v:0', '-map', '1:a:0', '-pix_fmt', 'yuv420p', - '-vf', f"scale=720:1280:force_original_aspect_ratio=increase,crop=720:1280,drawtext=text='{escaped_surah}':fontcolor={task.text_color}:fontsize=64:x=(w-text_w)/2:y=(h-text_h)/2:shadowcolor=black:shadowx=2:shadowy=2", + '-vf', vf_string, str(output_video) ], check=True) diff --git a/outputs/audio/task_6_full.mp3 b/outputs/audio/task_6_full.mp3 new file mode 100644 index 0000000..f16db53 Binary files /dev/null and b/outputs/audio/task_6_full.mp3 differ diff --git a/outputs/audio/task_7_full.mp3 b/outputs/audio/task_7_full.mp3 new file mode 100644 index 0000000..40c2cb1 Binary files /dev/null and b/outputs/audio/task_7_full.mp3 differ diff --git a/outputs/final_video/reels_6.mp4 b/outputs/final_video/reels_6.mp4 new file mode 100644 index 0000000..ab79c84 Binary files /dev/null and b/outputs/final_video/reels_6.mp4 differ diff --git a/outputs/final_video/reels_7.mp4 b/outputs/final_video/reels_7.mp4 new file mode 100644 index 0000000..fbefeb1 Binary files /dev/null and b/outputs/final_video/reels_7.mp4 differ diff --git a/outputs/temp_audio/task_6_ayah_0.mp3 b/outputs/temp_audio/task_6_ayah_0.mp3 new file mode 100644 index 0000000..0e7d95a Binary files /dev/null and b/outputs/temp_audio/task_6_ayah_0.mp3 differ diff --git a/outputs/temp_audio/task_6_ayah_1.mp3 b/outputs/temp_audio/task_6_ayah_1.mp3 new file mode 100644 index 0000000..b16789d Binary files /dev/null and b/outputs/temp_audio/task_6_ayah_1.mp3 differ diff --git a/outputs/temp_audio/task_6_ayah_2.mp3 b/outputs/temp_audio/task_6_ayah_2.mp3 new file mode 100644 index 0000000..faf50f9 Binary files /dev/null and b/outputs/temp_audio/task_6_ayah_2.mp3 differ diff --git a/outputs/temp_audio/task_6_ayah_3.mp3 b/outputs/temp_audio/task_6_ayah_3.mp3 new file mode 100644 index 0000000..dd88584 Binary files /dev/null and b/outputs/temp_audio/task_6_ayah_3.mp3 differ diff --git a/outputs/temp_audio/task_6_ayah_4.mp3 b/outputs/temp_audio/task_6_ayah_4.mp3 new file mode 100644 index 0000000..4e6f3e5 Binary files /dev/null and b/outputs/temp_audio/task_6_ayah_4.mp3 differ diff --git a/outputs/temp_audio/task_6_ayah_5.mp3 b/outputs/temp_audio/task_6_ayah_5.mp3 new file mode 100644 index 0000000..7fb7fdc Binary files /dev/null and b/outputs/temp_audio/task_6_ayah_5.mp3 differ diff --git a/outputs/temp_audio/task_6_ayah_6.mp3 b/outputs/temp_audio/task_6_ayah_6.mp3 new file mode 100644 index 0000000..f8fb167 Binary files /dev/null and b/outputs/temp_audio/task_6_ayah_6.mp3 differ diff --git a/outputs/temp_audio/task_7_ayah_0.mp3 b/outputs/temp_audio/task_7_ayah_0.mp3 new file mode 100644 index 0000000..60bcee5 Binary files /dev/null and b/outputs/temp_audio/task_7_ayah_0.mp3 differ