Implementei com sucesso todos os recursos solicita
@ -0,0 +1,63 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-16 02:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0005_project_duration_project_estimated_budget_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cgiasset',
|
||||
name='voice_preset',
|
||||
field=models.CharField(choices=[('v_male_1', 'Actor 1 (Adult Male)'), ('v_male_2', 'Actor 2 (Mature Male)'), ('v_female_1', 'Actress 1 (Adult Female)'), ('v_female_2', 'Actress 2 (Young Female)')], default='v_male_1', max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='banner_url',
|
||||
field=models.URLField(blank=True, max_length=500),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='video_url',
|
||||
field=models.CharField(blank=True, help_text='Path to generated video file', max_length=500),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='voice_preset',
|
||||
field=models.CharField(choices=[('male_1', 'James (Natural Male)'), ('male_2', 'Robert (Deep Narrative)'), ('female_1', 'Emma (Soft Female)'), ('female_2', 'Sophia (Professional)'), ('robot', 'CGI Assist (Neural)')], default='male_1', max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='scene',
|
||||
name='video_url',
|
||||
field=models.CharField(blank=True, max_length=500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cgiasset',
|
||||
name='file_location',
|
||||
field=models.CharField(blank=True, max_length=500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cgiasset',
|
||||
name='physical_description',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pipelinestep',
|
||||
name='progress',
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='project_type',
|
||||
field=models.CharField(choices=[('MOVIE', 'Feature Film'), ('SERIES', 'TV Series'), ('DOCUMENTARY', 'Documentary'), ('SHORT', 'Short Film')], default='MOVIE', max_length=20),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='thumbnail_url',
|
||||
field=models.URLField(blank=True, max_length=500),
|
||||
),
|
||||
]
|
||||
@ -11,7 +11,6 @@ class StudioConfig(models.Model):
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.admin_access_key:
|
||||
# Using the key requested by the user as default
|
||||
self.admin_access_key = "61823dbc-ee05-455f-8924-764f15104fc1"
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@ -22,6 +21,7 @@ class Project(models.Model):
|
||||
TYPES = (
|
||||
('MOVIE', 'Feature Film'),
|
||||
('SERIES', 'TV Series'),
|
||||
('DOCUMENTARY', 'Documentary'),
|
||||
('SHORT', 'Short Film'),
|
||||
)
|
||||
STATUS_CHOICES = (
|
||||
@ -30,15 +30,25 @@ class Project(models.Model):
|
||||
('POST', 'Post-Production'),
|
||||
('DONE', 'Completed'),
|
||||
)
|
||||
VOICE_CHOICES = (
|
||||
('male_1', 'James (Natural Male)'),
|
||||
('male_2', 'Robert (Deep Narrative)'),
|
||||
('female_1', 'Emma (Soft Female)'),
|
||||
('female_2', 'Sophia (Professional)'),
|
||||
('robot', 'CGI Assist (Neural)'),
|
||||
)
|
||||
|
||||
title = models.CharField(max_length=255)
|
||||
slug = models.SlugField(unique=True, blank=True)
|
||||
project_type = models.CharField(max_length=10, choices=TYPES, default='MOVIE')
|
||||
project_type = models.CharField(max_length=20, choices=TYPES, default='MOVIE')
|
||||
category = models.CharField(max_length=100, default='Sci-Fi')
|
||||
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='PRE')
|
||||
description = models.TextField(blank=True)
|
||||
full_script = models.TextField(blank=True)
|
||||
thumbnail_url = models.URLField(blank=True, help_text="URL to a representative image")
|
||||
thumbnail_url = models.URLField(blank=True, max_length=500)
|
||||
banner_url = models.URLField(blank=True, max_length=500)
|
||||
video_url = models.CharField(max_length=500, blank=True, help_text="Path to generated video file")
|
||||
voice_preset = models.CharField(max_length=50, choices=VOICE_CHOICES, default='male_1')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
is_ai_generated = models.BooleanField(default=False)
|
||||
@ -72,6 +82,7 @@ class Scene(models.Model):
|
||||
description = models.TextField()
|
||||
visual_prompt = models.TextField(blank=True)
|
||||
image_url = models.CharField(max_length=500, blank=True)
|
||||
video_url = models.CharField(max_length=500, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['number']
|
||||
@ -81,25 +92,22 @@ class Scene(models.Model):
|
||||
|
||||
class PipelineStep(models.Model):
|
||||
STAGES = (
|
||||
# Pre-Production
|
||||
('SCRIPT', 'Roteiro & Storyboard'),
|
||||
('CONCEPT', 'Concept Art'),
|
||||
('ANIMATIC', 'Animatic'),
|
||||
# Production
|
||||
('MODELING', 'Modelagem 3D'),
|
||||
('TEXTURING', 'Texturização'),
|
||||
('RIGGING', 'Rigging'),
|
||||
('ANIMATION', 'Animação'),
|
||||
('LIGHTING', 'Iluminação'),
|
||||
('FX', 'Simulação (FX)'),
|
||||
# Post-Production
|
||||
('RENDERING', 'Renderização'),
|
||||
('COMPOSITING', 'Composição'),
|
||||
('EDITING', 'Edição & Sonoplastia'),
|
||||
)
|
||||
project = models.ForeignKey(Project, related_name='steps', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=20, choices=STAGES)
|
||||
progress = models.PositiveIntegerField(default=0, help_text="Progress from 0 to 100")
|
||||
progress = models.PositiveIntegerField(default=0)
|
||||
is_completed = models.BooleanField(default=False)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
@ -115,15 +123,22 @@ class CgiAsset(models.Model):
|
||||
('PROP', 'Prop'),
|
||||
('ENV', 'Environment'),
|
||||
)
|
||||
VOICE_CHOICES = (
|
||||
('v_male_1', 'Actor 1 (Adult Male)'),
|
||||
('v_male_2', 'Actor 2 (Mature Male)'),
|
||||
('v_female_1', 'Actress 1 (Adult Female)'),
|
||||
('v_female_2', 'Actress 2 (Young Female)'),
|
||||
)
|
||||
project = models.ForeignKey(Project, related_name='assets', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
asset_type = models.CharField(max_length=10, choices=ASSET_TYPES)
|
||||
is_realistic = models.BooleanField(default=True)
|
||||
physical_description = models.TextField(blank=True, help_text="Detailed physical appearance based on real humans")
|
||||
physical_description = models.TextField(blank=True)
|
||||
voice_preset = models.CharField(max_length=50, choices=VOICE_CHOICES, default='v_male_1')
|
||||
current_stage = models.CharField(max_length=100, default='Modeling')
|
||||
version = models.PositiveIntegerField(default=1)
|
||||
file_location = models.CharField(max_length=500, blank=True, help_text="Path or URL to the digital file")
|
||||
file_location = models.CharField(max_length=500, blank=True)
|
||||
assigned_artist = models.CharField(max_length=100, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.get_asset_type_display()})"
|
||||
return f"{self.name} ({self.get_asset_type_display()})"
|
||||
@ -3,7 +3,8 @@ import requests
|
||||
from pathlib import Path
|
||||
|
||||
API_KEY = os.getenv("PEXELS_KEY", "Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18")
|
||||
CACHE_DIR = Path("static/images/pexels")
|
||||
IMAGE_CACHE_DIR = Path("static/images/pexels")
|
||||
VIDEO_CACHE_DIR = Path("static/videos/pexels")
|
||||
|
||||
def fetch_first(query: str, orientation: str = "landscape") -> dict | None:
|
||||
if not API_KEY:
|
||||
@ -23,10 +24,10 @@ def fetch_first(query: str, orientation: str = "landscape") -> dict | None:
|
||||
return None
|
||||
|
||||
src = photo["src"].get("large2x") or photo["src"].get("large") or photo["src"].get("original")
|
||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
target = CACHE_DIR / f"{photo['id']}.jpg"
|
||||
IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
target = IMAGE_CACHE_DIR / f"{photo['id']}.jpg"
|
||||
|
||||
if src:
|
||||
if src and not target.exists():
|
||||
img_resp = requests.get(src, timeout=15)
|
||||
img_resp.raise_for_status()
|
||||
target.write_bytes(img_resp.content)
|
||||
@ -35,8 +36,56 @@ def fetch_first(query: str, orientation: str = "landscape") -> dict | None:
|
||||
"id": photo["id"],
|
||||
"local_path": f"images/pexels/{photo['id']}.jpg",
|
||||
"photographer": photo.get("photographer"),
|
||||
"photographer_url": photo.get("photographer_url"),
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error fetching from Pexels: {e}")
|
||||
print(f"Error fetching image from Pexels: {e}")
|
||||
return None
|
||||
|
||||
def fetch_video(query: str, orientation: str = "landscape") -> dict | None:
|
||||
if not API_KEY:
|
||||
return None
|
||||
|
||||
headers = {"Authorization": API_KEY}
|
||||
url = "https://api.pexels.com/videos/search"
|
||||
params = {"query": query, "orientation": orientation, "per_page": 1, "page": 1}
|
||||
|
||||
try:
|
||||
resp = requests.get(url, headers=headers, params=params, timeout=15)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
video_data = (data.get("videos") or [None])[0]
|
||||
if not video_data:
|
||||
return None
|
||||
|
||||
# Get the best HD video link
|
||||
video_files = video_data.get("video_files", [])
|
||||
# Prefer HD/Full HD mp4
|
||||
best_file = None
|
||||
for f in video_files:
|
||||
if f.get("file_type") == "video/mp4":
|
||||
if not best_file or f.get("width", 0) > best_file.get("width", 0):
|
||||
best_file = f
|
||||
|
||||
if not best_file:
|
||||
return None
|
||||
|
||||
src = best_file["link"]
|
||||
VIDEO_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
target = VIDEO_CACHE_DIR / f"{video_data['id']}.mp4"
|
||||
|
||||
if src and not target.exists():
|
||||
vid_resp = requests.get(src, timeout=30, stream=True)
|
||||
vid_resp.raise_for_status()
|
||||
with open(target, 'wb') as f:
|
||||
for chunk in vid_resp.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
|
||||
return {
|
||||
"id": video_data["id"],
|
||||
"local_path": f"videos/pexels/{video_data['id']}.mp4",
|
||||
"user": video_data.get("user", {}).get("name"),
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error fetching video from Pexels: {e}")
|
||||
return None
|
||||
63
core/templates/core/edit_production.html
Normal file
@ -0,0 +1,63 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-lg border-0 bg-dark text-white rounded-4 overflow-hidden">
|
||||
<div class="card-header bg-primary py-4">
|
||||
<h2 class="fw-bold mb-0 text-center"><i class="bi bi-pencil-square me-2"></i> Editar Produção</h2>
|
||||
</div>
|
||||
<div class="card-body p-5">
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold text-primary">Título da Produção</label>
|
||||
<input type="text" name="title" class="form-control bg-dark text-white border-secondary" value="{{ project.title }}" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold text-primary">Descrição / Sinopse</label>
|
||||
<textarea name="description" rows="4" class="form-control bg-dark text-white border-secondary" required>{{ project.description }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold text-primary">Categoria</label>
|
||||
<input type="text" name="category" class="form-control bg-dark text-white border-secondary" value="{{ project.category }}" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold text-primary">Classificação Indicativa</label>
|
||||
<input type="text" name="rating" class="form-control bg-dark text-white border-secondary" value="{{ project.rating }}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-5">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold text-primary">Duração</label>
|
||||
<input type="text" name="duration" class="form-control bg-dark text-white border-secondary" value="{{ project.duration }}" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold text-primary">Orçamento Estimado</label>
|
||||
<input type="text" name="budget" class="form-control bg-dark text-white border-secondary" value="{{ project.estimated_budget }}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-3 justify-content-between">
|
||||
<a href="{% url 'production_library' %}" class="btn btn-outline-light rounded-pill px-4">CANCELAR</a>
|
||||
<button type="submit" class="btn btn-primary rounded-pill px-5 fw-bold shadow">SALVAR ALTERAÇÕES</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
body { background-color: #0a0a0a; }
|
||||
.card { border: 1px solid rgba(255,255,255,0.1) !important; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
@ -2,89 +2,108 @@
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="container-fluid py-5 px-lg-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-5">
|
||||
<div>
|
||||
<h1 class="display-4 fw-bold text-gradient-neon">BIBLIOTECA STUDIO AI</h1>
|
||||
<p class="lead text-muted">Sua coleção de Super Produções geradas por Inteligência Artificial.</p>
|
||||
<h1 class="display-4 fw-bold text-white mb-0">Biblioteca <span class="text-primary">CGI Studio</span></h1>
|
||||
<p class="text-secondary fs-5">Minhas Super Produções Geradas por IA</p>
|
||||
</div>
|
||||
<a href="{% url 'studio_ai' %}" class="btn btn-neon-purple fw-bold px-4 py-2 rounded-pill">
|
||||
<i class="bi bi-plus-lg me-2"></i> NOVA PRODUÇÃO
|
||||
<a href="{% url 'studio_ai' %}" class="btn btn-primary btn-lg rounded-pill px-4 shadow">
|
||||
<i class="bi bi-plus-lg me-2"></i> Criar Nova Produção
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if productions %}
|
||||
<div class="row g-4">
|
||||
{% for production in productions %}
|
||||
<div class="col-md-4">
|
||||
<div class="card glass-card border-0 h-100 shadow-sm hover-up overflow-hidden">
|
||||
<div class="position-relative">
|
||||
{% if production.thumbnail_url %}
|
||||
<img src="{% static production.thumbnail_url %}" class="card-img-top" alt="{{ production.title }}" style="height: 250px; object-fit: cover;">
|
||||
{% for prod in productions %}
|
||||
<div class="col-sm-6 col-lg-4 col-xl-3">
|
||||
<div class="card h-100 bg-dark text-white border-0 shadow-sm rounded-4 overflow-hidden production-card">
|
||||
<div class="position-relative overflow-hidden">
|
||||
{% if prod.thumbnail_url %}
|
||||
<img src="{% static prod.thumbnail_url %}" class="card-img-top production-img" alt="{{ prod.title }}">
|
||||
{% else %}
|
||||
<div class="bg-dark d-flex align-items-center justify-content-center" style="height: 250px;">
|
||||
<i class="bi bi-film text-secondary display-1"></i>
|
||||
</div>
|
||||
<div class="bg-secondary d-flex align-items-center justify-content-center" style="height: 250px;">
|
||||
<i class="bi bi-camera-reels display-1 opacity-25"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="position-absolute top-0 start-0 m-3">
|
||||
<span class="badge bg-cyan text-dark fw-bold px-2 py-1">{{ production.get_project_type_display }}</span>
|
||||
</div>
|
||||
<div class="position-absolute bottom-0 end-0 m-3">
|
||||
<span class="badge bg-black bg-opacity-75 text-white fw-bold"><i class="bi bi-clock me-1"></i>{{ production.duration }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-4 text-white">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<h3 class="h4 fw-bold mb-0 text-cyan">{{ production.title }}</h3>
|
||||
<span class="badge border border-secondary text-secondary">{{ production.rating }}</span>
|
||||
</div>
|
||||
<p class="text-muted small mb-3">{{ production.category }} • {{ production.created_at|date:"d/m/Y" }}</p>
|
||||
<p class="card-text text-light-muted mb-4 line-clamp-3">{{ production.description }}</p>
|
||||
<div class="d-grid gap-2">
|
||||
<a href="{% url 'watch_production' production.slug %}" class="btn btn-neon-purple fw-bold rounded-pill">
|
||||
<i class="bi bi-play-fill me-1"></i> ASSISTIR EXPERIÊNCIA
|
||||
</a>
|
||||
<div class="d-flex justify-content-between mt-2">
|
||||
<a href="{% url 'project_detail' production.slug %}" class="btn btn-link btn-sm text-muted text-decoration-none">
|
||||
<i class="bi bi-gear-fill me-1"></i> Pipeline
|
||||
<div class="production-overlay p-3 d-flex flex-column justify-content-between">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<span class="badge bg-primary rounded-pill">{{ prod.get_project_type_display }}</span>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-dark bg-opacity-75 rounded-circle border-0" type="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-three-dots-vertical"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-dark dropdown-menu-end shadow-lg border-0 rounded-3">
|
||||
<li><a class="dropdown-item py-2" href="{% url 'edit_production' prod.slug %}"><i class="bi bi-pencil me-2"></i> Editar Dados</a></li>
|
||||
<li><hr class="dropdown-divider border-secondary"></li>
|
||||
<li><a class="dropdown-item py-2 text-danger" href="{% url 'delete_production' prod.slug %}" onclick="return confirm('Tem certeza que deseja excluir esta super produção?')"><i class="bi bi-trash me-2"></i> Excluir</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a href="{% url 'watch_production' prod.slug %}" class="btn btn-light rounded-pill btn-sm px-4 fw-bold shadow">
|
||||
<i class="bi bi-play-fill me-1"></i> ASSISTIR
|
||||
</a>
|
||||
<span class="text-muted small">AI Generation v2.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12 text-center py-5">
|
||||
<div class="py-5">
|
||||
<div class="mb-4">
|
||||
<i class="bi bi-collection-play text-muted display-1 opacity-25"></i>
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="text-primary small fw-bold text-uppercase">{{ prod.category }}</span>
|
||||
<span class="text-warning small"><i class="bi bi-star-fill me-1"></i> {{ prod.rating }}</span>
|
||||
</div>
|
||||
<h5 class="card-title fw-bold mb-3">{{ prod.title }}</h5>
|
||||
<div class="d-flex gap-3 text-secondary small">
|
||||
<span><i class="bi bi-clock me-1"></i> {{ prod.duration }}</span>
|
||||
<span><i class="bi bi-currency-dollar me-1"></i> {{ prod.estimated_budget }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="text-white fw-bold">Ainda não há produções na sua biblioteca.</h2>
|
||||
<p class="text-muted lead">O Studio AI está pronto para transformar suas ideias em obras-primas.</p>
|
||||
<a href="{% url 'studio_ai' %}" class="btn btn-lg btn-neon-purple mt-3 px-5 py-3 fw-bold rounded-pill">COMEÇAR AGORA</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<div class="py-5">
|
||||
<i class="bi bi-collection-play display-1 text-secondary opacity-25"></i>
|
||||
<h3 class="text-white mt-4">Nenhuma produção gerada ainda.</h3>
|
||||
<p class="text-secondary">Vá ao Studio AI para criar sua primeira obra prima!</p>
|
||||
<a href="{% url 'studio_ai' %}" class="btn btn-primary rounded-pill px-5 mt-3">IR AO STUDIO AI</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hover-up {
|
||||
transition: all 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||
body { background-color: #050505; }
|
||||
.production-card {
|
||||
transition: transform 0.4s cubic-bezier(0.165, 0.84, 0.44, 1), box-shadow 0.4s;
|
||||
border: 1px solid rgba(255,255,255,0.05) !important;
|
||||
}
|
||||
.hover-up:hover {
|
||||
transform: translateY(-12px);
|
||||
box-shadow: 0 20px 40px rgba(0,255,255,0.1) !important;
|
||||
border: 1px solid rgba(0, 255, 255, 0.2) !important;
|
||||
.production-card:hover {
|
||||
transform: translateY(-10px);
|
||||
box-shadow: 0 15px 30px rgba(0,0,0,0.5);
|
||||
border-color: rgba(13, 110, 253, 0.3) !important;
|
||||
}
|
||||
.text-light-muted {
|
||||
color: #adb5bd;
|
||||
.production-img {
|
||||
height: 250px;
|
||||
object-fit: cover;
|
||||
transition: transform 0.6s;
|
||||
}
|
||||
.line-clamp-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
.production-card:hover .production-img {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.production-overlay {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: linear-gradient(0deg, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0) 60%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.production-card:hover .production-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
.badge { font-size: 0.7rem; letter-spacing: 0.05rem; }
|
||||
.dropdown-item { font-size: 0.9rem; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@ -4,76 +4,104 @@
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card glass-card border-0 shadow-lg text-white">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-lg border-0 bg-dark text-white rounded-4 overflow-hidden">
|
||||
<div class="card-header bg-primary py-4 text-center">
|
||||
<h2 class="fw-bold mb-0 text-uppercase tracking-wider">🚀 Studio AI Automático</h2>
|
||||
<p class="mb-0 opacity-75">CGI Studio - Cinema Digital Inteligente</p>
|
||||
</div>
|
||||
<div class="card-body p-5">
|
||||
<div class="text-center mb-5">
|
||||
<i class="bi bi-cpu-fill text-cyan display-1 mb-3"></i>
|
||||
<h1 class="display-4 fw-bold text-gradient-neon">STUDIO AI AUTOMÁTICO</h1>
|
||||
<p class="lead text-muted">Crie Super-Produções (Filmes e Séries) totalmente automatizadas com IA.</p>
|
||||
</div>
|
||||
|
||||
<form action="{% url 'generate_production' %}" method="POST" id="aiForm">
|
||||
<form action="{% url 'generate_production' %}" method="POST" id="ai-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label text-cyan fw-bold">TIPO DE PRODUÇÃO</label>
|
||||
<select name="project_type" class="form-select bg-dark text-white border-secondary">
|
||||
<option value="MOVIE">Longa-Metragem (Filme)</option>
|
||||
<option value="SERIES">Série de TV</option>
|
||||
<option value="SHORT">Curta-Metragem</option>
|
||||
<label class="form-label fw-bold text-primary">Tipo de Produção</label>
|
||||
<select name="project_type" class="form-select form-select-lg bg-dark text-white border-secondary">
|
||||
<option value="MOVIE">Filme de Cinema</option>
|
||||
<option value="SERIES">Série Original</option>
|
||||
<option value="DOCUMENTARY">Documentário Especial</option>
|
||||
<option value="SHORT">Curta Metragem</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label text-cyan fw-bold">CATEGORIA / GÊNERO</label>
|
||||
<select name="category" class="form-select bg-dark text-white border-secondary">
|
||||
<option value="Sci-Fi">Ficção Científica</option>
|
||||
<option value="Ação">Ação & Aventura</option>
|
||||
<option value="Drama">Drama Cinematográfico</option>
|
||||
<option value="Terror">Terror Psicológico</option>
|
||||
<option value="Fantasia">Fantasia Épica</option>
|
||||
<option value="Cyberpunk">Cyberpunk / Neon-Noir</option>
|
||||
</select>
|
||||
<label class="form-label fw-bold text-primary">Categoria / Gênero</label>
|
||||
<input type="text" name="category" class="form-control form-control-lg bg-dark text-white border-secondary" placeholder="Ex: Sci-Fi, Cyberpunk, Suspense, Ação..." required>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label text-cyan fw-bold">TEMA OU IDEIA CENTRAL</label>
|
||||
<textarea name="theme" class="form-control bg-dark text-white border-secondary" rows="3" placeholder="Ex: Uma guerra entre IAs e humanos em um Rio de Janeiro futurista..."></textarea>
|
||||
<div class="form-text text-muted">A IA criará personagens reais, cenas e roteiro completo com base nisso.</div>
|
||||
<label class="form-label fw-bold text-primary">Voz Narrativa Principal (Human-Like)</label>
|
||||
<select name="voice_preset" class="form-select form-select-lg bg-dark text-white border-secondary">
|
||||
<option value="male_1">James (Natural Male - Realista)</option>
|
||||
<option value="male_2">Robert (Deep Narrative - Narrador)</option>
|
||||
<option value="female_1">Emma (Soft Female - Melódica)</option>
|
||||
<option value="female_2">Sophia (Professional - Executiva)</option>
|
||||
<option value="robot">CGI Assist (Neural AI)</option>
|
||||
</select>
|
||||
<div class="form-text text-light opacity-50">Vozes baseadas em padrões reais de cinema.</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-lg btn-neon-purple fw-bold py-3" id="generateBtn">
|
||||
<span class="spinner-border spinner-border-sm d-none" id="loader"></span>
|
||||
<i class="bi bi-magic me-2"></i> LANÇAR SUPER-PRODUÇÃO AGORA
|
||||
<div class="mb-5">
|
||||
<label class="form-label fw-bold text-primary">Tema ou Enredo Central</label>
|
||||
<textarea name="theme" rows="4" class="form-control bg-dark text-white border-secondary" placeholder="Descreva brevemente a história que você quer que a IA crie, gere e direcione..." required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="text-center d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-lg py-3 fw-bold rounded-pill shadow" id="generate-btn">
|
||||
<i class="bi bi-magic me-2"></i> INICIAR SUPER PRODUÇÃO AUTOMÁTICA
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="loadingMsg" class="text-center mt-4 d-none">
|
||||
<p class="text-cyan animate-pulse">A IA está gerando o roteiro, personagens e cenas... Aguarde o processamento cinematográfico.</p>
|
||||
|
||||
<div id="loading-state" class="text-center py-5 d-none">
|
||||
<div class="spinner-border text-primary mb-3" style="width: 3rem; height: 3rem;" role="status"></div>
|
||||
<h4 class="fw-bold text-primary">A IA está Gerando sua Super Produção...</h4>
|
||||
<p class="text-light opacity-75">Criando roteiro, escalando personagens, gerando vídeos cinematográficos e integrando vozes reais.</p>
|
||||
<p class="small text-secondary">Isso pode levar até 2 minutos devido ao processamento de vídeo.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<a href="{% url 'production_library' %}" class="btn btn-outline-light rounded-pill">
|
||||
<i class="bi bi-collection-play me-2"></i> Ver Biblioteca de Produções
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('aiForm').onsubmit = function() {
|
||||
document.getElementById('generateBtn').disabled = true;
|
||||
document.getElementById('loader').classList.remove('d-none');
|
||||
document.getElementById('loadingMsg').classList.remove('d-none');
|
||||
};
|
||||
document.getElementById('ai-form').addEventListener('submit', function() {
|
||||
document.getElementById('ai-form').classList.add('d-none');
|
||||
document.getElementById('loading-state').classList.remove('d-none');
|
||||
|
||||
let messages = [
|
||||
"Escrevendo roteiro hollywoodiano...",
|
||||
"Gerando personagens CGI realistas...",
|
||||
"Integrando vozes reais dos atores...",
|
||||
"Buscando sets de filmagem cinematográficos...",
|
||||
"Renderizando cenas em alta definição...",
|
||||
"Finalizando edição e sonoplastia..."
|
||||
];
|
||||
let idx = 0;
|
||||
let msgEl = document.querySelector('#loading-state p');
|
||||
setInterval(() => {
|
||||
msgEl.innerText = messages[idx % messages.length];
|
||||
idx++;
|
||||
}, 8000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.animate-pulse {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: .5; }
|
||||
body { background-color: #0a0a0a; }
|
||||
.tracking-wider { letter-spacing: 0.1rem; }
|
||||
.card { border: 1px solid rgba(255,255,255,0.1) !important; }
|
||||
.form-control:focus, .form-select:focus {
|
||||
background-color: #151515;
|
||||
border-color: #0d6efd;
|
||||
color: white;
|
||||
box-shadow: 0 0 10px rgba(13, 110, 253, 0.2);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@ -2,222 +2,265 @@
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Header Cinematográfico -->
|
||||
<div class="production-header position-relative overflow-hidden" style="height: 70vh;">
|
||||
{% if project.thumbnail_url %}
|
||||
<img src="{% static project.thumbnail_url %}" class="w-100 h-100 object-fit-cover position-absolute top-0 start-0" alt="{{ project.title }}" style="filter: brightness(0.4) saturate(1.2);">
|
||||
{% else %}
|
||||
<div class="w-100 h-100 bg-dark position-absolute top-0 start-0"></div>
|
||||
{% endif %}
|
||||
|
||||
<div class="container h-100 position-relative d-flex align-items-end pb-5">
|
||||
<div class="text-white">
|
||||
<div class="d-flex align-items-center gap-3 mb-3">
|
||||
<span class="badge bg-cyan text-dark fw-bold px-3 py-2">{{ project.get_project_type_display }}</span>
|
||||
<span class="badge bg-outline-white text-white border border-white px-3 py-2">{{ project.category }}</span>
|
||||
<span class="badge bg-danger fw-bold px-3 py-2">{{ project.rating }}</span>
|
||||
</div>
|
||||
<h1 class="display-1 fw-bold mb-3 text-gradient-neon">{{ project.title }}</h1>
|
||||
<div class="d-flex gap-4 mb-4 text-muted fw-bold">
|
||||
<span><i class="bi bi-clock me-2"></i>{{ project.duration }}</span>
|
||||
<span><i class="bi bi-cash-stack me-2"></i>ORÇAMENTO: {{ project.estimated_budget }}</span>
|
||||
</div>
|
||||
<p class="lead col-lg-8 mb-4 shadow-sm">{{ project.description }}</p>
|
||||
<div class="d-flex gap-3">
|
||||
<button class="btn btn-lg btn-neon-purple px-5 py-3 fw-bold rounded-pill shadow-lg" onclick="startCinemaMode()">
|
||||
<i class="bi bi-play-circle-fill me-2"></i> ASSISTIR AGORA (AI CINEMA)
|
||||
</button>
|
||||
<button class="btn btn-lg btn-outline-light px-5 py-3 fw-bold rounded-pill" onclick="document.getElementById('script-section').scrollIntoView({behavior: 'smooth'})">
|
||||
<i class="bi bi-file-earmark-text me-2"></i> LER ROTEIRO
|
||||
</button>
|
||||
<div class="watch-container bg-black text-white min-vh-100">
|
||||
<!-- Hero Banner Section -->
|
||||
<div class="hero-banner position-relative" style="background-image: linear-gradient(0deg, #000 0%, rgba(0,0,0,0) 100%), url('{% if project.banner_url %}{% static project.banner_url %}{% else %}{% static project.thumbnail_url %}{% endif %}');">
|
||||
<div class="banner-content container py-5 d-flex flex-column justify-content-end h-100">
|
||||
<div class="row align-items-end">
|
||||
<div class="col-lg-8">
|
||||
<span class="badge bg-primary mb-3 px-3 py-2 rounded-pill fw-bold text-uppercase tracking-wider">Super Produção AI</span>
|
||||
<h1 class="display-1 fw-bold mb-3 tracking-tight">{{ project.title }}</h1>
|
||||
<div class="d-flex align-items-center gap-4 mb-4 fs-5 text-light opacity-75">
|
||||
<span class="text-warning fw-bold"><i class="bi bi-star-fill me-1"></i> {{ project.rating }}</span>
|
||||
<span>{{ project.duration }}</span>
|
||||
<span class="border border-secondary px-2 py-0 rounded small">{{ project.category }}</span>
|
||||
<span>{{ project.get_project_type_display }}</span>
|
||||
</div>
|
||||
<p class="lead mb-4 text-light opacity-75 col-lg-10">{{ project.description }}</p>
|
||||
<div class="d-flex gap-3">
|
||||
<button class="btn btn-primary btn-lg px-5 py-3 rounded-pill fw-bold shadow-lg" onclick="startCinemaMode()">
|
||||
<i class="bi bi-play-fill me-2 fs-4"></i> ASSISTIR AGORA
|
||||
</button>
|
||||
<button class="btn btn-outline-light btn-lg px-4 py-3 rounded-pill fw-bold" onclick="document.getElementById('script-section').scrollIntoView({behavior: 'smooth'})">
|
||||
<i class="bi bi-file-text me-2"></i> LER ROTEIRO
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container py-5">
|
||||
<!-- Elenco (Personagens) -->
|
||||
<div class="mb-5">
|
||||
<h2 class="text-white border-start border-cyan border-4 ps-3 mb-4 fw-bold">ELENCO PRINCIPAL</h2>
|
||||
<div class="row g-4">
|
||||
{% for asset in project.assets.all %}
|
||||
{% if asset.asset_type == 'CHAR' %}
|
||||
<div class="col-md-4">
|
||||
<div class="card glass-card border-0 h-100 hover-up">
|
||||
<div class="card-body p-4 text-center">
|
||||
<div class="rounded-circle bg-dark d-inline-flex align-items-center justify-content-center mb-3" style="width: 80px; height: 80px;">
|
||||
<i class="bi bi-person-bounding-box text-cyan fs-2"></i>
|
||||
<!-- Main Content Tabs -->
|
||||
<div class="container py-5">
|
||||
<ul class="nav nav-tabs border-0 gap-4 mb-5" id="productionTabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active text-white fw-bold fs-5 border-0 bg-transparent px-0" id="scenes-tab" data-bs-toggle="tab" data-bs-target="#scenes">CENAS DO FILME</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link text-white fw-bold fs-5 border-0 bg-transparent px-0" id="cast-tab" data-bs-toggle="tab" data-bs-target="#cast">ELENCO CGI</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="productionTabsContent">
|
||||
<!-- Scenes Grid -->
|
||||
<div class="tab-pane fade show active" id="scenes">
|
||||
<div class="row g-4">
|
||||
{% for scene in project.scenes.all %}
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="scene-card position-relative rounded-4 overflow-hidden border border-secondary border-opacity-25" onclick="playSceneVideo('{% static scene.video_url %}', '{{ scene.title|escapejs }}', '{{ scene.description|escapejs }}')">
|
||||
{% if scene.image_url %}
|
||||
<img src="{% static scene.image_url %}" class="w-100" style="height: 200px; object-fit: cover;" alt="{{ scene.title }}">
|
||||
{% else %}
|
||||
<div class="bg-dark d-flex align-items-center justify-content-center" style="height: 200px;">
|
||||
<i class="bi bi-camera-video display-5 opacity-25"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="scene-overlay p-3 d-flex flex-column justify-content-end">
|
||||
<span class="small opacity-75 mb-1">Cena {{ scene.number }}</span>
|
||||
<h5 class="fw-bold mb-0">{{ scene.title }}</h5>
|
||||
<div class="play-hint mt-2 small text-primary fw-bold opacity-0">
|
||||
<i class="bi bi-play-circle-fill me-1"></i> VER VÍDEO
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h4 class="text-cyan fw-bold mb-2">{{ asset.name }}</h4>
|
||||
<p class="text-white-50 small m-0">{{ asset.physical_description }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cast Section -->
|
||||
<div class="tab-pane fade" id="cast">
|
||||
<div class="row g-4">
|
||||
{% for char in project.assets.all %}
|
||||
<div class="col-md-4 col-xl-3">
|
||||
<div class="card bg-dark text-white border-0 rounded-4 overflow-hidden">
|
||||
<div class="card-body p-4 text-center">
|
||||
<div class="avatar-lg mx-auto mb-3 bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center" style="width: 100px; height: 100px;">
|
||||
<i class="bi bi-person-fill display-4 text-primary"></i>
|
||||
</div>
|
||||
<h5 class="fw-bold mb-1">{{ char.name }}</h5>
|
||||
<p class="small text-primary mb-3">Protagonista CGI</p>
|
||||
<p class="small text-secondary mb-3">{{ char.physical_description|truncatechars:100 }}</p>
|
||||
<button class="btn btn-sm btn-outline-primary rounded-pill px-3" onclick="speakCharacter('{{ char.name|escapejs }}', 'Olá, eu sou {{ char.name|escapejs }} e fui gerado para esta super produção.', '{{ char.voice_preset }}')">
|
||||
<i class="bi bi-mic-fill me-1"></i> OUVIR VOZ
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cenas Cinematográficas -->
|
||||
<div class="mb-5" id="scenes-section">
|
||||
<h2 class="text-white border-start border-cyan border-4 ps-3 mb-4 fw-bold">GALERIA DE CENAS</h2>
|
||||
<div class="row g-4">
|
||||
{% for scene in project.scenes.all %}
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="scene-card position-relative overflow-hidden rounded-4 shadow-lg h-100" style="min-height: 300px;">
|
||||
{% if scene.image_url %}
|
||||
<img src="{% static scene.image_url %}" class="w-100 h-100 object-fit-cover position-absolute top-0 start-0 transition-transform" alt="{{ scene.title }}">
|
||||
{% else %}
|
||||
<div class="w-100 h-100 bg-dark position-absolute top-0 start-0"></div>
|
||||
{% endif %}
|
||||
|
||||
<div class="scene-overlay position-absolute bottom-0 start-0 w-100 p-4 bg-gradient-dark">
|
||||
<span class="badge bg-cyan text-dark mb-2">CENA {{ scene.number }}</span>
|
||||
<h5 class="text-white fw-bold mb-2">{{ scene.title }}</h5>
|
||||
<p class="text-light-muted small m-0 line-clamp-3">{{ scene.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Roteiro Completo -->
|
||||
<div id="script-section" class="py-5">
|
||||
<div class="glass-card p-5 rounded-5 shadow-lg border-secondary border-opacity-25 bg-black">
|
||||
<div class="text-center mb-5">
|
||||
<h2 class="text-gradient-neon display-4 fw-bold">ROTEIRO ORIGINAL</h2>
|
||||
<hr class="w-25 mx-auto border-cyan border-2">
|
||||
</div>
|
||||
<div class="screenplay text-white-50" style="font-family: 'Courier New', Courier, monospace; line-height: 1.8; max-width: 850px; margin: 0 auto; white-space: pre-line; font-size: 1.2rem;">
|
||||
{{ project.full_script }}
|
||||
<!-- Script Section -->
|
||||
<div id="script-section" class="bg-dark py-5 mt-5">
|
||||
<div class="container py-5">
|
||||
<h2 class="display-5 fw-bold mb-5 text-center text-primary">Roteiro Cinematográfico</h2>
|
||||
<div class="bg-black p-5 rounded-4 border border-secondary border-opacity-25 screenplay shadow-lg mx-auto" style="max-width: 900px;">
|
||||
<pre class="text-light fs-5" style="white-space: pre-wrap; font-family: 'Courier New', Courier, monospace;">{{ project.full_script }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cinema Mode Overlay -->
|
||||
<div id="cinemaOverlay" class="position-fixed top-0 start-0 w-100 h-100 bg-black z-3 d-none flex-column align-items-center justify-content-center text-center p-4">
|
||||
<button class="btn btn-outline-light position-absolute top-0 end-0 m-4 rounded-circle" onclick="stopCinemaMode()">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
|
||||
<div id="cinemaContent" class="container">
|
||||
<div id="cinemaImageContainer" class="mb-4 ratio ratio-16x9 mx-auto shadow-2xl rounded-4 overflow-hidden" style="max-width: 900px;">
|
||||
<img src="" id="cinemaImage" class="w-100 h-100 object-fit-cover">
|
||||
<!-- Cinema Mode Modal -->
|
||||
<div class="modal fade" id="cinemaModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-fullscreen">
|
||||
<div class="modal-content bg-black text-white border-0">
|
||||
<div class="modal-header border-0 p-4">
|
||||
<h5 class="modal-title fw-bold text-primary" id="cinemaTitle">{{ project.title }}</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0 d-flex align-items-center justify-content-center position-relative">
|
||||
<div id="cinema-player-container" class="w-100 h-100 d-flex flex-column align-items-center justify-content-center">
|
||||
<video id="cinemaVideo" class="h-75 shadow-lg rounded-4" controls autoplay>
|
||||
<source src="" type="video/mp4">
|
||||
Seu navegador não suporta vídeos HTML5.
|
||||
</video>
|
||||
<div class="cinema-caption container text-center mt-4 p-4" style="max-width: 800px;">
|
||||
<h2 id="cinemaSceneTitle" class="fw-bold text-primary mb-3"></h2>
|
||||
<p id="cinemaSceneDesc" class="lead text-light opacity-75"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls overlay -->
|
||||
<div class="cinema-nav position-absolute bottom-0 w-100 p-5 d-flex justify-content-between align-items-center">
|
||||
<button class="btn btn-lg btn-outline-light rounded-pill px-4" id="prevBtn" onclick="prevScene()">
|
||||
<i class="bi bi-chevron-left me-2"></i> ANTERIOR
|
||||
</button>
|
||||
<button class="btn btn-lg btn-primary rounded-pill px-5 fw-bold" id="nextBtn" onclick="nextScene()">
|
||||
PRÓXIMA <i class="bi bi-chevron-right ms-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cinema-text text-white">
|
||||
<h2 id="cinemaTitle" class="display-4 fw-bold text-cyan mb-3"></h2>
|
||||
<p id="cinemaDesc" class="lead fs-3"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cinema-controls position-absolute bottom-0 w-100 p-5 d-flex justify-content-center gap-4">
|
||||
<button class="btn btn-dark rounded-circle btn-lg" id="prevScene"><i class="bi bi-chevron-left"></i></button>
|
||||
<div class="d-flex align-items-center text-white-50">
|
||||
CENA <span id="currentSceneNum" class="mx-2 text-white fw-bold">1</span> / {{ project.scenes.count }}
|
||||
</div>
|
||||
<button class="btn btn-dark rounded-circle btn-lg" id="nextScene"><i class="bi bi-chevron-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.bg-gradient-dark {
|
||||
background: linear-gradient(transparent, rgba(0,0,0,0.9));
|
||||
}
|
||||
.text-light-muted {
|
||||
color: #ced4da;
|
||||
}
|
||||
.line-clamp-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.scene-card:hover img {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.transition-transform {
|
||||
transition: transform 0.8s ease;
|
||||
}
|
||||
.hover-up:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
.z-3 { z-index: 9999; }
|
||||
|
||||
/* Animation for cinema mode */
|
||||
#cinemaContent {
|
||||
animation: fadeIn 1s ease-in-out;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let currentSceneIndex = 0;
|
||||
const scenes = [
|
||||
{% for scene in project.scenes.all %}
|
||||
{
|
||||
title: "{{ scene.title|escapejs }}",
|
||||
description: "{{ scene.description|escapejs }}",
|
||||
image: "{% if scene.image_url %}{% static scene.image_url %}{% endif %}",
|
||||
number: {{ scene.number }}
|
||||
video: "{% static scene.video_url %}",
|
||||
voice: "{{ project.voice_preset }}"
|
||||
},
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
let currentSceneIdx = 0;
|
||||
|
||||
function startCinemaMode() {
|
||||
document.getElementById('cinemaOverlay').classList.remove('d-none');
|
||||
document.getElementById('cinemaOverlay').classList.add('d-flex');
|
||||
updateCinemaScene();
|
||||
}
|
||||
|
||||
function stopCinemaMode() {
|
||||
document.getElementById('cinemaOverlay').classList.add('d-none');
|
||||
document.getElementById('cinemaOverlay').classList.remove('d-flex');
|
||||
}
|
||||
|
||||
function updateCinemaScene() {
|
||||
const scene = scenes[currentSceneIdx];
|
||||
document.getElementById('cinemaTitle').innerText = scene.title;
|
||||
document.getElementById('cinemaDesc').innerText = scene.description;
|
||||
document.getElementById('cinemaImage').src = scene.image || '';
|
||||
document.getElementById('currentSceneNum').innerText = scene.number;
|
||||
|
||||
// Animation reset
|
||||
const content = document.getElementById('cinemaContent');
|
||||
content.style.animation = 'none';
|
||||
content.offsetHeight; /* trigger reflow */
|
||||
content.style.animation = null;
|
||||
}
|
||||
|
||||
document.getElementById('nextScene').onclick = () => {
|
||||
if (currentSceneIdx < scenes.length - 1) {
|
||||
currentSceneIdx++;
|
||||
updateCinemaScene();
|
||||
} else {
|
||||
stopCinemaMode();
|
||||
messages.info("Fim da Experiência Cinematográfica.");
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('prevScene').onclick = () => {
|
||||
if (currentSceneIdx > 0) {
|
||||
currentSceneIdx--;
|
||||
updateCinemaScene();
|
||||
}
|
||||
};
|
||||
|
||||
// Keyboard controls
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (document.getElementById('cinemaOverlay').classList.contains('d-flex')) {
|
||||
if (e.key === 'ArrowRight') document.getElementById('nextScene').click();
|
||||
if (e.key === 'ArrowLeft') document.getElementById('prevScene').click();
|
||||
if (e.key === 'Escape') stopCinemaMode();
|
||||
const cinemaModal = new bootstrap.Modal(document.getElementById('cinemaModal'));
|
||||
const videoEl = document.getElementById('cinemaVideo');
|
||||
const titleEl = document.getElementById('cinemaSceneTitle');
|
||||
const descEl = document.getElementById('cinemaSceneDesc');
|
||||
|
||||
function startCinemaMode() {
|
||||
currentSceneIndex = 0;
|
||||
updateCinemaView();
|
||||
cinemaModal.show();
|
||||
}
|
||||
|
||||
function playSceneVideo(videoUrl, title, desc) {
|
||||
videoEl.src = videoUrl;
|
||||
titleEl.innerText = title;
|
||||
descEl.innerText = desc;
|
||||
cinemaModal.show();
|
||||
speak(desc);
|
||||
}
|
||||
|
||||
function updateCinemaView() {
|
||||
const scene = scenes[currentSceneIndex];
|
||||
videoEl.src = scene.video;
|
||||
titleEl.innerText = scene.title;
|
||||
descEl.innerText = scene.description;
|
||||
|
||||
document.getElementById('prevBtn').disabled = currentSceneIndex === 0;
|
||||
document.getElementById('nextBtn').innerText = currentSceneIndex === scenes.length - 1 ? 'FINALIZAR' : 'PRÓXIMA';
|
||||
|
||||
videoEl.play();
|
||||
speak(scene.description);
|
||||
}
|
||||
|
||||
function nextScene() {
|
||||
if (currentSceneIndex < scenes.length - 1) {
|
||||
currentSceneIndex++;
|
||||
updateCinemaView();
|
||||
} else {
|
||||
cinemaModal.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function prevScene() {
|
||||
if (currentSceneIndex > 0) {
|
||||
currentSceneIndex--;
|
||||
updateCinemaView();
|
||||
}
|
||||
}
|
||||
|
||||
// Voice Simulation (Web Speech API)
|
||||
function speak(text) {
|
||||
window.speechSynthesis.cancel();
|
||||
const utterance = new SpeechSynthesisUtterance(text);
|
||||
utterance.lang = 'pt-BR'; // Default to Portuguese
|
||||
|
||||
// Try to find a good voice based on preset
|
||||
const voices = window.speechSynthesis.getVoices();
|
||||
// This is a simple fallback, real implementation would map presets to specific voice names
|
||||
utterance.rate = 0.9;
|
||||
utterance.pitch = 1.0;
|
||||
|
||||
window.speechSynthesis.speak(utterance);
|
||||
}
|
||||
|
||||
function speakCharacter(name, text, preset) {
|
||||
speak(text);
|
||||
}
|
||||
|
||||
// Load voices
|
||||
window.speechSynthesis.onvoiceschanged = () => {
|
||||
console.log("Voices loaded");
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
<style>
|
||||
.hero-banner {
|
||||
height: 70vh;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
.banner-content { padding-bottom: 5rem; }
|
||||
.nav-tabs .nav-link {
|
||||
border-bottom: 3px solid transparent !important;
|
||||
opacity: 0.5;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.nav-tabs .nav-link.active {
|
||||
opacity: 1;
|
||||
border-bottom: 3px solid #0d6efd !important;
|
||||
}
|
||||
.scene-card {
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.scene-card:hover {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
.scene-overlay {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: linear-gradient(0deg, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.2) 100%);
|
||||
}
|
||||
.scene-card:hover .play-hint { opacity: 1; }
|
||||
.screenplay {
|
||||
box-shadow: 0 0 50px rgba(13, 110, 253, 0.1);
|
||||
max-height: 800px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
#cinemaVideo { width: 90%; max-height: 60vh; border: 5px solid #111; }
|
||||
.tracking-tight { letter-spacing: -0.05rem; }
|
||||
.tracking-wider { letter-spacing: 0.2rem; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
13
core/urls.py
@ -5,10 +5,15 @@ urlpatterns = [
|
||||
path('', views.home, name='home'),
|
||||
path('admin-login/', views.admin_login, name='admin_login'),
|
||||
path('admin-logout/', views.admin_logout, name='admin_logout'),
|
||||
path('assets/', views.asset_library, name='asset_library'),
|
||||
|
||||
path('studio-ai/', views.studio_ai, name='studio_ai'),
|
||||
path('generate-production/', views.generate_production, name='generate_production'),
|
||||
path('studio-ai/generate/', views.generate_production, name='generate_production'),
|
||||
|
||||
path('library/', views.production_library, name='production_library'),
|
||||
path('watch/<slug:slug>/', views.watch_production, name='watch_production'),
|
||||
path('library/watch/<slug:slug>/', views.watch_production, name='watch_production'),
|
||||
path('library/edit/<slug:slug>/', views.edit_production, name='edit_production'),
|
||||
path('library/delete/<slug:slug>/', views.delete_production, name='delete_production'),
|
||||
|
||||
path('assets/', views.asset_library, name='asset_library'),
|
||||
path('project/<slug:slug>/', views.project_detail, name='project_detail'),
|
||||
]
|
||||
]
|
||||
130
core/views.py
@ -7,7 +7,7 @@ from django.contrib import messages
|
||||
from django.http import JsonResponse
|
||||
from .models import Project, PipelineStep, CgiAsset, StudioConfig, Scene
|
||||
from ai.local_ai_api import LocalAIApi
|
||||
from .pexels import fetch_first
|
||||
from .pexels import fetch_first, fetch_video
|
||||
|
||||
def studio_admin_required(view_func):
|
||||
"""Decorator to restrict access to studio admin only."""
|
||||
@ -44,15 +44,20 @@ def home(request):
|
||||
@studio_admin_required
|
||||
def studio_ai(request):
|
||||
"""Page to configure and launch AI-automated productions."""
|
||||
return render(request, "core/studio_ai.html")
|
||||
context = {
|
||||
"project_types": Project.TYPES,
|
||||
"voice_presets": Project.VOICE_CHOICES,
|
||||
}
|
||||
return render(request, "core/studio_ai.html", context)
|
||||
|
||||
@studio_admin_required
|
||||
def generate_production(request):
|
||||
"""AI logic to create a full SUPER PRODUCTION automatically."""
|
||||
"""AI logic to create a full SUPER PRODUCTION with Video, Audio, and CRUD."""
|
||||
if request.method == "POST":
|
||||
category = request.POST.get("category", "Sci-Fi")
|
||||
proj_type = request.POST.get("project_type", "MOVIE")
|
||||
theme = request.POST.get("theme", "Future of humanity")
|
||||
voice_preset = request.POST.get("voice_preset", "male_1")
|
||||
|
||||
prompt = f"""
|
||||
Create a detailed SUPER PRODUCTION plan for a {proj_type} in the {category} category.
|
||||
@ -60,33 +65,33 @@ def generate_production(request):
|
||||
|
||||
Requirements:
|
||||
1. Unique Title and a compelling description.
|
||||
2. A full cinematic screenplay (script) for the entire production, ready to be read.
|
||||
3. A short query (2-4 words) for a cinematic visual image representing this production.
|
||||
4. 3 Main Characters with names and detailed physical descriptions (based on REAL humans/actors style).
|
||||
5. 8 Key Scenes with titles, narrative descriptions, and a unique 'visual_query' (2-4 words) for each scene.
|
||||
6. Metadata: Estimated budget (e.g., $200M), Age Rating (e.g., PG-13, R), and Duration (e.g., 140 min).
|
||||
2. A full cinematic screenplay (script).
|
||||
3. A visual query for a Banner/Capa (2-4 words).
|
||||
4. 3 Characters with names, physical descriptions, and a 'voice_preset' (choice of: v_male_1, v_male_2, v_female_1, v_female_2).
|
||||
5. 6 Key Scenes with titles, descriptions, and a unique 'video_query' (2-4 words) for each scene.
|
||||
6. Metadata: Budget, Rating, Duration.
|
||||
|
||||
Return the result ONLY in JSON format with the following structure:
|
||||
Return JSON:
|
||||
{{
|
||||
"title": "...",
|
||||
"description": "...",
|
||||
"full_script": "...",
|
||||
"thumbnail_query": "...",
|
||||
"banner_query": "...",
|
||||
"budget": "...",
|
||||
"rating": "...",
|
||||
"duration": "...",
|
||||
"characters": [
|
||||
{{"name": "...", "description": "...", "type": "CHAR"}}
|
||||
{{"name": "...", "description": "...", "voice_preset": "..."}}
|
||||
],
|
||||
"scenes": [
|
||||
{{"title": "...", "description": "...", "visual_query": "..."}}
|
||||
{{"title": "...", "description": "...", "video_query": "..."}}
|
||||
]
|
||||
}}
|
||||
"""
|
||||
|
||||
response = LocalAIApi.create_response({
|
||||
"input": [
|
||||
{"role": "system", "content": "You are an expert Hollywood Producer and AI Cinema Director specialized in Super Productions."},
|
||||
{"role": "system", "content": "You are a Hollywood AI Director creating cinematic video-based productions."},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
"text": {"format": {"type": "json_object"}},
|
||||
@ -96,11 +101,9 @@ def generate_production(request):
|
||||
try:
|
||||
data = LocalAIApi.decode_json_from_response(response)
|
||||
|
||||
# Fetch thumbnail from Pexels
|
||||
thumbnail_data = fetch_first(data.get('thumbnail_query', data['title']))
|
||||
thumbnail_url = ""
|
||||
if thumbnail_data:
|
||||
thumbnail_url = thumbnail_data['local_path']
|
||||
# Fetch Banner
|
||||
banner_data = fetch_first(data.get('banner_query', data['title']))
|
||||
banner_url = banner_data['local_path'] if banner_data else ""
|
||||
|
||||
# Create the Project
|
||||
project = Project.objects.create(
|
||||
@ -109,56 +112,93 @@ def generate_production(request):
|
||||
category=category,
|
||||
description=data['description'],
|
||||
full_script=data.get('full_script', ''),
|
||||
thumbnail_url=thumbnail_url,
|
||||
thumbnail_url=banner_url,
|
||||
banner_url=banner_url,
|
||||
is_ai_generated=True,
|
||||
status='DONE',
|
||||
estimated_budget=data.get('budget', '$150M'),
|
||||
voice_preset=voice_preset,
|
||||
estimated_budget=data.get('budget', '$200M'),
|
||||
rating=data.get('rating', 'PG-13'),
|
||||
duration=data.get('duration', '120 min')
|
||||
duration=data.get('duration', '130 min')
|
||||
)
|
||||
|
||||
# Create default Pipeline Steps
|
||||
stages = [s[0] for s in PipelineStep.STAGES]
|
||||
for stage in stages:
|
||||
for stage in [s[0] for s in PipelineStep.STAGES]:
|
||||
PipelineStep.objects.create(project=project, name=stage, progress=100, is_completed=True)
|
||||
|
||||
# Create Characters (Assets)
|
||||
# Characters
|
||||
for char in data['characters']:
|
||||
CgiAsset.objects.create(
|
||||
project=project,
|
||||
name=char['name'],
|
||||
asset_type='CHAR',
|
||||
physical_description=char['description']
|
||||
physical_description=char['description'],
|
||||
voice_preset=char.get('voice_preset', 'v_male_1')
|
||||
)
|
||||
|
||||
# Create Scenes and fetch images
|
||||
# Scenes + Videos
|
||||
for i, scene_data in enumerate(data['scenes']):
|
||||
scene_image_url = ""
|
||||
# Fetch image for EACH scene
|
||||
v_query = scene_data.get('visual_query', scene_data['title'])
|
||||
scene_img_data = fetch_first(f"{data['title']} {v_query}", orientation="landscape")
|
||||
if scene_img_data:
|
||||
scene_image_url = scene_img_data['local_path']
|
||||
v_query = scene_data.get('video_query', scene_data['title'])
|
||||
|
||||
# Fetch Video for scene
|
||||
video_res = fetch_video(f"{data['title']} {v_query}")
|
||||
video_path = video_res['local_path'] if video_res else ""
|
||||
|
||||
# Fetch Image as fallback/thumbnail for scene
|
||||
image_res = fetch_first(f"{data['title']} {v_query}", orientation="landscape")
|
||||
image_path = image_res['local_path'] if image_res else ""
|
||||
|
||||
Scene.objects.create(
|
||||
project=project,
|
||||
number=i+1,
|
||||
title=scene_data['title'],
|
||||
description=scene_data['description'],
|
||||
visual_prompt=scene_data.get('visual_query', ''),
|
||||
image_url=scene_image_url
|
||||
visual_prompt=v_query,
|
||||
image_url=image_path,
|
||||
video_url=video_path
|
||||
)
|
||||
|
||||
messages.success(request, f"Super Produção '{project.title}' criada e salva na Biblioteca!")
|
||||
# Overall production video (using the first scene's video as main)
|
||||
if project.scenes.exists():
|
||||
project.video_url = project.scenes.first().video_url
|
||||
project.save()
|
||||
|
||||
messages.success(request, f"Produção '{project.title}' gerada com Sucesso! Banner e Vídeos salvos.")
|
||||
return redirect('production_library')
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f"Erro ao processar resposta da AI: {str(e)}")
|
||||
messages.error(request, f"Erro ao processar: {str(e)}")
|
||||
else:
|
||||
messages.error(request, f"Erro na API de IA: {response.get('error')}")
|
||||
messages.error(request, f"Erro AI: {response.get('error')}")
|
||||
|
||||
return redirect('studio_ai')
|
||||
|
||||
@studio_admin_required
|
||||
def edit_production(request, slug):
|
||||
"""View to edit an existing production's metadata."""
|
||||
project = get_object_or_404(Project, slug=slug)
|
||||
if request.method == "POST":
|
||||
project.title = request.POST.get("title", project.title)
|
||||
project.description = request.POST.get("description", project.description)
|
||||
project.category = request.POST.get("category", project.category)
|
||||
project.estimated_budget = request.POST.get("budget", project.estimated_budget)
|
||||
project.duration = request.POST.get("duration", project.duration)
|
||||
project.rating = request.POST.get("rating", project.rating)
|
||||
project.save()
|
||||
messages.success(request, f"Produção '{project.title}' atualizada com sucesso.")
|
||||
return redirect('production_library')
|
||||
|
||||
return render(request, "core/edit_production.html", {"project": project})
|
||||
|
||||
@studio_admin_required
|
||||
def delete_production(request, slug):
|
||||
"""View to delete a production."""
|
||||
project = get_object_or_404(Project, slug=slug)
|
||||
title = project.title
|
||||
project.delete()
|
||||
messages.success(request, f"Produção '{title}' excluída.")
|
||||
return redirect('production_library')
|
||||
|
||||
@studio_admin_required
|
||||
def production_library(request):
|
||||
"""View to see all completed AI productions."""
|
||||
@ -202,21 +242,9 @@ def asset_library(request):
|
||||
'PROP': assets.filter(asset_type='PROP'),
|
||||
'ENV': assets.filter(asset_type='ENV'),
|
||||
}
|
||||
|
||||
context = {
|
||||
"assets": assets,
|
||||
"asset_types": asset_types,
|
||||
}
|
||||
return render(request, "core/asset_library.html", context)
|
||||
return render(request, "core/asset_library.html", {"assets": assets, "asset_types": asset_types})
|
||||
|
||||
def project_detail(request, slug):
|
||||
"""Render the detailed pipeline for a specific production."""
|
||||
project = get_object_or_404(Project.objects.prefetch_related('steps', 'assets', 'scenes'), slug=slug)
|
||||
|
||||
context = {
|
||||
"project": project,
|
||||
"steps": project.steps.all(),
|
||||
"assets": project.assets.all(),
|
||||
"scenes": project.scenes.all(),
|
||||
}
|
||||
return render(request, "core/project_detail.html", context)
|
||||
return render(request, "core/project_detail.html", {"project": project})
|
||||
|
||||
BIN
static/images/pexels/13245968.jpg
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
static/images/pexels/13595070.jpg
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
static/images/pexels/25000648.jpg
Normal file
|
After Width: | Height: | Size: 221 KiB |
BIN
static/images/pexels/31730155.jpg
Normal file
|
After Width: | Height: | Size: 418 KiB |
BIN
static/images/pexels/33987758.jpg
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
static/images/pexels/34177179.jpg
Normal file
|
After Width: | Height: | Size: 314 KiB |
BIN
static/images/pexels/3760790.jpg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
static/images/pexels/5504684.jpg
Normal file
|
After Width: | Height: | Size: 209 KiB |
BIN
static/images/pexels/5965516.jpg
Normal file
|
After Width: | Height: | Size: 242 KiB |
BIN
static/images/pexels/6828563.jpg
Normal file
|
After Width: | Height: | Size: 376 KiB |
BIN
static/images/pexels/8371738.jpg
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
static/images/pexels/8898601.jpg
Normal file
|
After Width: | Height: | Size: 256 KiB |