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!