feat: aprimorada a edição por voz com validação de frases, margens

- aprimorada a edição por voz com validação de frases, margens seguras e backup único antes da timeline.
- adicionada consulta expansível dos detalhes de cada trilha diretamente do banco

Resumo:
- 23 arquivos alterados
- 5 novos
- 18 modificados
- 0 removidos

 18 files changed, 321 insertions(+), 216 deletions(-)

Arquivos:
  - .gitignore
  - .jhonny/analises.db
  - code/cep-plugin/index.html
  - code/cep-plugin/main.js
  - code/cep-plugin/styles.css
  - code/engine/analisar_trilhas.py
  - code/engine/aplicar_plano_de_edicao.py
  - code/engine/editor/__init__.py
  - code/engine/editor/backup_de_sequencia.py
  - code/engine/persistencia/__init__.py
  - code/engine/testes/test_aplicar_plano_de_edicao.py
  - code/plugins/premiere-pro/skills/edit-video-by-voice/SKILL.md
  - code/plugins/premiere-pro/skills/editar-por-voz/SKILL.md
  - code/plugins/premiere-pro/skills/editar-por-voz/criterios/05-zoom.md
  - code/plugins/premiere-pro/skills/editar-por-voz/criterios/06-texto-corte-marcador.md
  - code/plugins/premiere-pro/skills/editar-por-voz/criterios/08-formato-de-saida.md
  - code/plugins/premiere-pro/skills/editar-por-voz/criterios/10-revisao-humana.md
  - code/plugins/premiere-pro/skills/transcript-to-edit-actions/SKILL.md
  - code/engine/consultar_detalhes_de_trilha.py
  - code/engine/editor/validacao_semantica.py
  - code/engine/persistencia/leitura_semantica_do_plano.py
  - code/engine/testes/test_leitura_semantica_do_plano.py
  - code/engine/testes/test_validador_semantico_de_plano.py
This commit is contained in:
João Henrique
2026-09-10 14:38:42 -04:00
parent b038d9b2e4
commit 9edde2df37
23 changed files with 873 additions and 216 deletions
+2 -2
View File
@@ -197,8 +197,8 @@
<div class="step-body">
<label class="field-label" for="editorialPlanJson">Plano carregado do banco</label>
<p class="field-help" id="editorialSavedPlanStatus">O plano será lido diretamente do banco da edição concluída.</p>
<textarea id="editorialPlanJson" class="editorial-plan-json" rows="10" spellcheck="false" placeholder='Cole aqui o JSON com source e actions.'></textarea>
<p class="field-help" id="editorialPlanJsonStatus">Cole o plano recebido da IA. O painel valida o formato antes de executar.</p>
<textarea id="editorialPlanJson" class="editorial-plan-json" rows="10" spellcheck="false" placeholder='Carregue do banco o JSON com source e actions.'></textarea>
<p class="field-help" id="editorialPlanJsonStatus">Carregue o plano salvo no SQLite. O painel valida frases, palavras e margens antes de executar.</p>
<label class="field-label" for="editorialPlanPath">Arquivo do plano (JSON devolvido pela IA)</label>
<div class="file-field">
<input type="text" id="editorialPlanPath" spellcheck="false" autocomplete="off" placeholder="/caminho/para/plano.json">
+89 -51
View File
@@ -1149,6 +1149,7 @@ var scannerProcesso = null;
var scannerCancelSolicitado = false;
var SCANNER_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/executar_scanner.py";
var TRILHAS_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/analisar_trilhas.py";
var TRILHAS_CONSULTAR_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/consultar_detalhes_de_trilha.py";
var TRILHAS_BANCO_PATH = "";
var TRILHAS_MODELOS_PATH = "";
var TRILHAS_CONFIG_KEY = "mcp_trilhas_config_v1";
@@ -2959,17 +2960,49 @@ function trilhasRenderizarLista(nomes) {
var item = document.createElement("div");
item.className = "trilha-item";
item.id = "trilha-item-" + indice;
item.setAttribute("role", "button");
item.setAttribute("tabindex", "0");
item.setAttribute("aria-expanded", "false");
item.setAttribute("data-caminho", (typeof registro === "string" ? path.join(document.getElementById("trilhasPasta").value.trim(), nome) : registro.caminho) || "");
item.setAttribute("data-state", status === "concluida" ? "concluida" : status === "erro" ? "erro" : "aguardando");
item.innerHTML = '<span class="trilha-check" aria-hidden="true"></span><div class="trilha-info"><strong></strong><span>Aguardando</span><div class="progress-track"><div class="progress-fill" style="width:0%"></div></div></div>';
item.innerHTML = '<span class="trilha-check" aria-hidden="true"></span><div class="trilha-info"><strong></strong><span>Aguardando</span><div class="progress-track"><div class="progress-fill" style="width:0%"></div></div><div class="trilha-detalhes" hidden></div></div>';
item.querySelector("strong").textContent = nome;
item.querySelector(".trilha-info > span").textContent = status === "concluida" ? "Analisada" : status === "erro" ? "Erro na análise anterior" : "Aguardando análise";
if (status === "concluida") item.querySelector(".progress-fill").style.width = "100%";
item.onclick = function () { trilhasAlternarDetalhes(item); };
item.onkeydown = function (evento) { if (evento.key === "Enter" || evento.key === " ") { evento.preventDefault(); trilhasAlternarDetalhes(item); } };
lista.appendChild(item);
});
document.getElementById("trilhasContagem").textContent = (nomes || []).length + " trilha(s)";
document.getElementById("trilhasProgressoCard").hidden = !(nomes || []).length;
}
function trilhasAlternarDetalhes(item) {
var detalhes = item.querySelector(".trilha-detalhes");
var aberto = !detalhes.hidden;
detalhes.hidden = aberto;
item.setAttribute("aria-expanded", String(!aberto));
if (!aberto && !detalhes.getAttribute("data-carregado")) trilhasCarregarDetalhes(item, detalhes);
}
function trilhasCarregarDetalhes(item, detalhes) {
detalhes.removeAttribute("data-carregado");
detalhes.innerHTML = "Consultando análise salva…";
var resposta = childProcess.spawnSync(SCANNER_PYTHON_BIN, [TRILHAS_CONSULTAR_SCRIPT, item.getAttribute("data-caminho"), "--banco", TRILHAS_BANCO_PATH], { encoding: "utf8" });
if (resposta.error || resposta.status !== 0) { detalhes.textContent = "Não foi possível consultar os detalhes desta trilha."; return; }
try {
var dados = JSON.parse(resposta.stdout.trim());
if (!dados.encontrada) { detalhes.textContent = dados.mensagem || "Ainda não há análise salva."; return; }
var etiquetas = dados.etiquetas || [];
var generos = etiquetas.filter(function (etiqueta) { return etiqueta.tipo === "genero"; });
var humores = etiquetas.filter(function (etiqueta) { return etiqueta.tipo === "humor"; });
function formatar(valor, casas) { return valor === null || valor === undefined ? "—" : Number(valor).toFixed(casas || 2); }
function listaEtiquetas(itens) { return itens.length ? itens.map(function (item) { return item.etiqueta + " (" + formatar(Number(item.confianca) * 100, 1) + "%)"; }).join(" · ") : "Não classificado"; }
detalhes.innerHTML = '<div class="trilha-detalhes-grid"><div><small>Duração</small><strong>' + formatar(dados.duracao_em_segundos, 1) + ' s</strong></div><div><small>BPM</small><strong>' + formatar(dados.batidas_por_minuto, 1) + '</strong></div><div><small>Tonalidade</small><strong>' + (dados.tonalidade || "—") + ' ' + (dados.modo || "") + '</strong></div><div><small>Intensidade</small><strong>' + formatar(dados.intensidade, 2) + '</strong></div><div><small>Dançabilidade</small><strong>' + formatar(dados.dancabilidade, 2) + '</strong></div></div><div class="trilha-etiquetas"><div><small>Estilos</small><p>' + listaEtiquetas(generos) + '</p></div><div><small>Humores</small><p>' + listaEtiquetas(humores) + '</p></div></div><small class="trilha-fonte">Análise salva por ' + (dados.provedor || "Essentia") + (dados.modelo ? " · " + dados.modelo : "") + '</small>';
detalhes.setAttribute("data-carregado", "true");
} catch (_) { detalhes.textContent = "A resposta da análise está inválida."; }
}
function trilhasAtualizarEvento(evento) {
if (evento.evento === "catalogo_atualizado") { trilhasRenderizarLista(evento.trilhas || []); return; }
if (evento.evento === "lote_iniciado") { trilhasRenderizarLista(evento.trilhas || evento.nomes || []); return; }
@@ -3610,6 +3643,15 @@ function lerPlanoEditorialDaTela() {
return plano;
}
function invalidarVinculoDoPlanoEditorial() {
planoEditorialIdCarregado = null;
var status = document.getElementById("editorialPlanJsonStatus");
if (status) {
status.textContent = "Plano alterado fora do banco. Registre e carregue a nova versão antes de aplicar cortes.";
status.className = "field-help err";
}
}
function applyEditorialActions() {
if (!silenceState.sequenceName) {
silenceLog("Detecte o clipe/sequência primeiro (passo 1).", "err");
@@ -3632,57 +3674,46 @@ function applyEditorialActions() {
if (typeof window.confirm === "function" && !window.confirm(summary)) return;
setBusy("btnApplyEditorialActions", true);
setStep(3, "running", "Aplicando…");
taskStart("Aplicando o plano de edição", "Criando backup da sequência");
silenceLog("Criando backup da sequência…");
cs.evalScript(
"(function(){try{app.project.activeSequence.clone();return 'ok';}catch(e){return 'error:'+String(e);}}())",
function (raw) {
if (String(raw).indexOf("ok") !== 0) {
silenceLog("Não foi possível criar o backup automático (" + raw + "). Aplicando mesmo assim.", "err");
} else {
silenceLog("Backup criado.", "ok");
}
var caminhoDoPlano = path.join(silenceState.outputDir || os.tmpdir(), "plano-editorial-" + Date.now() + ".json");
try { fs.writeFileSync(caminhoDoPlano, JSON.stringify(plan, null, 2), "utf-8"); } catch (erroEscrita) {
setBusy("btnApplyEditorialActions", false);
setStep(3, "error", "Falhou");
taskEnd(false, "Não foi possível salvar o plano.", erroEscrita.message);
setStep(3, "running", "Validando…");
taskStart("Aplicando o plano de edição", "Validando frases e bordas de corte");
var caminhoDoPlano = path.join(silenceState.outputDir || os.tmpdir(), "plano-editorial-" + Date.now() + ".json");
try { fs.writeFileSync(caminhoDoPlano, JSON.stringify(plan, null, 2), "utf-8"); } catch (erroEscrita) {
setBusy("btnApplyEditorialActions", false);
setStep(3, "error", "Falhou");
taskEnd(false, "Não foi possível salvar o plano.", erroEscrita.message);
return;
}
var processoAplicacao = null;
silenceLog("Validando o plano antes de alterar a timeline…");
taskStart("Aplicando o plano de edição", "Validando e executando pela engine Python", null, function () {
if (processoAplicacao) processoAplicacao.kill("SIGTERM");
});
processoAplicacao = runStreamed(
SCANNER_PYTHON_BIN,
[TESTAR_ACOES_ENGINE_SCRIPT, caminhoDoPlano, "--exigir-validacao-semantica"].concat(
planoEditorialIdCarregado === null ? [] : ["--plano-id", String(planoEditorialIdCarregado), "--banco", ANALISES_DB_PATH]
),
function (line) { silenceLog(line); },
function (code, lastErr, lastOutput) {
processoAplicacao = null;
setBusy("btnApplyEditorialActions", false);
if (code !== 0) {
var detalheDaAplicacao = "";
try {
var resultadoDaAplicacao = JSON.parse(lastOutput || "{}");
var falhasDaAplicacao = (resultadoDaAplicacao.acoes || []).filter(function (acao) { return acao.sucesso === false; });
if (falhasDaAplicacao.length) detalheDaAplicacao = falhasDaAplicacao.map(function (acao) { return acao.detalhe; }).join(" | ");
if (!detalheDaAplicacao && resultadoDaAplicacao.erro) detalheDaAplicacao = resultadoDaAplicacao.erro;
} catch (_) {}
setStep(3, "error", "Bloqueado");
var mensagemDaFalha = detalheDaAplicacao || lastErr || "A engine não retornou detalhes.";
taskEnd(false, "O plano não foi aplicado (código " + code + ").", mensagemDaFalha);
silenceLog("Plano não aplicado (código " + code + "): " + mensagemDaFalha, "err");
return;
}
var processoAplicacao = null;
silenceLog("Aplicando plano pela engine Python…");
taskStart("Aplicando o plano de edição", "Executando pela engine Python", null, function () {
if (processoAplicacao) processoAplicacao.kill("SIGTERM");
});
processoAplicacao = runStreamed(
SCANNER_PYTHON_BIN,
[TESTAR_ACOES_ENGINE_SCRIPT, caminhoDoPlano].concat(
planoEditorialIdCarregado === null ? [] : ["--plano-id", String(planoEditorialIdCarregado), "--banco", ANALISES_DB_PATH]
),
function (line) { silenceLog(line); },
function (code, lastErr, lastOutput) {
processoAplicacao = null;
setBusy("btnApplyEditorialActions", false);
if (code !== 0) {
var detalheDaAplicacao = "";
try {
var resultadoDaAplicacao = JSON.parse(lastOutput || "{}");
var falhasDaAplicacao = (resultadoDaAplicacao.acoes || []).filter(function (acao) { return acao.sucesso === false; });
if (falhasDaAplicacao.length) detalheDaAplicacao = falhasDaAplicacao.map(function (acao) { return acao.detalhe; }).join(" | ");
} catch (_) {}
setStep(3, "error", "Falhou");
var mensagemDaFalha = detalheDaAplicacao || lastErr || "A engine não retornou detalhes.";
taskEnd(false, "Falha ao aplicar o plano (código " + code + ").", mensagemDaFalha);
silenceLog("Falha ao aplicar o plano (código " + code + "): " + mensagemDaFalha, "err");
return;
}
setStep(3, "done", "Aplicado");
taskEnd(true, numActions + " ação(ões) aplicadas na timeline!");
silenceLog("Plano de edição aplicado com sucesso!", "ok");
}
);
setStep(3, "done", "Aplicado");
taskEnd(true, numActions + " ação(ões) aplicadas na timeline!");
silenceLog("Plano de edição aplicado com sucesso!", "ok");
}
);
}
@@ -3700,7 +3731,10 @@ function pickEditorialPlanFile() {
}
var result = api.showOpenDialog(false, false, "Escolha o plano de edição (JSON)", "", ["json"]);
var chosen = result && result.data && result.data.length ? result.data[0] : "";
if (chosen) input.value = toFsPath(chosen);
if (chosen) {
input.value = toFsPath(chosen);
invalidarVinculoDoPlanoEditorial();
}
} catch (e) {
input.focus();
showToast("err", "Não foi possível abrir o seletor: " + e.message);
@@ -4328,6 +4362,10 @@ function downloadModel(model, btn, approxMB) {
renderEditorPersonalities();
restaurarConfiguracaoDeLegendas();
renderizarTiposDeVideoParaEdicao();
var campoDoPlanoEditorial = document.getElementById("editorialPlanJson");
if (campoDoPlanoEditorial) {
campoDoPlanoEditorial.addEventListener("input", invalidarVinculoDoPlanoEditorial);
}
// A aba Editar vídeo começa com a análise já produzida pelo Scanner.
setStep(1, "locked", "Scanner");
+9
View File
@@ -271,6 +271,15 @@ h1, h2, h3 { margin: 0; }
.trilha-item[data-state="erro"] .trilha-check { border-color: var(--red); }
.trilha-item[data-state="erro"] .trilha-info > span { color: #ffb0b0; }
.trilha-item[data-state="erro"] .progress-fill { background: var(--red); }
.trilha-item { cursor: pointer; }
.trilha-item:focus-visible { outline: 2px solid var(--violet); outline-offset: 2px; }
.trilha-detalhes { margin-top: 9px; padding-top: 9px; border-top: 1px solid var(--border); color: var(--text-secondary); cursor: default; }
.trilha-detalhes-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 7px; }
.trilha-detalhes small { display: block; color: var(--text-muted); font-size: 8px; text-transform: uppercase; letter-spacing: .05em; }
.trilha-detalhes strong { display: block; margin-top: 2px; color: var(--text); font-size: 10px; }
.trilha-etiquetas { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 9px; }
.trilha-etiquetas p { margin: 3px 0 0; color: var(--text); font-size: 9.5px; line-height: 1.45; }
.trilha-fonte { margin-top: 8px; }
.task-foot { display: flex; align-items: baseline; gap: 8px; margin-top: 7px; }
.task-stage { flex: 1; min-width: 0; overflow: hidden; color: var(--text-secondary); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }