38702-vm/core/video_engine.py
Flatlogic Bot 2b93a1bd15 4
2026-02-23 12:32:16 +00:00

164 lines
5.9 KiB
Python

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