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',
|
||||
]
|
||||
|
||||
MEDIA_URL = '/outputs/'
|
||||
MEDIA_ROOT = BASE_DIR / 'outputs'
|
||||
|
||||
# Email
|
||||
EMAIL_BACKEND = os.getenv(
|
||||
"EMAIL_BACKEND",
|
||||
@ -182,4 +179,4 @@ if EMAIL_USE_SSL:
|
||||
# Default primary key field type
|
||||
# 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:
|
||||
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
||||
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 .models import VideoTask
|
||||
|
||||
@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')
|
||||
# Register your models here.
|
||||
|
||||
@ -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
|
||||
|
||||
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}"
|
||||
# Create your models here.
|
||||
|
||||
@ -1,285 +1,145 @@
|
||||
{% 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">
|
||||
<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>
|
||||
{% extends "base.html" %}
|
||||
|
||||
<nav class="navbar navbar-dark">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="#">
|
||||
{{ project_name }}
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
{% block title %}{{ project_name }}{% endblock %}
|
||||
|
||||
<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>
|
||||
{% 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);
|
||||
}
|
||||
|
||||
<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>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
<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>
|
||||
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;
|
||||
}
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">Generate Video</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
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;
|
||||
}
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<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();
|
||||
@keyframes bg-pan {
|
||||
0% {
|
||||
background-position: 0% 0%;
|
||||
}
|
||||
|
||||
// Stop video when modal is closed
|
||||
document.getElementById('videoModal').addEventListener('hidden.bs.modal', function () {
|
||||
previewPlayer.pause();
|
||||
previewPlayer.src = "";
|
||||
});
|
||||
</script>
|
||||
100% {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
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>
|
||||
</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 .views import home, generate_video_view, get_surah_details
|
||||
|
||||
from .views import home
|
||||
|
||||
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"),
|
||||
]
|
||||
]
|
||||
|
||||
@ -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 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
|
||||
import platform
|
||||
|
||||
from django import get_version as django_version
|
||||
from django.shortcuts import render
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
def home(request):
|
||||
# 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]
|
||||
"""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()
|
||||
|
||||
context = {
|
||||
"surahs": surahs,
|
||||
"reciters": reciters,
|
||||
"backgrounds": backgrounds,
|
||||
"tasks": tasks,
|
||||
"project_name": "Quran Reels Gen",
|
||||
"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", ""),
|
||||
}
|
||||
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