quran gen

This commit is contained in:
Flatlogic Bot 2026-02-23 08:34:41 +00:00
parent 78dd5a9e55
commit 0c65e736c6
21 changed files with 450 additions and 166 deletions

0
backgrounds/nature.mp4 Normal file
View File

0
backgrounds/space.mp4 Normal file
View File

Binary file not shown.

View File

@ -1,3 +1,8 @@
from django.contrib import admin
from .models import VideoTask
# Register your models here.
@admin.register(VideoTask)
class VideoTaskAdmin(admin.ModelAdmin):
list_display = ('surah_name', 'verse_start', 'verse_end', 'reciter_name', 'status', 'created_at')
list_filter = ('status', 'created_at')
search_fields = ('surah_name', 'reciter_name')

View File

@ -0,0 +1,33 @@
# Generated by Django 5.2.7 on 2026-02-23 08:03
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='VideoTask',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('surah_number', models.IntegerField()),
('surah_name', models.CharField(max_length=255)),
('reciter_identifier', models.CharField(max_length=255)),
('reciter_name', models.CharField(max_length=255)),
('verse_start', models.IntegerField()),
('verse_end', models.IntegerField()),
('background_video', models.CharField(max_length=255)),
('text_color', models.CharField(default='#FFFFFF', max_length=20)),
('status', models.CharField(choices=[('pending', 'Pending'), ('processing', 'Processing'), ('completed', 'Completed'), ('failed', 'Failed')], default='pending', max_length=20)),
('output_path', models.CharField(blank=True, max_length=500, null=True)),
('error_message', models.TextField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
),
]

View File

@ -1,3 +1,26 @@
from django.db import models
# Create your models here.
class VideoTask(models.Model):
STATUS_CHOICES = [
('pending', 'Pending'),
('processing', 'Processing'),
('completed', 'Completed'),
('failed', 'Failed'),
]
surah_number = models.IntegerField()
surah_name = models.CharField(max_length=255)
reciter_identifier = models.CharField(max_length=255)
reciter_name = models.CharField(max_length=255)
verse_start = models.IntegerField()
verse_end = models.IntegerField()
background_video = models.CharField(max_length=255)
text_color = models.CharField(max_length=20, default='#FFFFFF')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
output_path = models.CharField(max_length=500, blank=True, null=True)
error_message = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.surah_name} ({self.verse_start}-{self.verse_end}) - {self.status}"

View File

@ -1,145 +1,214 @@
{% extends "base.html" %}
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ project_name }} - Quran Reels Generator</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Amiri&family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style>
:root {
--primary-emerald: #064E3B;
--accent-gold: #D4AF37;
--soft-cream: #FCF8F1;
--text-dark: #1F2937;
}
body {
background-color: var(--soft-cream);
font-family: 'Inter', sans-serif;
color: var(--text-dark);
}
h1, h2, h3, .arabic {
font-family: 'Amiri', serif;
}
.navbar {
background-color: var(--primary-emerald);
padding: 1rem 0;
border-bottom: 3px solid var(--accent-gold);
}
.navbar-brand {
color: var(--accent-gold) !important;
font-weight: 700;
font-size: 1.5rem;
}
.hero {
background: linear-gradient(135deg, var(--primary-emerald) 0%, #065F46 100%);
color: white;
padding: 4rem 0;
margin-bottom: 3rem;
text-align: center;
}
.card {
border: none;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
background: white;
margin-bottom: 2rem;
}
.card-header {
background-color: white;
border-bottom: 1px solid #F3F4F6;
font-weight: 600;
padding: 1.25rem;
color: var(--primary-emerald);
}
.btn-primary {
background-color: var(--primary-emerald);
border: none;
padding: 0.75rem 2rem;
border-radius: 8px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover {
background-color: #065F46;
transform: translateY(-1px);
}
.status-badge {
padding: 0.4rem 0.8rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
}
.status-pending { background: #FEF3C7; color: #92400E; }
.status-processing { background: #DBEAFE; color: #1E40AF; }
.status-completed { background: #D1FAE5; color: #065F46; }
.status-failed { background: #FEE2E2; color: #991B1B; }
.form-label { font-weight: 600; margin-bottom: 0.5rem; }
.form-select, .form-control {
border-radius: 8px;
border: 1px solid #D1D5DB;
padding: 0.6rem;
}
</style>
</head>
<body>
{% block title %}{{ project_name }}{% endblock %}
{% block head %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% {
background-position: 0% 0%;
}
100% {
background-position: 100% 100%;
}
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2.5rem 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
}
h1 {
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
font-weight: 700;
margin: 0 0 1.2rem;
letter-spacing: -0.02em;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
opacity: 0.92;
}
.loader {
margin: 1.5rem auto;
width: 56px;
height: 56px;
border: 4px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.runtime code {
background: rgba(0, 0, 0, 0.25);
padding: 0.15rem 0.45rem;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
footer {
position: absolute;
bottom: 1rem;
width: 100%;
text-align: center;
font-size: 0.85rem;
opacity: 0.75;
}
</style>
{% endblock %}
{% block content %}
<main>
<div class="card">
<h1>Analyzing your requirements and generating your app…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
<nav class="navbar navbar-dark">
<div class="container">
<a class="navbar-brand" href="#">
{{ project_name }}
</a>
</div>
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
<p class="runtime">
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
</p>
</div>
</main>
<footer>
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
</footer>
{% endblock %}
</nav>
<div class="hero">
<div class="container">
<h1>Create Beautiful Quran Reels</h1>
<p class="lead">Select verses, pick a reciter, and generate professional videos in seconds.</p>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-header">Video Settings</div>
<div class="card-body">
<form action="{% url 'generate_video' %}" method="POST" id="genForm">
{% csrf_token %}
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Select Surah</label>
<select name="surah" id="surahSelect" class="form-select" required>
<option value="">Choose...</option>
{% for s in surahs %}
<option value="{{ s.number }}">{{ s.number }}. {{ s.englishName }} ({{ s.name }})</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label">Reciter</label>
<select name="reciter" class="form-select" required>
{% for r in reciters %}
<option value="{{ r.identifier }}" {% if r.identifier == "ar.alafasy" %}selected{% endif %}>{{ r.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Start Verse</label>
<input type="number" name="verse_start" id="verseStart" class="form-control" value="1" min="1" required>
</div>
<div class="col-md-6">
<label class="form-label">End Verse</label>
<input type="number" name="verse_end" id="verseEnd" class="form-control" value="5" min="1" required>
<small class="text-muted" id="maxAyahs">Max: -</small>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label">Background Video</label>
<select name="background" class="form-select" required>
{% for b in backgrounds %}
<option value="{{ b }}">{{ b }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label">Text Color</label>
<input type="color" name="text_color" class="form-control form-control-color w-100" value="#FFFFFF">
</div>
</div>
<button type="submit" class="btn btn-primary w-100">Generate Video</button>
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header">Recent Generations</div>
<div class="card-body p-0">
<ul class="list-group list-group-flush">
{% for task in tasks %}
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-start">
<div>
<strong class="d-block">{{ task.surah_name }}</strong>
<small class="text-muted">Ayahs {{ task.verse_start }}-{{ task.verse_end }}</small>
</div>
<span class="status-badge status-{{ task.status }}">
{{ task.status|title }}
</span>
</div>
{% if task.status == 'completed' %}
<a href="{{ task.output_path }}" class="btn btn-sm btn-outline-success mt-2 w-100" download>Download</a>
{% elif task.status == 'failed' %}
<p class="text-danger x-small mt-1 mb-0">{{ task.error_message|truncatechars:50 }}</p>
{% endif %}
</li>
{% empty %}
<li class="list-group-item text-center text-muted py-4">No tasks yet</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
</div>
<script>
document.getElementById('surahSelect').addEventListener('change', function() {
const surahNum = this.value;
if (!surahNum) return;
fetch(`/surah-details/${surahNum}/`)
.then(response => response.json())
.then(data => {
const max = data.numberOfAyahs;
document.getElementById('maxAyahs').textContent = `Max Ayahs: ${max}`;
document.getElementById('verseStart').max = max;
document.getElementById('verseEnd').max = max;
});
});
</script>
</body>
</html>

View File

@ -1,7 +1,8 @@
from django.urls import path
from .views import home
from .views import home, generate_video_view, get_surah_details
urlpatterns = [
path("", home, name="home"),
]
path("generate/", generate_video_view, name="generate_video"),
path("surah-details/<int:surah_number>/", get_surah_details, name="surah_details"),
]

92
core/video_engine.py Normal file
View File

@ -0,0 +1,92 @@
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"
def generate_video(task_id):
from core.models import VideoTask
task = VideoTask.objects.get(id=task_id)
task.status = 'processing'
task.save()
try:
# 1. Fetch Verses Text and Audio
# For simplicity in this slice, we fetch from api.alquran.cloud
# We need both the text (for ImageMagick) and the audio (for FFmpeg)
verses_data = []
audio_files = []
# We'll use the 'ar.alafasy' or user-selected reciter
# Example: https://api.alquran.cloud/v1/surah/1/ar.alafasy
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 prepare 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'],
'audio': str(audio_path)
})
# 2. Combine Audio
combined_audio = AUDIO_DIR / f"task_{task.id}_full.mp3"
# ffmpeg -i "concat:file1.mp3|file2.mp3" -acodec copy output.mp3
concat_str = "|".join(audio_files)
subprocess.run([
'ffmpeg', '-y', '-i', f'concat:{concat_str}', '-acodec', 'libmp3lame', str(combined_audio)
], check=True)
# 3. Generate Video with FFmpeg
# We use a background video and overlay the audio
bg_video = BACKGROUNDS_DIR / task.background_video
if not bg_video.exists():
# Fallback to a placeholder or first available background
available = list(BACKGROUNDS_DIR.glob("*.mp4"))
if available:
bg_video = available[0]
else:
raise Exception("No background video found in backgrounds/ folder")
output_video = FINAL_VIDEO_DIR / f"reels_{task.id}.mp4"
# Simple FFmpeg command: loop background, add audio, trim to audio length
# For text overlay, we'd ideally use drawtext or ImageMagick.
# Here we do a basic version:
subprocess.run([
'ffmpeg', '-y', '-stream_loop', '-1', '-i', str(bg_video),
'-i', str(combined_audio), '-shortest', '-map', '0:v:0', '-map', '1:a:0',
'-pix_fmt', 'yuv420p', '-vf', f"drawtext=text='{task.surah_name}':fontcolor={task.text_color}:fontsize=48:x=(w-text_w)/2:y=(h-text_h)/2",
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()
raise e

View File

@ -1,25 +1,86 @@
import os
import platform
from django import get_version as django_version
from django.shortcuts import render
from django.utils import timezone
import requests
from django.shortcuts import render, redirect, get_object_or_404
from django.http import JsonResponse
from .models import VideoTask
from .video_engine import generate_video
import threading
def home(request):
"""Render the landing screen with loader and environment details."""
host_name = request.get_host().lower()
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
now = timezone.now()
# Fetch surahs and reciters for the form
surahs = []
reciters = []
try:
surah_resp = requests.get("https://api.alquran.cloud/v1/surah", timeout=5)
if surah_resp.status_code == 200:
surahs = surah_resp.json()['data']
reciter_resp = requests.get("https://api.alquran.cloud/v1/edition?format=audio&language=ar&type=versebyverse", timeout=5)
if reciter_resp.status_code == 200:
reciters = reciter_resp.json()['data']
except:
pass
backgrounds = []
if os.path.exists('backgrounds'):
backgrounds = [f for f in os.listdir('backgrounds') if f.endswith(('.mp4', '.mov'))]
if not backgrounds:
backgrounds = ["nature.mp4"]
tasks = VideoTask.objects.all().order_by('-created_at')[:10]
context = {
"project_name": "New Style",
"agent_brand": agent_brand,
"django_version": django_version(),
"python_version": platform.python_version(),
"current_time": now,
"host_name": host_name,
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
"surahs": surahs,
"reciters": reciters,
"backgrounds": backgrounds,
"tasks": tasks,
"project_name": "Quran Reels Gen",
}
return render(request, "core/index.html", context)
def generate_video_view(request):
if request.method == "POST":
surah_number = request.POST.get('surah')
reciter = request.POST.get('reciter')
verse_start = request.POST.get('verse_start')
verse_end = request.POST.get('verse_end')
background = request.POST.get('background')
text_color = request.POST.get('text_color', '#FFFFFF')
# Get surah name
surah_name = "Unknown"
try:
surah_resp = requests.get(f"https://api.alquran.cloud/v1/surah/{surah_number}", timeout=5)
if surah_resp.status_code == 200:
surah_name = surah_resp.json()['data']['englishName']
except:
pass
task = VideoTask.objects.create(
surah_number=surah_number,
surah_name=surah_name,
reciter_identifier=reciter,
reciter_name=reciter,
verse_start=verse_start,
verse_end=verse_end,
background_video=background,
text_color=text_color,
status='pending'
)
# Run generation in background thread
thread = threading.Thread(target=generate_video, args=(task.id,))
thread.start()
return redirect('home')
return redirect('home')
def get_surah_details(request, surah_number):
try:
resp = requests.get(f"https://api.alquran.cloud/v1/surah/{surah_number}", timeout=5)
if resp.status_code == 200:
return JsonResponse(resp.json()['data'])
except:
pass
return JsonResponse({'error': 'Failed to fetch'}, status=400)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.