VSL / templates /vsl.html
RaiSantos's picture
oi1
ea1eee6
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VSL Player</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', sans-serif;
background-color: white;
color: #111827;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.input-section {
background: white;
border-radius: 16px;
padding: 30px;
margin-bottom: 30px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
.input-group {
margin-bottom: 25px;
}
.input-group:last-child {
margin-bottom: 0;
}
.input-label {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
color: #374151;
margin-bottom: 10px;
}
.input-label i {
font-size: 1.2em;
color: #667eea;
}
.file-input-wrapper {
position: relative;
width: 100%;
height: 120px;
border: 2px dashed #e5e7eb;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
}
.file-input-wrapper:hover {
border-color: #667eea;
background: rgba(102, 126, 234, 0.05);
}
.file-input-wrapper input[type="file"] {
position: absolute;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
.file-input-content {
text-align: center;
}
.file-input-icon {
font-size: 2em;
color: #667eea;
margin-bottom: 10px;
}
.file-input-text {
color: #6b7280;
}
.source-selector {
display: flex;
gap: 15px;
margin-bottom: 20px;
}
.source-option {
flex: 1;
padding: 15px;
border: 2px solid #e5e7eb;
border-radius: 12px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
}
.source-option:hover {
border-color: #667eea;
background: rgba(102, 126, 234, 0.05);
}
.source-option.active {
border-color: #667eea;
background: rgba(102, 126, 234, 0.1);
}
.source-option i {
font-size: 1.5em;
color: #667eea;
margin-bottom: 8px;
}
.preview-section {
background: white;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
.preview-header {
padding: 20px;
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: center;
justify-content: space-between;
}
.preview-title {
font-weight: 600;
color: #374151;
}
.preview-controls {
display: flex;
gap: 10px;
}
.preview {
padding: 60px 40px;
font-size: 42px;
text-align: center;
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
line-height: 1.4;
}
.preview.fullscreen {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
margin: 0;
padding: 40px;
background: white;
font-size: 64px;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.preview.fullscreen p {
max-width: 1200px;
white-space: pre-line;
line-height: 1.4;
text-align: center;
}
.preview.fullscreen.hd {
width: 1920px;
height: 1080px;
transform-origin: top left;
transform: scale(calc(100vh / 1080));
}
.preview.fullscreen.hd p {
max-width: 1600px;
}
.btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.btn i {
font-size: 1.2em;
}
.btn-secondary {
background: #6b7280;
}
.status-bar {
display: flex;
gap: 20px;
margin-top: 20px;
padding: 15px;
background: #f8fafc;
border-radius: 8px;
font-size: 14px;
}
.status-item {
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #cbd5e1;
}
.status-dot.active {
background: #22c55e;
}
.bold-black {
font-weight: 700;
color: black;
}
.bold-red {
font-weight: 700;
color: #dc2626;
}
@media (max-width: 768px) {
.preview {
font-size: 32px;
padding: 30px 20px;
}
.source-selector {
flex-direction: column;
}
.source-option {
padding: 10px;
}
}
/* Ajustes no preview para manter consistência */
.preview p {
max-width: 800px;
margin: 0 auto;
white-space: pre-line;
line-height: 1.4;
font-family: 'Open Sans', sans-serif;
}
/* Melhorias no seletor de transcrições */
.transcription-selector {
background: #f8fafc;
border-radius: 12px;
padding: 20px;
margin-top: 15px;
}
.transcription-list {
display: grid;
gap: 10px;
margin-top: 15px;
}
.transcription-item {
background: white;
border: 2px solid #e5e7eb;
border-radius: 8px;
padding: 15px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 15px;
}
.transcription-item:hover {
border-color: #667eea;
background: rgba(102, 126, 234, 0.05);
}
.transcription-item.selected {
border-color: #667eea;
background: rgba(102, 126, 234, 0.1);
}
.transcription-icon {
width: 40px;
height: 40px;
background: #667eea;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.5em;
}
.transcription-info {
flex: 1;
}
.transcription-name {
font-weight: 600;
color: #374151;
margin-bottom: 4px;
}
.transcription-date {
font-size: 0.9em;
color: #6b7280;
}
/* Botões de ação */
.action-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
}
/* Editor JSON */
.json-editor {
display: none;
margin-top: 20px;
}
.json-editor textarea {
width: 100%;
height: 300px;
font-family: monospace;
padding: 15px;
border: 2px solid #e5e7eb;
border-radius: 8px;
resize: vertical;
}
/* Exportação de vídeo */
.export-section {
display: none;
margin-top: 30px;
padding: 20px;
background: #f8fafc;
border-radius: 12px;
}
.export-progress {
margin-top: 15px;
}
.progress-bar {
width: 100%;
height: 8px;
background: #e5e7eb;
border-radius: 4px;
overflow: hidden;
margin: 10px 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
width: 0%;
transition: width 0.3s ease;
}
.progress-steps {
margin-top: 20px;
}
.progress-step {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
color: #6b7280;
}
.progress-step.done {
color: #22c55e;
}
.progress-step i {
font-size: 1.2em;
}
</style>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/fonts/remixicon.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="input-section">
<div class="input-group">
<div class="source-selector">
<div class="source-option active" data-source="local">
<i class="ri-hard-drive-line"></i>
<div>Arquivo Local</div>
</div>
<div class="source-option" data-source="server">
<i class="ri-cloud-line"></i>
<div>Transcrições Salvas</div>
</div>
</div>
<!-- Upload Local -->
<div id="localUpload">
<div class="input-label">
<i class="ri-file-text-line"></i>
<span>Importar Transcrição (JSON)</span>
</div>
<div class="file-input-wrapper">
<input type="file" id="jsonFile" accept=".json" />
<div class="file-input-content">
<div class="file-input-icon">📄</div>
<div class="file-input-text">Clique ou arraste o arquivo JSON</div>
</div>
</div>
</div>
<!-- Seletor Server -->
<div id="serverUpload" style="display: none;" class="transcription-selector">
<div class="input-label">
<i class="ri-cloud-line"></i>
<span>Selecionar Transcrição</span>
</div>
<div id="transcriptionList" class="transcription-list">
<div class="transcription-item">
<div class="transcription-icon">
<i class="ri-file-text-line"></i>
</div>
<div class="transcription-info">
<div class="transcription-name">Carregando transcrições...</div>
</div>
</div>
</div>
</div>
</div>
<div class="input-group">
<div class="input-label">
<i class="ri-mic-line"></i>
<span>Importar Narração (Áudio)</span>
</div>
<div class="file-input-wrapper">
<input type="file" id="audio" accept="audio/*" />
<div class="file-input-content">
<div class="file-input-icon">🎤</div>
<div class="file-input-text">Clique ou arraste o arquivo de áudio</div>
</div>
</div>
</div>
<div class="input-group">
<audio controls id="player" style="width: 100%;"></audio>
</div>
<div class="status-bar">
<div class="status-item">
<div id="transcriptionStatus" class="status-dot"></div>
<span>Transcrição</span>
</div>
<div class="status-item">
<div id="audioStatus" class="status-dot"></div>
<span>Áudio</span>
</div>
</div>
</div>
<div class="action-buttons">
<button class="btn" onclick="salvarTranscricao()" id="saveButton">
<i class="ri-save-line"></i>
<span>Salvar Transcrição</span>
</button>
<button class="btn btn-secondary" onclick="toggleEditor()" id="editButton">
<i class="ri-edit-line"></i>
<span>Editar JSON</span>
</button>
</div>
<div class="json-editor" id="jsonEditor">
<div class="input-label">
<i class="ri-code-line"></i>
<span>Editor JSON</span>
</div>
<textarea id="jsonContent" spellcheck="false"></textarea>
<div class="action-buttons">
<button class="btn" onclick="aplicarEdicao()">
<i class="ri-check-line"></i>
<span>Aplicar Edição</span>
</button>
<button class="btn btn-secondary" onclick="toggleEditor()">
<i class="ri-close-line"></i>
<span>Cancelar</span>
</button>
</div>
</div>
<div class="preview-section">
<div class="preview-header">
<div class="preview-title">Visualização</div>
<div class="preview-controls">
<button class="btn" onclick="startSync()" id="startButton">
<i class="ri-play-line"></i>
<span>Iniciar</span>
</button>
<button class="btn btn-secondary" onclick="toggleFullscreen()" id="fullscreenButton">
<i class="ri-fullscreen-line"></i>
</button>
<button class="btn btn-secondary" onclick="toggle1080p()" id="hdButton">
<i class="ri-hd-line"></i>
</button>
</div>
</div>
<div class="preview" id="slidePreview"><p></p></div>
</div>
</div>
<script>
const player = document.getElementById("player");
const preview = document.getElementById("slidePreview").querySelector("p");
const transcriptionStatus = document.getElementById("transcriptionStatus");
const audioStatus = document.getElementById("audioStatus");
const startButton = document.getElementById("startButton");
const serverFiles = document.getElementById("serverFiles");
let slides = [];
let audioReady = false;
let isPlaying = false;
// Palavras importantes
const importantWords = new Set([
'DOR', 'PESO', 'VERGONHA', 'TRISTEZA', 'SOFRIMENTO', 'CULPA', 'FRACASSO',
'LIMITE', 'BALANÇA', 'FOME', 'FRACA', 'TRANCAR', 'ROUBEI', 'FUGIR',
'TRISTE', 'COMPULSÃO', 'AUTOESTIMA', 'REALIDADE', 'ESCONDER', 'MENTIRA',
'DIETAS', 'EFEITO SANFONA', 'EXCESSO', 'RESULTADO', 'ESFORÇO', 'ENERGIA',
'MUDANÇA', 'ESCOLHA', 'RENASCIMENTO', 'ESPERANÇA', 'CONQUISTA',
'TRANSFORMAÇÃO', 'CORAGEM', 'SOLUÇÃO', 'FUNCIONA', 'EMAGRECER', 'ALIMENTAR',
'VIDA', 'SAÚDE', 'LIBERDADE', 'VERDADE', 'MILAGRE', 'EMOÇÃO', 'SUPORTE',
'JEJUM', 'PERSONAL', 'METABOLISMO', 'SAUDÁVEL', 'LEVE', 'SE OLHAR', 'VER',
'SORRIR', 'ORGULHO', 'MERECE', 'BRASILEIRA', 'REAL', 'CAMINHO', 'PASSO',
'PLANO', 'REFEIÇÕES', 'PRAIA', 'ESCONDERIJO', 'CONSCIÊNCIA', 'RESPOSTA',
'DESAFIO', 'COBRIR', 'RECOMEÇO', 'REJEIÇÃO', 'RECOMPENSA', 'DECISÃO',
'ACEITAÇÃO', 'VITÓRIA', 'SUPERAÇÃO', 'MOTIVAÇÃO', 'PROGRESSO',
'MUDOU MINHA VIDA', 'TRANSFORMOU MEU CORPO', 'NADA DAVA CERTO', 'DESISTIR',
'SOZINHA', 'ANSIEDADE', 'INSEGURANÇA', '35 ANOS', '9,3 QUILOS',
'20 QUILOS', '2 QUILOS', '55 CENTAVOS', '53 CENTAVOS', 'R$55', 'R$197',
'PARABÉNS', 'CETOX', 'PERDER', 'QUEIMANDO', 'DELICIOSAS', 'EXTREMAMENTE',
'RESPONSABILIDADE', 'ATENÇÃO', 'URGENTE', 'INDISPONÍVEL', 'MANTER', 'PERDER',
'DESCOBRI', 'ÚNICA', 'ARMADURA', 'EFEITO', 'SANFONA', 'CERTEZA', 'GARANTIR',
'PESO', 'SEMPRE', 'FESTAS', 'VIAJANDO', 'CAÓTICAS', 'MÃOS', 'SEU', 'PARA',
'SEMPRE', 'COMER', 'ANIVERSÁRIOS', 'SABOTAR', 'TUDO', 'ATERRORIZAVA', 'NOITES',
'PROCESSO', 'EMAGRECIMENTO', 'ADIANTARIA', 'RECUPERAR', 'MESES', 'MULHERES',
'BRASILEIRAS', 'DIETA', 'PESQUISA', 'UNIVERSIDADE', 'SÃO', 'PAULO', 'BALANÇA',
'SUBIR', 'RESISTIR', 'MOMENTOS', 'CONTROLE', 'TPM', 'ESTRESSE', 'VIAGENS',
'ALMOÇOS', 'FAMÍLIA', 'RESPOSTA', 'DURA', 'MAIORIA', 'DIETAS', 'FRACASSA',
'AGORA', 'EMAGRECER', 'CORPO', 'LUTAR', 'NOVO', 'DIMINUI', 'HORMÔNIOS',
'SACIEDADE', 'AUMENTA', 'FOME', 'DERRUBA', 'ENERGIA', 'SABOTA', 'SILENCIOSAMENTE',
'NOME', 'HORMONAL', 'HISTÓRIA', 'MUDAR', 'PERSPECTIVA', 'CELEBRANDO', 'PRIMEIRA',
'CONQUISTA', 'HAVIA', 'PERDIDO', 'PREOCUPAR', 'ASSUSTADOR', 'ENGORDAR', 'NUNCA',
'MAIS', 'PESO', 'SENSAÇÃO', 'FRACASSO', 'ESPELHO', 'TRANSFORMAÇÃO', 'VERGONHA', '31', '12,3', 'QUILOS', '18', '97%'
]);
const veryImportantWords = new Set([
'NUNCA MAIS', 'DEFINITIVAMENTE', 'LIBERTAR', 'INSUPORTÁVEL', 'INSEGURANÇA',
'TRANSFORMAÇÃO', 'MUDAR TUDO', 'A VERDADE', 'É AGORA',
'MOMENTO', 'DECISÃO', 'A RESPOSTA', 'ME LIBERTAR', 'O COMEÇO',
'TUDO', 'NADA', 'MUDA', 'CULPA', 'MERECE',
'SIMPLES', 'ACESSÍVEL', 'HOJE', 'ÚLTIMA', 'AGORA',
'NOVA', 'LIBERDADE', 'CAMINHO CERTO', 'NUNCA MAIS VOU VIVER ASSIM',
'EU ROUBEI', 'GORDA', 'TRAVEI', 'GORDINHA', 'PAZ', 'SÓ QUEM SENTE SABE',
'NÃO AGUENTO MAIS', 'CHEGA', 'ACORDEI', 'EU DECIDI', 'FOI NAQUELE MOMENTO',
'SONHO', 'ALÉM', 'POSSÍVEL', 'REAL',
'ME VI NO ESPELHO', 'EMOCIONEI', 'MUDOU', 'VIREI',
'NUNCA', 'MAIS', 'PERDER', 'TUDO', 'ATERRORIZAVA', 'FRACASSA', 'REPROGRAMAR',
'ATERRORIZANTE', 'ENGANAR', 'MEMÓRIA', 'EXCLUSIVA', 'VIDA', 'ESTILO', 'DOERAM', 'DOR', 'SOCO', 'SEMPRE', 'CERTEZA', 'ABSOLUTA', 'CONTROLE', 'TOTAL', 'LIBERDADE',
'SEGURANÇA', 'CONFIANÇA', 'PAZ', 'TRANSFORMAÇÃO', 'BLINDADA', 'PROMESSA',
'CHANCE', 'ÚNICA', 'DECIDE', 'AGORA', 'HOJE', 'GARANTIA', 'GRATUITOS',
'INVESTIR', 'URGENTE', 'INDISPONÍVEL', 'RESPONSABILIDADE', 'EXTREMAMENTE',
'MANTER', 'SEU', 'ATENÇÃO', 'REALIDADE', 'FRACASSO', 'JUREI', 'OUTRA',
'PESSOA', 'ENGANAR', 'DESCOBRI', 'BÔNUS', 'GRATUITO', 'ECONOMIZANDO',
'PAGANDO', 'REALIDADES', 'DIFERENTES', 'INTELIGENTE', 'HESITAR', 'ARRISCAR',
'CONTADOR', 'EXPIRA', 'TEMPO', 'DEIXE', 'CLIQUE', 'ESPERO', 'LADO',
'DECISÃO', 'INTELIGENTE', 'BEM-VINDA', 'VIDA', 'NOVA', 'COMEÇA'
]);
// Funções para edição do JSON
let jsonAtual = null;
function toggleEditor() {
const editor = document.getElementById('jsonEditor');
const textarea = document.getElementById('jsonContent');
if (editor.style.display === 'none') {
editor.style.display = 'block';
textarea.value = jsonAtual ? JSON.stringify(jsonAtual, null, 2) : '';
} else {
editor.style.display = 'none';
}
}
function aplicarEdicao() {
const textarea = document.getElementById('jsonContent');
try {
const data = JSON.parse(textarea.value);
jsonAtual = data;
processTranscription(data);
toggleEditor();
} catch (error) {
alert('JSON inválido. Verifique a formatação.');
console.error(error);
}
}
async function salvarTranscricao() {
if (!jsonAtual) {
alert('Nenhuma transcrição carregada.');
return;
}
try {
const response = await fetch('/save_transcription', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(jsonAtual)
});
const result = await response.json();
if (result.success) {
alert('Transcrição salva com sucesso!');
} else {
throw new Error(result.error);
}
} catch (error) {
alert('Erro ao salvar transcrição: ' + error.message);
console.error(error);
}
}
// Carregar transcrições do servidor
async function loadServerFiles() {
try {
const response = await fetch('/list_transcriptions');
const files = await response.json();
const transcriptionList = document.getElementById('transcriptionList');
if (files.length > 0) {
transcriptionList.innerHTML = files.map(f => `
<div class="transcription-item" data-file="${f.name}">
<div class="transcription-icon">
<i class="ri-file-text-line"></i>
</div>
<div class="transcription-info">
<div class="transcription-name">${f.name}</div>
<div class="transcription-date">${new Date(f.modified).toLocaleDateString()}</div>
</div>
</div>
`).join('');
// Adicionar eventos de clique
document.querySelectorAll('.transcription-item').forEach(item => {
item.addEventListener('click', async () => {
// Remover seleção anterior
document.querySelectorAll('.transcription-item').forEach(i => i.classList.remove('selected'));
// Adicionar seleção atual
item.classList.add('selected');
try {
const response = await fetch(`/get_transcription/${item.dataset.file}`);
const data = await response.json();
jsonAtual = data;
processTranscription(data);
transcriptionStatus.classList.add("active");
} catch (error) {
console.error('Erro ao carregar transcrição:', error);
alert('Erro ao carregar a transcrição selecionada.');
}
});
});
} else {
transcriptionList.innerHTML = `
<div class="transcription-item">
<div class="transcription-icon" style="background: #6b7280;">
<i class="ri-inbox-line"></i>
</div>
<div class="transcription-info">
<div class="transcription-name">Nenhuma transcrição encontrada</div>
<div class="transcription-date">Faça uma transcrição primeiro</div>
</div>
</div>
`;
}
} catch (error) {
console.error('Erro ao carregar transcrições:', error);
transcriptionList.innerHTML = `
<div class="transcription-item">
<div class="transcription-icon" style="background: #dc2626;">
<i class="ri-error-warning-line"></i>
</div>
<div class="transcription-info">
<div class="transcription-name">Erro ao carregar transcrições</div>
<div class="transcription-date">Tente novamente mais tarde</div>
</div>
</div>
`;
}
}
// Alternar fonte dos arquivos
document.querySelectorAll('.source-option').forEach(option => {
option.addEventListener('click', () => {
document.querySelectorAll('.source-option').forEach(opt => opt.classList.remove('active'));
option.classList.add('active');
const source = option.dataset.source;
document.getElementById('localUpload').style.display = source === 'local' ? 'block' : 'none';
document.getElementById('serverUpload').style.display = source === 'server' ? 'block' : 'none';
if (source === 'server') {
loadServerFiles();
}
});
});
// Carregar arquivo do servidor
serverFiles.addEventListener('change', async function() {
if (!this.value) return;
try {
const response = await fetch(`/get_transcription/${this.value}`);
const data = await response.json();
jsonAtual = data;
processTranscription(data);
} catch (error) {
console.error('Erro ao carregar transcrição:', error);
alert('Erro ao carregar a transcrição selecionada.');
}
});
function marcarPalavras(texto) {
return texto.split(/(\s+)/).map(palavra => {
const clean = palavra.normalize("NFD").replace(/\p{Diacritic}/gu, "").replace(/[.,!?;:"""]/g, "").toUpperCase();
if (veryImportantWords.has(clean)) return `<span class='bold-red'>${palavra}</span>`;
if (importantWords.has(clean)) return `<span class='bold-black'>${palavra}</span>`;
return palavra;
}).join('');
}
function processTranscription(data) {
slides = [];
let bloco = [], lastStart = 0;
for (let i = 0; i < data.words.length; i++) {
const word = data.words[i];
if (bloco.length === 0) lastStart = word.start;
bloco.push(word);
const isLast = i === data.words.length - 1;
const nextWord = i < data.words.length - 1 ? data.words[i + 1] : null;
const longPause = nextWord && (nextWord.start - word.end > 0.7);
const isEnd = /[.!?]/.test(word.word);
const isPausaIntencional = longPause || isEnd;
const criarNovoSlide =
isLast ||
(isPausaIntencional && bloco.length >= 1) ||
(!isPausaIntencional && bloco.length >= 12);
if (criarNovoSlide) {
if (bloco.length === 1 && !isPausaIntencional && !isLast) continue;
const palavras = bloco.map(b => b.word);
if (palavras.length > 0) {
palavras[0] = palavras[0].charAt(0).toUpperCase() + palavras[0].slice(1).toLowerCase();
}
slides.push({
text: marcarPalavras(palavras.join(" ").trim()) + '...',
start: lastStart
});
bloco = [];
}
}
preview.innerHTML = slides[0]?.text || '';
transcriptionStatus.classList.add("active");
updateStartButton();
}
// Corrigir upload de arquivos
document.getElementById("jsonFile").addEventListener("change", function () {
const reader = new FileReader();
reader.onload = function () {
try {
const data = JSON.parse(reader.result);
jsonAtual = data;
processTranscription(data);
transcriptionStatus.classList.add("active");
updateStartButton();
} catch (error) {
alert("Erro ao processar o arquivo JSON. Verifique se o formato está correto.");
console.error(error);
transcriptionStatus.classList.remove("active");
}
};
if (this.files[0]) {
reader.readAsText(this.files[0]);
}
});
document.getElementById("audio").addEventListener("change", function () {
if (this.files[0]) {
try {
const file = this.files[0];
const url = URL.createObjectURL(file);
player.src = url;
player.onloadeddata = function() {
audioReady = true;
audioStatus.classList.add("active");
updateStartButton();
};
player.onerror = function() {
alert("Erro ao carregar o áudio. Verifique se o formato é suportado.");
audioReady = false;
audioStatus.classList.remove("active");
updateStartButton();
};
} catch (error) {
alert("Erro ao processar o arquivo de áudio.");
console.error(error);
audioReady = false;
audioStatus.classList.remove("active");
updateStartButton();
}
}
});
// Adicionar drag and drop
const fileInputs = document.querySelectorAll('.file-input-wrapper');
fileInputs.forEach(wrapper => {
const input = wrapper.querySelector('input[type="file"]');
wrapper.addEventListener('dragover', (e) => {
e.preventDefault();
wrapper.style.borderColor = '#667eea';
wrapper.style.background = 'rgba(102, 126, 234, 0.05)';
});
wrapper.addEventListener('dragleave', () => {
wrapper.style.borderColor = '#e5e7eb';
wrapper.style.background = 'none';
});
wrapper.addEventListener('drop', (e) => {
e.preventDefault();
wrapper.style.borderColor = '#e5e7eb';
wrapper.style.background = 'none';
const file = e.dataTransfer.files[0];
if (file) {
const dt = new DataTransfer();
dt.items.add(file);
input.files = dt.files;
input.dispatchEvent(new Event('change'));
}
});
wrapper.addEventListener('click', () => {
input.click();
});
});
function updateStartButton() {
startButton.disabled = !(audioReady && slides.length > 0);
startButton.style.opacity = startButton.disabled ? "0.5" : "1";
}
function toggleFullscreen() {
const preview = document.getElementById("slidePreview");
preview.classList.toggle("fullscreen");
preview.classList.remove("hd");
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
preview.requestFullscreen();
}
}
function toggle1080p() {
const preview = document.getElementById("slidePreview");
preview.classList.add("fullscreen");
preview.classList.toggle("hd");
if (!document.fullscreenElement) {
preview.requestFullscreen();
}
}
function startSync() {
if (!audioReady || slides.length === 0) {
alert("Por favor, importe o áudio e a transcrição primeiro.");
return;
}
if (isPlaying) {
player.pause();
isPlaying = false;
startButton.innerHTML = '<i class="ri-play-line"></i><span>Iniciar</span>';
return;
}
const countdown = 5;
let timeLeft = countdown;
preview.innerHTML = `Iniciando em ${timeLeft}...`;
startButton.disabled = true;
const timer = setInterval(() => {
timeLeft--;
if (timeLeft > 0) {
preview.innerHTML = `Iniciando em ${timeLeft}...`;
} else {
clearInterval(timer);
player.currentTime = 0;
player.playbackRate = 1.08;
player.play();
isPlaying = true;
startButton.disabled = false;
startButton.innerHTML = '<i class="ri-pause-line"></i><span>Pausar</span>';
let i = 0;
function syncSlides() {
if (!isPlaying) return;
if (i < slides.length - 1 && player.currentTime >= slides[i + 1].start) i++;
preview.innerHTML = slides[i].text;
requestAnimationFrame(syncSlides);
}
syncSlides();
}
}, 1000);
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const preview = document.getElementById("slidePreview");
preview.classList.remove("fullscreen");
preview.classList.remove("hd");
if (document.fullscreenElement) {
document.exitFullscreen();
}
} else if (e.key === ' ') {
e.preventDefault();
startSync();
}
});
player.addEventListener('ended', function() {
isPlaying = false;
startButton.innerHTML = '<i class="ri-play-line"></i><span>Iniciar</span>';
});
// Inicialização
updateStartButton();
// Funções para exportação de vídeo
async function exportVideo() {
const exportSection = document.getElementById('exportSection');
const progressBar = document.getElementById('exportProgress');
const steps = document.getElementById('exportSteps').children;
const downloadButton = document.getElementById('downloadButton');
if (!audioReady || !slides.length) {
alert('Carregue o áudio e a transcrição primeiro.');
return;
}
exportSection.style.display = 'block';
downloadButton.style.display = 'none';
progressBar.style.width = '0%';
// Resetar passos
Array.from(steps).forEach(step => {
step.classList.remove('done');
step.querySelector('i').className = 'ri-loader-4-line';
});
try {
// Preparar slides
await updateProgress('prepare', 25);
// Renderizar vídeo
await updateProgress('render', 50);
// Sincronizar áudio
await updateProgress('audio', 75);
// Finalizar
await updateProgress('finish', 100);
// Mostrar botão de download
downloadButton.style.display = 'inline-flex';
} catch (error) {
alert('Erro ao exportar vídeo: ' + error.message);
console.error(error);
}
}
function updateProgress(step, progress) {
return new Promise((resolve) => {
const progressBar = document.getElementById('exportProgress');
const stepElement = document.querySelector(`[data-step="${step}"]`);
progressBar.style.width = `${progress}%`;
// Atualizar ícone do passo atual
stepElement.querySelector('i').className = 'ri-loader-4-line animate-spin';
// Simular processamento
setTimeout(() => {
stepElement.classList.add('done');
stepElement.querySelector('i').className = 'ri-check-line';
resolve();
}, 1500);
});
}
async function downloadVideo() {
try {
const response = await fetch('/export_video', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
slides: slides,
audio: player.src
})
});
if (!response.ok) throw new Error('Erro ao gerar vídeo');
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'vsl_video.mp4';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
} catch (error) {
alert('Erro ao baixar vídeo: ' + error.message);
console.error(error);
}
}
// Adicionar botão de exportar na preview-controls
document.querySelector('.preview-controls').innerHTML += `
<button class="btn btn-secondary" onclick="exportVideo()" id="exportButton">
<i class="ri-movie-line"></i>
</button>
`;
</script>
</body>
</html>