Spaces:
Running
Running
| <html lang="pt-BR"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>CriaVideo AI - Construtor de Anúncios Inteligente</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&family=Space+Grotesk:wght@500;700&display=swap'); | |
| :root { | |
| --neon-blue: #00f3ff; | |
| --neon-purple: #bc13fe; | |
| --dark-bg: #0a0a0f; | |
| --panel-bg: #13131f; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: var(--dark-bg); | |
| color: #e2e8f0; | |
| overflow-x: hidden; | |
| } | |
| h1, h2, h3, .brand-font { | |
| font-family: 'Space Grotesk', sans-serif; | |
| } | |
| /* Custom Scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: var(--dark-bg); | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #334155; | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #475569; | |
| } | |
| /* Animations */ | |
| @keyframes gradient-xy { | |
| 0%, 100% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| } | |
| .animate-gradient { | |
| background-size: 200% 200%; | |
| animation: gradient-xy 6s ease infinite; | |
| } | |
| .glass-panel { | |
| background: rgba(19, 19, 31, 0.7); | |
| backdrop-filter: blur(12px); | |
| border: 1px solid rgba(255, 255, 255, 0.08); | |
| box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3); | |
| } | |
| .step-active { | |
| border-color: var(--neon-blue); | |
| box-shadow: 0 0 15px rgba(0, 243, 255, 0.2); | |
| } | |
| .step-completed { | |
| border-color: #10b981; | |
| } | |
| /* Loading Spinner */ | |
| .loader { | |
| width: 48px; | |
| height: 48px; | |
| border: 5px solid #FFF; | |
| border-bottom-color: var(--neon-purple); | |
| border-radius: 50%; | |
| display: inline-block; | |
| box-sizing: border-box; | |
| animation: rotation 1s linear infinite; | |
| } | |
| @keyframes rotation { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .typing-cursor::after { | |
| content: '|'; | |
| animation: blink 1s step-start infinite; | |
| } | |
| @keyframes blink { | |
| 50% { opacity: 0; } | |
| } | |
| /* Range Slider Styling */ | |
| input[type=range] { | |
| -webkit-appearance: none; | |
| background: transparent; | |
| } | |
| input[type=range]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| height: 16px; | |
| width: 16px; | |
| border-radius: 50%; | |
| background: var(--neon-blue); | |
| cursor: pointer; | |
| margin-top: -6px; | |
| } | |
| input[type=range]::-webkit-slider-runnable-track { | |
| width: 100%; | |
| height: 4px; | |
| cursor: pointer; | |
| background: #334155; | |
| border-radius: 2px; | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen flex flex-col"> | |
| <!-- Navbar --> | |
| <nav class="border-b border-gray-800 bg-black/50 backdrop-blur-md sticky top-0 z-50"> | |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | |
| <div class="flex items-center justify-between h-16"> | |
| <div class="flex items-center gap-3"> | |
| <div class="w-8 h-8 rounded bg-gradient-to-tr from-cyan-500 to-purple-600 flex items-center justify-center"> | |
| <i class="fa-solid fa-bolt text-white text-sm"></i> | |
| </div> | |
| <span class="text-xl font-bold tracking-tight text-white">Cria<span class="text-cyan-400">Video</span> AI</span> | |
| </div> | |
| <div class="flex items-center gap-4"> | |
| <button onclick="openSettings()" class="text-gray-400 hover:text-white transition-colors"> | |
| <i class="fa-solid fa-gear"></i> Configurações | |
| </button> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="text-xs bg-gray-800 hover:bg-gray-700 px-3 py-1.5 rounded-full text-gray-300 transition-colors border border-gray-700"> | |
| Built with <span class="text-cyan-400 font-semibold">anycoder</span> | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| </nav> | |
| <!-- Main Content --> | |
| <main class="flex-grow p-6 max-w-7xl mx-auto w-full grid grid-cols-1 lg:grid-cols-12 gap-8"> | |
| <!-- Left Sidebar: Steps --> | |
| <div class="lg:col-span-3 space-y-4"> | |
| <div class="glass-panel rounded-xl p-6 sticky top-24"> | |
| <h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-4">Progresso</h3> | |
| <div class="space-y-4 relative"> | |
| <!-- Connecting Line --> | |
| <div class="absolute left-3.5 top-2 bottom-2 w-0.5 bg-gray-800 -z-10"></div> | |
| <!-- Step 1 --> | |
| <div id="step-indicator-1" class="flex items-center gap-3 p-3 rounded-lg border border-transparent bg-gray-800/50 step-active transition-all"> | |
| <div class="w-7 h-7 rounded-full bg-cyan-500/20 text-cyan-400 flex items-center justify-center text-xs font-bold border border-cyan-500/50">1</div> | |
| <div> | |
| <p class="font-medium text-sm">Roteiro</p> | |
| <p class="text-xs text-gray-500">Copy & Story</p> | |
| </div> | |
| </div> | |
| <!-- Step 2 --> | |
| <div id="step-indicator-2" class="flex items-center gap-3 p-3 rounded-lg border border-transparent opacity-50 transition-all"> | |
| <div class="w-7 h-7 rounded-full bg-gray-700 text-gray-400 flex items-center justify-center text-xs font-bold border border-gray-600">2</div> | |
| <div> | |
| <p class="font-medium text-sm">Narração</p> | |
| <p class="text-xs text-gray-500">Voz & Áudio</p> | |
| </div> | |
| </div> | |
| <!-- Step 3 --> | |
| <div id="step-indicator-3" class="flex items-center gap-3 p-3 rounded-lg border border-transparent opacity-50 transition-all"> | |
| <div class="w-7 h-7 rounded-full bg-gray-700 text-gray-400 flex items-center justify-center text-xs font-bold border border-gray-600">3</div> | |
| <div> | |
| <p class="font-medium text-sm">Legendas</p> | |
| <p class="text-xs text-gray-500">Transcrição</p> | |
| </div> | |
| </div> | |
| <!-- Step 4 --> | |
| <div id="step-indicator-4" class="flex items-center gap-3 p-3 rounded-lg border border-transparent opacity-50 transition-all"> | |
| <div class="w-7 h-7 rounded-full bg-gray-700 text-gray-400 flex items-center justify-center text-xs font-bold border border-gray-600">4</div> | |
| <div> | |
| <p class="font-medium text-sm">Cenas</p> | |
| <p class="text-xs text-gray-500">Imagens AI</p> | |
| </div> | |
| </div> | |
| <!-- Step 5 --> | |
| <div id="step-indicator-5" class="flex items-center gap-3 p-3 rounded-lg border border-transparent opacity-50 transition-all"> | |
| <div class="w-7 h-7 rounded-full bg-gray-700 text-gray-400 flex items-center justify-center text-xs font-bold border border-gray-600">5</div> | |
| <div> | |
| <p class="font-medium text-sm">Render</p> | |
| <p class="text-xs text-gray-500">Vídeo Final</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Center: Workspace --> | |
| <div class="lg:col-span-9 space-y-6"> | |
| <!-- STEP 1: ROTEIRO --> | |
| <div id="step-1" class="glass-panel rounded-xl p-8 border-t-4 border-cyan-500"> | |
| <div class="flex justify-between items-start mb-6"> | |
| <div> | |
| <h2 class="text-2xl font-bold text-white mb-1">1. Criação do Roteiro</h2> | |
| <p class="text-gray-400 text-sm">Defina o conceito ou deixe a IA criar para você.</p> | |
| </div> | |
| <div class="flex gap-2"> | |
| <button onclick="setScriptMode('manual')" id="btn-manual" class="px-4 py-2 rounded-lg bg-gray-700 text-white text-sm hover:bg-gray-600 transition">Manual</button> | |
| <button onclick="setScriptMode('ai')" id="btn-ai" class="px-4 py-2 rounded-lg bg-cyan-600 text-white text-sm hover:bg-cyan-500 transition shadow-lg shadow-cyan-500/20">Gerar com IA</button> | |
| </div> | |
| </div> | |
| <!-- AI Generation Inputs --> | |
| <div id="ai-inputs" class="space-y-4 mb-6 animate-fade-in"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-xs font-medium text-gray-400 mb-1">Produto/Serviço</label> | |
| <input type="text" id="product-input" placeholder="Ex: Curso de Marketing Digital" class="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500 outline-none transition"> | |
| </div> | |
| <div> | |
| <label class="block text-xs font-medium text-gray-400 mb-1">Público Alvo</label> | |
| <input type="text" id="audience-input" placeholder="Ex: Empreendedores iniciantes" class="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500 outline-none transition"> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-xs font-medium text-gray-400 mb-1">Tom de Voz</label> | |
| <select id="tone-select" class="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:border-cyan-500 outline-none"> | |
| <option value="profissional">Profissional & Sério</option> | |
| <option value="entusiasmado">Entusiasmado & Energético</option> | |
| <option value="dramatico">Storytelling Dramático</option> | |
| <option value="vsl">VSL (Video Sales Letter) Direto</option> | |
| </select> | |
| </div> | |
| <div class="flex items-center gap-4"> | |
| <div class="flex-1"> | |
| <label class="block text-xs font-medium text-gray-400 mb-1">Duração Estimada: <span id="duration-display" class="text-cyan-400">30s</span></label> | |
| <input type="range" id="duration-slider" min="15" max="90" value="30" step="15" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer"> | |
| </div> | |
| </div> | |
| <button onclick="generateScript()" class="w-full bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 text-white font-bold py-3 rounded-lg transition-all transform hover:scale-[1.01] flex items-center justify-center gap-2"> | |
| <i class="fa-solid fa-wand-magic-sparkles"></i> Gerar Roteiro com IA | |
| </button> | |
| </div> | |
| <!-- Editor --> | |
| <div class="relative"> | |
| <label class="block text-xs font-medium text-gray-400 mb-1">Roteiro (Copy)</label> | |
| <textarea id="script-editor" rows="8" class="w-full bg-gray-900 border border-gray-700 rounded-lg p-4 text-white font-mono text-sm focus:border-cyan-500 outline-none resize-none" placeholder="Cole seu roteiro aqui ou gere com IA..."></textarea> | |
| <div class="absolute bottom-3 right-3 text-xs text-gray-500"> | |
| <span id="char-count">0</span> chars | ~<span id="word-time">0</span>s | |
| </div> | |
| </div> | |
| <div class="mt-6 flex justify-end"> | |
| <button onclick="nextStep(2)" class="bg-white text-black font-bold py-2 px-6 rounded-lg hover:bg-gray-200 transition flex items-center gap-2"> | |
| Próximo: Narração <i class="fa-solid fa-arrow-right"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- STEP 2: NARRAÇÃO --> | |
| <div id="step-2" class="glass-panel rounded-xl p-8 border-t-4 border-purple-500 hidden"> | |
| <h2 class="text-2xl font-bold text-white mb-6">2. Geração de Narração</h2> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8"> | |
| <div class="bg-gray-800/50 p-4 rounded-lg border border-gray-700 hover:border-purple-500 cursor-pointer transition group" onclick="selectVoiceProvider('elevenlabs')"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <span class="font-bold text-purple-400">ElevenLabs</span> | |
| <i class="fa-solid fa-check-circle text-gray-600 group-hover:text-purple-500"></i> | |
| </div> | |
| <p class="text-xs text-gray-400">Qualidade Ultra-Realista. Melhor para VSLs.</p> | |
| </div> | |
| <div class="bg-gray-800/50 p-4 rounded-lg border border-gray-700 hover:border-purple-500 cursor-pointer transition group" onclick="selectVoiceProvider('azure')"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <span class="font-bold text-blue-400">Azure TTS</span> | |
| <i class="fa-solid fa-check-circle text-gray-600 group-hover:text-blue-500"></i> | |
| </div> | |
| <p class="text-xs text-gray-400">Estável e rápido. Ótimo custo-benefício.</p> | |
| </div> | |
| <div class="bg-gray-800/50 p-4 rounded-lg border border-gray-700 hover:border-purple-500 cursor-pointer transition group" onclick="selectVoiceProvider('minimax')"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <span class="font-bold text-green-400">MiniMax</span> | |
| <i class="fa-solid fa-check-circle text-gray-600 group-hover:text-green-500"></i> | |
| </div> | |
| <p class="text-xs text-gray-400">Modelo novo com boa entonação.</p> | |
| </div> | |
| </div> | |
| <div class="bg-gray-900 rounded-lg p-6 border border-gray-800"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <h3 class="font-semibold text-white">Processamento de Blocos</h3> | |
| <span class="text-xs bg-purple-900/50 text-purple-300 px-2 py-1 rounded border border-purple-700">Modo: Chunking Inteligente</span> | |
| </div> | |
| <div id="audio-progress-container" class="space-y-3"> | |
| <!-- Generated via JS --> | |
| <div class="text-center py-8 text-gray-500"> | |
| <p>Aguardando início...</p> | |
| </div> | |
| </div> | |
| <div class="mt-6 flex justify-center"> | |
| <button onclick="generateAudio()" id="btn-generate-audio" class="bg-purple-600 hover:bg-purple-500 text-white font-bold py-3 px-8 rounded-full shadow-lg shadow-purple-500/30 transition-all flex items-center gap-2"> | |
| <i class="fa-solid fa-microphone"></i> Sintetizar Voz | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mt-6 flex justify-between"> | |
| <button onclick="prevStep(1)" class="text-gray-400 hover:text-white font-medium py-2 px-4">Voltar</button> | |
| <button onclick="nextStep(3)" id="btn-next-3" class="bg-white text-black font-bold py-2 px-6 rounded-lg hover:bg-gray-200 transition opacity-50 cursor-not-allowed" disabled> | |
| Próximo: Legendas <i class="fa-solid fa-arrow-right"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- STEP 3: LEGENDAS --> | |
| <div id="step-3" class="glass-panel rounded-xl p-8 border-t-4 border-yellow-500 hidden"> | |
| <h2 class="text-2xl font-bold text-white mb-2">3. Transcrição & Legendas</h2> | |
| <p class="text-gray-400 text-sm mb-6">Transcrevendo o áudio com precisão de palavra (Whisper).</p> | |
| <div class="bg-black rounded-lg p-1 border border-gray-800 relative overflow-hidden h-64 flex items-center justify-center" id="waveform-container"> | |
| <canvas id="audio-visualizer" class="absolute inset-0 w-full h-full"></canvas> | |
| <div id="transcribing-text" class="relative z-10 text-center hidden"> | |
| <div class="loader mb-4"></div> | |
| <p class="text-yellow-400 font-mono animate-pulse">Analisando áudio...</p> | |
| </div> | |
| </div> | |
| <div class="mt-4 bg-gray-900 rounded-lg p-4 border border-gray-800 h-48 overflow-y-auto font-mono text-sm text-gray-300" id="transcription-preview"> | |
| <!-- Preview of SRT/JSON format --> | |
| <span class="text-gray-600 italic">A transcrição aparecerá aqui...</span> | |
| </div> | |
| <div class="mt-6 flex justify-between"> | |
| <button onclick="prevStep(2)" class="text-gray-400 hover:text-white font-medium py-2 px-4">Voltar</button> | |
| <button onclick="transcribeAudio()" id="btn-transcribe" class="bg-yellow-600 hover:bg-yellow-500 text-white font-bold py-2 px-6 rounded-lg transition"> | |
| <i class="fa-solid fa-closed-captioning"></i> Gerar Legendas | |
| </button> | |
| <button onclick="nextStep(4)" id="btn-next-4" class="hidden bg-white text-black font-bold py-2 px-6 rounded-lg hover:bg-gray-200 transition"> | |
| Próximo: Cenas <i class="fa-solid fa-arrow-right"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- STEP 4: IMAGENS --> | |
| <div id="step-4" class="glass-panel rounded-xl p-8 border-t-4 border-pink-500 hidden"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <div> | |
| <h2 class="text-2xl font-bold text-white">4. Construção de Cenas</h2> | |
| <p class="text-gray-400 text-sm">Gerando imagens contextuais a cada 3 segundos.</p> | |
| </div> | |
| <div class="text-right"> | |
| <p class="text-xs text-gray-500 uppercase">Modelo Selecionado</p> | |
| <p class="text-pink-400 font-bold">Nano Banana (SDXL)</p> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-2 md:grid-cols-4 gap-4" id="scene-grid"> | |
| <!-- Scenes generated here --> | |
| </div> | |
| <div class="mt-8 p-4 bg-gray-800/50 rounded-lg border border-gray-700"> | |
| <h4 class="text-sm font-bold text-white mb-2"><i class="fa-solid fa-lightbulb text-yellow-400"></i> Contexto Visual Atual</h4> | |
| <p class="text-xs text-gray-400">O sistema analisou o roteiro e determinou o tema: <span id="visual-theme" class="text-cyan-400 font-mono">[Tema não definido]</span>.</p> | |
| </div> | |
| <div class="mt-6 flex justify-between"> | |
| <button onclick="prevStep(3)" class="text-gray-400 hover:text-white font-medium py-2 px-4">Voltar</button> | |
| <button onclick="nextStep(5)" class="bg-white text-black font-bold py-2 px-6 rounded-lg hover:bg-gray-200 transition"> | |
| Finalizar Vídeo <i class="fa-solid fa-film"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- STEP 5: RENDER --> | |
| <div id="step-5" class="glass-panel rounded-xl p-8 border-t-4 border-green-500 hidden"> | |
| <h2 class="text-2xl font-bold text-white mb-6 text-center">5. Renderização Final</h2> | |
| <div class="max-w-2xl mx-auto bg-black rounded-xl overflow-hidden border border-gray-800 shadow-2xl relative aspect-video group"> | |
| <!-- Video Player Simulation --> | |
| <div id="video-player" class="w-full h-full flex items-center justify-center bg-gray-900 relative"> | |
| <img id="render-preview-img" src="" class="absolute inset-0 w-full h-full object-cover opacity-50 transition-opacity duration-300"> | |
| <div class="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent"></div> | |
| <!-- Playback Controls Overlay --> | |
| <div class="absolute bottom-0 left-0 right-0 p-4 z-20"> | |
| <div class="w-full bg-gray-700 h-1 rounded-full mb-4 overflow-hidden"> | |
| <div id="progress-bar" class="bg-green-500 h-full w-0 transition-all duration-100"></div> | |
| </div> | |
| <div class="flex justify-between items-center text-white"> | |
| <div class="flex gap-4"> | |
| <button id="play-pause-btn" class="hover:text-green-400"><i class="fa-solid fa-play"></i></button> | |
| <span class="text-xs font-mono mt-1">00:00 / <span id="total-duration">00:00</span></span> | |
| </div> | |
| <div class="flex gap-4"> | |
| <button class="hover:text-green-400"><i class="fa-solid fa-closed-captioning"></i></button> | |
| <button class="hover:text-green-400"><i class="fa-solid fa-expand"></i></button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Subtitle Overlay --> | |
| <div class="absolute bottom-16 left-0 right-0 text-center px-8 z-10 pointer-events-none"> | |
| <p id="video-subtitle" class="text-white text-lg md:text-2xl font-bold drop-shadow-md stroke-black" style="text-shadow: 2px 2px 0 #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;"> | |
| Legendas aparecerão aqui | |
| </p> | |
| </div> | |
| <!-- Center Play Button (Initial State) --> | |
| <button id="center-play-btn" class="absolute z-30 w-16 h-16 bg-white/20 backdrop-blur rounded-full flex items-center justify-center hover:scale-110 transition text-white"> | |
| <i class="fa-solid fa-play text-2xl ml-1"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mt-8 flex justify-center gap-4"> | |
| <button onclick="resetApp()" class="px-6 py-3 rounded-lg border border-gray-600 text-gray-300 hover:bg-gray-800 transition">Novo Projeto</button> | |
| <button class="px-6 py-3 rounded-lg bg-green-600 hover:bg-green-500 text-white font-bold shadow-lg shadow-green-500/20 transition flex items-center gap-2"> | |
| <i class="fa-solid fa-download"></i> Baixar MP4 (Simulado) | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Settings Modal --> | |
| <div id="settings-modal" class="fixed inset-0 bg-black/80 backdrop-blur-sm z-[100] hidden flex items-center justify-center"> | |
| <div class="bg-gray-900 border border-gray-700 rounded-xl w-full max-w-lg p-6 shadow-2xl transform transition-all scale-100"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h3 class="text-xl font-bold text-white">Configurações de API</h3> | |
| <button onclick="closeSettings()" class="text-gray-400 hover:text-white"><i class="fa-solid fa-xmark text-xl"></i></button> | |
| </div> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-300 mb-1">OpenRouter API Key</label> | |
| <input type="password" id="api-openrouter" class="w-full bg-black border border-gray-700 rounded px-3 py-2 text-white focus:border-cyan-500 outline-none" placeholder="sk-or-..."> | |
| <p class="text-xs text-gray-500 mt-1">Usado para geração de roteiros.</p> | |
| </div> | |
| <div class="border-t border-gray-800 my-4"></div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-300 mb-1">ElevenLabs API Key</label> | |
| <input type="password" id="api-eleven" class="w-full bg-black border border-gray-700 rounded px-3 py-2 text-white focus:border-purple-500 outline-none"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-300 mb-1">Azure Speech Key</label> | |
| <input type="password" id="api-azure" class="w-full bg-black border border-gray-700 rounded px-3 py-2 text-white focus:border-blue-500 outline-none"> | |
| </div> | |
| </div> | |
| <div class="mt-8 flex justify-end"> | |
| <button onclick="saveSettings()" class="bg-cyan-600 hover:bg-cyan-500 text-white px-6 py-2 rounded-lg font-medium transition">Salvar Configurações</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // --- STATE MANAGEMENT --- | |
| const state = { | |
| step: 1, | |
| script: "", | |
| audioUrl: null, | |
| transcription: [], // Array of { text, start, end } | |
| scenes: [], // Array of image URLs | |
| duration: 30, | |
| isPlaying: false | |
| }; | |
| // --- MOCK DATA & UTILS --- | |
| const MOCK_IMAGES = [ | |
| "https://images.unsplash.com/photo-1557804506-669a67965ba0?w=800&q=80", // Business | |
| "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=800&q=80", // Laptop | |
| "https://images.unsplash.com/photo-1556761175-5973dc0f32e7?w=800&q=80", // Meeting | |
| "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&q=80", // Charts | |
| "https://images.unsplash.com/photo-1507679799987-c73779587ccf?w=800&q=80", // Suit | |
| "https://images.unsplash.com/photo-1531297424006-dbfa3e92ea1d?w=800&q=80" // Tech | |
| ]; | |
| // --- NAVIGATION --- | |
| function nextStep(step) { | |
| document.getElementById(`step-${state.step}`).classList.add('hidden'); | |
| document.getElementById(`step-indicator-${state.step}`).classList.remove('step-active'); | |
| document.getElementById(`step-indicator-${state.step}`).classList.add('step-completed'); | |
| state.step = step; | |
| document.getElementById(`step-${state.step}`).classList.remove('hidden'); | |
| document.getElementById(`step-indicator-${state.step}`).classList.remove('opacity-50'); | |
| document.getElementById(`step-indicator-${state.step}`).classList.add('step-active'); | |
| // Scroll to top | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| } | |
| function prevStep(step) { | |
| document.getElementById(`step-${state.step}`).classList.add('hidden'); | |
| document.getElementById(`step-indicator-${state.step}`).classList.add('opacity-50'); | |
| document.getElementById(`step-indicator-${state.step}`).classList.remove('step-active'); | |
| state.step = step; | |
| document.getElementById(`step-${state.step}`).classList.remove('hidden'); | |
| document.getElementById(`step-indicator-${state.step}`).classList.add('step-active'); | |
| } | |
| // --- STEP 1: SCRIPT --- | |
| function setScriptMode(mode) { | |
| const btnManual = document.getElementById('btn-manual'); | |
| const btnAi = document.getElementById('btn-ai'); | |
| const aiInputs = document.getElementById('ai-inputs'); | |
| if (mode === 'manual') { | |
| btnManual.classList.replace('bg-gray-700', 'bg-cyan-600'); | |
| btnAi.classList.replace('bg-cyan-600', 'bg-gray-700'); | |
| aiInputs.classList.add('hidden'); | |
| } else { | |
| btnAi.classList.replace('bg-gray-700', 'bg-cyan-600'); | |
| btnManual.classList.replace('bg-cyan-600', 'bg-gray-700'); | |
| aiInputs.classList.remove('hidden'); | |
| } | |
| } | |
| // Character counting | |
| document.getElementById('script-editor').addEventListener('input', (e) => { | |
| const text = e.target.value; | |
| document.getElementById('char-count').innerText = text.length; | |
| // Rough estimate: 130 words per minute. Avg 5 chars per word. ~650 chars per min -> ~11 chars per sec | |
| const seconds = Math.ceil(text.length / 11); | |
| document.getElementById('word-time').innerText = seconds; | |
| }); | |
| // Duration Slider | |
| document.getElementById('duration-slider').addEventListener('input', (e) => { | |
| document.getElementById('duration-display').innerText = e.target.value + 's'; | |
| state.duration = parseInt(e.target.value); | |
| }); | |
| async function generateScript() { | |
| const product = document.getElementById('product-input').value; | |
| const audience = document.getElementById('audience-input').value; | |
| const tone = document.getElementById('tone-select').value; | |
| const btn = document.querySelector('#ai-inputs button'); | |
| if (!product || !audience) { | |
| alert("Preencha o produto e público alvo."); | |
| return; | |
| } | |
| const originalText = btn.innerHTML; | |
| btn.innerHTML = `<i class="fa-solid fa-circle-notch fa-spin"></i> Gerando...`; | |
| btn.disabled = true; | |
| // Simulate API Call to OpenRouter | |
| await new Promise(r => setTimeout(r, 2000)); | |
| // Mock Content Generation | |
| const hooks = [ | |
| "Você está perdendo dinheiro todos os dias sem perceber.", | |
| "Imagine acordar com vendas automáticas na sua conta.", | |
| "O mercado mudou. E quem não adaptar, será engolido." | |
| ]; | |
| const bodies = [ | |
| `O ${product} não é apenas mais um curso. É um sistema validado por ${audience} que já faturaram milhões.`, | |
| `Nós testamos essa estratégia com 500 ${audience}. O resultado? Um aumento de 300% em conversão.`, | |
| `Pare de tentar adivinhar. Use dados. Use inteligência. Use o ${product}.` | |
| ]; | |
| const closes = [ | |
| "Clique no botão abaixo agora e transforme seu resultado.", | |
| "As vagas estão acabando. Garanta a sua agora.", | |
| "Seu futuro começa com uma decisão. Decida agora." | |
| ]; | |
| const generatedScript = `${hooks[Math.floor(Math.random()*hooks.length)]}\n\n${bodies[Math.floor(Math.random()*bodies.length)]}\n\n${closes[Math.floor(Math.random()*closes.length)]}`; | |
| // Typewriter effect | |
| const editor = document.getElementById('script-editor'); | |
| editor.value = ""; | |
| let i = 0; | |
| const type = () => { | |
| if (i < generatedScript.length) { | |
| editor.value += generatedScript.charAt(i); | |
| i++; | |
| setTimeout(type, 20); | |
| } else { | |
| btn.innerHTML = originalText; | |
| btn.disabled = false; | |
| // Trigger input event to update counters | |
| editor.dispatchEvent(new Event('input')); | |
| } | |
| }; | |
| type(); | |
| } | |
| // --- STEP 2: AUDIO --- | |
| function selectVoiceProvider(provider) { | |
| // Visual selection logic | |
| const cards = document.querySelectorAll('#step-2 .grid > div'); | |
| cards.forEach(c => c.classList.remove('border-purple-500', 'bg-gray-800')); | |
| cards.forEach(c => c.classList.add('border-gray-700', 'bg-gray-800/50')); | |
| // Find clicked (simplified for demo) | |
| event.currentTarget.classList.remove('border-gray-700', 'bg-gray-800/50'); | |
| event.currentTarget.classList.add('border-purple-500', 'bg-gray-800'); | |
| } | |
| async function generateAudio() { | |
| const container = document.getElementById('audio-progress-container'); | |
| const btn = document.getElementById('btn-generate-audio'); | |
| const nextBtn = document.getElementById('btn-next-3'); | |
| btn.innerHTML = `<i class="fa-solid fa-circle-notch fa-spin"></i> Processando...`; | |
| btn.disabled = true; | |
| // Simulate chunking | |
| container.innerHTML = ''; | |
| const chunks = 4; | |
| for (let i = 1; i <= chunks; i++) { | |
| const div = document.createElement('div'); | |
| div.className = "flex items-center gap-3 bg-gray-800 p-3 rounded border border-gray-700"; | |
| div.innerHTML = ` | |
| <div class="w-2 h-2 rounded-full bg-yellow-500 animate-pulse"></div> | |
| <div class="flex-1"> | |
| <div class="flex justify-between text-xs mb-1"> | |
| <span class="text-gray-300">Bloco de Áudio ${i}/${chunks}</span> | |
| <span class="text-gray-500">Processando...</span> | |
| </div> | |
| <div class="w-full bg-gray-700 h-1.5 rounded-full overflow-hidden"> | |
| <div class="bg-purple-500 h-full w-0 transition-all duration-1000" style="width: 0%" id="prog-${i}"></div> | |
| </div> | |
| </div> | |
| `; | |
| container.appendChild(div); | |
| // Simulate processing time per chunk | |
| await new Promise(r => setTimeout(r, 800)); | |
| document.getElementById(`prog-${i}`).style.width = '100%'; | |
| div.querySelector('.text-gray-500').innerText = "Concluído"; | |
| div.querySelector('.bg-yellow-500').classList.replace('bg-yellow-500', 'bg-green-500'); | |
| div.querySelector('.bg-yellow-500').classList.remove('animate-pulse'); | |
| } | |
| // Finalize | |
| btn.innerHTML = `<i class="fa-solid fa-check"></i> Áudio Gerado`; | |
| btn.classList.replace('bg-purple-600', 'bg-green-600'); | |
| nextBtn.disabled = false; | |
| nextBtn.classList.remove('opacity-50', 'cursor-not-allowed'); | |
| // Create a dummy audio URL (Tone.js synth for demo) | |
| // In a real app, this would be the concatenated Blob URL | |
| } | |
| // --- STEP 3: TRANSCRIPTION --- | |
| async function transcribeAudio() { | |
| const btn = document.getElementById('btn-transcribe'); | |
| const nextBtn = document.getElementById('btn-next-4'); | |
| const loader = document.getElementById('transcribing-text'); | |
| const preview = document.getElementById('transcription-preview'); | |
| btn.classList.add('hidden'); | |
| loader.classList.remove('hidden'); | |
| loader.classList.add('flex', 'flex-col', 'items-center'); | |
| // Simulate Whisper API delay | |
| await new Promise(r => setTimeout(r, 2500)); | |
| // Generate mock transcription data | |
| const scriptText = document.getElementById('script-editor').value; | |
| const words = scriptText.split(' '); | |
| const wordDuration = 0.4; // seconds per word approx | |
| let currentTime = 0; | |
| let srtContent = ""; | |
| words.forEach((word, index) => { | |
| const start = currentTime; | |
| const end = currentTime + wordDuration + (Math.random() * 0.2); | |
| state.transcription.push({ | |
| word: word, | |
| start: start, | |
| end: end | |
| }); | |
| srtContent += `<div class="mb-1"><span class="text-yellow-500">${start.toFixed(2)}s</span> ${word}</div>`; | |
| currentTime = end; | |
| }); | |
| loader.classList.add('hidden'); | |
| loader.classList.remove('flex'); | |
| preview.innerHTML = srtContent; | |
| nextBtn.classList.remove('hidden'); | |
| } | |
| // --- STEP 4: IMAGES --- | |
| // Pre-load step 4 visuals when entering | |
| function initStep4() { | |
| const grid = document.getElementById('scene-grid'); | |
| grid.innerHTML = ''; | |
| // Determine number of scenes based on duration (every 3 seconds) | |
| const numScenes = Math.ceil(state.duration / 3); | |
| const theme = document.getElementById('product-input').value || "Marketing Digital"; | |
| document.getElementById('visual-theme').innerText = theme; | |
| for(let i=0; i<numScenes; i++) { | |
| const div = document.createElement('div'); | |
| div.className = "relative group aspect-video bg-gray-800 rounded-lg overflow-hidden border border-gray-700"; | |
| div.innerHTML = ` | |
| <div class="absolute inset-0 flex items-center justify-center bg-gray-900 z-10" id="img-loader-${i}"> | |
| <i class="fa-solid fa-image text-gray-600 text-2xl animate-bounce"></i> | |
| </div> | |
| <img id="scene-img-${i}" src="" class="w-full h-full object-cover opacity-0 transition-opacity duration-500"> | |
| <div class="absolute bottom-0 left-0 right-0 bg-black/70 p-2 translate-y-full group-hover:translate-y-0 transition-transform"> | |
| <button class="text-xs text-white hover:text-cyan-400 w-full text-left"><i class="fa-solid fa-rotate"></i> Regenerar</button> | |
| <button class="text-xs text-white hover:text-red-400 w-full text-left"><i class="fa-solid fa-trash"></i> Remover</button> | |
| </div> | |
| <div class="absolute top-2 right-2 bg-black/50 text-white text-xs px-2 py-1 rounded backdrop-blur"> | |
| 00:${(i*3).toString().padStart(2,'0')} | |
| </div> | |
| `; | |
| grid.appendChild(div); | |
| // Simulate Image Gen Delay | |
| setTimeout(() => { | |
| const img = document.getElementById(`scene-img-${i}`); | |
| const loader = document.getElementById(`img-loader-${i}`); | |
| // Pick random mock image | |
| img.src = MOCK_IMAGES[Math.floor(Math.random() * MOCK_IMAGES.length)]; | |
| img.onload = () => { | |
| img.classList.remove('opacity-0'); | |
| loader.classList.add('hidden'); | |
| }; | |
| state.scenes.push(img.src); | |
| }, 500 + (i * 800)); | |
| } | |
| } | |
| // Hook step 4 init | |
| const observer = new MutationObserver((mutations) => { | |
| mutations.forEach((mutation) => { | |
| if (mutation.target.id === 'step-4' && !mutation.target.classList.contains('hidden')) { | |
| initStep4(); | |
| } | |
| }); | |
| }); | |
| observer.observe(document.getElementById('step-4'), { attributes: true, attributeFilter: ['class'] }); | |
| // --- STEP 5: PLAYER LOGIC --- | |
| let playbackInterval; | |
| // Hook step 5 init | |
| const observer5 = new MutationObserver((mutations) => { | |
| mutations.forEach((mutation) => { | |
| if (mutation.target.id === 'step-5' && !mutation.target.classList.contains('hidden')) { | |
| initPlayer(); | |
| } | |
| }); | |
| }); | |
| observer5.observe(document.getElementById('step-5'), { attributes: true, attributeFilter: ['class'] }); | |
| function initPlayer() { | |
| const playBtn = document.getElementById('center-play-btn'); | |
| const playPauseBtn = document.getElementById('play-pause-btn'); | |
| playBtn.onclick = togglePlay; | |
| playPauseBtn.onclick = togglePlay; | |
| // Set total duration | |
| const mins = Math.floor(state.duration / 60); | |
| const secs = state.duration % 60; | |
| document.getElementById('total-duration').innerText = `${mins.toString().padStart(2,'0')}:${secs.toString().padStart(2,'0')}`; | |
| // Set first image | |
| if(state.scenes.length > 0) { | |
| document.getElementById('render-preview-img').src = state.scenes[0]; | |
| } | |
| } | |
| function togglePlay() { | |
| state.isPlaying = !state.isPlaying; | |
| const icon = state.isPlaying ? '<i class="fa-solid fa-pause"></i>' : '<i class="fa-solid fa-play"></i>'; | |
| document.getElementById('play-pause-btn').innerHTML = icon; | |
| document.getElementById('center-play-btn').style.opacity = state.isPlaying ? '0' : '1'; | |
| document.getElementById('center-play-btn').style.pointerEvents = state.isPlaying ? 'none' : 'auto'; | |
| if (state.isPlaying) { | |
| let currentTime = 0; | |
| const progressBar = document.getElementById('progress-bar'); | |
| const subtitleEl = document.getElementById('video-subtitle'); | |
| const imgEl = document.getElementById('render-preview-img'); | |
| playbackInterval = setInterval(() => { | |
| currentTime += 0.1; | |
| // Update Progress Bar | |
| const pct = (currentTime / state.duration) * 100; | |
| progressBar.style.width = `${pct}%`; | |
| // Update Subtitles | |
| // Find current word | |
| const currentWordObj = state.transcription.find(t => t.start <= currentTime && t.end >= currentTime); | |
| if (currentWordObj) { | |
| subtitleEl.innerText = currentWordObj.word; | |
| } else { | |
| // Simple logic to show phrase if word precision fails (mock) | |
| // In a real app, we map time to phrase | |
| } |