Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -155,9 +155,6 @@ STATICFILES_DIRS = [
|
|||||||
BASE_DIR / 'node_modules',
|
BASE_DIR / 'node_modules',
|
||||||
]
|
]
|
||||||
|
|
||||||
MEDIA_URL = '/outputs/'
|
|
||||||
MEDIA_ROOT = BASE_DIR / 'outputs'
|
|
||||||
|
|
||||||
# Email
|
# Email
|
||||||
EMAIL_BACKEND = os.getenv(
|
EMAIL_BACKEND = os.getenv(
|
||||||
"EMAIL_BACKEND",
|
"EMAIL_BACKEND",
|
||||||
@ -182,4 +179,4 @@ if EMAIL_USE_SSL:
|
|||||||
# Default primary key field type
|
# Default primary key field type
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|||||||
@ -27,4 +27,3 @@ urlpatterns = [
|
|||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
||||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,8 +1,3 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import VideoTask
|
|
||||||
|
|
||||||
@admin.register(VideoTask)
|
# Register your models here.
|
||||||
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')
|
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
# 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)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
Binary file not shown.
@ -1,26 +1,3 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
class VideoTask(models.Model):
|
# Create your models here.
|
||||||
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}"
|
|
||||||
|
|||||||
@ -1,285 +1,145 @@
|
|||||||
{% load static %}
|
{% extends "base.html" %}
|
||||||
<!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">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
|
||||||
<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;
|
|
||||||
}
|
|
||||||
.video-preview-btn {
|
|
||||||
cursor: pointer;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
.video-preview-btn:hover {
|
|
||||||
color: var(--primary-emerald);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<nav class="navbar navbar-dark">
|
{% block title %}{{ project_name }}{% endblock %}
|
||||||
<div class="container">
|
|
||||||
<a class="navbar-brand" href="#">
|
|
||||||
{{ project_name }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="hero">
|
{% block head %}
|
||||||
<div class="container">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<h1>Create Beautiful Quran Reels</h1>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<p class="lead">Select verses, pick a reciter, and generate professional videos in seconds.</p>
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||||
</div>
|
<style>
|
||||||
</div>
|
: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);
|
||||||
|
}
|
||||||
|
|
||||||
<div class="container">
|
* {
|
||||||
<div class="row">
|
box-sizing: border-box;
|
||||||
<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">
|
body {
|
||||||
<div class="col-md-6">
|
margin: 0;
|
||||||
<label class="form-label">Background Video</label>
|
font-family: 'Inter', sans-serif;
|
||||||
<select name="background" class="form-select" required>
|
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||||
{% for b in backgrounds %}
|
color: var(--text-color);
|
||||||
<option value="{{ b }}">{{ b }}</option>
|
display: flex;
|
||||||
{% endfor %}
|
justify-content: center;
|
||||||
</select>
|
align-items: center;
|
||||||
</div>
|
min-height: 100vh;
|
||||||
<div class="col-md-6">
|
text-align: center;
|
||||||
<label class="form-label">Text Color</label>
|
overflow: hidden;
|
||||||
<input type="color" name="text_color" class="form-control form-control-color w-100" value="#FFFFFF">
|
position: relative;
|
||||||
</div>
|
}
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary w-100">Generate Video</button>
|
body::before {
|
||||||
</form>
|
content: '';
|
||||||
</div>
|
position: absolute;
|
||||||
</div>
|
inset: 0;
|
||||||
</div>
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
<div class="col-lg-4">
|
@keyframes bg-pan {
|
||||||
<div class="card">
|
0% {
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
background-position: 0% 0%;
|
||||||
<span>Recent Generations</span>
|
|
||||||
<button class="btn btn-sm btn-link p-0 text-decoration-none" onclick="location.reload()">
|
|
||||||
<i class="bi bi-arrow-clockwise"></i> Refresh
|
|
||||||
</button>
|
|
||||||
</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 mb-2">
|
|
||||||
<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' %}
|
|
||||||
<div class="btn-group w-100 mt-2" role="group">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
|
||||||
onclick="previewVideo('{{ task.output_path }}', '{{ task.surah_name }} ({{ task.verse_start }}-{{ task.verse_end }})')">
|
|
||||||
<i class="bi bi-play-fill"></i> Preview
|
|
||||||
</button>
|
|
||||||
<a href="{{ task.output_path }}" class="btn btn-sm btn-outline-success" download>
|
|
||||||
<i class="bi bi-download"></i> Download
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% elif task.status == 'failed' %}
|
|
||||||
<p class="text-danger small mt-1 mb-0"><i class="bi bi-exclamation-triangle"></i> {{ task.error_message|truncatechars:100 }}</p>
|
|
||||||
{% elif task.status == 'processing' or task.status == 'pending' %}
|
|
||||||
<div class="progress mt-2" style="height: 5px;">
|
|
||||||
<div class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" style="width: 100%"></div>
|
|
||||||
</div>
|
|
||||||
<p class="text-muted small mt-1 mb-0">Processing... Please refresh in a moment.</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>
|
|
||||||
|
|
||||||
<!-- Video Preview Modal -->
|
|
||||||
<div class="modal fade" id="videoModal" tabindex="-1" aria-labelledby="videoModalLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="videoModalLabel">Video Preview</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body p-0 bg-black">
|
|
||||||
<div class="ratio ratio-9x16">
|
|
||||||
<video id="previewPlayer" controls>
|
|
||||||
<source src="" type="video/mp4">
|
|
||||||
Your browser does not support the video tag.
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<a id="downloadBtn" href="" class="btn btn-success" download>Download Video</a>
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<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;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const videoModal = new bootstrap.Modal(document.getElementById('videoModal'));
|
|
||||||
const previewPlayer = document.getElementById('previewPlayer');
|
|
||||||
const downloadBtn = document.getElementById('downloadBtn');
|
|
||||||
const modalTitle = document.getElementById('videoModalLabel');
|
|
||||||
|
|
||||||
function previewVideo(url, title) {
|
|
||||||
previewPlayer.src = url;
|
|
||||||
downloadBtn.href = url;
|
|
||||||
modalTitle.textContent = "Preview: " + title;
|
|
||||||
videoModal.show();
|
|
||||||
previewPlayer.play();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop video when modal is closed
|
100% {
|
||||||
document.getElementById('videoModal').addEventListener('hidden.bs.modal', function () {
|
background-position: 100% 100%;
|
||||||
previewPlayer.pause();
|
}
|
||||||
previewPlayer.src = "";
|
}
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
main {
|
||||||
</html>
|
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>
|
||||||
|
</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 %}
|
||||||
@ -1,8 +1,7 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from .views import home, generate_video_view, get_surah_details
|
|
||||||
|
from .views import home
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", home, name="home"),
|
path("", home, name="home"),
|
||||||
path("generate/", generate_video_view, name="generate_video"),
|
]
|
||||||
path("surah-details/<int:surah_number>/", get_surah_details, name="surah_details"),
|
|
||||||
]
|
|
||||||
|
|||||||
@ -1,164 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,86 +1,25 @@
|
|||||||
import os
|
import os
|
||||||
import requests
|
import platform
|
||||||
from django.shortcuts import render, redirect, get_object_or_404
|
|
||||||
from django.http import JsonResponse
|
from django import get_version as django_version
|
||||||
from .models import VideoTask
|
from django.shortcuts import render
|
||||||
from .video_engine import generate_video
|
from django.utils import timezone
|
||||||
import threading
|
|
||||||
|
|
||||||
def home(request):
|
def home(request):
|
||||||
# Fetch surahs and reciters for the form
|
"""Render the landing screen with loader and environment details."""
|
||||||
surahs = []
|
host_name = request.get_host().lower()
|
||||||
reciters = []
|
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
||||||
try:
|
now = timezone.now()
|
||||||
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 = {
|
context = {
|
||||||
"surahs": surahs,
|
"project_name": "New Style",
|
||||||
"reciters": reciters,
|
"agent_brand": agent_brand,
|
||||||
"backgrounds": backgrounds,
|
"django_version": django_version(),
|
||||||
"tasks": tasks,
|
"python_version": platform.python_version(),
|
||||||
"project_name": "Quran Reels Gen",
|
"current_time": now,
|
||||||
|
"host_name": host_name,
|
||||||
|
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||||
|
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||||
}
|
}
|
||||||
return render(request, "core/index.html", context)
|
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.
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user