feat: corrigida a exportação dos tipos de vídeo para preservar car
- corrigida a exportação dos tipos de vídeo para preservar caracteres UTF-8 no J-SOM - adicionada revisão editável de legendas com geração offline e exportação SRT no painel Resumo: - 4 arquivos alterados - 1 novos - 3 modificados - 0 removidos 3 files changed, 121 insertions(+), 12 deletions(-) Arquivos: - code/cep-plugin/index.html - code/cep-plugin/main.js - code/cep-plugin/styles.css - code/tests/cep-tipos-video-encoding.test.ts
This commit is contained in:
@@ -630,8 +630,18 @@
|
||||
<input class="text-field" id="legendasMogrtPath" type="text" spellcheck="false" placeholder="/caminho/para/legenda.mogrt">
|
||||
<p class="field-help">A fonte e a animação pertencem ao próprio template MOGRT.</p>
|
||||
<button class="button button-primary" id="btnSalvarLegendas" onclick="salvarConfiguracaoDeLegendas()" type="button">Salvar configuração</button>
|
||||
<button class="button button-primary" id="btnGerarLegendas" onclick="gerarLegendasDaTranscricao()" type="button">Gerar legendas da transcrição</button>
|
||||
<p class="field-help" id="legendasStatus" aria-live="polite"></p>
|
||||
</section>
|
||||
<section class="card" id="legendasRevisao">
|
||||
<div class="card-head"><h2>Revisão <span id="legendasContagem">(0)</span></h2><span class="step-chip">Editável</span></div>
|
||||
<p class="field-help">Clique no texto para corrigir. Exclua blocos antes de inserir na timeline.</p>
|
||||
<div id="legendasLista" role="list" aria-label="Lista de legendas"><p class="empty-state">Nenhuma legenda carregada. Gere as legendas a partir da transcrição.</p></div>
|
||||
<div class="button-row">
|
||||
<button class="button button-ghost" onclick="exportarLegendasSrt()" type="button">↓ Exportar SRT</button>
|
||||
<button class="button button-primary" onclick="inserirLegendasNaTimeline()" type="button">↓ Inserir na timeline</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- =============================== AJUSTES =============================== -->
|
||||
@@ -686,8 +696,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="CSInterface.js"></script>
|
||||
<script src="updater.cjs"></script>
|
||||
<script src="main.js"></script>
|
||||
<script charset="utf-8" src="CSInterface.js"></script>
|
||||
<script charset="utf-8" src="updater.cjs"></script>
|
||||
<script charset="utf-8" src="main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+105
-9
@@ -292,6 +292,98 @@ function salvarConfiguracaoDeLegendas() {
|
||||
document.getElementById("legendasStatus").textContent = "Configuração de legendas salva neste computador.";
|
||||
}
|
||||
|
||||
var legendasGeradas = [];
|
||||
|
||||
function legendaNumeroParaTempo(segundos) {
|
||||
var horas = Math.floor(segundos / 3600);
|
||||
var minutos = Math.floor((segundos % 3600) / 60);
|
||||
var segundosInteiros = Math.floor(segundos % 60);
|
||||
var milissegundos = Math.round((segundos - Math.floor(segundos)) * 1000);
|
||||
if (milissegundos >= 1000) { segundosInteiros++; milissegundos = 0; }
|
||||
return [horas, minutos, segundosInteiros].map(function (n) { return String(n).padStart(2, "0"); }).join(":") + "," + String(milissegundos).padStart(3, "0");
|
||||
}
|
||||
|
||||
function legendasExtrairPalavras(transcricao) {
|
||||
var segmentos = transcricao.segmentos || transcricao.segments || [];
|
||||
var palavras = [];
|
||||
segmentos.forEach(function (segmento) {
|
||||
var itens = segmento.palavras || segmento.words || [];
|
||||
itens.forEach(function (palavra) {
|
||||
var texto = String(palavra.texto || palavra.word || "").trim();
|
||||
var inicio = Number(palavra.inicio !== undefined ? palavra.inicio : palavra.start);
|
||||
var fim = Number(palavra.fim !== undefined ? palavra.fim : palavra.end);
|
||||
if (texto && isFinite(inicio) && isFinite(fim) && fim > inicio) palavras.push({ texto: texto, inicio: inicio, fim: fim });
|
||||
});
|
||||
});
|
||||
return palavras.sort(function (a, b) { return a.inicio - b.inicio; });
|
||||
}
|
||||
|
||||
function legendasIntervaloCortado(palavra, cortes) {
|
||||
return cortes.some(function (corte) { return palavra.inicio < corte.end && palavra.fim > corte.start; });
|
||||
}
|
||||
|
||||
function legendasGerarBlocos(palavras, cortes, configuracao) {
|
||||
var mantidas = palavras.filter(function (palavra) { return !legendasIntervaloCortado(palavra, cortes); });
|
||||
var blocos = [], bloco = [], caracteres = 0;
|
||||
function adicionar() {
|
||||
if (!bloco.length) return;
|
||||
var primeiro = bloco[0], ultimo = bloco[bloco.length - 1];
|
||||
// Para o primeiro corte, todos os cortes anteriores são considerados.
|
||||
var inicio = primeiro.inicio - cortes.reduce(function (total, corte) { return total + (corte.end <= primeiro.inicio ? corte.end - corte.start : 0); }, 0);
|
||||
var fim = ultimo.fim - cortes.reduce(function (total, corte) { return total + (corte.end <= ultimo.fim ? corte.end - corte.start : 0); }, 0);
|
||||
blocos.push({ texto: bloco.map(function (item) { return item.texto; }).join(" "), inicio: inicio, fim: fim });
|
||||
bloco = []; caracteres = 0;
|
||||
}
|
||||
mantidas.forEach(function (palavra) {
|
||||
var espaco = bloco.length ? 1 : 0;
|
||||
if (bloco.length && (bloco.length >= configuracao.max_palavras || caracteres + espaco + palavra.texto.length > configuracao.max_caracteres)) adicionar();
|
||||
bloco.push(palavra); caracteres += (bloco.length > 1 ? 1 : 0) + palavra.texto.length;
|
||||
});
|
||||
adicionar();
|
||||
return blocos;
|
||||
}
|
||||
|
||||
function legendasRenderizar() {
|
||||
var lista = document.getElementById("legendasLista");
|
||||
document.getElementById("legendasContagem").textContent = "(" + legendasGeradas.length + ")";
|
||||
lista.textContent = "";
|
||||
if (!legendasGeradas.length) { lista.innerHTML = '<p class="empty-state">Nenhuma legenda carregada.</p>'; return; }
|
||||
legendasGeradas.forEach(function (legenda, indice) {
|
||||
var linha = document.createElement("div"); linha.className = "legenda-row"; linha.setAttribute("role", "listitem");
|
||||
var cabecalho = document.createElement("div"); cabecalho.className = "legenda-timecode"; cabecalho.textContent = (indice + 1) + " " + legendaNumeroParaTempo(legenda.inicio) + " → " + legendaNumeroParaTempo(legenda.fim);
|
||||
var campo = document.createElement("input"); campo.className = "text-field"; campo.value = legenda.texto; campo.setAttribute("aria-label", "Texto da legenda " + (indice + 1));
|
||||
campo.oninput = function () { legendasGeradas[indice].texto = campo.value; };
|
||||
var excluir = document.createElement("button"); excluir.className = "button button-ghost"; excluir.type = "button"; excluir.textContent = "×"; excluir.title = "Excluir legenda";
|
||||
excluir.onclick = function () { legendasGeradas.splice(indice, 1); legendasRenderizar(); };
|
||||
linha.appendChild(cabecalho); linha.appendChild(campo); linha.appendChild(excluir); lista.appendChild(linha);
|
||||
});
|
||||
}
|
||||
|
||||
function gerarLegendasDaTranscricao() {
|
||||
if (!silenceState.transcriptPath) { document.getElementById("legendasStatus").textContent = "Execute ou carregue uma transcrição primeiro."; return; }
|
||||
try {
|
||||
var transcricao = JSON.parse(fs.readFileSync(silenceState.transcriptPath, "utf-8"));
|
||||
var plano = JSON.parse(document.getElementById("testarAcoesJson").value || "{}");
|
||||
var cortes = (plano.actions || []).filter(function (acao) { return acao.kind === "cut"; }).map(function (acao) { return { start: Number(acao.start), end: Number(acao.end) }; });
|
||||
var configuracao = carregarConfiguracaoDeLegendas();
|
||||
legendasGeradas = legendasGerarBlocos(legendasExtrairPalavras(transcricao), cortes, configuracao);
|
||||
legendasRenderizar();
|
||||
document.getElementById("legendasStatus").textContent = legendasGeradas.length + " legenda(s) gerada(s) para revisão.";
|
||||
} catch (erro) { document.getElementById("legendasStatus").textContent = "Não foi possível gerar: " + erro.message; }
|
||||
}
|
||||
|
||||
function exportarLegendasSrt() {
|
||||
if (!legendasGeradas.length) { document.getElementById("legendasStatus").textContent = "Gere as legendas antes de exportar."; return; }
|
||||
var destino = path.join(silenceState.outputDir || os.tmpdir(), "legendas-geradas.srt");
|
||||
var conteudo = legendasGeradas.map(function (legenda, indice) { return (indice + 1) + "\n" + legendaNumeroParaTempo(legenda.inicio) + " --> " + legendaNumeroParaTempo(legenda.fim) + "\n" + legenda.texto + "\n"; }).join("\n");
|
||||
fs.writeFileSync(destino, conteudo, "utf-8");
|
||||
document.getElementById("legendasStatus").textContent = "SRT exportado em " + destino;
|
||||
}
|
||||
|
||||
function inserirLegendasNaTimeline() {
|
||||
document.getElementById("legendasStatus").textContent = "A lista está pronta. A inserção automática dos MOGRTs será conectada na próxima etapa, após validar o estilo escolhido.";
|
||||
}
|
||||
|
||||
// ---- Teste isolado de ações pela engine Python ----------------------------
|
||||
var TESTAR_ACOES_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/aplicar_plano_de_edicao.py";
|
||||
|
||||
@@ -665,15 +757,19 @@ function copiarTextoParaAreaDeTransferencia(texto) {
|
||||
else showToast("err", "Não foi possível copiar o JSON. Selecione e copie manualmente.");
|
||||
}
|
||||
|
||||
try {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(texto).then(function () {
|
||||
showToast("ok", "JSON resumido copiado para a área de transferência.");
|
||||
}).catch(function () { copiarComPbcopy(texto, copiarComSelecaoDoPainel); });
|
||||
return;
|
||||
}
|
||||
} catch (_) {}
|
||||
copiarComPbcopy(texto, copiarComSelecaoDoPainel);
|
||||
// O CEP antigo pode corromper caracteres UTF-8 pela API de clipboard do Chromium.
|
||||
// O pbcopy recebe bytes UTF-8 diretamente e evita essa conversão intermediária.
|
||||
copiarComPbcopy(texto, function () {
|
||||
try {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(texto).then(function () {
|
||||
showToast("ok", "JSON resumido copiado para a área de transferência.");
|
||||
}).catch(copiarComSelecaoDoPainel);
|
||||
return;
|
||||
}
|
||||
} catch (_) {}
|
||||
copiarComSelecaoDoPainel();
|
||||
});
|
||||
}
|
||||
|
||||
function copiarComPbcopy(texto, fallback) {
|
||||
|
||||
@@ -327,6 +327,9 @@ h1, h2, h3 { margin: 0; }
|
||||
}
|
||||
.card > * + * { margin-top: 10px; }
|
||||
.card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
.form-grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
||||
.legenda-row { display: grid; grid-template-columns: 1fr minmax(0, 5fr) auto; align-items: center; gap: 8px; padding: 10px 0; border-bottom: 1px solid var(--border); }
|
||||
.legenda-timecode { color: var(--muted); font-size: 10px; font-variant-numeric: tabular-nums; }
|
||||
.card-head h2 { font-size: 11.5px; font-weight: 650; }
|
||||
.card-kicker { margin-left: 6px; color: var(--text-muted); font-size: 8.2px; font-weight: 600; letter-spacing: .07em; text-transform: uppercase; }
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const raiz = process.cwd();
|
||||
|
||||
describe("exportação dos tipos de vídeo no CEP", () => {
|
||||
it("declara UTF-8 explicitamente nos scripts do painel", () => {
|
||||
const painel = readFileSync(join(raiz, "cep-plugin", "index.html"), "utf8");
|
||||
|
||||
expect(painel).toContain('<script charset="utf-8" src="CSInterface.js"></script>');
|
||||
expect(painel).toContain('<script charset="utf-8" src="updater.cjs"></script>');
|
||||
expect(painel).toContain('<script charset="utf-8" src="main.js"></script>');
|
||||
});
|
||||
|
||||
it("prioriza a cópia nativa com entrada UTF-8", () => {
|
||||
const lógica = readFileSync(join(raiz, "cep-plugin", "main.js"), "utf8");
|
||||
const início = lógica.indexOf("function copiarTextoParaAreaDeTransferencia(texto)");
|
||||
const fim = lógica.indexOf("\n}\n\nfunction copiarComPbcopy", início);
|
||||
const função = lógica.slice(início, fim);
|
||||
|
||||
expect(função.indexOf("copiarComPbcopy(texto, copiarComSelecaoDoPainel)")).toBeLessThan(
|
||||
função.indexOf("navigator.clipboard.writeText"),
|
||||
);
|
||||
expect(lógica).toContain('copia.stdin.write(texto, "utf8");');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user