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">
|
<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>
|
<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="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>
|
<p class="field-help" id="legendasStatus" aria-live="polite"></p>
|
||||||
</section>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- =============================== AJUSTES =============================== -->
|
<!-- =============================== AJUSTES =============================== -->
|
||||||
@@ -686,8 +696,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="CSInterface.js"></script>
|
<script charset="utf-8" src="CSInterface.js"></script>
|
||||||
<script src="updater.cjs"></script>
|
<script charset="utf-8" src="updater.cjs"></script>
|
||||||
<script src="main.js"></script>
|
<script charset="utf-8" src="main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+105
-9
@@ -292,6 +292,98 @@ function salvarConfiguracaoDeLegendas() {
|
|||||||
document.getElementById("legendasStatus").textContent = "Configuração de legendas salva neste computador.";
|
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 ----------------------------
|
// ---- 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";
|
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.");
|
else showToast("err", "Não foi possível copiar o JSON. Selecione e copie manualmente.");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// O CEP antigo pode corromper caracteres UTF-8 pela API de clipboard do Chromium.
|
||||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
// O pbcopy recebe bytes UTF-8 diretamente e evita essa conversão intermediária.
|
||||||
navigator.clipboard.writeText(texto).then(function () {
|
copiarComPbcopy(texto, function () {
|
||||||
showToast("ok", "JSON resumido copiado para a área de transferência.");
|
try {
|
||||||
}).catch(function () { copiarComPbcopy(texto, copiarComSelecaoDoPainel); });
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
return;
|
navigator.clipboard.writeText(texto).then(function () {
|
||||||
}
|
showToast("ok", "JSON resumido copiado para a área de transferência.");
|
||||||
} catch (_) {}
|
}).catch(copiarComSelecaoDoPainel);
|
||||||
copiarComPbcopy(texto, copiarComSelecaoDoPainel);
|
return;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
copiarComSelecaoDoPainel();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function copiarComPbcopy(texto, fallback) {
|
function copiarComPbcopy(texto, fallback) {
|
||||||
|
|||||||
@@ -327,6 +327,9 @@ h1, h2, h3 { margin: 0; }
|
|||||||
}
|
}
|
||||||
.card > * + * { margin-top: 10px; }
|
.card > * + * { margin-top: 10px; }
|
||||||
.card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
.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-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; }
|
.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