anycoder-86ce6d32 / index.html
eubottura's picture
Upload folder using huggingface_hub
3b58a4a verified
<!DOCTYPE html>
<html lang="pt-BR" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PromptCraft Studio v2.0</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<style>
body {
font-family: 'Inter', system-ui, sans-serif;
background-color: #050505;
color: #ffffff;
}
.step {
display: none;
opacity: 0;
transform: translateY(10px);
transition: opacity 0.4s ease, transform 0.4s ease;
}
.step.active {
display: block;
opacity: 1;
transform: translateY(0);
}
.drop-zone {
transition: all 0.2s ease;
border-color: #333;
}
.drop-zone.drag-active {
border-color: #fff;
background-color: rgba(255, 255, 255, 0.05);
transform: scale(1.01);
}
.glass-panel {
background: rgba(20, 20, 20, 0.6);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.btn-primary {
background: white;
color: black;
transition: all 0.2s;
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(255, 255, 255, 0.15);
}
.loader {
border: 3px solid rgba(255, 255, 255, 0.1);
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #111;
}
::-webkit-scrollbar-thumb {
background: #333;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #444;
}
.concept-card {
transition: all 0.3s ease;
}
.concept-card.selected {
border-color: white;
background-color: rgba(255, 255, 255, 0.05);
}
.fade-in {
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.color-chip {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background-color: rgba(55, 65, 81, 0.5);
border-radius: 9999px;
border: 1px solid rgba(55, 65, 81, 0.7);
}
.toast {
position: fixed;
bottom: 1rem;
right: 1rem;
background-color: rgba(31, 41, 55, 0.9);
color: white;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
animation: pulse 2s infinite;
}
.toast.success {
background-color: rgba(34, 197, 94, 0.9);
}
.toast.error {
background-color: rgba(239, 68, 68, 0.9);
}
</style>
</head>
<body class="min-h-screen flex flex-col antialiased">
<!-- Header -->
<header class="border-b border-gray-800 glass-panel sticky top-0 z-50">
<div class="container mx-auto px-6 py-4 flex justify-between items-center">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-white rounded flex items-center justify-center text-black font-bold text-lg">P</div>
<span class="font-bold text-xl tracking-tight">PromptCraft Studio</span>
</div>
<div class="text-xs text-gray-500 font-mono">v2.0 Optimized</div>
</div>
</header>
<!-- Main Content -->
<main class="flex-grow container mx-auto px-4 py-8">
<div class="max-w-5xl mx-auto">
<!-- Hero Section -->
<section class="text-center mb-10">
<h1 class="text-4xl md:text-5xl font-extrabold mb-4 bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-400">Product Photography AI</h1>
<p class="text-gray-400 text-lg max-w-2xl mx-auto">
Transforme produtos simples em campanhas editoriais de alta performance com Inteligência Artificial.
</p>
</section>
<!-- App Container -->
<div class="glass-panel rounded-2xl p-6 md:p-10 shadow-2xl relative overflow-hidden">
<!-- Progress Bar -->
<div class="absolute top-0 left-0 h-1 bg-gradient-to-r from-blue-500 to-purple-500 transition-all duration-500" id="progress-bar" style="width: 25%"></div>
<!-- NOTIFICATIONS / TOASTS -->
<div id="toast-container" class="fixed top-24 right-6 flex flex-col gap-2 z-50 pointer-events-none"></div>
<!-- STEP 1: PRODUCT & IMAGES -->
<section id="step-product" class="step active" aria-label="Passo 1: Produto e Imagens">
<h2 class="text-2xl font-bold mb-6 text-center">O que vamos fotografar hoje?</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Imagens de Referência</label>
<div id="drop-area"
class="drop-zone border-2 border-dashed border-gray-700 rounded-xl p-8 text-center cursor-pointer hover:border-gray-500 transition-all h-64 flex flex-col items-center justify-center relative bg-gray-900/50">
<i data-feather="upload-cloud" class="w-10 h-10 mb-3 text-gray-400"></i>
<p class="text-lg font-medium text-white">Arraste fotos aqui</p>
<p class="text-sm text-gray-500 mt-1">ou clique para selecionar</p>
<input type="file" id="image-upload" accept="image/*" multiple class="hidden">
<div id="uploading-overlay" class="hidden absolute inset-0 bg-black/80 flex flex-col items-center justify-center rounded-xl z-10">
<div class="loader border-blue-500 mb-2"></div>
<span class="text-xs text-gray-300">Enviando para Imgur...</span>
</div>
</div>
<!-- Preview Container -->
<div id="image-preview-container" class="hidden mt-4 grid grid-cols-3 gap-2"></div>
</div>
<div class="flex flex-col gap-6">
<div>
<label for="product-input" class="block text-sm font-medium text-gray-300 mb-2">Nome do Produto</label>
<input type="text" id="product-input"
class="w-full bg-gray-800 border border-gray-700 rounded-xl px-4 py-3 focus:ring-2 focus:ring-white focus:border-transparent outline-none transition-all placeholder-gray-600"
placeholder="Ex: Camiseta Oversized Algodão Egípcio 100%">
</div>
</div>
</div>
<div class="mt-8 flex justify-end">
<button id="btn-next-1" onclick="nextStep('colors')"
class="btn-primary px-8 py-3 rounded-xl font-bold flex items-center gap-2">
Continuar <i data-feather="arrow-right" class="w-4 h-4"></i>
</button>
</div>
</section>
<!-- STEP 2: COLORS -->
<section id="step-colors" class="step hidden" aria-label="Passo 2: Cores e Detalhes">
<h2 class="text-2xl font-bold mb-6 text-center">Paleta de Cores</h2>
<div class="bg-gray-800 rounded-xl p-6 mb-6 border border-gray-700">
<h3 class="text-sm font-medium text-gray-400 mb-3 uppercase tracking-wider">Cores Detectadas</h3>
<div id="detected-colors" class="flex flex-wrap gap-3 min-h-[40px]">
<span class="text-sm text-gray-400 italic">Nenhuma cor detectada</span>
</div>
</div>
<div class="mb-8">
<label for="colors-input" class="block text-sm font-medium text-gray-300 mb-2">Cores Adicionais</label>
<input type="text" id="colors-input"
class="w-full bg-gray-800 border border-gray-700 rounded-xl px-4 py-3 focus:ring-2 focus:ring-white focus:border-transparent outline-none placeholder-gray-600"
placeholder="Ex: Off-White, Bege, Navy (separadas por vírgula)">
</div>
<div class="flex justify-between items-center">
<button class="text-gray-400 hover:text-white px-4 py-2 font-medium transition-colors" onclick="prevStep('product')">
Voltar
</button>
<button id="btn-next-2" onclick="nextStep('concept')"
class="btn-primary px-8 py-3 rounded-xl font-bold flex items-center gap-2">
Definir Conceito <i data-feather="image" class="w-4 h-4"></i>
</button>
</div>
</section>
<!-- STEP 3: CONCEPTS -->
<section id="step-concept" class="step hidden" aria-label="Passo 3: Conceito Visual">
<h2 class="text-2xl font-bold mb-8 text-center">Escolha o Estilo</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8" role="radiogroup">
<button class="concept-card w-full text-left p-6 rounded-xl border border-gray-700 hover:border-white hover:bg-white/5 transition-all group"
data-concept="FOTO PRINCIPAL" onclick="selectConcept('FOTO PRINCIPAL')">
<i data-feather="aperture" class="w-8 h-8 mb-4 text-gray-400 group-hover:text-white"></i>
<h3 class="font-bold text-lg mb-1">Foto Principal (Catálogo)</h3>
<p class="text-sm text-gray-500">Fundo infinito, iluminação de estúdio perfeita, foco 100% no produto.</p>
</button>
<button class="concept-card w-full text-left p-6 rounded-xl border border-gray-700 hover:border-white hover:bg-white/5 transition-all group"
data-concept="FOTO DESCRIÇÃO" onclick="selectConcept('FOTO DESCRIÇÃO')">
<i data-feather="zoom-in" class="w-8 h-8 mb-4 text-gray-400 group-hover:text-white"></i>
<h3 class="font-bold text-lg mb-1">Close-up Textura</h3>
<p class="text-sm text-gray-500">Detalhe macro do tecido e acabamento. Sensação tátil.</p>
</button>
<button class="concept-card w-full text-left p-6 rounded-xl border border-gray-700 hover:border-white hover:bg-white/5 transition-all group"
data-concept="FOTO AVALIAÇÃO" onclick="selectConcept('FOTO AVALIAÇÃO')">
<i data-feather="smartphone" class="w-8 h-8 mb-4 text-gray-400 group-hover:text-white"></i>
<h3 class="font-bold text-lg mb-1">Estilo "Review Real"</h3>
<p class="text-sm text-gray-500">Foto orgânica, estilo UGC (User Generated Content), luz natural.</p>
</button>
<button class="concept-card w-full text-left p-6 rounded-xl border border-gray-700 hover:border-white hover:bg-white/5 transition-all group"
data-concept="FOTO DESCRIÇÃO 2" onclick="selectConcept('FOTO DESCRIÇÃO 2')">
<i data-feather="user" class="w-8 h-8 mb-4 text-gray-400 group-hover:text-white"></i>
<h3 class="font-bold text-lg mb-1">Lifestyle / Modelo</h3>
<p class="text-sm text-gray-500">Produto sendo usado em contexto urbano ou estúdio.</p>
</button>
</div>
<div class="flex justify-between items-center">
<button class="text-gray-400 hover:text-white px-4 py-2 font-medium transition-colors" onclick="prevStep('colors')">Voltar</button>
</div>
</section>
<!-- STEP 4: GENERATION -->
<section id="step-result" class="step hidden" aria-label="Passo 4: Geração">
<h2 class="text-2xl font-bold mb-6 text-center">Seu Prompt Otimizado</h2>
<!-- API Key Management -->
<div class="mb-4">
<div class="flex justify-between items-center mb-2">
<label class="text-xs font-bold text-gray-500 uppercase tracking-widest">Google Gemini API Key</label>
</div>
<input type="password" id="api-key-input"
value="AIzaSyDGL3xn15bIkEHkI-24OMO9Ub2G_pDnQ-M"
class="w-full bg-black/30 border border-gray-800 rounded px-3 py-2 text-xs text-gray-400 focus:text-white transition-colors">
</div>
<div class="bg-gray-900 p-6 rounded-lg mb-6 relative group">
<div class="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity">
<button id="btn-copy-prompt" class="bg-white text-black px-3 py-1 text-xs font-bold rounded shadow hover:bg-gray-200 transition-colors">Copiar</button>
</div>
<pre id="prompt-output" class="p-6 text-xs md:text-sm font-mono text-gray-300 whitespace-pre-wrap max-h-64 overflow-y-auto custom-scrollbar"></pre>
</div>
<div class="mt-8 flex justify-center">
<button class="text-gray-500 hover:text-white text-sm" onclick="resetForm()">Começar Novo Projeto</button>
</div>
</section>
</div>
</div>
</main>
<!-- Footer -->
<footer class="border-t border-gray-900 mt-auto py-8">
<div class="container mx-auto px-4 text-center text-gray-600 text-sm">
PromptCraft Studio v2.0 • Powered by Google Gemini & Imagen
</div>
</footer>
<!-- Image Modal -->
<div id="image-modal" class="fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-50 hidden">
<div class="relative max-w-4xl max-h-screen">
<button id="close-modal" class="absolute -top-10 right-0 text-white text-3xl cursor-pointer hover:text-gray-300">×</button>
<img id="modal-image" src="" alt="Full size preview" class="max-w-full max-h-screen object-contain">
</div>
</div>
<!-- APPLICATION LOGIC -->
<script>
// Sistema Imgur GARANTIDO - Upload Direto + Detecção Melhorada
// Gemini Flash Integration - Geração Interna de Imagens
// Store user inputs
let userData = {
product: '',
colors: '',
concept: '',
images: [], // Armazena objetos {file, dataUrl, uploadedUrl}
additionalColors: [],
hostedImageUrls: [],
detectedColors: []
};
// Store image URLs for sharing
let imageUrls = [];
// Variáveis de controle
let useImgurDirect = true; // SEMPRE usar Imgur diretamente
let uploadMethod = 'Imgur Direto'; // Uploader Imgur Direto (GARANTIDO)
class ImgurDirectUploader {
constructor() {
// Client ID público do Imgur (funciona sem configuração)
this.clientId = '546c25a59c58ad7';
}
async uploadImage(file, onProgress) {
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append('image', file);
formData.append('type', 'file');
formData.append('name', file.name);
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
onProgress && onProgress(Math.round(percentComplete));
}});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.success && response.data) {
resolve({
id: response.data.id,
url: response.data.link,
deletehash: response.data.deletehash,
size: response.data.size,
name: file.name,
originalSize: file.size,
type: response.data.type || 'image/jpeg',
width: response.data.width,
height: response.data.height
});
} else {
reject(new Error('Resposta Imgur inválida'));
}
} catch (e) {
reject(new Error('Erro ao processar resposta Imgur'));
}
} else {
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
}});
xhr.addEventListener('error', () => {
reject(new Error('Erro de rede Imgur'));
}});
xhr.addEventListener('timeout', () => {
reject(new Error('Timeout Imgur'));
}});
xhr.open('POST', 'https://api.imgur.com/3/image');
xhr.timeout = 30000;
xhr.setRequestHeader('Authorization', `Client-ID ${this.clientId}`));
xhr.send(formData);
}});
}
async uploadMultiple(files, onProgress) {
const results = [];
const totalFiles = files.length;
for (let i = 0; i < files.length; i++) {
try {
const file = files[i];
const result = await this.uploadImage(file, (progress) => {
const overallProgress = ((i + progress/100) / totalFiles) * 100;
onProgress && onProgress(Math.round(overallProgress), i + 1, totalFiles));
}});
results.push({ ...result, success: true });
} catch (error) {
console.warn(`Erro upload Imgur ${files[i].name}:`, error);
results.push({ success: false, error: error.message, name: files[i].name });
}
}
return results;
}
}
// Inicialização
let imgurUploader = null;
// Inicializar uploader Imgur
function initializeImgurUploader() {
imgurUploader = new ImgurDirectUploader();
console.log('✅ Uploader Imgur inicializado - Upload garantido!');
}
// Upload para Imgur (GARANTIDO)
async function uploadToImgur(imageFiles) {
if (!imgurUploader) {
initializeImgurUploader();
}
uploadMethod = 'Imgur Direto';
showUploadingState(true, uploadMethod);
try {
const results = await imgurUploader.uploadMultiple(
imageFiles.map(img => img.file),
(progress, current, total) => updateUploadProgress(progress, current, total)
);
const successful = results.filter(r => r.success);
if (successful.length > 0) {
userData.hostedImageUrls = successful.map(r => r.url);
console.log(`✅ Upload Imgur: ${successful.length}/${results.length} imagens');
// Atualizar status das imagens
results.forEach((result, index) => {
updateImageStatus(index + 1, result.success ? '✓ Imgur OK' : '❌ Falhou'));
}}});
return results;
} else {
throw new Error('Nenhuma imagem foi enviada com sucesso'));
}
} catch (error) {
console.error('Erro upload Imgur:', error);
throw error;
}
}
// Fallback Local (só se Imgur falhar completamente)
async function uploadToLocalFallback(imageFiles) {
uploadMethod = 'Local (Fallback)';
showUploadingState(true, uploadMethod);
const results = [];
for (let i = 0; i < imageFiles.length; i++) {
const imgData = imageFiles[i];
updateImageStatus(i + 1, '⚙️ Processando...'));
try {
await new Promise(resolve => setTimeout(resolve, 300));
userData.hostedImageUrls.push(imgData.dataUrl);
results.push({ success: true, url: imgData.dataUrl });
updateImageStatus(i + 1, '✓ Local OK'));
} catch (error) {
results.push({ success: false, error: error.message });
updateImageStatus(i + 1, '❌ Erro'));
}
const progress = Math.round(((i + 1) / imageFiles.length) * 100;
updateUploadProgress(progress, i + 1, imageFiles.length));
}}
return results;
}
// Navegação entre steps
async function nextStep(nextStepId) {
const currentStep = document.querySelector('.step.active'));
const nextStepElement = document.getElementById(`step-${nextStepId}`));
if (!currentStep || !nextStepElement) {
console.error('Step não encontrado'));
return;
}
if (currentStep.id === 'step-product') {
userData.product = document.getElementById('product-input').value.trim());
if (!userData.product) {
alert('Por favor, descreva o produto.'));
return;
}
// Inicializar uploader Imgur
initializeImgurUploader();
// Processar imagens
if (userData.images.length > 0) {
try {
// Tentar upload Imgur PRIMEIRO
try {
await uploadToImgur(userData.images);
} catch (imgurError) {
console.warn('⚠️ Imgur falhou, tentando fallback local:', imgurError);
await uploadToLocalFallback(userData.images);
}
// Detectar cores
const detectedColors = await detectColorsFromImages(userData.hostedImageUrls);
displayDetectedColors(detectedColors);
} catch (error) {
console.error('Erro crítico no processamento:', error);
alert('Erro ao processar imagens. Tente novamente.'));
return;
} finally {
showUploadingState(false));
}
}
// Navegar
currentStep.classList.remove('active'));
currentStep.classList.add('hidden'));
nextStepElement.classList.remove('hidden'));
nextStepElement.classList.add('active'));
} else if (currentStep.id === 'step-colors') {
const additionalColorsInput = document.getElementById('colors-input').value.trim());
if (additionalColorsInput) {
userData.additionalColors = additionalColorsInput.split(',').map(color => color.trim()));
} else {
userData.additionalColors = [];
}
const allColors = [...new Set([...(userData.detectedColors || []), ...userData.additionalColors])];
userData.colors = allColors.join(', '));
if (!userData.colors && userData.detectedColors.length === 0) {
alert('Por favor, informe as cores disponíveis.'));
return;
}
currentStep.classList.remove('active'));
currentStep.classList.add('hidden'));
nextStepElement.classList.remove('hidden'));
nextStepElement.classList.add('active'));
} else if (currentStep.id === 'step-concept') {
if (!userData.concept) {
alert('Por favor, selecione um conceito.'));
return;
}
await generatePrompt());
currentStep.classList.remove('active'));
currentStep.classList.add('hidden'));
nextStepElement.classList.remove('hidden'));
nextStepElement.classList.add('active'));
}
}
function prevStep(prevStepId) {
const currentStep = document.querySelector('.step.active'));
const prevStepElement = document.getElementById(`step-${prevStepId}`));
if (!currentStep || !prevStepElement) {
console.error('Step não encontrado'));
return;
}
currentStep.classList.remove('active'));
currentStep.classList.add('hidden'));
prevStepElement.classList.remove('hidden'));
prevStepElement.classList.add('active'));
}
// Estado de upload
function showUploadingState(show, mode = 'Imgur Direto') {
let loadingDiv = document.getElementById('uploading-overlay');
if (show) {
loadingDiv = document.createElement('div');
loadingDiv.id = 'uploading-overlay';
loadingDiv.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
loadingDiv.innerHTML = `
<div class="bg-gray-900 rounded-lg p-6 flex flex-col items-center gap-4 max-w-md w-full mx-4">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 ${mode.includes('Imgur') ? 'border-blue-500' : 'border-green-500'}"></div>
<span class="text-white text-center font-medium">
${mode.includes('Imgur') ? 'Enviando para Imgur...' : 'Processando localmente...'}
</span>
<div class="w-full">
<div class="flex justify-between text-xs text-gray-400 mb-1">
<span id="upload-status">Preparando...</span>
<span id="upload-progress">0%</span>
</div>
<div class="w-full bg-gray-800 rounded-full h-2">
<div id="upload-bar" class="${mode.includes('Imgur') ? 'bg-blue-500' : 'bg-green-500'} h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
</div>
<div class="text-xs text-gray-500 text-center">
${mode.includes('Imgur') ? 'Upload rápido e persistente' : 'Processamento no navegador'}
</div>
</div>
`;
document.body.appendChild(loadingDiv);
} else if (loadingDiv) {
loadingDiv.remove());
}
}
function updateUploadProgress(percent, current, total) {
const statusEl = document.getElementById('upload-status'));
const progressEl = document.getElementById('upload-progress'));
const barEl = document.getElementById('upload-bar'));
if (statusEl && progressEl && barEl) {
statusEl.textContent = `Enviando ${current || '?'} de ${total || '?'}';
progressEl.textContent = `${percent}%`;
barEl.style.width = `${percent}%`;
}
}
function updateImageStatus(imageIndex, status) {
const previews = document.querySelectorAll('#image-preview-container > div');
if (previews[imageIndex - 1]) {
const statusEl = previews[imageIndex - 1].querySelector('.upload-status'));
if (statusEl) {
statusEl.textContent = status;
const parentEl = statusEl.parentElement;
parentEl.classList.remove('bg-green-600', 'bg-blue-600', 'bg-red-600', 'bg-yellow-600', 'bg-gray-600'));
if (status.includes('✓ Imgur')) {
parentEl.classList.add('bg-blue-600'));
} else if (status.includes('✓ Local')) {
parentEl.classList.add('bg-green-600'));
} else if (status.includes('Enviando') || status.includes('Processando')) {
parentEl.classList.add('bg-yellow-600'));
} else if (status.includes('Erro')) {
parentEl.classList.add('bg-red-600'));
} else {
parentEl.classList.add('bg-gray-600'));
}
}
}
// Detecção de cores
async function detectColorsFromImages(imageSources) {
const detectedColors = [];
for (const source of imageSources) {
try {
const colors = await extractColorsFromImageSource(source));
detectedColors.push(...colors));
} catch (error) {
console.error('Erro ao processar imagem:', error);
}
}
const uniqueColors = [...new Set(detectedColors)].slice(0, 10);
return uniqueColors;
}
function extractColorsFromImageSource(imageSource) {
return new Promise((resolve) => {
const img = new Image();
if (imageSource.startsWith('http')) {
img.crossOrigin = 'anonymous';
}
img.onload = function() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d'));
const maxSize = 100;
let { width, height } = img;
if (width > height) {
if (width > maxSize) {
height = (height * maxSize) / width;
width = maxSize;
}
} else {
if (height > maxSize) {
width = (width * maxSize) / height;
height = maxSize;
}
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
const colors = [];
const sampleRate = 5;
for (let i = 0; i < data.length; i += 4 * sampleRate) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const a = data[i + 3];
if (a < 128) continue;
const hex = rgbToHex(r, g, b);
colors.push(hex);
}
const dominantColors = getDominantColors(colors, 3);
resolve(dominantColors);
};
img.onerror = () => {
console.warn('Não foi possível carregar imagem para análise de cores'));
resolve([]));
}};
img.src = imageSource;
}});
}
// Funções de cor
function rgbToHex(r, g, b) {
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase());
}
function getDominantColors(colors, count) {
const colorCount = {};
colors.forEach(color => {
colorCount[color] = (colorCount[color] || 0) + 1;
}});
const sortedColors = Object.entries(colorCount)
.sort(([,a], [,b]) => b - a)
.map(([color]) => color);
const filteredColors = filterSimilarColors(sortedColors);
return filteredColors.slice(0, count);
}
function filterSimilarColors(colors) {
const filtered = [];
const threshold = 30;
for (const color of colors) {
let isSimilar = false;
for (const filteredColor of filtered) {
if (colorDifference(color, filteredColor) < threshold) {
isSimilar = true;
break;
}
}
if (!isSimilar) {
filtered.push(color));
}
}
return filtered;
}
function colorDifference(color1, color2) {
const rgb1 = hexToRgb(color1));
const rgb2 = hexToRgb(color2));
return Math.sqrt(
Math.pow(rgb1.r - rgb2.r, 2) +
Math.pow(rgb1.g - rgb2.g, 2) +
Math.pow(rgb1.b - rgb2.b, 2)
);
}
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex));
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : {r: 0, g: 0, b: 0};
}
function displayDetectedColors(colors) {
const container = document.getElementById('detected-colors'));
userData.detectedColors = colors;
if (colors.length === 0) {
container.innerHTML = '<span class="text-sm text-gray-400">Nenhuma cor detectada</span>';
return;
}
container.innerHTML = '';
colors.forEach((color, index) => {
const colorElement = document.createElement('div'));
colorElement.className = 'flex items-center gap-2 px-3 py-2 bg-gray-800 rounded-lg relative';
colorElement.innerHTML = `
<div class="w-4 h-4 rounded-full" style="background-color: ${color}"></div>
<span class="text-sm">${color}</span>
<button class="remove-color ml-2 text-red-500 hover:text-red-300" data-index="${index}">
<i data-feather="x" class="w-4 h-4"></i>
</button>
`;
container.appendChild(colorElement));
}});
container.querySelectorAll('.remove-color').forEach(button => {
button.addEventListener('click', function() {
const index = parseInt(this.getAttribute('data-index')));
removeDetectedColor(index));
}});
feather.replace());
}
function removeDetectedColor(index) {
if (userData.detectedColors && index >= 0 && index < userData.detectedColors.length) {
userData.detectedColors.splice(index, 1);
displayDetectedColors(userData.detectedColors));
const allColors = [...new Set([...userData.detectedColors, ...userData.additionalColors])];
userData.colors = allColors.join(', '));
}
}
// Seleção de conceito
async function selectConcept(concept) {
console.log('selectConcept() chamado com:', concept);
userData.concept = concept;
await nextStep('result'));
}
// Gerar prompt
async function generatePrompt() {
console.log('generatePrompt() iniciado');
showLoadingState(true));
try {
updatePromptDisplay(userData.concept));
createConceptSelector());
console.log('Prompt gerado com sucesso'));
} catch (error) {
console.error('Erro ao gerar prompt:', error);
alert('⚠️ Erro ao gerar prompt. Tente novamente.'));
} finally {
showLoadingState(false));
}
}
// Estado de loading
function showLoadingState(show) {
let loadingDiv = document.getElementById('loading-overlay'));
if (show) {
loadingDiv = document.createElement('div'));
loadingDiv.id = 'loading-overlay';
loadingDiv.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
loadingDiv.innerHTML = `
<div class="bg-gray-900 rounded-lg p-6 flex flex-col items-center gap-4">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
<span class="text-white">Processando...</span>
</div>
`;
document.body.appendChild(loadingDiv));
} else if (loadingDiv) {
loadingDiv.remove());
}
}
// Validador Técnico Imgur - Garantia de Integridade Visual
class ImgurTechnicalValidator {
constructor() {
this.validationResults = [];
}
async validateImageUrls(urls) {
this.validationResults = [];
const results = [];
for (let i = 0; i < urls.length; i++) {
const url = urls[i];
const result = await this.validateSingleImage(url, i + 1));
results.push(result);
this.validationResults.push(result));
}
return results;
}
async validateSingleImage(url, index) {
const isImgur = url.includes('imgur.com'));
try {
if (!isImgur) {
return {
index,
url,
status: 'WARNING',
message: 'URL não é do Imgur - integridade não garantida',
format: this.getFormatFromUrl(url),
size: 'N/A',
availability: 'Unknown'
};
}
// Validação técnica Imgur
const img = new Image();
img.crossOrigin = 'anonymous';
return new Promise((resolve) => {
const timeout = setTimeout(() => {
resolve({
index,
url,
status: 'ERROR',
message: 'Timeout na validação',
format: 'Unknown',
size: 'N/A',
availability: 'Failed'
}}, 10000));
img.onload = () => {
clearTimeout(timeout);
resolve({
index,
url,
status: 'VALIDATED',
message: 'URL Imgur validada com sucesso',
format: img.naturalWidth > 0 ? `${img.naturalWidth}x${img.naturalHeight}` : 'Unknown',
size: `${(img.naturalWidth * img.naturalHeight * 3 / 1024 / 1024).toFixed(2)}MB estimado',
availability: 'Online'
}};
};
img.onerror = () => {
clearTimeout(timeout);
resolve({
index,
url,
status: 'ERROR',
message: 'Imagem não disponível',
format: 'Unknown',
size: 'N/A',
availability: 'Offline'
}};
};
img.src = url;
}});
} catch (error) {
return {
index,
url,
status: 'ERROR',
message: error.message,
format: 'Unknown',
size: 'N/A',
availability: 'Failed'
};
}
}
getFormatFromUrl(url) {
if (url.startsWith('data:')) {
return 'Base64';
}
const extension = url.split('.').pop().split('?')[0];
return extension ? extension.toUpperCase() : 'Unknown';
}
generateValidationReport() {
const valid = this.validationResults.filter(r => r.status === 'VALIDATED').length;
const total = this.validationResults.length;
const hasImgur = this.validationResults.some(r => r.url.includes('imgur.com')));
return {
total,
valid,
invalid: total - valid,
hasImgur,
integrity: hasImgur && valid === total ? 'GARANTIDA' : 'PARCIAL',
report: this.validationResults
};
}
}
// Master V3.0 Prompt Structure - FINAL REFORCED
function generateMasterPromptV3(concept) {
console.log('🚀 GERANDO MASTER PROMPT v3.0 - FINAL REFOrCED');
// Validação técnica das imagens
const validator = new ImgurTechnicalValidator());
let validationReport = {
total: 0,
valid: 0,
invalid: 0,
hasImgur: false,
integrity: 'NENHUMA',
report: []
};
if (userData.hostedImageUrls && userData.hostedImageUrls.length > 0) {
validationReport = validator.generateValidationReport());
}
const masterPromptConfig = {
"prompt_version": "FINAL_REFORCED_v3.0",
"ai_role": "GEMINI - MASTER E-COMMERCE PHOTOGRAPHIC RENDERING AI WITH ULTRA-REALISM CAPABILITIES",
"objective": "GENERATE ULTRA-REALISTIC CINEMATIC IMAGE FOR PREMIUM E-COMMERCE",
"product_main": {
"name": userData.product,
"material": "High Quality Material (Texture focus)",
"quantity": "As specified in product name",
"colors": userData.colors,
"bonuses": "Include if mentioned in product name",
"reference_image": "Use input images as strict reference"
},
"imgur_validation": {
"total_images": validationReport.total,
"validated_images": validationReport.valid,
"integrity_status": validationReport.integrity,
"has_imgur_urls": validationReport.hasImgur,
"validation_details": validationReport.report.map(r => ({
index: r.index,
url: r.url,
status: r.status,
format: r.format,
availability: r.availability
}))
},
"imgur_urls": userData.hostedImageUrls,
"photo_blocks": {
"FOTO PRINCIPAL": {
"concept_name": "FOTO_PRINCIPAL",
"prompt_structure": {
"image_specifications": {
"format": "1:1 (square)",
"perspective": "completely realistic - front and aligned",
"prohibited_elements": ["promotional texts", "arrows", "seals", "watermarks", "graphic elements"]
},
"scene_composition": {
"background": "#F0F0F0 (very light gray)",
"base": "minimalist white marble pedestal with subtle veins",
"product_arrangement": {
"shirts": "OPEN AND ALIGNED HORIZONTALLY - NOT STACKED",
"spacing": "uniform spacing between each item",
"presentation": "each item fully opened/visible to show entire surface and texture"
},
"layout": "aesthetic and balanced arrangement - HORIZONTAL AND ORGANIZED",
"visibility_rule": "all items fully visible, no overlaps hiding details"
},
"lighting_style": {
"type": "professional studio lighting - MAXIMUM HIGHLIGHT",
"equipment": "softboxes + strategic spotlights",
"shadows": "soft, diffused and realistic - no distractions",
"contrast": "HIGH contrast for maximum outline and volume emphasis"
},
"visual_quality": {
"style": "hyper-realistic cinematic - MAXIMUM PROFESSIONALISM",
"reference_quality": "premium fashion magazine cover quality",
"depth_of_field": {
"main_focus": "ALL ITEMS SHARP FOCUS",
"accessories": "slight blur, but recognizable and sharp"
}
},
"strict_prohibitions": [
"DO NOT add promotional texts",
"DO NOT distort perspective",
"DO NOT create items not present",
"DO NOT hide important parts",
"DO NOT use