import React, { useState, useRef, useEffect } from ‘react’;
import {
Wand2, Play, Download, Settings,
ListVideo, Sparkles, Scissors, Volume2,
CheckCircle, Loader2, Music, Mic,
Clock, Smile, CheckSquare, Maximize,
Pause, RotateCcw, Trash, Type, Zap
} from ‘lucide-react’;
const videoColors = [“bg-blue-500”, “bg-rose-500”, “bg-amber-500”, “bg-emerald-500”, “bg-purple-500”, “bg-cyan-500”, “bg-fuchsia-500”, “bg-lime-500”, “bg-orange-500”, “bg-indigo-500″];
// NOVO: Detetor de Palavras-Chave para o Estilo Alex Hormozi
const getHormoziStyle = (word) => {
const w = word.toLowerCase().replace(/[^a-zãáâéêíóôúç]/g, ”);
let color = ‘#FFFFFF’;
let emoji = ”;
if ([‘dinheiro’, ‘rico’, ‘milionario’, ‘milhoes’, ‘lucro’, ‘venda’, ‘cash’].includes(w)) { color = ‘#4ADE80’; emoji = ‘💸’; }
else if ([‘nao’, ‘nunca’, ‘proibido’, ‘morte’, ‘perigo’, ‘pior’, ‘ruim’].includes(w)) { color = ‘#EF4444’; emoji = ‘🚫’; }
else if ([‘incrivel’, ‘segredo’, ‘descubra’, ‘wow’, ‘magica’, ‘fogo’, ‘explosao’, ‘bum’].includes(w)) { color = ‘#FBBF24’; emoji = ‘💥’; }
else if ([‘amor’, ‘coracao’, ‘paixao’, ‘sentimento’].includes(w)) { color = ‘#EC4899’; emoji = ‘❤️’; }
else if ([‘ideia’, ‘cerebro’, ‘inteligente’, ‘mente’, ‘pensar’].includes(w)) { color = ‘#60A5FA’; emoji = ‘🧠’; }
else if ([‘tempo’, ‘rapido’, ‘hora’, ‘agora’, ‘ja’].includes(w)) { color = ‘#F97316’; emoji = ‘⏱️’; }
// Rotação pseudo-aleatória fixa baseada na palavra
const rot = (word.length % 3 === 0 ? 0.06 : (word.length % 2 === 0 ? -0.04 : 0));
return { color, emoji, rot };
};
// NOVO: Detetor de Palavras-Chave para o Estilo MrBeast
const getMrBeastStyle = (word) => {
const w = word.toLowerCase().replace(/[^a-zãáâéêíóôúç]/g, ”);
let bgColor = ‘rgba(0, 0, 0, 0.85)’;
let textColor = ‘#FFFFFF’;
if ([‘dinheiro’, ‘rico’, ‘milhoes’, ‘lucro’, ‘venda’, ‘cash’, ‘ganhar’].includes(w)) { bgColor = ‘#FDE047’; textColor = ‘#000000’; }
else if ([‘nao’, ‘nunca’, ‘morte’, ‘perigo’, ‘pior’, ‘ruim’].includes(w)) { bgColor = ‘#EF4444’; textColor = ‘#FFFFFF’; }
else if ([‘incrivel’, ‘segredo’, ‘descubra’, ‘novo’, ‘magica’, ‘facil’].includes(w)) { bgColor = ‘#06B6D4’; textColor = ‘#FFFFFF’; }
else if ([‘amor’, ‘coracao’, ‘feliz’, ‘bom’].includes(w)) { bgColor = ‘#EC4899’; textColor = ‘#FFFFFF’; }
const rot = (word.length % 2 === 0 ? 0.04 : -0.04);
return { bgColor, textColor, rot };
};
// Utilitário Web Audio API nativa para SFX Cinematográficos
const playGeneratedSFX = (type, audioCtx, destination = audioCtx.destination, masterVol = 1.0) => {
const t = audioCtx.currentTime;
const masterGain = audioCtx.createGain();
masterGain.gain.value = masterVol;
masterGain.connect(destination);
if (type === ‘impact’) {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = ‘sine’;
osc.frequency.setValueAtTime(150, t);
osc.frequency.exponentialRampToValueAtTime(0.001, t + 1.5);
gain.gain.setValueAtTime(2.5, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 1.5);
osc.connect(gain); gain.connect(masterGain);
osc.start(t); osc.stop(t + 1.5);
const bufferSize = audioCtx.sampleRate * 0.2;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
const noise = audioCtx.createBufferSource();
noise.buffer = buffer;
const noiseFilter = audioCtx.createBiquadFilter();
noiseFilter.type = 'lowpass';
noiseFilter.frequency.setValueAtTime(1000, t);
noiseFilter.frequency.exponentialRampToValueAtTime(100, t + 0.2);
const noiseGain = audioCtx.createGain();
noiseGain.gain.setValueAtTime(1.5, t);
noiseGain.gain.exponentialRampToValueAtTime(0.01, t + 0.2);
noise.connect(noiseFilter); noiseFilter.connect(noiseGain); noiseGain.connect(masterGain);
noise.start(t);
} else if (type === 'riser') {
const duration = 2.0;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(50, t);
osc.frequency.exponentialRampToValueAtTime(1000, t + duration);
gain.gain.setValueAtTime(0, t);
gain.gain.linearRampToValueAtTime(0.4, t + duration - 0.2);
gain.gain.linearRampToValueAtTime(0, t + duration);
const filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(200, t);
filter.frequency.exponentialRampToValueAtTime(3000, t + duration);
osc.connect(filter); filter.connect(gain); gain.connect(masterGain);
osc.start(t); osc.stop(t + duration);
} else if (type === 'heartbeat') {
const playBeat = (offset, vol) => {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = ‘sine’;
osc.frequency.setValueAtTime(80, t + offset);
osc.frequency.exponentialRampToValueAtTime(20, t + offset + 0.2);
gain.gain.setValueAtTime(vol, t + offset);
gain.gain.exponentialRampToValueAtTime(0.01, t + offset + 0.2);
osc.connect(gain); gain.connect(masterGain);
osc.start(t + offset); osc.stop(t + offset + 0.2);
}
playBeat(0, 1.5);
playBeat(0.25, 2.0);
} else if (type === ‘gasp’) {
const bufferSize = audioCtx.sampleRate * 1.5;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
const noise = audioCtx.createBufferSource();
noise.buffer = buffer;
const bandpass = audioCtx.createBiquadFilter();
bandpass.type = 'bandpass';
bandpass.frequency.value = 800;
bandpass.Q.value = 1.0;
const gain = audioCtx.createGain();
gain.gain.setValueAtTime(0, t);
gain.gain.linearRampToValueAtTime(1.5, t + 0.2);
gain.gain.exponentialRampToValueAtTime(0.01, t + 1.0);
noise.connect(bandpass); bandpass.connect(gain); gain.connect(masterGain);
noise.start(t);
} else if (type === 'cash') {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(1200, t);
osc.frequency.setValueAtTime(1600, t + 0.1);
gain.gain.setValueAtTime(0, t);
gain.gain.linearRampToValueAtTime(0.5, t + 0.02);
gain.gain.linearRampToValueAtTime(0, t + 0.3);
osc.connect(gain); gain.connect(masterGain);
osc.start(t); osc.stop(t + 0.3);
}
};
// Utilitário PCM -> WAV
const pcmToWav = (pcmData, sampleRate) => {
const numChannels = 1;
const bitsPerSample = 16;
const blockAlign = numChannels * (bitsPerSample / 8);
const byteRate = sampleRate * blockAlign;
const dataSize = pcmData.length;
const chunkSize = 36 + dataSize;
const buffer = new ArrayBuffer(44 + dataSize);
const view = new DataView(buffer);
const writeString = (view, offset, string) => {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
};
writeString(view, 0, 'RIFF');
view.setUint32(4, chunkSize, true);
writeString(view, 8, 'WAVE');
writeString(view, 12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, numChannels, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, byteRate, true);
view.setUint16(32, blockAlign, true);
view.setUint16(34, bitsPerSample, true);
writeString(view, 36, 'data');
view.setUint32(40, dataSize, true);
for (let i = 0; i < dataSize; i++) {
view.setUint8(44 + i, pcmData[i]);
}
return new Blob([view], { type: 'audio/wav' });
};
// --- COMPONENTES SECUNDÁRIOS ---
const StepIndicator = ({ currentStep }) => {
const steps = [
{ id: 1, label: ‘Importar Links’ },
{ id: 2, label: ‘Editor & Timeline’ },
{ id: 3, label: ‘Exportar & Viralizar’ }
];
return (
{steps.map((step, index) => (
step.id ? ‘bg-emerald-500 text-white’ : ‘bg-gray-800 text-gray-500’}`}>
{currentStep > step.id ?
{step.label}
{index < steps.length - 1 && (
step.id ? 'bg-emerald-500' : 'bg-gray-800'}`} />
)}
))}
);
};
// — COMPONENTE PRINCIPAL —
export default function App() {
const [step, setStep] = useState(1);
const [isProcessing, setIsProcessing] = useState(false);
const [loadingMsg, setLoadingMsg] = useState(“”);
const [loadingProgress, setLoadingProgress] = useState(0);
const [magicTone, setMagicTone] = useState(“Sério e Impactante”);
const [magicNarrators, setMagicNarrators] = useState(1);
const [isGeneratingMagic, setIsGeneratingMagic] = useState(false);
const [magicContext, setMagicContext] = useState(“”);
const [globalPrefix, setGlobalPrefix] = useState(“TOP”);
const [showNumbers, setShowNumbers] = useState(true);
const [topCount, setTopCount] = useState(5);
const [bgmLibrary, setBgmLibrary] = useState([]);
const [useStickers, setUseStickers] = useState(true);
const [isExporting, setIsExporting] = useState(false);
const [exportProgress, setExportProgress] = useState(0);
const [exportMsg, setExportMsg] = useState(“Preparando arquivos…”);
const [finalVideoUrl, setFinalVideoUrl] = useState(null);
const [generalTitle, setGeneralTitle] = useState(“”);
const [videoType, setVideoType] = useState(“best”);
const [videos, setVideos] = useState(
Array.from({ length: 5 }, (_, i) => ({
id: i + 1, url: “”, title: “”, color: videoColors[i], localUrl: null, trimStart: 0, trimEnd: 100,
caption: “”, volume: 100, bgmUrl: null, bgmName: “”, bgmVolume: 30, narrationVolume: 100, videoDuration: 0,
zoom: 100, posX: 0, posY: 0, captionY: 15, narrationCaptionY: 80, showNarration: true,
narrationCaptionSize: 60, narrationCaptionStyle: ‘hormozi’, captionSize: 68, captionStyle: ‘style1’, customTopLabel: null,
filter: ‘none’, autoDucking: true,
narrations: [{ id: Math.random().toString(36).substring(2, 9), script: “”, narrationVoice: ‘Fenrir’, narrationTone: ‘enérgica e profissional’, customAudioUrl: null, audioDuration: 0, timedCaptions: [], isGeneratingScript: false, isGeneratingText: false, audioGenerated: false }],
sfxTracks: [],
vfxTracks: []
}))
);
const [playingItemId, setPlayingItemId] = useState(null);
const [currentTimePreview, setCurrentTimePreview] = useState(0);
const [activeToolTab, setActiveToolTab] = useState(‘enquadramento’);
const [isPreviewPlaying, setIsPreviewPlaying] = useState(true);
const previewVideoRef = useRef(null);
const bgmRef = useRef(null);
const previewAudioCtxRef = useRef(null);
const playedSFXIdsRef = useRef(new Set());
const shakeWrapperRef = useRef(null);
const activeVideoForPreview = videos.find(v => v.id === playingItemId) || videos[0];
const getFilterStyle = (filterType) => {
switch(filterType) {
case ‘cinematic’: return ‘contrast(1.2) saturate(1.3) brightness(0.9)’;
case ‘sigma’: return ‘grayscale(1) contrast(1.5) brightness(0.8)’;
case ‘vintage’: return ‘sepia(0.6) contrast(1.1) brightness(0.9) hue-rotate(-15deg)’;
case ‘cyberpunk’: return ‘contrast(1.3) saturate(1.8) hue-rotate(45deg)’;
default: return ‘none’;
}
};
useEffect(() => {
if (isPreviewPlaying) {
previewVideoRef.current?.play().catch(()=>{});
bgmRef.current?.play().catch(()=>{});
} else {
previewVideoRef.current?.pause();
bgmRef.current?.pause();
activeVideoForPreview?.narrations.forEach(n => {
const audioEl = document.getElementById(`audio-${n.id}`);
if (audioEl) audioEl.pause();
});
}
}, [isPreviewPlaying, playingItemId, activeVideoForPreview]);
useEffect(() => {
if (previewVideoRef.current && activeVideoForPreview) previewVideoRef.current.volume = activeVideoForPreview.volume / 100;
if (bgmRef.current && activeVideoForPreview) bgmRef.current.volume = activeVideoForPreview.bgmVolume / 100;
}, [activeVideoForPreview?.volume, activeVideoForPreview?.bgmVolume, playingItemId]);
useEffect(() => {
if (previewVideoRef.current && activeVideoForPreview) {
const startSec = (activeVideoForPreview.trimStart / 100) * activeVideoForPreview.videoDuration;
const endSec = (activeVideoForPreview.trimEnd / 100) * activeVideoForPreview.videoDuration;
if (previewVideoRef.current.currentTime < startSec || previewVideoRef.current.currentTime > endSec) {
previewVideoRef.current.currentTime = startSec;
if (bgmRef.current) bgmRef.current.currentTime = 0;
activeVideoForPreview.narrations.forEach(n => {
const audioEl = document.getElementById(`audio-${n.id}`);
if (audioEl) { audioEl.pause(); audioEl.currentTime = 0; }
});
}
}
}, [activeVideoForPreview?.trimStart, activeVideoForPreview?.trimEnd, playingItemId]);
useEffect(() => {
let animationFrameId;
const renderLoop = () => {
if (previewVideoRef.current && shakeWrapperRef.current && activeVideoForPreview) {
const t = previewVideoRef.current.currentTime;
const startSec = (activeVideoForPreview.trimStart / 100) * activeVideoForPreview.videoDuration;
const endSec = (activeVideoForPreview.trimEnd / 100) * activeVideoForPreview.videoDuration;
let dx = 0;
let dy = 0;
if (activeVideoForPreview.vfxTracks && isPreviewPlaying) {
activeVideoForPreview.vfxTracks.forEach(vfx => {
const sTime = startSec + (vfx.startTimePerc / 100) * (endSec – startSec);
const eTime = sTime + (vfx.durationPerc / 100) * (endSec – startSec);
if (t >= sTime && t <= eTime) {
const intensity = (vfx.intensity || 50) / 100 * 20;
const speed = (vfx.speed || 50) / 100 * 50;
const timeMs = t * 1000;
dx += (Math.sin(timeMs * 0.01 * speed) + Math.cos(timeMs * 0.013 * speed)) * intensity;
dy += (Math.cos(timeMs * 0.011 * speed) + Math.sin(timeMs * 0.009 * speed)) * intensity;
}
});
}
shakeWrapperRef.current.style.transform = `translate(${dx}px, ${dy}px)`;
}
animationFrameId = requestAnimationFrame(renderLoop);
};
renderLoop();
return () => cancelAnimationFrame(animationFrameId);
}, [isPreviewPlaying, activeVideoForPreview]);
const getSticker = (index) => {
const bestEmojis = [‘😱’, ‘🤩’, ‘🤯’, ‘💀’, ‘😲’, ‘☠️’, ‘🤩’, ‘😱’];
const worstEmojis = [‘😭’, ‘💀’, ‘😢’, ‘😱’, ‘☠️’, ‘😨’, ‘😭’, ‘🤦’];
const list = videoType === ‘best’ ? bestEmojis : worstEmojis;
return list[index % list.length];
};
const handleTopCountChange = (e) => {
const newCount = parseInt(e.target.value);
setTopCount(newCount);
setVideos(prev => {
const newVideos = […prev];
if (newCount > prev.length) {
for (let i = prev.length; i < newCount; i++) {
newVideos.push({
id: i + 1, url: "", title: "", color: videoColors[i % 10], localUrl: null, trimStart: 0, trimEnd: 100,
caption: "", volume: 100, bgmUrl: null, bgmName: "", bgmVolume: 30, narrationVolume: 100, videoDuration: 0,
zoom: 100, posX: 0, posY: 0, captionY: 15, narrationCaptionY: 80, showNarration: true,
narrationCaptionSize: 60, narrationCaptionStyle: 'hormozi', captionSize: 68, captionStyle: 'style1', customTopLabel: null,
filter: 'none', autoDucking: true,
narrations: [{ id: Math.random().toString(36).substring(2, 9), script: "", narrationVoice: 'Fenrir', narrationTone: 'enérgica e profissional', customAudioUrl: null, audioDuration: 0, timedCaptions: [], isGeneratingScript: false, isGeneratingText: false, audioGenerated: false }],
sfxTracks: [],
vfxTracks: []
});
}
} else if (newCount < prev.length) {
return newVideos.slice(0, newCount);
}
return newVideos;
});
};
const handleVideoChange = (id, field, value) => {
setVideos(prevVideos => prevVideos.map(v => v.id === id ? { …v, [field]: value } : v));
};
const handleFileUpload = (id, event) => {
const file = event.target.files[0];
if (file) {
const localUrl = URL.createObjectURL(file);
const video = document.createElement(‘video’);
video.preload = ‘metadata’;
video.onloadedmetadata = () => {
handleVideoChange(id, ‘videoDuration’, video.duration);
handleVideoChange(id, ‘localUrl’, localUrl);
};
video.src = localUrl;
}
};
const handleBgmFolderUpload = (event) => {
const files = Array.from(event.target.files).filter(file => file.type.startsWith(‘audio/’) || file.name.endsWith(‘.mp3’) || file.name.endsWith(‘.wav’));
if (files.length === 0) return;
const newLibrary = files.map(f => ({ name: f.name, url: URL.createObjectURL(f) }));
setBgmLibrary(prev => […prev, …newLibrary]);
alert(“Músicas carregadas com sucesso!”);
};
const setRandomBgm = (videoId) => {
if (bgmLibrary.length === 0) return alert(“Carregue músicas primeiro!”);
const randomTrack = bgmLibrary[Math.floor(Math.random() * bgmLibrary.length)];
handleVideoChange(videoId, ‘bgmUrl’, randomTrack.url);
handleVideoChange(videoId, ‘bgmName’, randomTrack.name);
};
const applyBgmToAll = (currentUrl, currentName, currentVolume) => {
if (!currentUrl) return alert(“Selecione uma música primeiro!”);
setVideos(videos.map(v => ({ …v, bgmUrl: currentUrl, bgmName: currentName, bgmVolume: currentVolume || 30 })));
alert(“🎵 Música aplicada a todos os clipes!”);
};
const updateNarration = (videoId, narrationId, field, value) => {
setVideos(prev => prev.map(v => {
if (v.id === videoId) {
return { …v, narrations: v.narrations.map(n => n.id === narrationId ? { …n, [field]: value } : n) };
}
return v;
}));
};
const addNarration = (videoId) => {
setVideos(prev => prev.map(v => {
if (v.id === videoId) {
return { …v, narrations: […v.narrations, { id: Math.random().toString(36).substring(2, 9), script: “”, narrationVoice: ‘Fenrir’, narrationTone: ‘enérgica e profissional’, customAudioUrl: null, audioDuration: 0, timedCaptions: [], sfxBlocks: [], isGeneratingScript: false, isGeneratingText: false, audioGenerated: false }] };
}
return v;
}));
};
const removeNarration = (videoId, narrationId) => {
setVideos(prev => prev.map(v => {
if (v.id === videoId) {
return { …v, narrations: v.narrations.filter(n => n.id !== narrationId) };
}
return v;
}));
};
const addSfxTrack = (videoId) => {
setVideos(prev => prev.map(v => {
if (v.id === videoId) {
return { …v, sfxTracks: […v.sfxTracks, { id: Math.random().toString(36).substring(2, 9), type: ‘impact’, startTimePerc: 50, volume: 100 }] };
}
return v;
}));
};
const updateSfxTrack = (videoId, sfxId, field, value) => {
setVideos(prev => prev.map(v => {
if (v.id === videoId) {
return { …v, sfxTracks: v.sfxTracks.map(sfx => sfx.id === sfxId ? { …sfx, [field]: value } : sfx) };
}
return v;
}));
};
const removeSfxTrack = (videoId, sfxId) => {
setVideos(prev => prev.map(v => {
if (v.id === videoId) {
return { …v, sfxTracks: v.sfxTracks.filter(sfx => sfx.id !== sfxId) };
}
return v;
}));
};
const addVfxTrack = (videoId) => {
setVideos(prev => prev.map(v => {
if (v.id === videoId) {
return { …v, vfxTracks: […v.vfxTracks, { id: Math.random().toString(36).substring(2, 9), type: ‘shake’, startTimePerc: 0, durationPerc: 100, intensity: 50, speed: 50 }] };
}
return v;
}));
};
const updateVfxTrack = (videoId, vfxId, field, value) => {
setVideos(prev => prev.map(v => {
if (v.id === videoId) {
return { …v, vfxTracks: v.vfxTracks.map(vfx => vfx.id === vfxId ? { …vfx, [field]: value } : vfx) };
}
return v;
}));
};
const removeVfxTrack = (videoId, vfxId) => {
setVideos(prev => prev.map(v => {
if (v.id === videoId) {
return { …v, vfxTracks: v.vfxTracks.filter(vfx => vfx.id !== vfxId) };
}
return v;
}));
};
const generateTimedCaptions = (text, totalDuration) => {
const words = text.split(/s+/).filter(w => w.length > 0);
const totalChars = words.join(”).length;
let currentStartTime = 0;
return words.map(word => {
const wordWeight = word.length / totalChars;
const wordDuration = totalDuration * wordWeight;
const startTime = currentStartTime;
const endTime = currentStartTime + wordDuration;
currentStartTime = endTime;
return { word, startTime, endTime };
});
};
const generateTTS = async (text, videoId, narrationId, voiceName = ‘Fenrir’, tone = ‘enérgica e profissional’) => {
if (!text) return;
updateNarration(videoId, narrationId, ‘isGeneratingScript’, true);
try {
const apiKey = “”;
const payload = {
contents: [{ parts: [{ text: `Fale de forma ${tone}: ${text}` }] }],
generationConfig: {
responseModalities: [“AUDIO”],
speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: voiceName } } }
},
model: “gemini-2.5-flash-preview-tts”
};
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent?key=${apiKey}`, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify(payload)
});
const result = await response.json();
const inlineData = result.candidates[0].content.parts[0].inlineData;
const base64Data = inlineData.data;
const mimeType = inlineData.mimeType;
const rateMatch = mimeType.match(/rate=(d+)/);
const sampleRate = rateMatch ? parseInt(rateMatch[1], 10) : 24000;
const binaryString = window.atob(base64Data);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
const wavBlob = pcmToWav(bytes, sampleRate);
const audioUrl = URL.createObjectURL(wavBlob);
const audio = new Audio();
audio.onloadedmetadata = () => {
const duration = audio.duration;
const timedCaptions = generateTimedCaptions(text, duration);
updateNarration(videoId, narrationId, ‘audioDuration’, duration);
updateNarration(videoId, narrationId, ‘customAudioUrl’, audioUrl);
updateNarration(videoId, narrationId, ‘audioGenerated’, true);
updateNarration(videoId, narrationId, ‘timedCaptions’, timedCaptions);
};
audio.src = audioUrl;
} catch (e) {
alert(“Erro na narração: ” + e.message);
} finally {
updateNarration(videoId, narrationId, ‘isGeneratingScript’, false);
}
};
const generateTextForNarration = async (videoId, narrationId) => {
const video = videos.find(v => v.id === videoId);
const currentNarrationIndex = video.narrations.findIndex(n => n.id === narrationId);
const currentNarration = video.narrations[currentNarrationIndex];
const previousNarrations = video.narrations.slice(0, currentNarrationIndex).map(n => n.script).join(” “);
const clipName = video.title || `Momento ${videoId}`;
updateNarration(videoId, narrationId, ‘isGeneratingText’, true);
try {
const apiKey = “”;
const prompt = `Você é um roteirista de vídeos virais curtos (TikTok/Shorts).
Tema Geral: “${generalTitle || ‘Tópicos Virais’}”
Assunto deste clipe específico: “${clipName}”
Tom/Emoção exigido para esta fala: ${currentNarration.narrationTone || ‘enérgica e profissional’}
${previousNarrations ? `O que já foi falado neste clipe até agora pelas outras vozes:n”${previousNarrations}”nnTarefa: Escreva APENAS a próxima fala, continuando a história/diálogo de forma super fluida, natural e viciante.` : ‘Tarefa: Escreva APENAS o roteiro inicial curto e super chamativo para este clipe (focado em reter a atenção).’}
Regras Absolutas:
– Responda APENAS com a fala direta. Sem aspas, sem marcações de quem está a falar, sem explicações.
– Curto, dinâmico, máximo de 2 a 3 frases.
– Retenha a atenção do espetador ao máximo!`;
const payload = {
contents: [{ parts: [{ text: prompt }] }],
model: “gemini-2.5-flash-preview-09-2025″
};
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify(payload)
});
const result = await response.json();
let generatedText = result.candidates[0].content.parts[0].text;
generatedText = generatedText.replace(/`/g, ”).replace(/^”/, ”).replace(/”$/, ”).trim();
updateNarration(videoId, narrationId, ‘script’, generatedText);
} catch (e) {
console.error(“Falha ao gerar texto:”, e);
alert(“Erro ao gerar roteiro IA para esta caixa: ” + e.message);
} finally {
updateNarration(videoId, narrationId, ‘isGeneratingText’, false);
}
};
const generateMagicScript = async () => {
const hasTitles = videos.some(v => v.title.trim() !== “”);
if (!generalTitle && !hasTitles && magicContext.trim() === “”) {
console.warn(“Preencha o Título Geral, nomes dos clipes ou cole uma descrição para a IA saber o que fazer!”);
return;
}
setIsGeneratingMagic(true);
try {
const apiKey = “”;
const prompt = `Você é um roteirista de vídeos virais curtos (TikTok/Shorts).
Crie um roteiro formato Top ${topCount}.
Título/Tema Geral: “${generalTitle || ‘Tópicos Virais’}”
Nomes dos clipes informados pelo usuário:
${videos.map((v, i) => `${i + 1}. ${v.title || ‘Crie um nome criativo’}`).join(‘n’)}
${magicContext.trim() !== “” ? `nContexto extra ou Roteiro Base fornecido pelo usuário:n”${magicContext}”n(Use este contexto como base e guia absoluto para escrever as narrações!)` : ”}
Diretrizes Obrigatórias:
– Estilo e Tom do vídeo: ${magicTone}.
– Número de locutores/narradores por clipe: ${magicNarrators}.
– Vozes disponíveis: Fenrir (Masculina enérgica), Kore (Feminina clara), Charon (Idoso), Puck (Adolescente).
– Se houver 2 ou 3 narradores, faça-os interagir, debater ou alternar as falas em cada clipe usando as vozes distintas.
– As falas devem ser dinâmicas e curtas (máximo 2 frases por fala).
– Retorne exatamente ${topCount} clipes no JSON.`;
const payload = {
contents: [{ parts: [{ text: prompt }] }],
generationConfig: {
responseMimeType: “application/json”,
responseSchema: {
type: “OBJECT”,
properties: {
generalTitle: { type: “STRING”, description: “Um título super viral e clickbait para o vídeo geral” },
clips: {
type: “ARRAY”,
items: {
type: “OBJECT”,
properties: {
title: { type: “STRING”, description: “Nome ou título muito curto do item” },
caption: { type: “STRING”, description: “Legenda de impacto/viral para exibir na tela” },
narrations: {
type: “ARRAY”,
items: {
type: “OBJECT”,
properties: {
text: { type: “STRING”, description: “A fala do narrador” },
voice: { type: “STRING”, description: “Uma das vozes: Fenrir, Kore, Charon, Puck” }
}
}
}
}
}
}
}
}
},
model: “gemini-2.5-flash-preview-09-2025″
};
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify(payload)
});
const result = await response.json();
const rawText = result.candidates[0].content.parts[0].text;
const cleanText = rawText.replace(/“`json/gi, ”).replace(/“`/g, ”).trim();
const parsed = JSON.parse(cleanText);
if (parsed.generalTitle && !generalTitle) setGeneralTitle(parsed.generalTitle);
if (parsed.clips && parsed.clips.length > 0) {
setVideos(prev => {
const newVids = […prev];
for (let i = 0; i < Math.min(newVids.length, parsed.clips.length); i++) {
const clipData = parsed.clips[i];
newVids[i] = {
...newVids[i],
title: clipData.title || newVids[i].title,
caption: clipData.caption || newVids[i].caption,
};
if (clipData.narrations && clipData.narrations.length > 0) {
newVids[i].narrations = clipData.narrations.map(n => ({
id: Math.random().toString(36).substring(2, 9),
script: n.text,
narrationVoice: n.voice || ‘Fenrir’,
narrationTone: magicTone,
customAudioUrl: null,
audioDuration: 0,
timedCaptions: [],
isGeneratingScript: false,
isGeneratingText: false,
audioGenerated: false
}));
}
}
return newVids;
});
}
} catch (e) {
console.error(“Falha ao gerar o roteiro: “, e);
} finally {
setIsGeneratingMagic(false);
}
};
const startExport = async () => {
const validVideos = videos.filter(v => v.localUrl);
if (validVideos.length === 0) return alert(“Anexe vídeos primeiro!”);
setIsExporting(true);
setExportProgress(0);
try {
const canvas = document.createElement(‘canvas’);
canvas.width = 720; canvas.height = 1280;
const ctx = canvas.getContext(‘2d’);
const audioCtx = new AudioContext();
const dest = audioCtx.createMediaStreamDestination();
const playWhoosh = () => {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = ‘sine’;
osc.frequency.setValueAtTime(400, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(40, audioCtx.currentTime + 0.4);
gain.gain.setValueAtTime(0, audioCtx.currentTime);
gain.gain.linearRampToValueAtTime(0.5, audioCtx.currentTime + 0.1);
gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.4);
osc.connect(gain);
gain.connect(dest);
osc.start(audioCtx.currentTime);
osc.stop(audioCtx.currentTime + 0.4);
};
const recorder = new MediaRecorder(new MediaStream([…canvas.captureStream(30).getVideoTracks(), …dest.stream.getAudioTracks()]), { mimeType: ‘video/webm’ });
const chunks = [];
recorder.ondataavailable = (e) => chunks.push(e.data);
const exportPromise = new Promise(resolve => recorder.onstop = () => resolve(new Blob(chunks, { type: ‘video/webm’ })));
recorder.start();
const hiddenDiv = document.createElement(‘div’);
hiddenDiv.style.display = ‘none’;
document.body.appendChild(hiddenDiv);
for (let i = 0; i < validVideos.length; i++) {
const v = validVideos[i];
setExportMsg(`Renderizando Clipe ${i + 1}...`);
setExportProgress((i / validVideos.length) * 100);
await new Promise(resolveClip => {
hiddenDiv.innerHTML = ”;
const videoEl = document.createElement(‘video’);
videoEl.src = v.localUrl; videoEl.playsInline = true;
hiddenDiv.appendChild(videoEl);
const source = audioCtx.createMediaElementSource(videoEl);
const gain = audioCtx.createGain(); gain.gain.value = v.volume / 100;
source.connect(gain); gain.connect(dest);
videoEl.onloadedmetadata = () => {
const startSec = (v.trimStart / 100) * videoEl.duration;
const endSec = (v.trimEnd / 100) * videoEl.duration;
videoEl.currentTime = startSec;
let whooshPlayed = false;
let bgmEl = null;
let bgmGain = null;
if (v.bgmUrl) {
bgmEl = document.createElement(‘audio’); bgmEl.src = v.bgmUrl; bgmEl.loop = true;
hiddenDiv.appendChild(bgmEl);
const bgmSource = audioCtx.createMediaElementSource(bgmEl);
bgmGain = audioCtx.createGain(); bgmGain.gain.value = v.bgmVolume / 100;
bgmSource.connect(bgmGain); bgmGain.connect(dest);
}
const validNarrations = v.narrations.filter(n => n.audioGenerated);
let currentOffset = startSec;
const exportNarrationBlocks = validNarrations.map(n => {
const block = { …n, startPlay: currentOffset, endPlay: currentOffset + n.audioDuration };
currentOffset += n.audioDuration;
return block;
});
const ttsElements = exportNarrationBlocks.map(block => {
const ttsEl = document.createElement(‘audio’);
ttsEl.src = block.customAudioUrl;
hiddenDiv.appendChild(ttsEl);
const ttsSource = audioCtx.createMediaElementSource(ttsEl);
const ttsGain = audioCtx.createGain();
ttsGain.gain.value = v.narrationVolume / 100;
ttsSource.connect(ttsGain);
ttsGain.connect(dest);
return { el: ttsEl, block, ttsGain };
});
videoEl.play(); if (bgmEl) bgmEl.play();
const exportPlayedSFX = new Set(); // NOVO: Controle de SFX na exportação
const drawFrame = () => {
if (videoEl.currentTime >= endSec || videoEl.ended) {
videoEl.pause(); if (bgmEl) bgmEl.pause();
ttsElements.forEach(({el}) => el.pause());
resolveClip(); return;
}
let isAnyVoicePlaying = false;
ttsElements.forEach(({ el, block, ttsGain }) => {
if (videoEl.currentTime >= block.startPlay && videoEl.currentTime < block.endPlay) {
isAnyVoicePlaying = true;
if (el.paused && el.currentTime === 0) el.play().catch(()=>{});
} else if (videoEl.currentTime >= block.endPlay && !el.paused) {
el.pause();
}
});
// Processamento dos Efeitos Sonoros Manuais (Exportação)
if (v.sfxTracks && v.sfxTracks.length > 0) {
v.sfxTracks.forEach(sfx => {
const sfxTime = startSec + (sfx.startTimePerc / 100) * (endSec – startSec);
if (videoEl.currentTime >= sfxTime && !exportPlayedSFX.has(sfx.id)) {
playGeneratedSFX(sfx.type, audioCtx, dest, sfx.volume / 100);
exportPlayedSFX.add(sfx.id);
}
});
}
if (bgmEl && bgmGain && v.autoDucking) {
const targetVol = isAnyVoicePlaying ? (v.bgmVolume * 0.15) / 100 : v.bgmVolume / 100;
bgmGain.gain.value += (targetVol – bgmGain.gain.value) * 0.1;
}
let vfxOffsetX = 0;
let vfxOffsetY = 0;
if (v.vfxTracks && v.vfxTracks.length > 0) {
v.vfxTracks.forEach(vfx => {
const sfxTime = startSec + (vfx.startTimePerc / 100) * (endSec – startSec);
const eTime = sfxTime + (vfx.durationPerc / 100) * (endSec – startSec);
if (videoEl.currentTime >= sfxTime && videoEl.currentTime <= eTime) {
const intensity = (vfx.intensity || 50) / 100 * 20;
const speed = (vfx.speed || 50) / 100 * 50;
const t = videoEl.currentTime * 1000;
vfxOffsetX += (Math.sin(t * 0.01 * speed) + Math.cos(t * 0.013 * speed)) * intensity;
vfxOffsetY += (Math.cos(t * 0.011 * speed) + Math.sin(t * 0.009 * speed)) * intensity;
}
});
}
ctx.fillStyle = '#000'; ctx.fillRect(0, 0, canvas.width, canvas.height);
const timeLeft = endSec - videoEl.currentTime;
const timeElapsed = videoEl.currentTime - startSec;
const fadeTime = 0.4;
let videoAlpha = 1;
if (i < validVideos.length - 1 && timeLeft <= fadeTime) {
videoAlpha = Math.max(0, timeLeft / fadeTime);
if (!whooshPlayed) {
playWhoosh();
whooshPlayed = true;
}
} else if (i > 0 && timeElapsed <= fadeTime) {
videoAlpha = Math.min(1, timeElapsed / fadeTime);
}
const zoom = v.zoom / 100;
const vRatio = videoEl.videoWidth / videoEl.videoHeight;
const cRatio = canvas.width / canvas.height;
let dw, dh;
if (vRatio > cRatio) {
dh = canvas.height;
dw = videoEl.videoWidth * (canvas.height / videoEl.videoHeight);
} else {
dw = canvas.width;
dh = videoEl.videoHeight * (canvas.width / videoEl.videoWidth);
}
const basePosX = (canvas.width – dw*zoom)/2;
const basePosY = (canvas.height – dh*zoom)/2;
let finalPosX = basePosX + (v.posX / 100) * canvas.width + vfxOffsetX;
let finalPosY = basePosY + (v.posY / 100) * canvas.height + vfxOffsetY;
ctx.globalAlpha = videoAlpha;
ctx.filter = getFilterStyle(v.filter || ‘none’);
ctx.drawImage(videoEl, finalPosX, finalPosY, dw*zoom, dh*zoom);
ctx.filter = ‘none’;
ctx.globalAlpha = 1;
const defaultLabel = topCount === 1 && globalPrefix === “TOP” ? “VÍDEO” : (showNumbers ? `${globalPrefix} ${topCount – i}`.trim() : globalPrefix.trim());
const labelText = v.customTopLabel !== null ? v.customTopLabel : defaultLabel;
if (labelText.trim() !== “”) {
ctx.fillStyle = ‘rgba(0,0,0,0.8)’;
ctx.fillRect(20, 20, 240, 90);
ctx.strokeStyle = ‘white’;
ctx.lineWidth = 2;
ctx.strokeRect(20, 20, 240, 90);
ctx.fillStyle = ‘white’;
ctx.font = ‘black 55px sans-serif’;
ctx.textAlign = ‘center’;
ctx.textBaseline = ‘middle’;
ctx.fillText(labelText, 140, 65);
}
if (useStickers) {
const emoji = getSticker(i);
const bounce = Math.sin(videoEl.currentTime * 5) * 15;
ctx.font = ‘100px Arial’;
ctx.textAlign = ‘center’;
ctx.textBaseline = ‘middle’;
ctx.fillText(emoji, canvas.width – 100, 120 + bounce);
}
if (v.caption) {
const py = (v.captionY / 100) * canvas.height;
const cSize = v.captionSize || 68;
const cStyle = v.captionStyle || ‘style1’;
const textUpper = v.caption.toUpperCase();
ctx.font = `900 ${cSize}px sans-serif`; ctx.textAlign = ‘center’; ctx.textBaseline = ‘middle’;
ctx.shadowColor = ‘transparent’; ctx.shadowBlur = 0; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0;
ctx.lineJoin = ’round’;
if (cStyle === ‘style1’) {
ctx.lineWidth = cSize * 0.2; ctx.strokeStyle = ‘black’; ctx.strokeText(textUpper, canvas.width/2, py);
ctx.fillStyle = ‘#FBBF24’; ctx.fillText(textUpper, canvas.width/2, py);
} else if (cStyle === ‘style2’) {
ctx.shadowColor = ‘rgba(0,0,0,0.8)’; ctx.shadowBlur = 15; ctx.shadowOffsetY = 5;
ctx.fillStyle = ‘white’; ctx.fillText(textUpper, canvas.width/2, py);
} else if (cStyle === ‘style3’) {
ctx.shadowColor = ‘#EC4899’; ctx.shadowBlur = 20;
ctx.fillStyle = ‘white’; ctx.fillText(textUpper, canvas.width/2, py);
ctx.fillText(textUpper, canvas.width/2, py);
} else if (cStyle === ‘style4’) {
const tWidth = ctx.measureText(textUpper).width;
ctx.fillStyle = ‘rgba(0,0,0,0.7)’;
ctx.fillRect(canvas.width/2 – tWidth/2 – 20, py – cSize/2 – 10, tWidth + 40, cSize + 20);
ctx.fillStyle = ‘white’; ctx.fillText(textUpper, canvas.width/2, py);
} else if (cStyle === ‘style5’) {
ctx.fillStyle = ‘#064E3B’; ctx.fillText(textUpper, canvas.width/2 + 6, py + 6);
ctx.lineWidth = cSize * 0.2; ctx.strokeStyle = ‘#14532D’; ctx.strokeText(textUpper, canvas.width/2, py);
ctx.fillStyle = ‘#4ADE80’; ctx.fillText(textUpper, canvas.width/2, py);
}
ctx.shadowColor = ‘transparent’; ctx.shadowBlur = 0; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0;
}
if (v.showNarration) {
const activeBlock = exportNarrationBlocks.find(b => videoEl.currentTime >= b.startPlay && videoEl.currentTime < b.endPlay);
if (activeBlock && activeBlock.timedCaptions.length > 0) {
const localCurrentTime = videoEl.currentTime – activeBlock.startPlay;
const segment = activeBlock.timedCaptions.find(s => localCurrentTime >= s.startTime && localCurrentTime <= s.endTime);
if (segment) {
const ny = (v.narrationCaptionY / 100) * canvas.height;
const nSize = v.narrationCaptionSize || 60;
const nStyle = v.narrationCaptionStyle || 'style1';
ctx.font = `900 ${nSize}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.lineJoin = 'round';
const textUpper = segment.word.toUpperCase();
ctx.shadowColor = 'transparent'; ctx.shadowBlur = 0; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0;
if (nStyle === 'hormozi') {
// Estilo Alex Hormozi (Animado, Cores Dinâmicas e Emojis)
const progress = localCurrentTime - segment.startTime;
const { color, emoji, rot } = getHormoziStyle(segment.word);
// Efeito de Pop-in (Bounce)
let bounceScale = 1;
if (progress < 0.15) {
const p = progress / 0.15;
bounceScale = 0.5 + Math.sin(p * Math.PI * 0.7) * 0.7; // Começa pequeno, passa de 1 e volta a 1
}
ctx.save();
ctx.translate(canvas.width/2, ny);
ctx.scale(bounceScale, bounceScale);
ctx.rotate(rot);
ctx.fillStyle = color;
ctx.lineWidth = nSize * 0.25;
ctx.strokeStyle = 'black';
ctx.strokeText(textUpper, 0, 0);
ctx.fillText(textUpper, 0, 0);
if (emoji) {
ctx.font = `${nSize * 0.9}px Arial`;
const bounceY = Math.sin(progress * 10) * 10; // Flutuação rápida do emoji
ctx.fillText(emoji, 0, -nSize + bounceY);
}
ctx.restore();
} else if (nStyle === 'mrbeast') {
// Estilo MrBeast (Fundo de cor sólida marcante com leve rotação)
const progress = localCurrentTime - segment.startTime;
const { bgColor, textColor, rot } = getMrBeastStyle(segment.word);
let bounceScale = 1;
if (progress < 0.1) bounceScale = 0.8 + (progress / 0.1) * 0.2;
ctx.save();
ctx.translate(canvas.width/2, ny);
ctx.scale(bounceScale, bounceScale);
ctx.rotate(rot);
const tWidth = ctx.measureText(textUpper).width;
const paddingX = nSize * 0.4;
const paddingY = nSize * 0.3;
// Desenhar a caixa de fundo estilo marcador
ctx.fillStyle = bgColor;
ctx.beginPath();
if (ctx.roundRect) {
ctx.roundRect(-tWidth/2 - paddingX, -nSize/2 - paddingY/2, tWidth + paddingX*2, nSize + paddingY, 12);
} else {
ctx.fillRect(-tWidth/2 - paddingX, -nSize/2 - paddingY/2, tWidth + paddingX*2, nSize + paddingY);
}
ctx.fill();
// Se o fundo for amarelo (texto preto), adicionar uma borda para realce
if (textColor === '#000000') {
ctx.lineWidth = 4;
ctx.strokeStyle = '#000000';
ctx.stroke();
}
// Desenhar o texto
ctx.fillStyle = textColor;
ctx.fillText(textUpper, 0, 0);
ctx.restore();
} else {
// Estilos Clássicos
if (nStyle === 'style1') {
ctx.fillStyle = '#064E3B'; ctx.fillText(textUpper, canvas.width/2 + 6, ny + 6);
ctx.lineWidth = nSize / 5; ctx.strokeStyle = '#14532D'; ctx.strokeText(textUpper, canvas.width/2, ny);
ctx.fillStyle = '#4ADE80'; ctx.fillText(textUpper, canvas.width/2, ny);
} else if (nStyle === 'style2') {
ctx.shadowColor = 'rgba(0,0,0,0.8)'; ctx.shadowBlur = 15; ctx.shadowOffsetY = 5;
ctx.fillStyle = 'white'; ctx.fillText(textUpper, canvas.width/2, ny);
} else if (nStyle === 'style3') {
ctx.shadowColor = '#EC4899'; ctx.shadowBlur = 20;
ctx.fillStyle = 'white'; ctx.fillText(textUpper, canvas.width/2, ny);
ctx.fillText(textUpper, canvas.width/2, ny);
} else if (nStyle === 'style4') {
const tWidth = ctx.measureText(textUpper).width;
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(canvas.width/2 - tWidth/2 - 20, ny - nSize/2 - 10, tWidth + 40, nSize + 20);
ctx.fillStyle = 'white'; ctx.fillText(textUpper, canvas.width/2, ny);
} else if (nStyle === 'style5') {
const st = nSizePrev * 0.15;
styleProps = { ...styleProps, color: 'white', textShadow: `-${st}px -${st}px 0 #9333EA, 0 -${st}px 0 #9333EA, ${st}px -${st}px 0 #9333EA, ${st}px 0 0 #9333EA, ${st}px ${st}px 0 #9333EA, 0 ${st}px 0 #9333EA, -${st}px ${st}px 0 #9333EA, -${st}px 0 0 #9333EA, 0 4px 15px rgba(147, 51, 234, 0.8)` };
}
}
ctx.shadowColor = 'transparent'; ctx.shadowBlur = 0; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0;
}
}
}
requestAnimationFrame(drawFrame);
};
drawFrame();
};
});
}
document.body.removeChild(hiddenDiv);
recorder.stop();
setFinalVideoUrl(URL.createObjectURL(await exportPromise));
setStep(3); setIsExporting(false);
} catch (e) { console.error(e); setIsExporting(false); }
};
const downloadVideo = () => {
const link = document.createElement(“a”);
link.href = finalVideoUrl; link.download = `Viral_Top${topCount}.webm`;
link.click();
};
const startProcessing = () => {
setIsProcessing(true);
setLoadingProgress(0);
const stages = [“Conectando…”, “Baixando vídeos…”, “Formatando 9:16…”, “Gerando narração…”, “Montando timeline…”];
let currentStage = 0;
const interval = setInterval(() => {
setLoadingProgress(prev => {
const next = prev + 1;
if (next >= 100) {
clearInterval(interval);
setTimeout(() => { setIsProcessing(false); setStep(2); setPlayingItemId(1); }, 500);
return 100;
}
return next;
});
if (Math.random() > 0.8 && currentStage < stages.length) {
setLoadingMsg(stages[currentStage]);
currentStage++;
}
}, 50);
};
const renderStep1 = () => (
Configuração Viral
Personalize a sua lista para o YouTube Shorts.
setGeneralTitle(e.target.value)} />
setGlobalPrefix(e.target.value.toUpperCase())} title=”Personalizar o prefixo para todas as etiquetas” />
{videos.map((video, index) => {
const defaultLabel = topCount === 1 && globalPrefix === “TOP” ? ‘VÍDEO’ : (showNumbers ? `${globalPrefix} ${topCount-index}`.trim() : globalPrefix.trim());
return (
handleVideoChange(video.id, ‘customTopLabel’, e.target.value)} title=”Personalizar etiqueta (ex: FATO 1)” />
handleVideoChange(video.id, ‘title’, e.target.value)} />
handleFileUpload(video.id, e)} className=”hidden” id={`f-${video.id}`} />
);
})}
Botão Mágico (Gerador de Roteiro IA)
Preencha os nomes dos vídeos acima e a IA criará o roteiro perfeito!
Enquadramento do Vídeo
handleVideoChange(activeVideo.id, ‘zoom’, Number(e.target.value))} className=”w-full accent-emerald-500 h-1.5″ />
handleVideoChange(activeVideo.id, ‘volume’, Number(e.target.value))} className=”w-full accent-blue-500 h-1.5″ />
handleVideoChange(activeVideo.id, ‘posX’, Number(e.target.value))} className=”w-full accent-white h-1.5″ />
handleVideoChange(activeVideo.id, ‘posY’, Number(e.target.value))} className=”w-full accent-white h-1.5″ />
)}
{activeToolTab === ‘bgm’ && (
Música de Fundo (BGM)
{bgmLibrary.length === 0 ? (
) : (
{activeVideo.bgmUrl && (
handleVideoChange(activeVideo.id, ‘bgmVolume’, Number(e.target.value))} className=”w-full accent-cyan-500 h-1.5″ />
{activeVideo.bgmVolume}%
)}
)}
)}
{activeToolTab === ‘legenda’ && (
Legenda de Título Fixo
handleVideoChange(activeVideo.id, ‘caption’, e.target.value)} />
handleVideoChange(activeVideo.id, ‘captionSize’, Number(e.target.value))} className=”w-full accent-yellow-500 h-1.5″ />
handleVideoChange(activeVideo.id, ‘captionY’, Number(e.target.value))} className=”w-full accent-yellow-500 h-1.5″ />
)}
{activeToolTab === ‘narracao’ && (
Geração de Narração IA
{activeVideo.narrations.map((narration, index) => (
Caixa de Narração {index + 1}
{activeVideo.narrations.length > 1 && (
)}
Configurações das Legendas da Voz
handleVideoChange(activeVideo.id, ‘narrationCaptionSize’, Number(e.target.value))} className=”w-full accent-white h-1.5″ />
handleVideoChange(activeVideo.id, ‘narrationCaptionY’, Number(e.target.value))} className=”w-full accent-white h-1.5″ />
handleVideoChange(activeVideo.id, ‘narrationVolume’, Number(e.target.value))} className=”w-full accent-rose-500 h-1.5″ />
)}
)}
{activeToolTab === ‘corte’ && (
Ajuste de Corte – Clipe #{activeVideo.id}
INÍCIO {activeVideo.trimStart}%
handleVideoChange(activeVideo.id, ‘trimStart’, Number(e.target.value))} className=”w-full accent-purple-500 h-2″ />
FIM {activeVideo.trimEnd}%
handleVideoChange(activeVideo.id, ‘trimEnd’, Number(e.target.value))} className=”w-full accent-purple-500 h-2″ />
)}
{activeToolTab === ‘sfx’ && (
{activeVideo.sfxTracks && activeVideo.sfxTracks.length === 0 && (
Nenhum efeito sonoro adicionado neste clipe.
)}
{activeVideo.sfxTracks && activeVideo.sfxTracks.map((sfx) => (
updateSfxTrack(activeVideo.id, sfx.id, ‘startTimePerc’, Number(e.target.value))} className=”w-full accent-yellow-500 h-1.5″ />
{sfx.startTimePerc}%
updateSfxTrack(activeVideo.id, sfx.id, ‘volume’, Number(e.target.value))} className=”w-full accent-yellow-500 h-1.5″ />
))}
)}
{activeToolTab === ‘vfx’ && (
{activeVideo.vfxTracks && activeVideo.vfxTracks.length === 0 && (
Nenhum efeito visual adicionado neste clipe.
)}
{activeVideo.vfxTracks && activeVideo.vfxTracks.map((vfx) => (
INÍCIO
updateVfxTrack(activeVideo.id, vfx.id, ‘startTimePerc’, Number(e.target.value))} className=”w-full accent-rose-500 h-1.5″ />
{vfx.startTimePerc}%
DURAÇÃO
updateVfxTrack(activeVideo.id, vfx.id, ‘durationPerc’, Number(e.target.value))} className=”w-full accent-rose-500 h-1.5″ />
INTENSIDADE
updateVfxTrack(activeVideo.id, vfx.id, ‘intensity’, Number(e.target.value))} className=”w-full accent-rose-500 h-1.5″ />
VELOCIDADE
updateVfxTrack(activeVideo.id, vfx.id, ‘speed’, Number(e.target.value))} className=”w-full accent-rose-500 h-1.5″ />
))}
)}
{/* TIMELINE */}
{videos.map((v, i) => {
const segmentDur = v.videoDuration * ((v.trimEnd – v.trimStart) / 100);
const isSelected = playingItemId === v.id;
const totalAudioDur = v.narrations.reduce((sum, n) => n.audioGenerated ? sum + n.audioDuration : sum, 0);
return (
setPlayingItemId(v.id)} className={`relative h-24 flex-shrink-0 rounded-xl border-2 transition-all cursor-pointer overflow-hidden bg-black group ${isSelected ? ‘border-purple-500 w-64 shadow-[0_0_20px_rgba(168,85,247,0.3)]’ : ‘border-gray-800 w-44 hover:border-gray-700’}`}>
{v.bgmUrl && ()}
{/* BARRAS DE VOZ MÚLTIPLAS (ALTO CONTRASTE E GLOW) */}
{v.narrations.filter(n => n.audioGenerated).reduce((acc, n, idx) => {
const startPerc = v.trimStart + (acc.offset / v.videoDuration) * 100;
const widthPerc = (n.audioDuration / v.videoDuration) * 100;
acc.elements.push(
);
acc.offset += n.audioDuration;
return acc;
}, { offset: 0, elements: [] }).elements}
{/* FAIXA DE EFEITOS (SFX) NA TIMELINE */}
{v.sfxTracks && v.sfxTracks.map(sfx => {
const sfxPerc = v.trimStart + (sfx.startTimePerc / 100) * (v.trimEnd – v.trimStart);
const emoji = sfx.type === ‘impact’ ? ‘💥’ : sfx.type === ‘riser’ ? ‘✨’ : sfx.type === ‘heartbeat’ ? ‘🫀’ : sfx.type === ‘gasp’ ? ‘😨’ : ‘💰’;
return (
{emoji}
);
})}
{/* FAIXA DE EFEITOS (VFX) NA TIMELINE */}
{v.vfxTracks && v.vfxTracks.map(vfx => {
const vfxStartPerc = v.trimStart + (vfx.startTimePerc / 100) * (v.trimEnd – v.trimStart);
const vfxWidthPerc = (vfx.durationPerc / 100) * (v.trimEnd – v.trimStart);
return (
);
})}
{v.customTopLabel !== null ? v.customTopLabel : (topCount === 1 && globalPrefix === “TOP” ? ‘VÍDEO’ : (showNumbers ? `${globalPrefix} ${topCount – i}`.trim() : globalPrefix.trim()))}
CLIP: {segmentDur.toFixed(1)}s
{totalAudioDur > 0 && (VOZ: {totalAudioDur.toFixed(1)}s)}
{v.title || “Vazio”}
);
})}
);
};
const renderStep3 = () => (
Renderizado!
{finalVideoUrl && }
Poste no YouTube Shorts para máxima visibilidade.
);
return (
Top{topCount}Creator
ENGINE WEBGL SYNC ON
{step === 1 && !isProcessing && renderStep1()}
{step === 2 && !isExporting && renderStep2()}
{isExporting && (
Renderizando Engine
{exportMsg}
)}
{step === 3 && renderStep3()}
);
}