Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59c4fafbc6 | ||
|
|
570bafbe47 | ||
|
|
e17e54022a | ||
|
|
91243baf92 | ||
|
|
a195d0853c | ||
|
|
cfc1f5d70a | ||
|
|
a13d1c0105 |
BIN
ai/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
ai/__pycache__/local_ai_api.cpython-311.pyc
Normal file
@ -1,35 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
LocalAIApi — lightweight Python client for the Flatlogic AI proxy.
|
LocalAIApi — lightweight Python client for the Flatlogic AI proxy.
|
||||||
|
|
||||||
Usage (inside the Django workspace):
|
|
||||||
|
|
||||||
from ai.local_ai_api import LocalAIApi
|
|
||||||
|
|
||||||
response = LocalAIApi.create_response({
|
|
||||||
"input": [
|
|
||||||
{"role": "system", "content": "You are a helpful assistant."},
|
|
||||||
{"role": "user", "content": "Summarise this text in two sentences."},
|
|
||||||
],
|
|
||||||
"text": {"format": {"type": "json_object"}},
|
|
||||||
})
|
|
||||||
|
|
||||||
if response.get("success"):
|
|
||||||
data = LocalAIApi.decode_json_from_response(response)
|
|
||||||
# ...
|
|
||||||
|
|
||||||
# Typical successful payload (truncated):
|
|
||||||
# {
|
|
||||||
# "id": "resp_xxx",
|
|
||||||
# "status": "completed",
|
|
||||||
# "output": [
|
|
||||||
# {"type": "reasoning", "summary": []},
|
|
||||||
# {"type": "message", "content": [{"type": "output_text", "text": "Your final answer here."}]}
|
|
||||||
# ],
|
|
||||||
# "usage": { "input_tokens": 123, "output_tokens": 456 }
|
|
||||||
# }
|
|
||||||
|
|
||||||
The helper automatically injects the project UUID header and falls back to
|
|
||||||
reading executor/.env if environment variables are missing.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@ -145,6 +115,7 @@ def request(path: Optional[str], payload: Dict[str, Any], options: Optional[Dict
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
cfg["project_header"]: project_uuid,
|
cfg["project_header"]: project_uuid,
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||||
}
|
}
|
||||||
extra_headers = options.get("headers")
|
extra_headers = options.get("headers")
|
||||||
if isinstance(extra_headers, Iterable):
|
if isinstance(extra_headers, Iterable):
|
||||||
@ -180,6 +151,7 @@ def fetch_status(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -
|
|||||||
headers: Dict[str, str] = {
|
headers: Dict[str, str] = {
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
cfg["project_header"]: project_uuid,
|
cfg["project_header"]: project_uuid,
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||||
}
|
}
|
||||||
extra_headers = options.get("headers")
|
extra_headers = options.get("headers")
|
||||||
if isinstance(extra_headers, Iterable):
|
if isinstance(extra_headers, Iterable):
|
||||||
@ -294,7 +266,6 @@ def _extract_text(response: Dict[str, Any]) -> str:
|
|||||||
return payload
|
return payload
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _config() -> Dict[str, Any]:
|
def _config() -> Dict[str, Any]:
|
||||||
global _CONFIG_CACHE # noqa: PLW0603
|
global _CONFIG_CACHE # noqa: PLW0603
|
||||||
if _CONFIG_CACHE is not None:
|
if _CONFIG_CACHE is not None:
|
||||||
@ -314,13 +285,12 @@ def _config() -> Dict[str, Any]:
|
|||||||
"project_id": project_id,
|
"project_id": project_id,
|
||||||
"project_uuid": os.getenv("PROJECT_UUID"),
|
"project_uuid": os.getenv("PROJECT_UUID"),
|
||||||
"project_header": os.getenv("AI_PROJECT_HEADER", "project-uuid"),
|
"project_header": os.getenv("AI_PROJECT_HEADER", "project-uuid"),
|
||||||
"default_model": os.getenv("AI_DEFAULT_MODEL", "gpt-5-mini"),
|
"default_model": os.getenv("AI_DEFAULT_MODEL", "gpt-4o-mini"),
|
||||||
"timeout": int(os.getenv("AI_TIMEOUT", "30")),
|
"timeout": int(os.getenv("AI_TIMEOUT", "30")),
|
||||||
"verify_tls": os.getenv("AI_VERIFY_TLS", "true").lower() not in {"0", "false", "no"},
|
"verify_tls": os.getenv("AI_VERIFY_TLS", "true").lower() not in {"0", "false", "no"},
|
||||||
}
|
}
|
||||||
return _CONFIG_CACHE
|
return _CONFIG_CACHE
|
||||||
|
|
||||||
|
|
||||||
def _build_url(path: str, base_url: str) -> str:
|
def _build_url(path: str, base_url: str) -> str:
|
||||||
trimmed = path.strip()
|
trimmed = path.strip()
|
||||||
if trimmed.startswith("http://") or trimmed.startswith("https://"):
|
if trimmed.startswith("http://") or trimmed.startswith("https://"):
|
||||||
@ -329,7 +299,6 @@ def _build_url(path: str, base_url: str) -> str:
|
|||||||
return f"{base_url}{trimmed}"
|
return f"{base_url}{trimmed}"
|
||||||
return f"{base_url}/{trimmed}"
|
return f"{base_url}/{trimmed}"
|
||||||
|
|
||||||
|
|
||||||
def _resolve_status_path(ai_request_id: Any, cfg: Dict[str, Any]) -> str:
|
def _resolve_status_path(ai_request_id: Any, cfg: Dict[str, Any]) -> str:
|
||||||
base_path = (cfg.get("responses_path") or "").rstrip("/")
|
base_path = (cfg.get("responses_path") or "").rstrip("/")
|
||||||
if not base_path:
|
if not base_path:
|
||||||
@ -338,7 +307,6 @@ def _resolve_status_path(ai_request_id: Any, cfg: Dict[str, Any]) -> str:
|
|||||||
base_path = f"{base_path}/ai-request"
|
base_path = f"{base_path}/ai-request"
|
||||||
return f"{base_path}/{ai_request_id}/status"
|
return f"{base_path}/{ai_request_id}/status"
|
||||||
|
|
||||||
|
|
||||||
def _http_request(url: str, method: str, body: Optional[bytes], headers: Dict[str, str],
|
def _http_request(url: str, method: str, body: Optional[bytes], headers: Dict[str, str],
|
||||||
timeout: int, verify_tls: bool) -> Dict[str, Any]:
|
timeout: int, verify_tls: bool) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
@ -413,7 +381,7 @@ def _ensure_env_loaded() -> None:
|
|||||||
continue
|
continue
|
||||||
key, value = stripped.split("=", 1)
|
key, value = stripped.split("=", 1)
|
||||||
key = key.strip()
|
key = key.strip()
|
||||||
value = value.strip().strip('\'"')
|
value = value.strip().strip('"')
|
||||||
if key and not os.getenv(key):
|
if key and not os.getenv(key):
|
||||||
os.environ[key] = value
|
os.environ[key] = value
|
||||||
except OSError:
|
except OSError:
|
||||||
|
|||||||
@ -14,13 +14,13 @@ Including another URLconf
|
|||||||
1. Import the include() function: from django.urls import include, path
|
1. Import the include() function: from django.urls import include, path
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
from django.contrib import admin
|
# from django.contrib import admin # Removed standard admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("admin/", admin.site.urls),
|
# path("admin/", admin.site.urls), # Removed standard admin path
|
||||||
path("", include("core.urls")),
|
path("", include("core.urls")),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
BIN
core/__pycache__/pexels.cpython-311.pyc
Normal file
@ -1,3 +1,41 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from .models import Project, PipelineStep, CgiAsset, Scene, StudioConfig
|
||||||
|
|
||||||
# Register your models here.
|
class PipelineStepInline(admin.TabularInline):
|
||||||
|
model = PipelineStep
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
class CgiAssetInline(admin.TabularInline):
|
||||||
|
model = CgiAsset
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
class SceneInline(admin.TabularInline):
|
||||||
|
model = Scene
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
@admin.register(Project)
|
||||||
|
class ProjectAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('title', 'project_type', 'category', 'status', 'is_ai_generated', 'created_at')
|
||||||
|
list_filter = ('project_type', 'status', 'is_ai_generated')
|
||||||
|
search_fields = ('title', 'description')
|
||||||
|
prepopulated_fields = {'slug': ('title',)}
|
||||||
|
inlines = [PipelineStepInline, CgiAssetInline, SceneInline]
|
||||||
|
|
||||||
|
@admin.register(PipelineStep)
|
||||||
|
class PipelineStepAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('project', 'name', 'progress', 'is_completed')
|
||||||
|
list_filter = ('name', 'is_completed')
|
||||||
|
|
||||||
|
@admin.register(CgiAsset)
|
||||||
|
class CgiAssetAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'project', 'asset_type', 'is_realistic')
|
||||||
|
list_filter = ('asset_type', 'is_realistic')
|
||||||
|
|
||||||
|
@admin.register(Scene)
|
||||||
|
class SceneAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('project', 'number', 'title')
|
||||||
|
list_filter = ('project',)
|
||||||
|
|
||||||
|
@admin.register(StudioConfig)
|
||||||
|
class StudioConfigAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'admin_access_key', 'is_setup')
|
||||||
54
core/migrations/0001_initial.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-16 00:18
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Project',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=255)),
|
||||||
|
('slug', models.SlugField(blank=True, unique=True)),
|
||||||
|
('project_type', models.CharField(choices=[('MOVIE', 'Feature Film'), ('SERIES', 'TV Series'), ('SHORT', 'Short Film')], default='MOVIE', max_length=10)),
|
||||||
|
('status', models.CharField(choices=[('PRE', 'Pre-Production'), ('PROD', 'Production'), ('POST', 'Post-Production'), ('DONE', 'Completed')], default='PRE', max_length=10)),
|
||||||
|
('description', models.TextField(blank=True)),
|
||||||
|
('thumbnail_url', models.URLField(blank=True, help_text='URL to a representative image')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PipelineStep',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(choices=[('CONCEPT', 'Concept & Storyboard'), ('MODELING', '3D Modeling'), ('RIGGING', 'Rigging'), ('ANIMATION', 'Animation'), ('LIGHTING', 'Lighting & FX'), ('RENDERING', 'Rendering'), ('COMPOSITING', 'Compositing')], max_length=20)),
|
||||||
|
('progress', models.PositiveIntegerField(default=0, help_text='Progress from 0 to 100')),
|
||||||
|
('is_completed', models.BooleanField(default=False)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='steps', to='core.project')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['id'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CgiAsset',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=255)),
|
||||||
|
('asset_type', models.CharField(choices=[('CHAR', 'Character'), ('PROP', 'Prop'), ('ENV', 'Environment')], max_length=10)),
|
||||||
|
('is_realistic', models.BooleanField(default=True)),
|
||||||
|
('current_stage', models.CharField(default='Modeling', max_length=100)),
|
||||||
|
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='core.project')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
18
core/migrations/0002_alter_pipelinestep_name.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-16 00:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='pipelinestep',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(choices=[('SCRIPT', 'Roteiro & Storyboard'), ('CONCEPT', 'Concept Art'), ('ANIMATIC', 'Animatic'), ('MODELING', 'Modelagem 3D'), ('TEXTURING', 'Texturização'), ('RIGGING', 'Rigging'), ('ANIMATION', 'Animação'), ('LIGHTING', 'Iluminação'), ('FX', 'Simulação (FX)'), ('RENDERING', 'Renderização'), ('COMPOSITING', 'Composição'), ('EDITING', 'Edição & Sonoplastia')], max_length=20),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-16 00:39
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0002_alter_pipelinestep_name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='StudioConfig',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('admin_access_key', models.CharField(max_length=100, unique=True)),
|
||||||
|
('is_setup', models.BooleanField(default=False)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cgiasset',
|
||||||
|
name='assigned_artist',
|
||||||
|
field=models.CharField(blank=True, max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cgiasset',
|
||||||
|
name='file_location',
|
||||||
|
field=models.CharField(blank=True, help_text='Path or URL to the digital file', max_length=500),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cgiasset',
|
||||||
|
name='version',
|
||||||
|
field=models.PositiveIntegerField(default=1),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-16 01:12
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0003_studioconfig_cgiasset_assigned_artist_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cgiasset',
|
||||||
|
name='physical_description',
|
||||||
|
field=models.TextField(blank=True, help_text='Detailed physical appearance based on real humans'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='category',
|
||||||
|
field=models.CharField(default='Sci-Fi', max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='full_script',
|
||||||
|
field=models.TextField(blank=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='is_ai_generated',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Scene',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('number', models.PositiveIntegerField(default=1)),
|
||||||
|
('title', models.CharField(max_length=255)),
|
||||||
|
('description', models.TextField()),
|
||||||
|
('visual_prompt', models.TextField(blank=True)),
|
||||||
|
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scenes', to='core.project')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['number'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-16 02:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0004_cgiasset_physical_description_project_category_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='duration',
|
||||||
|
field=models.CharField(blank=True, default='120 min', max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='estimated_budget',
|
||||||
|
field=models.CharField(blank=True, default='$150M', max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='rating',
|
||||||
|
field=models.CharField(default='PG-13', max_length=10),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='scene',
|
||||||
|
name='image_url',
|
||||||
|
field=models.CharField(blank=True, max_length=500),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
143
core/models.py
@ -1,3 +1,144 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.text import slugify
|
||||||
|
import uuid
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
# Create your models here.
|
class StudioConfig(models.Model):
|
||||||
|
"""Singleton model to store studio-wide settings and the unique admin key."""
|
||||||
|
admin_access_key = models.CharField(max_length=100, unique=True)
|
||||||
|
is_setup = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.admin_access_key:
|
||||||
|
self.admin_access_key = "61823dbc-ee05-455f-8924-764f15104fc1"
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Studio Configuration"
|
||||||
|
|
||||||
|
class Project(models.Model):
|
||||||
|
TYPES = (
|
||||||
|
('MOVIE', 'Feature Film'),
|
||||||
|
('SERIES', 'TV Series'),
|
||||||
|
('DOCUMENTARY', 'Documentary'),
|
||||||
|
('SHORT', 'Short Film'),
|
||||||
|
)
|
||||||
|
STATUS_CHOICES = (
|
||||||
|
('PRE', 'Pre-Production'),
|
||||||
|
('PROD', 'Production'),
|
||||||
|
('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=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, 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)
|
||||||
|
|
||||||
|
# New fields for "Super Production"
|
||||||
|
estimated_budget = models.CharField(max_length=100, blank=True, default="$150M")
|
||||||
|
rating = models.CharField(max_length=10, default="PG-13")
|
||||||
|
duration = models.CharField(max_length=50, blank=True, default="120 min")
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.slug:
|
||||||
|
base_slug = slugify(self.title)
|
||||||
|
if not base_slug:
|
||||||
|
base_slug = "project"
|
||||||
|
|
||||||
|
unique_slug = base_slug
|
||||||
|
while Project.objects.filter(slug=unique_slug).exists():
|
||||||
|
random_string = ''.join(random.choices(string.ascii_lowercase + string.digits, k=4))
|
||||||
|
unique_slug = f"{base_slug}-{random_string}"
|
||||||
|
|
||||||
|
self.slug = unique_slug
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
class Scene(models.Model):
|
||||||
|
project = models.ForeignKey(Project, related_name='scenes', on_delete=models.CASCADE)
|
||||||
|
number = models.PositiveIntegerField(default=1)
|
||||||
|
title = models.CharField(max_length=255)
|
||||||
|
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']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Scene {self.number}: {self.title}"
|
||||||
|
|
||||||
|
class PipelineStep(models.Model):
|
||||||
|
STAGES = (
|
||||||
|
('SCRIPT', 'Roteiro & Storyboard'),
|
||||||
|
('CONCEPT', 'Concept Art'),
|
||||||
|
('ANIMATIC', 'Animatic'),
|
||||||
|
('MODELING', 'Modelagem 3D'),
|
||||||
|
('TEXTURING', 'Texturização'),
|
||||||
|
('RIGGING', 'Rigging'),
|
||||||
|
('ANIMATION', 'Animação'),
|
||||||
|
('LIGHTING', 'Iluminação'),
|
||||||
|
('FX', 'Simulação (FX)'),
|
||||||
|
('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)
|
||||||
|
is_completed = models.BooleanField(default=False)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['id']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.project.title} - {self.get_name_display()}"
|
||||||
|
|
||||||
|
class CgiAsset(models.Model):
|
||||||
|
ASSET_TYPES = (
|
||||||
|
('CHAR', 'Character'),
|
||||||
|
('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)
|
||||||
|
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)
|
||||||
|
assigned_artist = models.CharField(max_length=100, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name} ({self.get_asset_type_display()})"
|
||||||
91
core/pexels.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import os
|
||||||
|
import requests
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
API_KEY = os.getenv("PEXELS_KEY", "Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18")
|
||||||
|
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:
|
||||||
|
return None
|
||||||
|
|
||||||
|
headers = {"Authorization": API_KEY}
|
||||||
|
url = "https://api.pexels.com/v1/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()
|
||||||
|
|
||||||
|
photo = (data.get("photos") or [None])[0]
|
||||||
|
if not photo:
|
||||||
|
return None
|
||||||
|
|
||||||
|
src = photo["src"].get("large2x") or photo["src"].get("large") or photo["src"].get("original")
|
||||||
|
IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
target = IMAGE_CACHE_DIR / f"{photo['id']}.jpg"
|
||||||
|
|
||||||
|
if src and not target.exists():
|
||||||
|
img_resp = requests.get(src, timeout=15)
|
||||||
|
img_resp.raise_for_status()
|
||||||
|
target.write_bytes(img_resp.content)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": photo["id"],
|
||||||
|
"local_path": f"images/pexels/{photo['id']}.jpg",
|
||||||
|
"photographer": photo.get("photographer"),
|
||||||
|
}
|
||||||
|
except Exception as 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
|
||||||
@ -1,25 +1,141 @@
|
|||||||
|
{% load static %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
{% if project_description %}
|
<title>{% block title %}CGI Studio{% endblock %}</title>
|
||||||
<meta name="description" content="{{ project_description }}">
|
<!-- Fonts -->
|
||||||
<meta property="og:description" content="{{ project_description }}">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<meta property="twitter:description" content="{{ project_description }}">
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
{% endif %}
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&family=Outfit:wght@300;500;700;900&family=Syncopate:wght@400;700&display=swap" rel="stylesheet">
|
||||||
{% if project_image_url %}
|
<!-- Bootstrap CSS -->
|
||||||
<meta property="og:image" content="{{ project_image_url }}">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
<!-- Bootstrap Icons -->
|
||||||
{% endif %}
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||||
{% load static %}
|
<!-- Custom CSS -->
|
||||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={% now 'U' %}">
|
||||||
{% block head %}{% endblock %}
|
<style>
|
||||||
|
:root {
|
||||||
|
--electric-cyan: #00e5ff;
|
||||||
|
--neon-purple: #7000ff;
|
||||||
|
--bg-deep: #0a0a0c;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
background-color: var(--bg-deep);
|
||||||
|
color: #f8f9fa;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, .font-syncopate {
|
||||||
|
font-family: 'Syncopate', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
.outfit { font-family: 'Outfit', sans-serif; }
|
||||||
|
|
||||||
|
.studio-navbar {
|
||||||
|
background: rgba(10, 10, 12, 0.8) !important;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-bottom: 1px solid rgba(0, 229, 255, 0.1);
|
||||||
|
}
|
||||||
|
.text-cyan { color: var(--electric-cyan) !important; }
|
||||||
|
.text-purple { color: var(--neon-purple) !important; }
|
||||||
|
.bg-purple { background-color: var(--neon-purple) !important; }
|
||||||
|
.glass-effect {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
.shadow-neon {
|
||||||
|
box-shadow: 0 0 15px rgba(0, 229, 255, 0.2);
|
||||||
|
}
|
||||||
|
.text-gradient-neon {
|
||||||
|
background: linear-gradient(45deg, var(--electric-cyan), var(--neon-purple));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
.btn-neon-purple {
|
||||||
|
background: var(--neon-purple);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 0 15px rgba(112, 0, 255, 0.3);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.btn-neon-purple:hover {
|
||||||
|
background: #8221ff;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 0 25px rgba(112, 0, 255, 0.5);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
.btn-outline-cyan {
|
||||||
|
border: 2px solid var(--electric-cyan);
|
||||||
|
color: var(--electric-cyan);
|
||||||
|
background: transparent;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.btn-outline-cyan:hover {
|
||||||
|
background: var(--electric-cyan);
|
||||||
|
color: var(--bg-deep);
|
||||||
|
box-shadow: 0 0 20px rgba(0, 229, 255, 0.4);
|
||||||
|
}
|
||||||
|
.glass-card {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% block extra_head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{% block content %}{% endblock %}
|
<nav class="navbar navbar-expand-lg navbar-dark studio-navbar sticky-top">
|
||||||
</body>
|
<div class="container">
|
||||||
|
<a class="navbar-brand font-syncopate fw-bold" href="{% url 'home' %}">
|
||||||
|
<span class="text-cyan">CGI</span> STUDIO
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'home' %}">Command Center</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'asset_library' %}">Assets</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'production_library' %}">Biblioteca AI</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link fw-bold text-cyan" href="{% url 'studio_ai' %}"><i class="bi bi-cpu-fill"></i> Studio AI</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
{% if messages %}
|
||||||
|
<div class="container mt-3">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert {% if message.tags %}alert-{{ message.tags }}{% endif %} bg-dark border-{{ message.tags }} text-white shadow-neon">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="py-5 mt-5 border-top border-secondary border-opacity-10">
|
||||||
|
<div class="container text-center">
|
||||||
|
<p class="text-muted small">© 2026 CGI Virtual Studio. Powered by AI Technology.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Bootstrap JS -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
{% block extra_js %}{% endblock %}
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
39
core/templates/core/admin_login.html
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container d-flex align-items-center justify-content-center" style="min-height: 70vh;">
|
||||||
|
<div class="card bg-dark border-cyan glass-effect p-5" style="max-width: 450px; width: 100%;">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<i class="bi bi-shield-lock-fill text-purple mb-3" style="font-size: 3rem;"></i>
|
||||||
|
<h2 class="text-cyan font-syncopate">Acesso Restrito</h2>
|
||||||
|
<p class="text-secondary">Insira sua Chave Privada de Administrador para prosseguir.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<input type="password" name="access_key" class="form-control bg-transparent text-white border-purple p-3" placeholder="Sua Chave Privada..." required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-cyan w-100 p-3 shadow-neon">
|
||||||
|
Validar Identidade <i class="bi bi-chevron-right"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<a href="{% url 'home' %}" class="text-secondary text-decoration-none small">
|
||||||
|
<i class="bi bi-arrow-left"></i> Voltar para o Painel Público
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.border-cyan { border: 1px solid var(--electric-cyan) !important; }
|
||||||
|
.border-purple { border: 1px solid var(--neon-purple) !important; }
|
||||||
|
.shadow-neon:hover {
|
||||||
|
box-shadow: 0 0 20px var(--electric-cyan);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.font-syncopate { font-family: 'Syncopate', sans-serif; }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
89
core/templates/core/asset_library.html
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-5">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-cyan font-syncopate display-4">Biblioteca de Assets</h1>
|
||||||
|
<p class="text-secondary">Catálogo central de personagens, cenários e objetos digitais.</p>
|
||||||
|
</div>
|
||||||
|
<div class="glass-effect p-3 border-purple rounded">
|
||||||
|
<span class="text-purple small d-block">Total de Assets</span>
|
||||||
|
<span class="h3 text-white mb-0">{{ assets|length }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Asset Tabs/Filters -->
|
||||||
|
<ul class="nav nav-pills mb-4" id="assetTabs" role="tablist">
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link active btn-outline-cyan me-2" id="all-tab" data-bs-toggle="pill" data-bs-target="#all">Todos</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link btn-outline-purple me-2" id="char-tab" data-bs-toggle="pill" data-bs-target="#char">Personagens</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link btn-outline-purple me-2" id="prop-tab" data-bs-toggle="pill" data-bs-target="#prop">Props</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link btn-outline-purple" id="env-tab" data-bs-toggle="pill" data-bs-target="#env">Cenários</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content" id="assetTabsContent">
|
||||||
|
<!-- All Assets -->
|
||||||
|
<div class="tab-pane fade show active" id="all">
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for asset in assets %}
|
||||||
|
<div class="col-md-4 col-lg-3">
|
||||||
|
<div class="card h-100 bg-dark border-secondary glass-effect hover-neon">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span class="badge bg-purple">{{ asset.get_asset_type_display }}</span>
|
||||||
|
<span class="text-secondary small">v{{ asset.version }}</span>
|
||||||
|
</div>
|
||||||
|
<h5 class="card-title text-white mb-1">{{ asset.name }}</h5>
|
||||||
|
<p class="text-secondary small mb-3">Projeto: {{ asset.project.title }}</p>
|
||||||
|
|
||||||
|
<div class="border-top border-secondary pt-2">
|
||||||
|
<span class="text-cyan small"><i class="bi bi-gear"></i> {{ asset.current_stage }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="col-12 text-center py-5">
|
||||||
|
<p class="text-secondary">Nenhum asset cadastrado ainda.</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Personagens -->
|
||||||
|
<div class="tab-pane fade" id="char">
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for asset in asset_types.CHAR %}
|
||||||
|
<div class="col-md-4 col-lg-3">
|
||||||
|
<!-- Similar card as above -->
|
||||||
|
<div class="card h-100 bg-dark border-purple glass-effect">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="text-white">{{ asset.name }}</h5>
|
||||||
|
<p class="text-secondary small">{{ asset.project.title }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- (Other tabs could be detailed further if needed) -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.nav-pills .nav-link { color: white; border: 1px solid transparent; }
|
||||||
|
.nav-pills .nav-link.active { background-color: transparent !important; border-color: var(--electric-cyan); color: var(--electric-cyan); }
|
||||||
|
.hover-neon:hover { border-color: var(--electric-cyan) !important; box-shadow: 0 0 10px rgba(0,229,255,0.3); }
|
||||||
|
.btn-outline-cyan { border: 1px solid var(--electric-cyan); }
|
||||||
|
.btn-outline-purple { border: 1px solid var(--neon-purple); }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
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 %}
|
||||||
@ -1,145 +1,110 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}{{ project_name }}{% endblock %}
|
{% block content %}
|
||||||
|
<div class="container-fluid py-5 px-lg-5">
|
||||||
|
<!-- Header/Dashboard Info -->
|
||||||
|
<div class="row align-items-center mb-5">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h1 class="display-3 text-cyan font-syncopate mb-0">STUDIO COMMAND CENTER</h1>
|
||||||
|
<p class="lead text-purple fw-bold mb-4">Gerenciamento de Super-Produções CGI</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6 text-lg-end">
|
||||||
|
<div class="d-inline-block p-3 glass-effect border-cyan rounded-3 text-center me-3">
|
||||||
|
<span class="d-block text-secondary small">Projetos Ativos</span>
|
||||||
|
<span class="h2 text-white mb-0">{{ active_productions }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-inline-block p-3 glass-effect border-purple rounded-3 text-center">
|
||||||
|
<span class="d-block text-secondary small">Status do Sistema</span>
|
||||||
|
<span class="h2 text-cyan mb-0">ONLINE</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation / Quick Actions -->
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="glass-effect p-3 border-secondary rounded-3 d-flex justify-content-between align-items-center">
|
||||||
|
<div class="d-flex gap-3">
|
||||||
|
<a href="{% url 'asset_library' %}" class="btn btn-outline-purple shadow-neon">
|
||||||
|
<i class="bi bi-box-seam me-2"></i> Biblioteca de Assets
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'studio_ai' %}" class="btn btn-neon-purple">
|
||||||
|
<i class="bi bi-magic me-2"></i> STUDIO AI (Criador Automático)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{% if is_admin %}
|
||||||
|
<span class="badge bg-purple p-2 me-2"><i class="bi bi-person-check-fill"></i> MODO ADMIN</span>
|
||||||
|
<a href="{% url 'admin_logout' %}" class="btn btn-sm btn-outline-danger">Sair</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'admin_login' %}" class="text-secondary small text-decoration-none">
|
||||||
|
<i class="bi bi-lock"></i> Acesso Restrito
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Grid -->
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for project in projects %}
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card bg-dark border-secondary h-100 glass-effect overflow-hidden hover-border-cyan">
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-4 bg-secondary d-flex align-items-center justify-content-center" style="min-height: 200px; background: linear-gradient(45deg, #0a0a0c, #121214);">
|
||||||
|
{% if project.thumbnail_url %}
|
||||||
|
<img src="{{ project.thumbnail_url }}" alt="{{ project.title }}" class="img-fluid object-fit-cover h-100">
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center">
|
||||||
|
<i class="bi bi-camera-reels text-purple display-1"></i>
|
||||||
|
{% if project.is_ai_generated %}
|
||||||
|
<div class="mt-2"><span class="badge bg-purple small">AI GENERATED</span></div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
|
<h3 class="card-title text-white font-syncopate h4 mb-0">{{ project.title }}</h3>
|
||||||
|
<div class="text-end">
|
||||||
|
<span class="badge {% if project.status == 'PROD' %}bg-cyan text-dark{% else %}bg-purple{% endif %} d-block mb-1">
|
||||||
|
{{ project.get_status_display }}
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-dark border border-secondary small">{{ project.category }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-secondary small mb-4">{{ project.description|truncatewords:20 }}</p>
|
||||||
|
|
||||||
|
<!-- Pipeline Progress Mini -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="d-flex justify-content-between small text-secondary mb-1">
|
||||||
|
<span>Status Pipeline</span>
|
||||||
|
<span>{% with last=project.steps.last %}{{ last.progress|default:0 }}{% endwith %}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress bg-black" style="height: 6px;">
|
||||||
|
<div class="progress-bar bg-cyan shadow-neon" role="progressbar" style="width: {% with last=project.steps.last %}{{ last.progress|default:0 }}{% endwith %}%;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{% url 'project_detail' project.slug %}" class="btn btn-link text-cyan p-0 text-decoration-none">
|
||||||
|
Ver Pipeline Detalhado <i class="bi bi-arrow-right ms-1"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</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>
|
<style>
|
||||||
:root {
|
.hover-border-cyan:hover {
|
||||||
--bg-color-start: #6a11cb;
|
border-color: var(--electric-cyan) !important;
|
||||||
--bg-color-end: #2575fc;
|
transition: 0.3s ease;
|
||||||
--text-color: #ffffff;
|
|
||||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
|
||||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
|
||||||
color: var(--text-color);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
text-align: center;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
|
||||||
animation: bg-pan 20s linear infinite;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bg-pan {
|
|
||||||
0% {
|
|
||||||
background-position: 0% 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
background-position: 100% 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: var(--card-bg-color);
|
|
||||||
border: 1px solid var(--card-border-color);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 2.5rem 2rem;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 0 1.2rem;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
opacity: 0.92;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader {
|
|
||||||
margin: 1.5rem auto;
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
|
||||||
border-top-color: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.runtime code {
|
|
||||||
background: rgba(0, 0, 0, 0.25);
|
|
||||||
padding: 0.15rem 0.45rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sr-only {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
padding: 0;
|
|
||||||
margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
}
|
||||||
|
.bg-cyan { background-color: var(--electric-cyan) !important; }
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% 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 %}
|
|
||||||
109
core/templates/core/production_library.html
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<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-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-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 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-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="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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
.production-img {
|
||||||
|
height: 250px;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.6s;
|
||||||
|
}
|
||||||
|
.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 %}
|
||||||
143
core/templates/core/project_detail.html
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{{ project.title }} | Studio AI Production{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="hero-section py-5">
|
||||||
|
<div class="container">
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'home' %}" class="text-cyan text-decoration-none">Command Center</a></li>
|
||||||
|
<li class="breadcrumb-item active text-muted" aria-current="page">{{ project.title }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<span class="pipeline-badge badge-{{ project.status|lower }} me-2">{{ project.get_status_display }}</span>
|
||||||
|
{% if project.is_ai_generated %}
|
||||||
|
<span class="badge bg-purple text-white rounded-pill px-3 py-2"><i class="bi bi-robot me-1"></i> AI GENERATED</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="badge bg-dark border border-secondary rounded-pill px-3 py-2 ms-2">{{ project.category }}</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="display-4 outfit mb-3 text-gradient-neon">{{ project.title }}</h1>
|
||||||
|
<p class="lead text-muted">{{ project.description }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4 text-lg-end">
|
||||||
|
<div class="stats-card bg-opacity-10 border-opacity-10">
|
||||||
|
<span class="text-muted small text-uppercase d-block mb-1">Status da Produção</span>
|
||||||
|
<span class="display-6 outfit fw-bold text-cyan">
|
||||||
|
{% with last_step=project.steps.last %}{{ last_step.progress|default:0 }}{% endwith %}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="py-5">
|
||||||
|
<div class="container">
|
||||||
|
<ul class="nav nav-pills mb-5 justify-content-center" id="prodTab" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active fw-bold px-4" id="pipeline-tab" data-bs-toggle="pill" data-bs-target="#pipeline" type="button" role="tab">PIPELINE CGI</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link fw-bold px-4" id="script-tab" data-bs-toggle="pill" data-bs-target="#script" type="button" role="tab">ROTEIRO & CENAS</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link fw-bold px-4" id="cast-tab" data-bs-toggle="pill" data-bs-target="#cast" type="button" role="tab">PERSONAGENS (REAL)</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content" id="prodTabContent">
|
||||||
|
<!-- Pipeline Tab -->
|
||||||
|
<div class="tab-pane fade show active" id="pipeline" role="tabpanel">
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for step in steps %}
|
||||||
|
<div class="col-md-4 col-lg-3">
|
||||||
|
<div class="project-card p-4 h-100 border-opacity-10">
|
||||||
|
<div class="text-muted small mb-2">ETAPA {{ forloop.counter }}</div>
|
||||||
|
<h4 class="h6 mb-3 outfit">{{ step.get_name_display }}</h4>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<span class="small text-muted">Progresso</span>
|
||||||
|
<span class="small text-cyan">{{ step.progress }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress" style="height: 4px;">
|
||||||
|
<div class="progress-bar {% if step.is_completed %}bg-cyan{% endif %}" style="width: {{ step.progress }}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Script & Scenes Tab -->
|
||||||
|
<div class="tab-pane fade" id="script" role="tabpanel">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-9">
|
||||||
|
{% for scene in scenes %}
|
||||||
|
<div class="scene-item mb-5 p-4 glass-card border-opacity-10 rounded-4">
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<div class="bg-cyan text-black fw-bold rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">
|
||||||
|
{{ scene.number }}
|
||||||
|
</div>
|
||||||
|
<h3 class="h4 outfit mb-0 text-cyan">{{ scene.title }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="scene-body text-light lead-sm border-start border-cyan border-opacity-25 ps-4 ms-3">
|
||||||
|
{{ scene.description|linebreaks }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<p class="text-center text-muted py-5">Nenhuma cena gerada para este roteiro.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cast & Characters Tab -->
|
||||||
|
<div class="tab-pane fade" id="cast" role="tabpanel">
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for asset in assets %}
|
||||||
|
{% if asset.asset_type == 'CHAR' %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="project-card p-4 h-100 border-opacity-10 d-flex gap-4">
|
||||||
|
<div class="char-placeholder rounded-4 bg-dark border border-secondary border-opacity-25 d-flex align-items-center justify-content-center" style="width: 120px; height: 160px; min-width: 120px;">
|
||||||
|
<i class="bi bi-person-bounding-box text-muted display-4"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="outfit text-cyan mb-2">{{ asset.name }}</h4>
|
||||||
|
<span class="badge bg-purple bg-opacity-10 text-purple border border-purple border-opacity-25 mb-3">APARÊNCIA REALISTA</span>
|
||||||
|
<p class="small text-muted mb-0"><strong>Físico:</strong> {{ asset.physical_description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% empty %}
|
||||||
|
<p class="text-center text-muted py-5">Nenhum personagem definido no elenco.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.nav-pills .nav-link {
|
||||||
|
color: rgba(255,255,255,0.5);
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
margin: 0 5px;
|
||||||
|
border-radius: 50px;
|
||||||
|
}
|
||||||
|
.nav-pills .nav-link.active {
|
||||||
|
background-color: #00e5ff;
|
||||||
|
color: #000;
|
||||||
|
box-shadow: 0 0 15px rgba(0,229,255,0.4);
|
||||||
|
}
|
||||||
|
.lead-sm {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
107
core/templates/core/studio_ai.html
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
{% 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 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">
|
||||||
|
<form action="{% url 'generate_production' %}" method="POST" id="ai-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<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 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 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="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="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('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>
|
||||||
|
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 %}
|
||||||
266
core/templates/core/watch_production.html
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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 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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentSceneIndex = 0;
|
||||||
|
const scenes = [
|
||||||
|
{% for scene in project.scenes.all %}
|
||||||
|
{
|
||||||
|
title: "{{ scene.title|escapejs }}",
|
||||||
|
description: "{{ scene.description|escapejs }}",
|
||||||
|
video: "{% static scene.video_url %}",
|
||||||
|
voice: "{{ project.voice_preset }}"
|
||||||
|
},
|
||||||
|
{% endfor %}
|
||||||
|
];
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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 %}
|
||||||
18
core/urls.py
@ -1,7 +1,19 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
from .views import home
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", home, name="home"),
|
path('', views.home, name='home'),
|
||||||
|
path('admin-login/', views.admin_login, name='admin_login'),
|
||||||
|
path('admin-logout/', views.admin_logout, name='admin_logout'),
|
||||||
|
|
||||||
|
path('studio-ai/', views.studio_ai, name='studio_ai'),
|
||||||
|
path('studio-ai/generate/', views.generate_production, name='generate_production'),
|
||||||
|
|
||||||
|
path('library/', views.production_library, name='production_library'),
|
||||||
|
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'),
|
||||||
]
|
]
|
||||||
257
core/views.py
@ -1,25 +1,250 @@
|
|||||||
import os
|
import os
|
||||||
import platform
|
import json
|
||||||
|
from functools import wraps
|
||||||
from django import get_version as django_version
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
from django.shortcuts import render
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
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, fetch_video
|
||||||
|
|
||||||
|
def studio_admin_required(view_func):
|
||||||
|
"""Decorator to restrict access to studio admin only."""
|
||||||
|
@wraps(view_func)
|
||||||
|
def _wrapped_view(request, *args, **kwargs):
|
||||||
|
if not request.session.get('is_studio_admin', False):
|
||||||
|
messages.warning(request, "Acesso restrito. Por favor, insira sua chave de administrador.")
|
||||||
|
return redirect('admin_login')
|
||||||
|
return view_func(request, *args, **kwargs)
|
||||||
|
return _wrapped_view
|
||||||
|
|
||||||
def home(request):
|
def home(request):
|
||||||
"""Render the landing screen with loader and environment details."""
|
"""Render the CGI Studio Command Center."""
|
||||||
host_name = request.get_host().lower()
|
config, created = StudioConfig.objects.get_or_create(id=1)
|
||||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
if created or not config.admin_access_key:
|
||||||
now = timezone.now()
|
config.save()
|
||||||
|
|
||||||
|
projects = Project.objects.prefetch_related('steps').all().order_by('-created_at')
|
||||||
|
|
||||||
|
total_projects = projects.count()
|
||||||
|
active_productions = projects.filter(status='PROD').count()
|
||||||
|
completed_projects = projects.filter(status='DONE').count()
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"project_name": "New Style",
|
"projects": projects,
|
||||||
"agent_brand": agent_brand,
|
"total_projects": total_projects,
|
||||||
"django_version": django_version(),
|
"active_productions": active_productions,
|
||||||
"python_version": platform.python_version(),
|
"completed_projects": completed_projects,
|
||||||
"current_time": now,
|
"current_time": timezone.now(),
|
||||||
"host_name": host_name,
|
"is_admin": request.session.get('is_studio_admin', False),
|
||||||
"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)
|
||||||
|
|
||||||
|
@studio_admin_required
|
||||||
|
def studio_ai(request):
|
||||||
|
"""Page to configure and launch AI-automated productions."""
|
||||||
|
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 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.
|
||||||
|
Theme: {theme}
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
1. Unique Title and a compelling description.
|
||||||
|
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 JSON:
|
||||||
|
{{
|
||||||
|
"title": "...",
|
||||||
|
"description": "...",
|
||||||
|
"full_script": "...",
|
||||||
|
"banner_query": "...",
|
||||||
|
"budget": "...",
|
||||||
|
"rating": "...",
|
||||||
|
"duration": "...",
|
||||||
|
"characters": [
|
||||||
|
{{"name": "...", "description": "...", "voice_preset": "..."}}
|
||||||
|
],
|
||||||
|
"scenes": [
|
||||||
|
{{"title": "...", "description": "...", "video_query": "..."}}
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = LocalAIApi.create_response({
|
||||||
|
"input": [
|
||||||
|
{"role": "system", "content": "You are a Hollywood AI Director creating cinematic video-based productions."},
|
||||||
|
{"role": "user", "content": prompt},
|
||||||
|
],
|
||||||
|
"text": {"format": {"type": "json_object"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
if response.get("success"):
|
||||||
|
try:
|
||||||
|
data = LocalAIApi.decode_json_from_response(response)
|
||||||
|
|
||||||
|
# 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(
|
||||||
|
title=data['title'],
|
||||||
|
project_type=proj_type,
|
||||||
|
category=category,
|
||||||
|
description=data['description'],
|
||||||
|
full_script=data.get('full_script', ''),
|
||||||
|
thumbnail_url=banner_url,
|
||||||
|
banner_url=banner_url,
|
||||||
|
is_ai_generated=True,
|
||||||
|
status='DONE',
|
||||||
|
voice_preset=voice_preset,
|
||||||
|
estimated_budget=data.get('budget', '$200M'),
|
||||||
|
rating=data.get('rating', 'PG-13'),
|
||||||
|
duration=data.get('duration', '130 min')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create default Pipeline Steps
|
||||||
|
for stage in [s[0] for s in PipelineStep.STAGES]:
|
||||||
|
PipelineStep.objects.create(project=project, name=stage, progress=100, is_completed=True)
|
||||||
|
|
||||||
|
# Characters
|
||||||
|
for char in data['characters']:
|
||||||
|
CgiAsset.objects.create(
|
||||||
|
project=project,
|
||||||
|
name=char['name'],
|
||||||
|
asset_type='CHAR',
|
||||||
|
physical_description=char['description'],
|
||||||
|
voice_preset=char.get('voice_preset', 'v_male_1')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Scenes + Videos
|
||||||
|
for i, scene_data in enumerate(data['scenes']):
|
||||||
|
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=v_query,
|
||||||
|
image_url=image_path,
|
||||||
|
video_url=video_path
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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: {str(e)}")
|
||||||
|
else:
|
||||||
|
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."""
|
||||||
|
productions = Project.objects.filter(is_ai_generated=True).order_by('-created_at')
|
||||||
|
return render(request, "core/production_library.html", {"productions": productions})
|
||||||
|
|
||||||
|
@studio_admin_required
|
||||||
|
def watch_production(request, slug):
|
||||||
|
"""View to 'watch' (read and experience) a complete production."""
|
||||||
|
project = get_object_or_404(Project.objects.prefetch_related('assets', 'scenes'), slug=slug)
|
||||||
|
return render(request, "core/watch_production.html", {"project": project})
|
||||||
|
|
||||||
|
def admin_login(request):
|
||||||
|
"""View to enter the unique admin access key."""
|
||||||
|
if request.method == "POST":
|
||||||
|
key = request.POST.get("access_key")
|
||||||
|
try:
|
||||||
|
config = StudioConfig.objects.get(id=1)
|
||||||
|
if key == config.admin_access_key:
|
||||||
|
request.session['is_studio_admin'] = True
|
||||||
|
messages.success(request, "Bem-vindo, Comandante do Estúdio!")
|
||||||
|
return redirect('home')
|
||||||
|
else:
|
||||||
|
messages.error(request, "Chave de acesso inválida.")
|
||||||
|
except StudioConfig.DoesNotExist:
|
||||||
|
messages.error(request, "Configuração do estúdio não encontrada.")
|
||||||
|
|
||||||
|
return render(request, "core/admin_login.html")
|
||||||
|
|
||||||
|
def admin_logout(request):
|
||||||
|
"""Logout the studio admin."""
|
||||||
|
request.session['is_studio_admin'] = False
|
||||||
|
return redirect('home')
|
||||||
|
|
||||||
|
@studio_admin_required
|
||||||
|
def asset_library(request):
|
||||||
|
"""View all digital assets (Characters, Props, Environments)."""
|
||||||
|
assets = CgiAsset.objects.select_related('project').all()
|
||||||
|
asset_types = {
|
||||||
|
'CHAR': assets.filter(asset_type='CHAR'),
|
||||||
|
'PROP': assets.filter(asset_type='PROP'),
|
||||||
|
'ENV': assets.filter(asset_type='ENV'),
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
return render(request, "core/project_detail.html", {"project": project})
|
||||||
|
|||||||
54
populate_demo.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
from core.models import Project, PipelineStep, CgiAsset
|
||||||
|
|
||||||
|
def run():
|
||||||
|
# Clear existing data
|
||||||
|
PipelineStep.objects.all().delete()
|
||||||
|
CgiAsset.objects.all().delete()
|
||||||
|
Project.objects.all().delete()
|
||||||
|
|
||||||
|
# Create a Movie Project
|
||||||
|
p1 = Project.objects.create(
|
||||||
|
title="O Último Guardião",
|
||||||
|
project_type="MOVIE",
|
||||||
|
status="PROD",
|
||||||
|
description="Um épico de ficção científica sobre o último protetor de uma civilização esquecida. Foco em CGI fotorrealista e ambientes vastos."
|
||||||
|
)
|
||||||
|
|
||||||
|
steps = [
|
||||||
|
('SCRIPT', 100, True),
|
||||||
|
('CONCEPT', 100, True),
|
||||||
|
('ANIMATIC', 100, True),
|
||||||
|
('MODELING', 85, False),
|
||||||
|
('TEXTURING', 60, False),
|
||||||
|
('RIGGING', 40, False),
|
||||||
|
('ANIMATION', 20, False),
|
||||||
|
('LIGHTING', 10, False),
|
||||||
|
('FX', 5, False),
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, progress, completed in steps:
|
||||||
|
PipelineStep.objects.create(
|
||||||
|
project=p1,
|
||||||
|
name=name,
|
||||||
|
progress=progress,
|
||||||
|
is_completed=completed
|
||||||
|
)
|
||||||
|
|
||||||
|
CgiAsset.objects.create(project=p1, name="Kaelen (Herói)", asset_type="CHAR", is_realistic=True, current_stage="Rigging")
|
||||||
|
CgiAsset.objects.create(project=p1, name="Cidade Flutuante", asset_type="ENV", is_realistic=True, current_stage="Texturing")
|
||||||
|
|
||||||
|
# Create a Series Project
|
||||||
|
p2 = Project.objects.create(
|
||||||
|
title="Crônicas de Cyber-Rio",
|
||||||
|
project_type="SERIES",
|
||||||
|
status="PRE",
|
||||||
|
description="Série de animação estilizada ambientada em um Rio de Janeiro futurista. Mistura de 2D e 3D."
|
||||||
|
)
|
||||||
|
|
||||||
|
PipelineStep.objects.create(project=p2, name="SCRIPT", progress=100, is_completed=True)
|
||||||
|
PipelineStep.objects.create(project=p2, name="CONCEPT", progress=40, is_completed=False)
|
||||||
|
|
||||||
|
print("Demo data created successfully!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
@ -1,3 +1,4 @@
|
|||||||
Django==5.2.7
|
Django==5.2.7
|
||||||
mysqlclient==2.2.7
|
mysqlclient==2.2.7
|
||||||
python-dotenv==1.1.1
|
python-dotenv==1.1.1
|
||||||
|
httpx
|
||||||
|
|||||||
@ -1,4 +1,116 @@
|
|||||||
/* Custom styles for the application */
|
/* CGI Studio Custom Styling */
|
||||||
body {
|
:root {
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
--bg-deep: #0a0a0c;
|
||||||
|
--bg-card: #141417;
|
||||||
|
--accent-cyan: #00e5ff;
|
||||||
|
--accent-purple: #7000ff;
|
||||||
|
--text-muted: #888891;
|
||||||
|
--glass-bg: rgba(10, 10, 12, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--bg-deep);
|
||||||
|
color: #f8f9fa;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-cyan { color: var(--accent-cyan); }
|
||||||
|
.text-purple { color: var(--accent-purple); }
|
||||||
|
|
||||||
|
.studio-navbar {
|
||||||
|
background: var(--glass-bg);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section {
|
||||||
|
padding: 140px 0 100px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 10% 20%, rgba(0, 229, 255, 0.05) 0%, transparent 40%),
|
||||||
|
radial-gradient(circle at 90% 80%, rgba(112, 0, 255, 0.05) 0%, transparent 40%);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-3 {
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cyan {
|
||||||
|
background: var(--accent-cyan);
|
||||||
|
color: #000;
|
||||||
|
font-weight: 700;
|
||||||
|
border: none;
|
||||||
|
padding: 14px 32px;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cyan:hover {
|
||||||
|
background: #4df1ff;
|
||||||
|
box-shadow: 0 0 30px rgba(0, 229, 255, 0.4);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 32px;
|
||||||
|
transition: all 0.4s ease;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card:hover {
|
||||||
|
border-color: rgba(0, 229, 255, 0.3);
|
||||||
|
background: #1a1a1f;
|
||||||
|
transform: translateY(-8px);
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 28px;
|
||||||
|
border-radius: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pipeline-badge {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 30px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-pre { background: rgba(136, 136, 145, 0.1); color: #888891; border: 1px solid rgba(136, 136, 145, 0.2); }
|
||||||
|
.badge-prod { background: rgba(112, 0, 255, 0.1); color: #b780ff; border: 1px solid rgba(112, 0, 255, 0.2); }
|
||||||
|
.badge-post { background: rgba(0, 229, 255, 0.1); color: #00e5ff; border: 1px solid rgba(0, 229, 255, 0.2); }
|
||||||
|
.badge-done { background: rgba(0, 255, 149, 0.1); color: #00ff95; border: 1px solid rgba(0, 255, 149, 0.2); }
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
height: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
background: linear-gradient(90deg, var(--accent-cyan), var(--accent-purple));
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|||||||
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 |