feat: Instalado essentia-tensorflow no venv whisperx-transcricao (

- Instalado essentia-tensorflow no venv whisperx-transcricao (usado pelo painel CEP), corrigindo erro "A classificação musical precisa do suporte TensorFlow do Essentia" no catálogo de trilhas.
- Ligada a classificação de emoção da fala (ProviderDeEmocaoHuggingFace) ao scanner via nova flag `classificar_emocao`, e adicionado emoção/confiança como critério de desempate secundário no zoom da skill editar-por-voz.
- Zoom do caminho Python (aplicar_plano_de_edicao.py) passou a usar adjustment layer numa faixa de vídeo própria, em vez de escalar o clipe inteiro via set_clip_properties — mesma técnica já usada pelo executor .mjs do painel CEP.
- Adicionado campo "Carregar por ID" na aba Editar Vídeo do painel CEP (step 2), para carregar um plano do SQLite direto pelo plano_id quando ele não está associado a nenhum video_id conhecido (ex.: plano de um vídeo que este banco nunca escaneou).
- Trocado o campo de ID numérico por uma lista suspensa (novo listar_planos_de_edicao.py + RepositorioDePlanos.listar_todos_os_planos) que mostra data/hora e a intenção registrada pela skill em cada plano salvo, para escolher sem decorar o id.

Resumo:
- 15 arquivos alterados
- 2 novos
- 13 modificados
- 0 removidos

 13 files changed, 451 insertions(+), 65 deletions(-)

Arquivos:
  - .jhonny/analises.db
  - code/cep-plugin/index.html
  - code/cep-plugin/main.js
  - code/engine/editor/aplicador_de_plano_de_edicao.py
  - code/engine/editor/escrita/escrita_no_editor.py
  - code/engine/executar_scanner.py
  - code/engine/persistencia/repositorio_de_planos.py
  - code/engine/scanner/configuracao_visual.py
  - code/engine/testes/duplos_de_premiere.py
  - code/engine/testes/test_aplicador_de_plano_de_edicao.py
  - code/engine/testes/test_escrita_no_editor.py
  - code/plugins/premiere-pro/skills/editar-por-voz/criterios/01-leitura-do-json.md
  - code/plugins/premiere-pro/skills/editar-por-voz/criterios/05-zoom.md
  - code/engine/listar_planos_de_edicao.py
  - code/engine/testes/test_configuracao_visual_do_scanner.py
This commit is contained in:
João Henrique
2026-09-10 16:04:15 -04:00
parent 2c9bdf2ff0
commit dd341e7d4c
15 changed files with 520 additions and 64 deletions
+126 -1
View File
@@ -269,7 +269,7 @@ function switchTab(name) {
if (name === "models") renderModelList();
if (name === "trilhas") trilhasAoAbrir();
if (name === "projeto") projetoListarSequencias();
if (name === "silence") { syncTranscribeModelSelect(); syncLanguageInfo(); renderizarTiposDeVideoParaEdicao(); }
if (name === "silence") { syncTranscribeModelSelect(); syncLanguageInfo(); renderizarTiposDeVideoParaEdicao(); carregarListaDePlanosEditoriais(); }
if (name === "settings") renderEditorPersonalities();
if (name === "scanner") { renderScannerTranscriptionOptions(); loadScannerProfile(); renderScannerHuggingFaceStatus(); scannerListarAnalisesExistentes(); }
if (name === "refinar") refinarAoAbrir();
@@ -951,6 +951,8 @@ var ANALISE_STATUS_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/co
var ANALISE_PREVIEW_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/consultar_preview_de_falantes.py";
var REGISTRAR_EDICAO_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/registrar_edicao.py";
var CARREGAR_PLANO_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/carregar_plano_de_edicao.py";
var LISTAR_PLANOS_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/listar_planos_de_edicao.py";
var planosEditoriaisListados = [];
var REGISTRAR_PROJETO_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/registrar_projeto_de_edicao.py";
var CONSULTAR_EDICAO_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/consultar_edicao_concluida.py";
var planoEditorialIdCarregado = null;
@@ -3553,6 +3555,129 @@ function carregarPlanoEditorialSalvo() {
);
}
// Popula o seletor da etapa 2 com os planos mais recentes de qualquer vídeo
// (não só o da sequência detectada) — cada opção mostra data/hora e a
// intenção que a skill de edição registrou, para escolher sem decorar o id.
function carregarListaDePlanosEditoriais() {
var seletor = document.getElementById("editorialPlanIdInput");
var detalhe = document.getElementById("editorialPlanIdDetalhe");
if (!seletor) return;
seletor.innerHTML = "";
seletor.appendChild(new Option("Carregando planos…", ""));
if (detalhe) detalhe.textContent = "";
runStreamed(
SCANNER_PYTHON_BIN,
[LISTAR_PLANOS_SCRIPT, "--banco", ANALISES_DB_PATH, "--limite", "50"],
function () {},
function (codigo, ultimoErro, ultimaSaida) {
seletor.innerHTML = "";
if (codigo !== 0) {
seletor.appendChild(new Option("Falha ao listar planos", ""));
silenceLog("Não foi possível listar os planos salvos.", "err");
return;
}
try {
var resposta = JSON.parse(ultimaSaida || "{}");
if (!resposta.ok) throw new Error(resposta.erro || "Resposta inválida do banco.");
planosEditoriaisListados = resposta.planos || [];
if (!planosEditoriaisListados.length) {
seletor.appendChild(new Option("Nenhum plano salvo ainda", ""));
return;
}
seletor.appendChild(new Option("Selecione um plano…", ""));
planosEditoriaisListados.forEach(function (plano) {
var quando = formatarDataHoraDoPlano(plano.criado_em);
var intencaoResumida = plano.intencao
? (plano.intencao.length > 70 ? plano.intencao.slice(0, 70) + "…" : plano.intencao)
: (plano.origem || "sem intenção registrada");
var rotulo = "#" + plano.id + " · " + quando + " · " + intencaoResumida;
seletor.appendChild(new Option(rotulo, String(plano.id)));
});
} catch (erro) {
seletor.innerHTML = "";
seletor.appendChild(new Option("Falha ao listar planos", ""));
silenceLog("Falha ao interpretar a lista de planos: " + erro.message, "err");
}
}
);
}
function formatarDataHoraDoPlano(criadoEm) {
if (!criadoEm) return "data desconhecida";
var data = new Date(criadoEm);
if (isNaN(data.getTime())) return criadoEm;
return data.toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
}
// Ao trocar a seleção, mostra a intenção completa e demais metadados do
// plano escolhido — o rótulo do <option> já vem truncado.
document.addEventListener("DOMContentLoaded", function () {
var seletor = document.getElementById("editorialPlanIdInput");
if (!seletor) return;
seletor.addEventListener("change", function () {
var detalhe = document.getElementById("editorialPlanIdDetalhe");
if (!detalhe) return;
var plano = planosEditoriaisListados.filter(function (p) { return String(p.id) === seletor.value; })[0];
if (!plano) { detalhe.textContent = ""; return; }
detalhe.textContent = "Origem: " + (plano.origem || "—") + " · " + plano.total_de_acoes + " ação(ões) · " +
(plano.modelo_da_ia || "modelo não registrado") +
(plano.intencao ? " — " + plano.intencao : "");
});
});
// Busca um plano pelo id escolhido na lista, sem depender do video_id da
// sequência detectada. Existe para o caso em que o plano foi registrado para
// uma mídia que este banco nunca escaneou (outro projeto/pasta) —
// registrar_plano_de_edicao.py aceita gravar sem video_id quando não acha
// correspondência em edicoes_de_video, e sem video_id o carregamento
// automático por sequência (carregarPlanoEditorialSalvo) nunca encontraria
// esse plano.
function carregarPlanoEditorialPorId() {
var campoId = document.getElementById("editorialPlanIdInput");
var planoId = parseInt(campoId.value, 10);
if (!planoId || planoId <= 0) {
showToast("err", "Selecione um plano na lista antes de carregar.");
return;
}
setBusy("btnLoadPlanById", true);
silenceLog("Lendo o plano " + planoId + " diretamente do banco…");
runStreamed(
SCANNER_PYTHON_BIN,
[CARREGAR_PLANO_SCRIPT, "--banco", ANALISES_DB_PATH, "--plano-id", String(planoId)],
function (linha) { silenceLog(linha); },
function (codigo, ultimoErro, ultimaSaida) {
setBusy("btnLoadPlanById", false);
if (codigo !== 0) {
silenceLog("Não foi possível carregar o plano " + planoId + ".", "err");
showToast("err", ultimoErro || "Plano " + planoId + " não encontrado no banco.");
return;
}
try {
var resposta = JSON.parse(ultimaSaida || "{}");
if (!resposta.ok || !resposta.plano) throw new Error("Resposta inválida do banco.");
// Plano sem video_id (ou de outra sequência) não tem como resolver a
// mídia real pelo cadastro de clipes — usa a mídia detectada na
// sequência ativa só quando o painel já a conhece; senão mantém o
// "source" gravado no plano e deixa a engine (que lê a sequência
// ativa do Premiere no momento da aplicação) resolver.
if (silenceState.mediaPath) resposta.plano.source = silenceState.mediaPath;
planoEditorialIdCarregado = resposta.plano.plano_id;
document.getElementById("editorialPlanJson").value = JSON.stringify(resposta.plano, null, 2);
document.getElementById("planoResumoEdicao").textContent = "Plano " + planoEditorialIdCarregado + " encontrado · " + resposta.plano.actions.length + " ação(ões) · aguardando aprovação.";
document.getElementById("editorialSavedPlanStatus").textContent = "Plano " + planoEditorialIdCarregado + " carregado do banco (por ID).";
document.getElementById("editorialPlanJsonStatus").textContent = "Plano salvo no SQLite; pronto para aprovação e aplicação.";
document.getElementById("btnApplyEditorialActions").disabled = false;
setStep(2, "done", "Plano carregado");
setStep(3, "ready", "Pronto para aplicar");
silenceLog("Plano " + planoEditorialIdCarregado + " carregado diretamente do SQLite (por ID).", "ok");
showToast("ok", "Plano " + planoEditorialIdCarregado + " carregado.");
} catch (erro) {
showToast("err", erro.message);
}
}
);
}
// Não gera decisões de corte: só empacota transcrição + tempos + locutores +
// métricas de voz + configurações de edição num JSON para um agente de IA
// externo analisar. Quem decide os cortes é a IA, não este painel.