Mini TelePrompter e Gerador de Legendas em JavaScript

Tempo de Leitura: 21 Minutos

A motivação, para fazer este pequeno script, surgiu, da necessidade como criador de conteúdo, de fazer a narração de vídeos, para cursos, tutoriais, canais dark, e muito mais, na criação de conteúdo em video e apresentações.

Exixtem dezenas ou centenas de softwares que fazem estas 2 funções, alguns gratuítos e outros pagos, porém, me deparei com 2 problemas, que não me permitiam utilizar os softwares que encontrei, nem mesmo os pagos. O primeiro problema surgiu devido ao transformar o texto corrido do roteiro, para blocos de legendas, e gerar estes blocos, com um espaço fixo digamos de 30 segundos, para importar essa legenda para softwares de edição de conteúdo como CANVA ou CAPCUT e gerar o audio através de inteligência artificial.

O segundo problema, foi não usar esses geradores, pois a narração gerada por IA ainda não é tão perfeita, e o vídeo ainda fica um pouco mecânico, gerando menos engajamento e claro um conteúdo de pior qualidade. Sei que existem centenas de softwares que fazem teleprompter e alguns com bastantes recursos, porém, de vários que testei, todos exatamente todos, usam a rolagem de texto corrido, em uma velocidade fixa, ou até mesmo variavel, porém nenhum permite páginas de texto.

Então, acabei escrevendo este código para automatizar essa tarefa. Vou deixar o código logo abaixo, ele é bem símples, um único arquivo, que faz tudo em tela, claro muitas coisas podem ser adicionadas, ou mesmo acrescentadas, porém esta primeira versão acredito que vai ajudar muita gente que precisa criar narrativas, e quem sabe mesmo utilizar para uso, assim como eu, para a criação de conteúdos. Se gostou deixa um comentário, vou deixar também em formato de link, se quiser utilizar ele online tambem é perfeitamente possivel.

<!DOCTYPE html>
<html lang="pt-br">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Teleprompter Simples e Conversor de Texto Para SRT</title>
    <link
      href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-LN+7fdVzj6u52u30Kp6M/trliBMCMKTyK833zpbD+pXdCLuTusPj697FH4R/5mcr"
      crossorigin="anonymous"
    />
    <style>
      body {
        font-family: Arial, sans-serif;
        background: #111;
        color: #ccc;
        margin: 0;
        padding: 20px;
      }

      textarea,
      input,
      button {
        width: 100%;
        margin: 10px 0;
        padding: 10px;
        font-family: monospace;
        font-size: 14px;
        border: none;
        border-radius: 4px;
      }

      button {
        cursor: pointer;
        background-color: #444;
        color: #eee;
      }

      #copiarBtn {
        background-color: rgb(95, 119, 110);
      }
      #teleprompterBtn {
        background-color: rgb(184, 66, 15);
      }
      #btnGerar {
        background-color: rgb(15, 155, 100);
      }
      #baixarBtn {
        background-color: rgb(184, 66, 15);
      }

      pre {
        background: #222;
        padding: 10px;
        white-space: pre-wrap;
        border-radius: 4px;
      }

      /* TELEPROMPTER */
      #teleprompterOverlay {
        position: fixed;
        top: 0;
        left: 0;
        width: 100vw;
        height: 100vh;
        background: black;
        z-index: 9999;
        display: none;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        padding: 5vw;
        box-sizing: border-box;
      }

      #teleprompterContent {
        font-size: 6rem;
        text-align: center;
        line-height: 1.2;
        color: yellow;
        max-width: 100vw;
        max-height: 100vh;
      }

      #teleprompterInfo {
        position: absolute;
        top: 10px;
        right: 20px;
        font-size: 1.2vw;
        color: white;
        font-family: monospace;
      }

      #teleprompterHelp {
        position: absolute;
        bottom: 0;
        width: 100%;
        padding: 10px;
        background: #222;
        color: #ccc;
        font-size: 1vw;
        text-align: center;
        font-family: monospace;
        border-top: 1px solid #444;
      }

      #teleprompterContent b {
        color: #f33;
        font-weight: bold;
      }

      #teleprompterContent i {
        color: #3f3;
        font-style: italic;
      }

      #teleprompterContent tt {
        color: #3cf;
        font-family: monospace;
        padding: 0 5px;
        border-radius: 3px;
      }

      .legenda {
        display: inline-block;
        font-size: 50%;
        background: #222;
        color: #777 !important;
      }

      #dropZone {
        border: 2px dashed #666;
        padding: 5px;
        border-radius: 6px;
        transition: background 0.3s ease;
      }

      #dropZone.dragover {
        background-color: #222;
        border-color: yellow;
      }
    </style>
  </head>
  <body>
    <h2>Teleprompter e Gerador de Legendas SRT Tempo Fixo</h2>
    <label>Texto com marcação HTML:</label>
    <div id="dropZone">
      <textarea
        id="inputText"
        rows="10"
        placeholder="Use <b>, <i>, <tt>, <br> para destacar partes do texto no teleprompter &#10;&#10; Arraste um arquivo .txt aqui ou digite seu conteúdo"
      ></textarea>
    </div>
    <div class="row">
      <div class="col-10">
        <button id="teleprompterBtn" onclick="iniciarTeleprompter()">
          Modo Teleprompter (F-11)
        </button>
      </div>
      <div class="col-2">
        <button id="Limpar" onclick="limparTXT()">
          Limpar Texto
        </button>
      </div>
    </div>
    <div
      class="row"
      style="
        border: 2px solid #ccc;
        margin-top: 20px;
        margin-bottom: 20px;
        margin-left: 20px;
        margin-right: 20px;
        padding: 10px;
        border-radius: 4px;
      "
    >
      <div class="col-4">
        <h4>Configurações do TelePrompter</h4>
        <div class="form-check form-switch">
          <input
            type="checkbox"
            id="ocultarAjuda"
            role="switch"
            class="form-check-input"
            checked
          />
          <label class="form-check-label" for="ocultarAjuda"
            >Ocultar Barra de Atalhos de Ajuda No TelePrompter</label
          >
        </div>
        <div class="form-check form-switch">
          <input
            type="checkbox"
            id="ocultarContador"
            role="switch"
            class="form-check-input"
          />
          <label class="form-check-label" for="ocultarContador"
            >Ocultar a Contagem de Slides</label
          >
        </div>
      </div>
      <div class="col-8">
        <h4>Configurações de Cores do Teleprompter</h4>
        <div class="row">
          <div class="col-2">
            <input
              type="color"
              id="corTexto"
              value="#FFFF00"
              class="form-control form-control-color"
              title="Cor do Texto comum"
              onchange="aplicarCores()"
            />
          </div>
          <div class="col-2">
            <input
              type="color"
              id="corFundo"
              value="#000000"
              class="form-control form-control-color"
              title="Cor de Background"
              onchange="aplicarCores()"
            />
          </div>
          <div class="col-2">
            <input
              type="color"
              id="corContador"
              value="#cccccc"
              class="form-control form-control-color"
              title="Cor de Background"
              onchange="aplicarCores()"
            />
          </div>
          <div class="col-2">
            <input
              type="color"
              id="corB"
              value="#FF6666"
              class="form-control form-control-color"
              title="Cor de BOLD"
              onchange="aplicarCores()"
            />
          </div>
          <div class="col-2">
            <input
              type="color"
              id="corI"
              value="#55CC55"
              class="form-control form-control-color"
              title="Cor de ITALICO"
              onchange="aplicarCores()"
            />
          </div>
          <div class="col-2">
            <input
              type="color"
              id="corTT"
              value="#5555FF"
              class="form-control form-control-color"
              title="Cor de TEXTO"
              onchange="aplicarCores()"
            />
          </div>
        </div>
      </div>
    </div>
    <div
      class="row"
      style="
        border: 2px solid #888;
        margin-top: 20px;
        margin-bottom: 20px;
        margin-left: 20px;
        margin-right: 20px;
        padding: 10px;
        border-radius: 4px;
      "
    >
      <h4>Configurações de Legendas</h4>
      <div class="col-6">
        <label>Duração por bloco (segundos):</label>
        <input type="number" id="duracao" value="4" step="0.1" />
      </div>
      <div class="col-6">
        <label>Intervalo entre blocos (milissegundos):</label>
        <input type="number" id="intervalo" value="500" step="100" />
      </div>
      <div class="col-4">
        <button onclick="gerarSRT()" id="btnGerar">Gerar SRT</button>
      </div>
      <div class="col-4">
        <button id="baixarBtn" onclick="baixarSRT()" class="btn-primary">
          Baixar SRT
        </button>
      </div>
      <div class="col-4">
        <button id="copiarBtn" onclick="copiarSRT()">
          Copiar SRT
        </button>
      </div>
    </div>
    <h3>Resultado:</h3>
    <pre id="outputSRT"></pre>

    <!-- TELEPROMPTER -->
    <div
      id="teleprompterOverlay"
      onclick="mouseAvancar()"
      oncontextmenu="mouseVoltar(event)"
      ondblclick="fecharTeleprompter()"
    >
      <div id="teleprompterInfo"></div>
      <div id="teleprompterContent"></div>
      <div id="teleprompterHelp">
        Espaço / Clique Esquerdo: Próximo • Seta ↑ / Clique Direito: Anterior •
        Duplo Clique / ESC: Fechar
      </div>
    </div>

    <script>
      function formatarTempo(ms) {
        const totalSegundos = Math.floor(ms / 1000);
        const horas = String(Math.floor(totalSegundos / 3600)).padStart(2, "0");
        const minutos = String(
          Math.floor((totalSegundos % 3600) / 60)
        ).padStart(2, "0");
        const segundos = String(totalSegundos % 60).padStart(2, "0");
        const milissegundos = String(ms % 1000).padStart(3, "0");
        return `${horas}:${minutos}:${segundos},${milissegundos}`;
      }

      function gerarSRT() {
        const texto = document.getElementById("inputText").value.trim();
        const duracaoSeg = parseFloat(document.getElementById("duracao").value);
        const intervaloMs = parseInt(
          document.getElementById("intervalo").value
        );

        const linhas = texto.split(/\r?\n/).filter((l) => l.trim() !== "");
        let srt = "";
        let tempoAtual = 0;

        for (let i = 0; i < linhas.length; i++) {
          const inicio = tempoAtual;
          const fim = tempoAtual + duracaoSeg * 1000;

          srt += `${i + 1}\n`;
          srt += `${formatarTempo(inicio)} --> ${formatarTempo(fim)}\n`;
          srt += linhas[i].replace(/<[^>]+>/g, "") + "\n\n";
          tempoAtual = fim + intervaloMs;
        }

        document.getElementById("outputSRT").textContent = srt;
      }

      function copiarSRT() {
        const texto = document.getElementById("outputSRT").textContent;
        if (!texto.trim()) {
          alert("Nada para copiar. Gere o SRT primeiro.");
          return;
        }

        navigator.clipboard
          .writeText(texto)
          .then(() => {
            const btn = document.getElementById("copiarBtn");
            const original = btn.textContent;
            btn.textContent = "Copiado!";
            setTimeout(() => {
              btn.textContent = original;
            }, 1500);
          })
          .catch((err) => {
            alert("Erro ao copiar: " + err);
          });
      }

      let parrafos = [];
      let indiceAtual = 0;

      function iniciarTeleprompter() {
        aplicarCores();
        const texto = document.getElementById("inputText").value.trim();
        parrafos = texto.split(/\r?\n/).filter((l) => l.trim() !== "");

        if (parrafos.length === 0) {
          alert("Nenhum parágrafo encontrado.");
          return;
        }

        parrafos.push("### FIM ###"); // Slide final
        indiceAtual = 0;

        const ocultarAjuda = document.getElementById("ocultarAjuda").checked;
        const ocultarContador =
          document.getElementById("ocultarContador").checked;

        document.getElementById("teleprompterHelp").style.display = ocultarAjuda
          ? "none"
          : "block";
        document.getElementById("teleprompterInfo").style.display =
          ocultarContador ? "none" : "block";

        atualizarTeleprompter();
        document.getElementById("teleprompterOverlay").style.display = "flex";
        document.addEventListener("keydown", controlarTeleprompter);
      }

      function atualizarTeleprompter() {
        const content = document.getElementById("teleprompterContent");
        const info = document.getElementById("teleprompterInfo");

        content.innerHTML = parrafos[indiceAtual];
        info.textContent = `${indiceAtual + 1} / ${parrafos.length}`;
        ajustarFonteTeleprompter();
      }

      function controlarTeleprompter(e) {
        if (e.key === "Escape") {
          fecharTeleprompter();
          return;
        }

        if (e.key === " " || e.key === "ArrowDown") {
          e.preventDefault();
          avancar();
        }

        if (e.key === "ArrowUp") {
          e.preventDefault();
          voltar();
        }
      }

      function avancar() {
        if (indiceAtual < parrafos.length - 1) {
          indiceAtual++;
          atualizarTeleprompter();
        }
      }

      function voltar() {
        if (indiceAtual > 0) {
          indiceAtual--;
          atualizarTeleprompter();
        }
      }

      function fecharTeleprompter() {
        document.getElementById("teleprompterOverlay").style.display = "none";
        document.removeEventListener("keydown", controlarTeleprompter);
      }

      function mouseAvancar() {
        avancar();
      }

      function mouseVoltar(event) {
        event.preventDefault();
        voltar();
      }

      function aplicarCores() {
        const corTexto = document.getElementById("corTexto").value;
        const corFundo = document.getElementById("corFundo").value;
        const corContador = document.getElementById("corContador").value;
        const corB = document.getElementById("corB").value;
        const corI = document.getElementById("corI").value;
        const corTT = document.getElementById("corTT").value;

        const overlay = document.getElementById("teleprompterOverlay");
        const info = document.getElementById("teleprompterInfo");
        const style = document.getElementById("teleprompterStyle");

        // Aplica cores via estilos inline ou alterando CSS variables
        overlay.style.backgroundColor = corFundo;
        document.getElementById("teleprompterContent").style.color = corTexto;
        info.style.color = corContador;

        // Atualiza as cores das tags diretamente em CSS dinamicamente:
        if (!style) {
          const styleTag = document.createElement("style");
          styleTag.id = "teleprompterStyle";
          document.head.appendChild(styleTag);
        }

        document.getElementById("teleprompterStyle").textContent = `
    #teleprompterContent b { color: ${corB} !important; }
    #teleprompterContent i { color: ${corI} !important; }
    #teleprompterContent tt { color: ${corTT} !important; }
  `;
      }

      function ajustarFonteTeleprompter() {
        const content = document.getElementById("teleprompterContent");
        const overlay = document.getElementById("teleprompterOverlay");

        let fontSize = 6; // em rem
        content.style.fontSize = fontSize + "rem";

        const maxHeight = overlay.clientHeight * 0.9;
        const maxWidth = overlay.clientWidth * 0.95;

        while (
          fontSize > 0.6 &&
          (content.scrollHeight > maxHeight || content.scrollWidth > maxWidth)
        ) {
          fontSize -= 0.1;
          content.style.fontSize = fontSize.toFixed(2) + "rem";
        }
      }

      function limparTXT() {
        document.getElementById('inputText').value = "";
      }

      function baixarSRT() {
        gerarSRT();
        const texto = document.getElementById("outputSRT").textContent;
        if (!texto.trim()) {
          alert("Nada para baixar. Gere o SRT primeiro.");
          return;
        }

        const blob = new Blob([texto], { type: "text/plain;charset=utf-8" });
        const url = URL.createObjectURL(blob);
        const link = document.createElement("a");
        link.href = url;
        link.download = "legenda.srt";
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        URL.revokeObjectURL(url);
      }

      document.addEventListener("keydown", function (e) {
        if (e.key === "F11") {
          e.preventDefault(); // evita o fullscreen padrão do navegador
          iniciarTeleprompter();
        }
      });

      const dropZone = document.getElementById("dropZone");
      const inputText = document.getElementById("inputText");

      // Previne comportamento padrão
      ["dragenter", "dragover", "dragleave", "drop"].forEach(eventName => {
        dropZone.addEventListener(eventName, e => e.preventDefault(), false);
        dropZone.addEventListener(eventName, e => e.stopPropagation(), false);
      });

      // Estilo visual ao arrastar
      dropZone.addEventListener("dragover", () => {
        dropZone.classList.add("dragover");
      });
      dropZone.addEventListener("dragleave", () => {
        dropZone.classList.remove("dragover");
      });
      dropZone.addEventListener("drop", e => {
        dropZone.classList.remove("dragover");

        const file = e.dataTransfer.files[0];
        if (
          !file ||
          (
            !file.name.endsWith(".txt") &&
            !file.name.endsWith(".html") &&
            !file.name.endsWith(".srt") &&
            !file.type.startsWith("text/") &&
            file.type !== "application/x-subrip"
          )
        ) {
          console.log(file.type);
          alert("Por favor, solte um arquivo .txt válido.");
          return;
        }

        const reader = new FileReader();
        reader.onload = function (evt) {
          const content = evt.target.result;

          if (file.name.endsWith(".srt") || file.type === "application/x-subrip") {
            const blocos = content
              .split(/\r?\n\r?\n+/)
              .map(bloco => {
                const linhas = bloco.split(/\r?\n/);
                if (linhas.length < 2) return null;

                const tempo = linhas[1];
                const texto = linhas.slice(2).join(" ");
                return `<strong class='legenda'>${tempo}</strong> <br>${texto.trim()}`;
              })
              .filter(l => l);

            inputText.value = blocos.join("\n\n");
          } else {
            inputText.value = content;
          }
        };

        reader.readAsText(file, "UTF-8");
      });
    </script>
    <script
      src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
      integrity="sha384-ndDqU0Gzau9qJ1lfW4pNLlhNTkCfHzAVBReH9diLvGRem5+R9g2FzA8ZGN954O5Q"
      crossorigin="anonymous"
    ></script>
  </body>
</html>

Caso queira testar ou utilizar ou visualizar o resultado deste script, Acesse o TelePrompter e Gerador de Legendas vou explicar os blocos de conteúdo do arquivo, e como você pode fazer as adaptações que desejar para adaptar as suas necessidades, é muito simples e além das opções de personalização, você pode deixa-lo totalmente funcional para o seu modelo ne utilização.

O projeto também está no GitHub, mesmo não sendo um ativo usuário da plataforma, este tipo de projeto pode acabar vindo a ser uma necessidade real, para criadores de conteúdo que não tem grande experiência com programação e podem se beneficiar do uso da ferramenta para os mesmos propositos que o meu.

O primeiro bloco interessante é da linha 7 a 12, onde importo de modo externo o Bootstrap que ajuda na formatação visual da tela. O segundo bloco é da linha 13 até a linha 143, que são a formatação visual (CSS) do conteúdo padrão e das cores da tela, botões. O conteúdo da tela está entre as linhas 146 e 307 neste bloco estão todos os botões, configurações, controles e outros itens visuais.

Entre as linhas 309 e 322 vem a parte visual da apreentação do TELEPROMPTER. Por fim, as linhas 608 a 614 finalizam os arquivos de formatação css e trazem algumas juncionalidades em javascript do Bootstrap que tem o objetivo somente estético neste projeto. O bloco mais importante que vemos aqui é o Script javascript que está entre as linhas 324 e 606 ou seja, em menos de 1000 linhas de código, é possivel fazer um software basico porém funcional. Abaixo vamos explicar os conceitos de cada uma das funções e o funcionamnto geral do software.

A função formatarTempo(ms) recebe o tempo em segundos, e o formata para a formatação necessária nas legendas SRT. Ou seja, se ele receber o valor 2000 que equivale a 2 segundos, ele retorna o formato 00:00:02,000 que é utilizado no formato das legendas.

A função gerarSRT() é um dos blocos importantes, é o responsavel por pegar o bloco de texto da textarea, e formata-lo de acordo com as configurações de tempo definidas, para cada bloco e do intervalo. Para explicar seu funcionamento, primeiro temos que entender como um SRT de legendas funciona, ele é um arquivo em formato texto, totlmente legivel a humanos, separados em diversos blocos de 3 linhas em um espaço em branco, como o exemplo abaixo:

9
00:00:51,360 --> 00:00:56,414
Se você está começando do zero, uma das
minhas principais dicas para ti é que você

10
00:00:56,480 --> 00:01:00,654
assista cada aula com calma, inclusive
aquelas aulas que você acha

11
00:01:00,720 --> 00:01:02,614
que são desnecessárias.

Nesta pequena transcrição vemos 3 blocos de legenda, na primeira linha, temos o número do bloco, que deve ser sempre sequencial iniciado de 1, e serve para o player de vídeo ir lendo e armazenando os blocos internamente, conforme a necessidade e o andamento do vídeo. A segunda linha, é composta por 2 carimbos de tempo, separados pelo bloco –> estes carimbos de tempo indicam o tempo inicial e o tempo final onde o bloco de legenda estará visivel na tela. Por exemplo o bloco 9 indica que aos 51 segundos e 360 milisegundos o bloco se tornará visivel e saira da tela aos 56 segundos e 414 milesimos, ou seja ele fica visivel por 5 segundos e 54 milesimos.
Na sequencia o bloco 10 se inicia aos 56 segundos e 480 milesimos, note que existe uma diferença de 66 milesimos entre o termino de uma e o aparecimento da outra, essa diferença é o espaço em tela, que fica sem exibição de legenda, e é necessário para que primeiro uma desapareça para depois a outra aparecer. Se os tempos se sobrepões, o player de vídeo pode ou encavalar um texto sobre o outro, ou os mais modernos acabam fazendo isso, inserindo em posições diferentes da tela. As legendas “ENCAVALADAS” geralmente servem para legendar um segundo idioma, por exemplo um video em inglês legendado em português, porém em um momento existe uma fala em japones, que é legendada em inglês e em português como os idiomas tem tempos diferentes de leitura, pode ser necessário encavalar ambas as legendas.
O formato SubRip Subtitle (SRT) é um dos mais comuns para legendar filmes, séries e outras centenas de conteúdos, não é o único existente, porém, sua natureza de ser facilmente lido por humanos e compreendido, o tornou amplamente utilizado.
As proximas linhas, que podem ser entre 0 (sem exibir nada, somente limpa o prompt de legendas) até 6 linhas (o formato não determina, porém os players limitam entre 3 e 15 sendo 6 a limitação da maioria dos players que testei) linhas de texto corrido, que será apresentada na saída de vídeo, efetivamente, não indicaria mais de 3 linhas pois além de ocupar espaço na tela, fica extremamente dificil de ler, é preferivel fazer mais blocos.
A especificação do formato, indica que seja apresentado entre 1 caractere e o máximo de 30 a 45 por linha para tornar a leitura mais fluída na maioria dos idiomas. Alguns players permitem linhas maiores e quebram a linha automaticamente na hora da exibição, outros simplesmente deixam centralizado e o texto some nas bordas, dai seguir a indicação de 30 além de visualmente mais agradavel é o ideal.

Os players de mídia mais modernos suportam além da configuração padrão uma formatação usando TAGs no mesmo estili HTML, extra oficialmente são suportados pelos principais players as tags:
<b> um texto en negrito, para dar uma enfase mais forte, alguns colocam esse texto em LETRAS MAIUSCULAS como um padrão.
<i> texto em italico para citações ou transcrições de outros idiomas (sub-legenda como explicado acima) e deixam muitas vezes em uma cor diferente.
<u> texto em underline para indicar uma “AÇÃO DE FUNDO” ou CONTEXTO, como a marcação de uma musica intensa, ou partes onde o video fica sem movimentos, ou dialogos fora da tela, onde se usa a marcação indicando <u>[personagem 1]</u> mostrando que é dito por um personagem específico.
<font color=”#000000″> que indica o uso de uma cor específica, exceto alguns players online como o youtube que aceita nomes de cores html, formato simples #000 a maioria só suporta o formato de cor html original #000000 composto por até #FFFFFF onde os 2 blocos iniciais indicam valores de 0 a 255 para a cor VERMELHA, os 2 intermediários indicam a cor VERDE e os 2 ultimos indicam a cor AZUL

Explicado isso, nosso teleprompter dá suporte a este destaque e também a marcação <tt> que indica um COMENTÁRIO DO DIRETOR e que aparece na tela, porém, não usa nada especifico, e fica em uma fonte menor (50% do tamanho) porém isso é facilmente configurado e alterado no CSS.

A marcação de LEGENDA também fica em uma fonte menor, quando o conteúdo é exibido no teleprompter indicando o tempo estimado da fala para uma dublagem ou lip-sync que é comum em alguns formatos de criação de conteúdo.

A lógica por traz da função é simples, ela lê o conteúdo da caixa de texto (textarea), e dos campos duração e intervalo. Depois, divide o conteúdo da textarea separando em um ARRAY onde cada sinal de quebra de linha (\n ou \r ou \n\r ou \r\n) vira uma entrada no array, como os sistemas Windows, Unix e MacOs usam marcas de termino de linha diferentes, qualquer uma dessas marcas é lida, e em caso de marcas compostas, é gerado 1 linha em branco, por isso, linhas totalmente vazias são ignoradas l.trim() se for vazio não cria entrada no array de saíd convertida.
É inicializado o tempo atual como 0 (inicio), e o for percorre todo o array, dentro do for, são atualizados os campos obrigatório de posição (linha do array + 1) pois o array em javascript inicia em 0 e o srt em 1. O tempo inicial em milisegundos da legenda, o tempo fical. a variavel srt contém as respectivas saídas, onde é inserido o número da leganda atual, uma quebra de linha; os tempos formatados de inicio e fim e a quebra de linha; limpa as tags html transformando em texto simples e acrescenta no sim da string com 2 quebras de linha, para já deixar a linha de espaço determinada no formato; por fim o intervalo em milisegundo do espaço da legenda é determinado para o tempo atual; no termino do for a DIV de saída é atualizada com o TEXTO do arquivo SRT, note que ele usa o texto, e não o html para que as tags de marcação não sejam convertidas pelo browser.

A função copiarSRT() é muito simples, ela pega o conteúdo da div outputSRT se este for vazio (não foi possivel gerar) ele emite um alerta, e caso contrário ele atualiza o objeto navigator.clipboard com o conteúdo desta em formato texto simples, para que não seja interpretado o HTML, caso tenha sucesso na cópia, ele troca o texto do botão para Copiado! para informar o usuário, e caso tenha algum erro durante essa cópia, como por exemplo se o vanegador não suporta a cópia, ou se o contexto do clipboard foi bloqueado pelo usuário do navegador, ou algum outro erro que possa ocorrer ele exibe a mensagem notificando o usuário.

No contexto global são criadas as variaveis paragrafos, que é um array do conteúdo da caixa de texto e indiceAtual que é o numero do paragrafo atual no teleprompter (para atualizar o contador).

A Função iniciarTeleprompter() é outro ponto importante, ela é a responsável por exibir o conteúdo e navegar entre os slides no modo teleprompter, primeiro ela garante que caso alguma cor tenha sido alterada nas configurações ela seja exibida corretemente, depois, a variavel global paragrafos é atualizada, onde cada entrada do array é separada por uma quebra de linha, e linhas totalmente em branco são descartadas; caso não tenha um conteúdo gerado, é enviado um alerta para o usuário; se não, o slide de FIM é acrescentado e o contexto de exibir a ajuda e a contagem é atualizado e chamada a função para atualizar o conteúdo do tele-prompter.

A função atualizarTeleprompter() é também bem simples, pega o elemento teleprompterContent e teleprompterInfo, atualiza o content.innerHTML (o html) com o conteúdo do texto do paragrafo, ou seja, ele interpreta as tags existente como parte do HTML exibindo a formatação, e atualiza a teleprompterInfo com o numero do paragrafo atual, e o total de paragrafos e chama a funcao ajustarFonteTeleprompter que será explicada abaixo.

A função ajustarFonteTeleprompter() é a responsável por dimensionar o paragrafo inteiro, para exibi-lo na tela de uma vez. Para isso, ela pega o conteúdo do teleprompterConten e o do teleprompterOverlay (tela cheia) captura o tamanho da altura e da largura do elemento exibido, e reduz as margens necessárias (10% na altura e 5% na largura) depois ele vai ele entra em um looping diminuindo a fonte de passo a passo até que a fonte atinja o tamanho mínimo de 0.6 (1/24 partes da tela aproximadamente) que em um monitor de 1920×1080 equivale a aproximadamente 14 px de altura, evitando que ela se torne ilegivel ou que não exista mais conteúdo fora da area visivel da tela. Ou seja, ela vai ajustar o tamanho do texto para permitir que todo o conteúdo caiba visivel, ou caso o tamanho mínomo seja atingido deixar o fim do texto oculto para ser rolado.

A proxima função controlarTeleprompter(e) recebe o evento e verifica se é uma das 3 teclas de função Escape (esc), ArrowUp (seta para cima) e ArrowDown (seta para baixo) se for alguma dessas 3 teclas, executa a ação programada, ESC fecha o teleprompter finalizando-o, ArrowUp volta para o slide anterior, ArrowDown avança para o proximo slide.

Para as funções avancar() e voltar() ambas fazem a atualização da variavel global indiceAtual que representa o slide ou paragrafo que será exibido, garantindo que esse indice esteja dentro do contexto (quantidade de slides validos) e atualizam a exibição atraves da função atualizarTeleprompter() explicada anteriormente.

A função fecharTeleprompter() simplesmente oculta o overlayTeleprompter que é a div que é exibida na tela, e para de “escutar” o pressionamento das teclas evento keydown, que chamava a função de controlar o que é exibido, deixando as teclas agirem do modo determinado pelo navegador.

As funções mouseVoltar(e) e mouseAvancar() fazem exatamente a mesma coisa que as respectivas avancar() e voltar() como o botão direiro do mouse tem um contexto próprio no navegador ele é “sobreposto” evitando que o menu de contexto seja aberto ao seu pressionamento.

Na função aplicarCores() os controles das configurações de cor dos textos, backgrounds e destaques são forçadamente atualizados para a exibição do teleprompter, para isso ela aplica a respetiva cor no CSS dos recursos de exibição dentro da tag div do teleprompterOverlay.

A função limparTXT() apaga o conteúdo da caixa de texto, e a baixarSRT() faz o download do arquivo legenda.srt que é o conteúdo textual da div outputSRT, caso ela esteja vazia, ou seja, não tenha um conteúdo é exibido uma mensagem de aviso, caso contrário o arquivo é baixado.

Por fim deixamos a captura da tecla F11 para inicializar o teleprompter quando este estiver fechado, é um atalho global, e definimos também a div dropZone e a caixa de texto inputText quando um arquivo externo é arrastado para dentro da div dragZone ao invés de executar a ação padrão, capturamos o evento e verificamos o tipo de arquivo, se é um TEXTO de qualquer tipo ou um arquivo de legendas SRT pegamos o seu conteúdo, se não é exibimos um alerta informando que so é possivel usar estes formatos.
Caso o conteúdo seja válido, fazemos a leitura do seu conteúdo e atualizamos a caixa de texto, quando é um texto simples simplesmente colocamos seu conteúdo na caixa, caso seja um arquivo de legendas, inserimos a marcação HTML para exibir as mensagens do tempo de legenda em um formato diferente.

Qualquer dúvida ou sugestão, deixe o seu comentário.

Algumas Vantagens e Conceitos do TelePrompter e do Conversor de Legendas SRT

Conversor de Legendas SRT

O conversor de legendas, diferente dos sistemas tradicionais que visam sincronizar o audio e as falas e persagens na tela, aqui serve para ser um guia para o editor, imagine que uma cena iteira está narrada, em uma legenda de digamos 1 minuto, com instruções para o prompt de um gerador de imagens ou de vídeo criar a cena, ou mesmo criar um único arquivo de audio de conversão de teto em fala, para ser importado pelo seu software de edição.

Neste fluxo de trabalho, você pode utilizar prompts para gerar o texto e as instruções para criar um cenário, e salva-los em arquivos de texto, depois, exportar o arquivo de roteiros, com os times do vídeo para seu software de edição ou de criação de imagens e pronto, todas as imagens com IA são geradas de modo automático e uma animação leve é adicionada, para que elas não fiquem estáticas. Basta selecionar o estilo e pronto.

TelePrompter

A verdadeira utilidade do projeto é ser um TelePrompter de paginaçao de texto, que permite o uso da marcação HTML para cada paragrafo, digamos que você vai fazer a narrativa do seu vídeo de conteúdo, e quer que além do conteúdo a ser lido, sejam exibidos também detalhes de direção como entonação desejada para o trecho, marcas de pausas e expressão vocal, que coloque conteúdos de direção como clima e tensão na voz.

Tudo isso é possivel, basta utilizar a marcação HTML, por exemplo, quer fazer um vídeo de QUIZ basta inserir em texto como a pergunta, e em negrito a resposta e na tela, eles vão aparecer formatados em cores diferentes. Se vai narrar um conteúdo misterioso, e quer deixar um comentário sobre o clima da cena, para o narrador ou narradores saberem qual a espectativa de direcionamento e aplicarem o clima e a tensão corretos na voz, também é possivel, basta inserir um texto com a marcação de comentários do diretor e pronto.

Vai ter um dialogo, entre vários participantes e quer marcar as falas de cada um, basta usar diferentes cores para cada personagem, também utilizando o HTML e tudo isso, pode ser feito em um bloco de notas ou copiado e colado diretemente do seu agente de IA ou gerador de GPT, de maneira simples e rápida.

Para a marcação, são usadas 4 tags HTML a seguir:
<b> </b> – todo o texto entre essas 2 marcações será escrito em negrito, por padrão na cor vermelha, para indicar uma enfase forte.
<i> </i> – todo o texto apresentado entre essas 2 marcações será escrito como uma citação na cor verde, indica uma fonte externa ou um tom mais frio ou sem emoção.
<u> </u> – esta tag indica underline, por padrão a cor rosa, usado para um clima mais sombrio ou uma emoção negativa.
<tt> </tt> – esta marcação, indica um texto de comentário de direção, por padrão em azul, e com metade do tamanho, entre [colchetes], indica algo que não precisa ser lido, porém, serve como uma guia para uma ação ou execução.

Além deste comando é possivel usar a tag <font color=’#111′></font> ou <font color=’#223344′></font> que vai marcar o texto com a cor selecionada, util para uso de personagens ou narrativas, onde cada personagem tem sua cor de base, facilitando a leitura de 2 vozes diferentes.

Para a colocação de quebras de linha, a tab <br> pode ser usada, forçando que naquele ponto o texto va para uma linha abaixo.

Operação do Prompter de Texto

Para operar o prompt, é muito simples, você pode deixar habilitado o visualizador de posição, que mostra no canto superior direito, qual o numero do slide atual, e qual o total de slides da apresentação.
Para avançar para o proximo slide, você pode usar a SETA PARA BAIXO, SETA DIREITA, ENTER, PG DOWN, BARRA DE ESPAÇO ou a tecla +
Para retroceder, SETA PARA CIMA, SETA ESQUERDA, BACKSPACE, PG UP, ou a tecla –
Primeiro Slide, HOME ou tecla /
Ultimo Slide, END ou tecla *
Para Fechar Apresentação ESC ou tecla .

Você também pode utilizar o mouse para navegar pelos slides, CLIQUE no botão ESQUEDO avança, DIREITO retrocede, DUPLO CLIQUE fecha.

Também é possivel usar os navegadores de Slide e as teclas de inicio, fechar, avançar e retroceder funcionam corretamente, com a grande parte dos modelos. Caso algum não funcione, é possivel configura-lo