Blog

  • Top 5 Creator

    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.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!