|
<!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; |
|
} |
|
} |
|
|
|
|
|
.preview p { |
|
max-width: 800px; |
|
margin: 0 auto; |
|
white-space: pre-line; |
|
line-height: 1.4; |
|
font-family: 'Open Sans', sans-serif; |
|
} |
|
|
|
|
|
.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; |
|
} |
|
|
|
|
|
.action-buttons { |
|
display: flex; |
|
gap: 10px; |
|
margin-top: 20px; |
|
padding-top: 20px; |
|
border-top: 1px solid #e5e7eb; |
|
} |
|
|
|
|
|
.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; |
|
} |
|
|
|
|
|
.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> |
|
|
|
|
|
<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> |
|
|
|
|
|
<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; |
|
|
|
|
|
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' |
|
]); |
|
|
|
|
|
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); |
|
} |
|
} |
|
|
|
|
|
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(''); |
|
|
|
|
|
document.querySelectorAll('.transcription-item').forEach(item => { |
|
item.addEventListener('click', async () => { |
|
|
|
document.querySelectorAll('.transcription-item').forEach(i => i.classList.remove('selected')); |
|
|
|
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> |
|
`; |
|
} |
|
} |
|
|
|
|
|
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(); |
|
} |
|
}); |
|
}); |
|
|
|
|
|
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(); |
|
} |
|
|
|
|
|
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(); |
|
} |
|
} |
|
}); |
|
|
|
|
|
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>'; |
|
}); |
|
|
|
|
|
updateStartButton(); |
|
|
|
|
|
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%'; |
|
|
|
|
|
Array.from(steps).forEach(step => { |
|
step.classList.remove('done'); |
|
step.querySelector('i').className = 'ri-loader-4-line'; |
|
}); |
|
|
|
try { |
|
|
|
await updateProgress('prepare', 25); |
|
|
|
|
|
await updateProgress('render', 50); |
|
|
|
|
|
await updateProgress('audio', 75); |
|
|
|
|
|
await updateProgress('finish', 100); |
|
|
|
|
|
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}%`; |
|
|
|
|
|
stepElement.querySelector('i').className = 'ri-loader-4-line animate-spin'; |
|
|
|
|
|
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); |
|
} |
|
} |
|
|
|
|
|
document.querySelector('.preview-controls').innerHTML += ` |
|
<button class="btn btn-secondary" onclick="exportVideo()" id="exportButton"> |
|
<i class="ri-movie-line"></i> |
|
</button> |
|
`; |
|
</script> |
|
</body> |
|
</html> |