import os import subprocess import requests import json from pathlib import Path WORKSPACE_ROOT = Path(__file__).resolve().parent.parent OUTPUTS_DIR = WORKSPACE_ROOT / "outputs" FINAL_VIDEO_DIR = OUTPUTS_DIR / "final_video" 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): 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.""" if not bg_path.exists() or bg_path.stat().st_size == 0: bg_path.parent.mkdir(parents=True, exist_ok=True) # Create a 5-second 720x1280 (vertical) placeholder video color = "black" if "nature" in bg_path.name: color = "#064E3B" # Deep Emerald elif "space" in bg_path.name: color = "#1E1B4B" # Deep Indigo subprocess.run([ 'ffmpeg', '-y', '-f', 'lavfi', '-i', f'color=c={color}:s=720x1280:d=5', '-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) task.status = 'processing' task.save() try: # Ensure directories exist for d in [FINAL_VIDEO_DIR, TEMP_AUDIO_DIR, AUDIO_DIR, BACKGROUNDS_DIR]: d.mkdir(parents=True, exist_ok=True) # 1. Fetch Verses Text and Audio verses_data = [] audio_files = [] api_url = f"https://api.alquran.cloud/v1/surah/{task.surah_number}/{task.reciter_identifier}" resp = requests.get(api_url) resp.raise_for_status() data = resp.json()['data'] all_ayahs = data['ayahs'] selected_ayahs = [a for a in all_ayahs if task.verse_start <= a['numberInSurah'] <= task.verse_end] # 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" with requests.get(audio_url, stream=True) as r: r.raise_for_status() with open(audio_path, 'wb') as f: for chunk in r.iter_content(chunk_size=8192): f.write(chunk) audio_files.append(str(audio_path)) verses_data.append({ 'text': ayah['text'], 'duration': get_audio_duration(audio_path) }) # 2. Combine Audio combined_audio = AUDIO_DIR / f"task_{task.id}_full.mp3" 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_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" # Build drawtext filters drawtext_filters = [] # 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), '-t', str(total_duration), '-map', '0:v:0', '-map', '1:a:0', '-pix_fmt', 'yuv420p', '-vf', vf_string, str(output_video) ], check=True) task.status = 'completed' task.output_path = f"/outputs/final_video/{output_video.name}" task.save() except Exception as e: task.status = 'failed' task.error_message = str(e) task.save() import traceback print(traceback.format_exc()) raise e