4
This commit is contained in:
parent
cc9fdb2fe8
commit
2b93a1bd15
Binary file not shown.
@ -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 = []
|
||||||
|
|
||||||
# Simple FFmpeg command: loop background, add audio, trim to audio length
|
# 1. Surah Name at top
|
||||||
# Using a vertical aspect ratio 720x1280 (common for Reels)
|
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([
|
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)
|
||||||
|
|
||||||
|
|||||||
BIN
outputs/audio/task_6_full.mp3
Normal file
BIN
outputs/audio/task_6_full.mp3
Normal file
Binary file not shown.
BIN
outputs/audio/task_7_full.mp3
Normal file
BIN
outputs/audio/task_7_full.mp3
Normal file
Binary file not shown.
BIN
outputs/final_video/reels_6.mp4
Normal file
BIN
outputs/final_video/reels_6.mp4
Normal file
Binary file not shown.
BIN
outputs/final_video/reels_7.mp4
Normal file
BIN
outputs/final_video/reels_7.mp4
Normal file
Binary file not shown.
BIN
outputs/temp_audio/task_6_ayah_0.mp3
Normal file
BIN
outputs/temp_audio/task_6_ayah_0.mp3
Normal file
Binary file not shown.
BIN
outputs/temp_audio/task_6_ayah_1.mp3
Normal file
BIN
outputs/temp_audio/task_6_ayah_1.mp3
Normal file
Binary file not shown.
BIN
outputs/temp_audio/task_6_ayah_2.mp3
Normal file
BIN
outputs/temp_audio/task_6_ayah_2.mp3
Normal file
Binary file not shown.
BIN
outputs/temp_audio/task_6_ayah_3.mp3
Normal file
BIN
outputs/temp_audio/task_6_ayah_3.mp3
Normal file
Binary file not shown.
BIN
outputs/temp_audio/task_6_ayah_4.mp3
Normal file
BIN
outputs/temp_audio/task_6_ayah_4.mp3
Normal file
Binary file not shown.
BIN
outputs/temp_audio/task_6_ayah_5.mp3
Normal file
BIN
outputs/temp_audio/task_6_ayah_5.mp3
Normal file
Binary file not shown.
BIN
outputs/temp_audio/task_6_ayah_6.mp3
Normal file
BIN
outputs/temp_audio/task_6_ayah_6.mp3
Normal file
Binary file not shown.
BIN
outputs/temp_audio/task_7_ayah_0.mp3
Normal file
BIN
outputs/temp_audio/task_7_ayah_0.mp3
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user