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"
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)

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.