This commit is contained in:
Flatlogic Bot 2026-02-23 12:32:16 +00:00
parent cc9fdb2fe8
commit 2b93a1bd15
14 changed files with 67 additions and 16 deletions

View File

@ -11,11 +11,26 @@ TEMP_AUDIO_DIR = OUTPUTS_DIR / "temp_audio"
AUDIO_DIR = OUTPUTS_DIR / "audio" AUDIO_DIR = OUTPUTS_DIR / "audio"
BACKGROUNDS_DIR = WORKSPACE_ROOT / "backgrounds" 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): def escape_ffmpeg_text(text):
# Escape for ffmpeg drawtext filter if not text:
# Single quotes need special handling: ' becomes '\'' return ""
# Also colons and other special chars might need escaping depending on context # Use ord() to check for backslash to avoid syntax errors with literals
return text.replace("'", "'\\''").replace(":", "\\:") 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): def ensure_bg_video(bg_path):
"""Ensures a background video exists and is not empty. Creates a placeholder if needed.""" """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) '-pix_fmt', 'yuv420p', str(bg_path)
], check=True) ], 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): def generate_video(task_id):
from core.models import VideoTask from core.models import VideoTask
task = VideoTask.objects.get(id=task_id) task = VideoTask.objects.get(id=task_id)
@ -56,7 +78,7 @@ def generate_video(task_id):
all_ayahs = data['ayahs'] all_ayahs = data['ayahs']
selected_ayahs = [a for a in all_ayahs if task.verse_start <= a['numberInSurah'] <= task.verse_end] 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): for i, ayah in enumerate(selected_ayahs):
audio_url = ayah['audio'] audio_url = ayah['audio']
audio_path = TEMP_AUDIO_DIR / f"task_{task.id}_ayah_{i}.mp3" 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)) audio_files.append(str(audio_path))
verses_data.append({ verses_data.append({
'text': ayah['text'], 'text': ayah['text'],
'audio': str(audio_path) 'duration': get_audio_duration(audio_path)
}) })
# 2. Combine Audio # 2. Combine Audio
combined_audio = AUDIO_DIR / f"task_{task.id}_full.mp3" 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_audio = "".join([f"[{i}:a]" for i in range(len(audio_files))]) + f"concat=n={len(audio_files)}:v=0:a=1[a]"
filter_complex = "".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'] cmd = ['ffmpeg', '-y']
for f in audio_files: for f in audio_files:
cmd.extend(['-i', f]) 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) subprocess.run(cmd, check=True)
total_duration = get_audio_duration(combined_audio)
# 3. Generate Video with FFmpeg # 3. Generate Video with FFmpeg
bg_video = BACKGROUNDS_DIR / (task.background_video or "nature.mp4") bg_video = BACKGROUNDS_DIR / (task.background_video or "nature.mp4")
ensure_bg_video(bg_video) ensure_bg_video(bg_video)
output_video = FINAL_VIDEO_DIR / f"reels_{task.id}.mp4" output_video = FINAL_VIDEO_DIR / f"reels_{task.id}.mp4"
# Escape text for drawtext # Build drawtext filters
escaped_surah = escape_ffmpeg_text(task.surah_name) 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)
# Simple FFmpeg command: loop background, add audio, trim to audio length
# Using a vertical aspect ratio 720x1280 (common for Reels)
subprocess.run([ subprocess.run([
'ffmpeg', '-y', '-stream_loop', '-1', '-i', str(bg_video), 'ffmpeg', '-y',
'-i', str(combined_audio), '-shortest', '-map', '0:v:0', '-map', '1:a:0', '-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', '-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) str(output_video)
], check=True) ], check=True)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.