feat: corrigida a reprodução do trecho de áudio do falante na tela
- corrigida a reprodução do trecho de áudio do falante na tela Refinar usando os tempos da origem do WAV. - alterado o passo de geração de JSON para concluir a edição e salvar a configuração editorial no banco. Resumo: - 9 arquivos alterados - 3 novos - 6 modificados - 0 removidos 6 files changed, 136 insertions(+), 109 deletions(-) Arquivos: - .jhonny/analises.db - code/cep-plugin/index.html - code/cep-plugin/main.js - code/engine/persistencia/__init__.py - code/engine/persistencia/esquema.py - code/engine/testes/test_persistencia_sqlite.py - code/engine/persistencia/repositorio_de_edicoes.py - code/engine/registrar_edicao.py - code/tests/cep-refinar-preview.test.ts
This commit is contained in:
Binary file not shown.
@@ -91,7 +91,7 @@
|
|||||||
<!-- ============================= EDITAR VÍDEO ============================= -->
|
<!-- ============================= EDITAR VÍDEO ============================= -->
|
||||||
<div class="tab-panel" id="tabSilence" role="tabpanel" aria-labelledby="tabBtnSilence" hidden>
|
<div class="tab-panel" id="tabSilence" role="tabpanel" aria-labelledby="tabBtnSilence" hidden>
|
||||||
|
|
||||||
<p class="tab-intro">A edição usa a análise já concluída pelo Scanner. Revise os dados, gere o plano JSON e aplique-o na timeline.</p>
|
<p class="tab-intro">A edição usa a análise já concluída pelo Scanner. Selecione o tipo de vídeo e conclua para salvar a configuração no banco.</p>
|
||||||
|
|
||||||
<section class="card scanner-analysis-card" id="scannerAnalysisCard">
|
<section class="card scanner-analysis-card" id="scannerAnalysisCard">
|
||||||
<div class="card-head"><h2>Análise do Scanner</h2><span class="step-chip" id="scannerAnalysisStatus">Não carregada</span></div>
|
<div class="card-head"><h2>Análise do Scanner</h2><span class="step-chip" id="scannerAnalysisStatus">Não carregada</span></div>
|
||||||
@@ -236,13 +236,13 @@
|
|||||||
<div class="step-head">
|
<div class="step-head">
|
||||||
<span class="step-number" aria-hidden="true">4</span>
|
<span class="step-number" aria-hidden="true">4</span>
|
||||||
<div class="step-title">
|
<div class="step-title">
|
||||||
<h2>Gerar arquivo JSON</h2>
|
<h2>Concluir edição</h2>
|
||||||
<p>Reúne transcrição, tempos, locutores, métricas de voz e suas configurações de edição. Nada é cortado — o arquivo vai para um agente de IA externo analisar.</p>
|
<p>Salva no banco a análise carregada, o tipo de vídeo e as configurações editoriais escolhidas. O JSON para a IA poderá ser gerado depois, quando necessário.</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="step-chip" id="step4Chip">Bloqueado</span>
|
<span class="step-chip" id="step4Chip">Bloqueado</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="step-body">
|
<div class="step-body">
|
||||||
<button id="btnBuildPlan" class="button button-primary" onclick="silenceGenerateJson()" type="button" disabled>Gerar arquivo JSON</button>
|
<button id="btnBuildPlan" class="button button-primary" onclick="silenceConcluirEdicao()" type="button" disabled>Concluir edição</button>
|
||||||
<dl class="kv" id="silenceJsonCard" hidden>
|
<dl class="kv" id="silenceJsonCard" hidden>
|
||||||
<div><dt>Trechos</dt><dd id="jsonSegmentCount">—</dd></div>
|
<div><dt>Trechos</dt><dd id="jsonSegmentCount">—</dd></div>
|
||||||
<div><dt>Arquivo</dt><dd class="kv-path" id="jsonOutputPath">—</dd></div>
|
<div><dt>Arquivo</dt><dd class="kv-path" id="jsonOutputPath">—</dd></div>
|
||||||
|
|||||||
+88
-104
@@ -945,6 +945,7 @@ var RETAKES_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/co
|
|||||||
var SCANNER_PYTHON_BIN = "/Volumes/Merongo/SISTEMAS/venvs/whisperx-transcricao/bin/python";
|
var SCANNER_PYTHON_BIN = "/Volumes/Merongo/SISTEMAS/venvs/whisperx-transcricao/bin/python";
|
||||||
var ANALISE_STATUS_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/consultar_status_da_analise.py";
|
var ANALISE_STATUS_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/consultar_status_da_analise.py";
|
||||||
var ANALISE_PREVIEW_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/consultar_preview_de_falantes.py";
|
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 retakesCasoAtual = null;
|
var retakesCasoAtual = null;
|
||||||
var retakesGrupos = [];
|
var retakesGrupos = [];
|
||||||
var retakesProcesso = null;
|
var retakesProcesso = null;
|
||||||
@@ -2346,8 +2347,23 @@ function silenceLoadScannerAnalysis() {
|
|||||||
atualizarCheckDaAnalise("scannerCheckTranscription", temTranscricao, temTranscricao ? "Carregado" : "Não encontrada");
|
atualizarCheckDaAnalise("scannerCheckTranscription", temTranscricao, temTranscricao ? "Carregado" : "Não encontrada");
|
||||||
atualizarCheckDaAnalise("scannerCheckDiarization", temDiarizacao, temDiarizacao ? "Carregado" : "Não realizada");
|
atualizarCheckDaAnalise("scannerCheckDiarization", temDiarizacao, temDiarizacao ? "Carregado" : "Não realizada");
|
||||||
atualizarCheckDaAnalise("scannerCheckVoiceFeatures", temMetricasDeVoz, temMetricasDeVoz ? "Carregado" : "Não realizada");
|
atualizarCheckDaAnalise("scannerCheckVoiceFeatures", temMetricasDeVoz, temMetricasDeVoz ? "Carregado" : "Não realizada");
|
||||||
var turnos = transcricao.segments.filter(function (segmento) { return segmento.falante; }).map(function (segmento) {
|
var intervalosDePreview = silenceConsultarPreviewDeFalantes(silenceState.sequenceId);
|
||||||
return { speaker: segmento.falante, start: segmento.start, end: segmento.end };
|
var turnos = intervalosDePreview && intervalosDePreview.length
|
||||||
|
? intervalosDePreview.filter(function (intervalo) {
|
||||||
|
return intervalo.falante && Number(intervalo.fim) > Number(intervalo.inicio);
|
||||||
|
}).map(function (intervalo) {
|
||||||
|
return {
|
||||||
|
speaker: intervalo.falante,
|
||||||
|
start: Number(intervalo.inicio),
|
||||||
|
end: Number(intervalo.fim),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: transcricao.segments.filter(function (segmento) { return segmento.falante; }).map(function (segmento) {
|
||||||
|
return {
|
||||||
|
speaker: segmento.falante,
|
||||||
|
start: Number(segmento.start),
|
||||||
|
end: Number(segmento.end),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
renderSpeakerConfiguration(falantes, turnos);
|
renderSpeakerConfiguration(falantes, turnos);
|
||||||
atualizarTelaRefinar(falantes, audioDoBanco);
|
atualizarTelaRefinar(falantes, audioDoBanco);
|
||||||
@@ -2418,9 +2434,6 @@ function atualizarTelaRefinar(falantes, audioDoBanco) {
|
|||||||
|
|
||||||
var speakerAudioPlayer = null;
|
var speakerAudioPlayer = null;
|
||||||
var speakerAudioQueue = null;
|
var speakerAudioQueue = null;
|
||||||
var speakerTimelineQueue = null;
|
|
||||||
var speakerTimelineTimer = null;
|
|
||||||
var speakerTimelineToken = 0;
|
|
||||||
|
|
||||||
// ---- Central transcript cache ----
|
// ---- Central transcript cache ----
|
||||||
// Evita rodar o Whisper de novo para um vídeo já transcrito antes: indexa
|
// Evita rodar o Whisper de novo para um vídeo já transcrito antes: indexa
|
||||||
@@ -3090,105 +3103,6 @@ function stopSpeakerAudio() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSpeakerTimeline(speakerId, button) {
|
|
||||||
if (speakerTimelineQueue && speakerTimelineQueue.speakerId === speakerId) {
|
|
||||||
stopSpeakerAudio();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
stopSpeakerAudio();
|
|
||||||
var ranges = silenceState.speakerTurns.filter(function (turn) {
|
|
||||||
return turn.speaker === speakerId && Number(turn.end) > Number(turn.start);
|
|
||||||
}).sort(function (a, b) { return Number(a.start) - Number(b.start); });
|
|
||||||
if (!ranges.length) {
|
|
||||||
showToast("err", "Não há intervalos de vídeo disponíveis para " + speakerId + ".");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var preview = document.getElementById("refinarSpeakerPreview");
|
|
||||||
var info = document.getElementById("refinarSpeakerPreviewInfo");
|
|
||||||
if (!preview) {
|
|
||||||
showToast("err", "Preview de vídeo indisponível nesta versão do painel.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
speakerTimelineQueue = {
|
|
||||||
speakerId: speakerId,
|
|
||||||
ranges: ranges,
|
|
||||||
index: 0,
|
|
||||||
button: button,
|
|
||||||
inicioPendente: false,
|
|
||||||
};
|
|
||||||
preview.hidden = false;
|
|
||||||
if (info) info.textContent = "Reproduzindo os trechos de " + speakerId + ".";
|
|
||||||
button.textContent = "■ timeline";
|
|
||||||
button.classList.add("is-playing");
|
|
||||||
iniciarPreviewNaTimeline(speakerTimelineToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
function iniciarPreviewNaTimeline(token) {
|
|
||||||
if (!speakerTimelineQueue || token !== speakerTimelineToken) return;
|
|
||||||
var indiceDoTrecho = speakerTimelineQueue.index;
|
|
||||||
if (speakerTimelineQueue.inicioPendente) return;
|
|
||||||
var range = speakerTimelineQueue.ranges[indiceDoTrecho];
|
|
||||||
if (!range) { stopSpeakerAudio(); return; }
|
|
||||||
var inicio = Math.max(0, Number(range.start) - 0.15);
|
|
||||||
var inicioJson = JSON.stringify(inicio);
|
|
||||||
speakerTimelineQueue.inicioPendente = true;
|
|
||||||
cs.evalScript(
|
|
||||||
"(function(){try{" +
|
|
||||||
"var seq=app.project.activeSequence;" +
|
|
||||||
"if(!seq)return JSON.stringify({ok:false,error:'Nenhuma sequência ativa.'});" +
|
|
||||||
"if(String(seq.sequenceID)!==" + JSON.stringify(String(silenceState.sequenceId)) + ")return JSON.stringify({ok:false,error:'A sequência da análise não está ativa.'});" +
|
|
||||||
"seq.setPlayerPosition(String(Math.round(" + inicioJson + "*254016000000)));" +
|
|
||||||
"app.enableQE();qe.startPlayback();" +
|
|
||||||
"return JSON.stringify({ok:true});" +
|
|
||||||
"}catch(e){return JSON.stringify({ok:false,error:String(e)});}}())",
|
|
||||||
function (raw) {
|
|
||||||
if (!speakerTimelineQueue || token !== speakerTimelineToken || speakerTimelineQueue.index !== indiceDoTrecho) return;
|
|
||||||
speakerTimelineQueue.inicioPendente = false;
|
|
||||||
var resultado;
|
|
||||||
try { resultado = JSON.parse(raw); } catch (_) { resultado = { ok: false, error: "Resposta inválida do Premiere." }; }
|
|
||||||
if (!resultado.ok) {
|
|
||||||
stopSpeakerAudio();
|
|
||||||
showToast("err", resultado.error || "Não foi possível iniciar o preview na timeline.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
agendarFimDoTrechoNaTimeline(token, indiceDoTrecho, inicio, Number(range.end));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function agendarFimDoTrechoNaTimeline(token, indiceDoTrecho, inicio, fim) {
|
|
||||||
if (!speakerTimelineQueue || token !== speakerTimelineToken || speakerTimelineQueue.index !== indiceDoTrecho) return;
|
|
||||||
var duracaoEmMilissegundos = Math.max(250, (fim - inicio) * 1000);
|
|
||||||
speakerTimelineTimer = setTimeout(function () {
|
|
||||||
speakerTimelineTimer = null;
|
|
||||||
if (!speakerTimelineQueue || token !== speakerTimelineToken || speakerTimelineQueue.index !== indiceDoTrecho) return;
|
|
||||||
pausarEAvancarPreviewNaTimeline(token);
|
|
||||||
}, duracaoEmMilissegundos);
|
|
||||||
}
|
|
||||||
|
|
||||||
function pausarEAvancarPreviewNaTimeline(token) {
|
|
||||||
if (!speakerTimelineQueue || token !== speakerTimelineToken) return;
|
|
||||||
cs.evalScript(
|
|
||||||
"(function(){try{if(typeof qe!==\"undefined\"&&qe.stopPlayback)qe.stopPlayback();return JSON.stringify({ok:true});}catch(e){return JSON.stringify({ok:false,error:String(e)});}}())",
|
|
||||||
function (raw) {
|
|
||||||
if (!speakerTimelineQueue || token !== speakerTimelineToken) return;
|
|
||||||
var resultado;
|
|
||||||
try { resultado = JSON.parse(raw); } catch (_) { resultado = { ok: false, error: "Resposta inválida do Premiere." }; }
|
|
||||||
if (!resultado.ok) {
|
|
||||||
stopSpeakerAudio();
|
|
||||||
showToast("err", resultado.error || "Não foi possível pausar o preview na timeline.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
speakerTimelineQueue.index += 1;
|
|
||||||
if (speakerTimelineQueue.index >= speakerTimelineQueue.ranges.length) {
|
|
||||||
stopSpeakerAudio();
|
|
||||||
} else {
|
|
||||||
iniciarPreviewNaTimeline(token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleSpeakerAudio(speakerId, button) {
|
function toggleSpeakerAudio(speakerId, button) {
|
||||||
if (speakerAudioPlayer && speakerAudioQueue && speakerAudioQueue.speakerId === speakerId) {
|
if (speakerAudioPlayer && speakerAudioQueue && speakerAudioQueue.speakerId === speakerId) {
|
||||||
stopSpeakerAudio();
|
stopSpeakerAudio();
|
||||||
@@ -3328,6 +3242,76 @@ function silenceComputeVoiceFeatures() {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Registra a decisão editorial sem duplicar a análise normalizada do Scanner.
|
||||||
|
// O JSON continua disponível para o fluxo da IA, mas não é pré-requisito para
|
||||||
|
// concluir a edição no painel.
|
||||||
|
function silenceConcluirEdicao() {
|
||||||
|
if (!silenceState.sequenceId || !silenceState.sequenceName) {
|
||||||
|
showToast("err", "Carregue uma análise do Scanner antes de concluir.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!silenceState.tipoVideo) {
|
||||||
|
showToast("err", "Selecione o tipo de vídeo antes de concluir a edição.");
|
||||||
|
setStep(1, "ready", "Selecione um tipo");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pedido = {
|
||||||
|
video_id: String(silenceState.sequenceId),
|
||||||
|
sequencia: silenceState.sequenceName,
|
||||||
|
origem: silenceState.mediaPath,
|
||||||
|
tipo_de_video: silenceState.tipoVideo.id,
|
||||||
|
configuracao: {
|
||||||
|
nome_do_tipo: silenceState.tipoVideo.nome,
|
||||||
|
instrucoes_do_tipo: tipoVideoResumoParaJson(silenceState.tipoVideo),
|
||||||
|
configuracoes_de_fala: speakerConfigurationForJson(),
|
||||||
|
configuracoes_de_edicao: {
|
||||||
|
remocao_de_silencio: {
|
||||||
|
habilitada: document.getElementById("silenceRemovalEnabled").checked,
|
||||||
|
duracao_minima_segundos: Number(document.getElementById("silenceRemovalMinDuration").value) || 0.8,
|
||||||
|
},
|
||||||
|
legendas: carregarConfiguracaoDeLegendas(),
|
||||||
|
},
|
||||||
|
transcricao: {
|
||||||
|
caminho: silenceState.transcriptPath,
|
||||||
|
origem: "Scanner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
var caminhoDoPedido = path.join(os.tmpdir(), "pedido-concluir-edicao-" + Date.now() + ".json");
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(caminhoDoPedido, JSON.stringify(pedido, null, 2), "utf-8");
|
||||||
|
} catch (erroEscrita) {
|
||||||
|
showToast("err", "Não foi possível preparar a conclusão: " + erroEscrita.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBusy("btnBuildPlan", true);
|
||||||
|
setStep(4, "running", "Salvando…");
|
||||||
|
taskStart("Concluindo a edição", "Salvando a configuração no banco");
|
||||||
|
silenceLog("Salvando o tipo de vídeo e as configurações editoriais no banco…");
|
||||||
|
runStreamed(
|
||||||
|
SCANNER_PYTHON_BIN,
|
||||||
|
[REGISTRAR_EDICAO_SCRIPT, caminhoDoPedido, "--banco", ANALISES_DB_PATH],
|
||||||
|
function (linha) { silenceLog(linha); },
|
||||||
|
function (codigo, ultimoErro) {
|
||||||
|
setBusy("btnBuildPlan", false);
|
||||||
|
if (codigo !== 0) {
|
||||||
|
setStep(4, "error", "Falhou");
|
||||||
|
taskEnd(false, "Não foi possível concluir a edição.", ultimoErro);
|
||||||
|
silenceLog("Falha ao salvar a configuração editorial.", "err");
|
||||||
|
showToast("err", ultimoErro || "Não foi possível salvar a edição no banco.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStep(4, "done", "Edição concluída");
|
||||||
|
setStep(5, "locked", "Aguardando plano da IA");
|
||||||
|
taskEnd(true, "Edição concluída e salva no banco.");
|
||||||
|
silenceLog("Edição concluída e salva no banco de análises.", "ok");
|
||||||
|
showToast("ok", "Edição concluída e salva no banco.");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Não gera decisões de corte: só empacota transcrição + tempos + locutores +
|
// 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
|
// 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.
|
// externo analisar. Quem decide os cortes é a IA, não este painel.
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from .repositorio_de_planos import (
|
|||||||
RepositorioDePlanos,
|
RepositorioDePlanos,
|
||||||
plano_de_dict,
|
plano_de_dict,
|
||||||
)
|
)
|
||||||
|
from .repositorio_de_edicoes import EdicaoDeVideo, ErroDeEdicao, RepositorioDeEdicoes, edicao_de_dict
|
||||||
from .repositorio_de_analises_sqlite import RepositorioDeAnalisesSQLite
|
from .repositorio_de_analises_sqlite import RepositorioDeAnalisesSQLite
|
||||||
from .repositorio_de_retakes_sqlite import RepositorioDeRetakesSQLite
|
from .repositorio_de_retakes_sqlite import RepositorioDeRetakesSQLite
|
||||||
from .repositorio_de_timeline_sqlite import RepositorioDeTimelineSQLite
|
from .repositorio_de_timeline_sqlite import RepositorioDeTimelineSQLite
|
||||||
@@ -37,6 +38,8 @@ __all__ = [
|
|||||||
"ErroDeBusca",
|
"ErroDeBusca",
|
||||||
"ErroDeIngestaoDeVoz",
|
"ErroDeIngestaoDeVoz",
|
||||||
"ErroDePlano",
|
"ErroDePlano",
|
||||||
|
"ErroDeEdicao",
|
||||||
|
"EdicaoDeVideo",
|
||||||
"IndexadorSemantico",
|
"IndexadorSemantico",
|
||||||
"IngestorDeVozNoBanco",
|
"IngestorDeVozNoBanco",
|
||||||
"LeitorDeDadosParaIA",
|
"LeitorDeDadosParaIA",
|
||||||
@@ -44,10 +47,12 @@ __all__ = [
|
|||||||
"PlanoDeEdicao",
|
"PlanoDeEdicao",
|
||||||
"RepositorioDeEnunciados",
|
"RepositorioDeEnunciados",
|
||||||
"RepositorioDePlanos",
|
"RepositorioDePlanos",
|
||||||
|
"RepositorioDeEdicoes",
|
||||||
"ResultadoDeBusca",
|
"ResultadoDeBusca",
|
||||||
"abrir_banco",
|
"abrir_banco",
|
||||||
"criar_esquema",
|
"criar_esquema",
|
||||||
"plano_de_dict",
|
"plano_de_dict",
|
||||||
|
"edicao_de_dict",
|
||||||
"reconstruir_indice_de_busca",
|
"reconstruir_indice_de_busca",
|
||||||
"RepositorioDeAnalisesSQLite",
|
"RepositorioDeAnalisesSQLite",
|
||||||
"RepositorioDeRetakesSQLite",
|
"RepositorioDeRetakesSQLite",
|
||||||
|
|||||||
@@ -285,6 +285,20 @@ CREATE TABLE IF NOT EXISTS aplicacoes_do_plano (
|
|||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_aplicacoes_plano ON aplicacoes_do_plano(plano_id);
|
CREATE INDEX IF NOT EXISTS idx_aplicacoes_plano ON aplicacoes_do_plano(plano_id);
|
||||||
|
|
||||||
|
-- Configuração editorial escolhida no painel, antes de existir um plano da IA.
|
||||||
|
-- A análise continua normalizada nas tabelas do Scanner; esta tabela guarda
|
||||||
|
-- somente a decisão do usuário e suas opções para a edição.
|
||||||
|
CREATE TABLE IF NOT EXISTS edicoes_de_video (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
video_id TEXT NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
|
||||||
|
sequencia TEXT NOT NULL,
|
||||||
|
origem TEXT,
|
||||||
|
tipo_de_video TEXT NOT NULL,
|
||||||
|
configuracao TEXT NOT NULL,
|
||||||
|
concluida_em TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_edicoes_video ON edicoes_de_video(video_id, concluida_em);
|
||||||
|
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
-- Leituras achatadas
|
-- Leituras achatadas
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
"""Persistência da configuração editorial escolhida no painel CEP.
|
||||||
|
|
||||||
|
Este módulo registra a conclusão da seleção de um tipo de vídeo e suas opções.
|
||||||
|
Ele não executa cortes, não gera o plano da IA e não duplica a análise do
|
||||||
|
Scanner; apenas cria o vínculo persistido entre a análise e a edição desejada.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
class ErroDeEdicao(ValueError):
|
||||||
|
"""Configuração editorial inválida para persistência."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class EdicaoDeVideo:
|
||||||
|
"""Representa a configuração editorial concluída para um vídeo."""
|
||||||
|
|
||||||
|
video_id: str
|
||||||
|
sequencia: str
|
||||||
|
tipo_de_video: str
|
||||||
|
configuracao: dict[str, object]
|
||||||
|
origem: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class RepositorioDeEdicoes:
|
||||||
|
"""Grava configurações editoriais sem alterar os dados da análise."""
|
||||||
|
|
||||||
|
def __init__(self, conexao: sqlite3.Connection) -> None:
|
||||||
|
"""Inicializa o repositório com uma conexão SQLite preparada."""
|
||||||
|
self.conexao = conexao
|
||||||
|
|
||||||
|
def registrar(self, edicao: EdicaoDeVideo) -> int:
|
||||||
|
"""Registra uma edição concluída e devolve seu identificador."""
|
||||||
|
if not edicao.video_id.strip():
|
||||||
|
raise ErroDeEdicao("A edição precisa indicar o vídeo da análise.")
|
||||||
|
if not edicao.sequencia.strip():
|
||||||
|
raise ErroDeEdicao("A edição precisa indicar a sequência.")
|
||||||
|
if not edicao.tipo_de_video.strip():
|
||||||
|
raise ErroDeEdicao("A edição precisa indicar o tipo de vídeo.")
|
||||||
|
if not edicao.configuracao:
|
||||||
|
raise ErroDeEdicao("A edição precisa ter uma configuração.")
|
||||||
|
|
||||||
|
with self.conexao:
|
||||||
|
self.conexao.execute(
|
||||||
|
"INSERT OR IGNORE INTO videos (id, nome) VALUES (?, ?)",
|
||||||
|
(edicao.video_id, edicao.sequencia),
|
||||||
|
)
|
||||||
|
cursor = self.conexao.execute(
|
||||||
|
"""INSERT INTO edicoes_de_video
|
||||||
|
(video_id, sequencia, origem, tipo_de_video, configuracao)
|
||||||
|
VALUES (?, ?, ?, ?, ?)""",
|
||||||
|
(
|
||||||
|
edicao.video_id,
|
||||||
|
edicao.sequencia,
|
||||||
|
edicao.origem,
|
||||||
|
edicao.tipo_de_video,
|
||||||
|
json.dumps(edicao.configuracao, ensure_ascii=False),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return int(cursor.lastrowid)
|
||||||
|
|
||||||
|
|
||||||
|
def edicao_de_dict(dados: dict[str, object]) -> EdicaoDeVideo:
|
||||||
|
"""Converte o pedido externo do painel em uma entidade validável."""
|
||||||
|
try:
|
||||||
|
video_id = str(dados["video_id"])
|
||||||
|
sequencia = str(dados["sequencia"])
|
||||||
|
tipo_de_video = str(dados["tipo_de_video"])
|
||||||
|
configuracao = dados["configuracao"]
|
||||||
|
except KeyError as erro:
|
||||||
|
raise ErroDeEdicao(f"Campo obrigatório ausente: {erro.args[0]}.") from erro
|
||||||
|
if not isinstance(configuracao, dict):
|
||||||
|
raise ErroDeEdicao("O campo configuracao precisa ser um objeto.")
|
||||||
|
origem = dados.get("origem")
|
||||||
|
return EdicaoDeVideo(video_id, sequencia, tipo_de_video, configuracao, str(origem) if origem else None)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["EdicaoDeVideo", "ErroDeEdicao", "RepositorioDeEdicoes", "edicao_de_dict"]
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""Entrada de linha de comando para registrar uma edição concluída no banco."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
CAMINHO_DO_CODIGO = Path(__file__).resolve().parent.parent
|
||||||
|
if str(CAMINHO_DO_CODIGO) not in sys.path:
|
||||||
|
sys.path.insert(0, str(CAMINHO_DO_CODIGO))
|
||||||
|
|
||||||
|
from engine.persistencia.conexao import abrir_banco
|
||||||
|
from engine.persistencia.repositorio_de_edicoes import RepositorioDeEdicoes, edicao_de_dict
|
||||||
|
|
||||||
|
|
||||||
|
def registrar(caminho_do_pedido: Path, caminho_do_banco: Path) -> int:
|
||||||
|
"""Lê o pedido do painel e registra a configuração editorial."""
|
||||||
|
dados = json.loads(caminho_do_pedido.read_text(encoding="utf-8"))
|
||||||
|
repositorio = RepositorioDeEdicoes(abrir_banco(caminho_do_banco))
|
||||||
|
return repositorio.registrar(edicao_de_dict(dados))
|
||||||
|
|
||||||
|
|
||||||
|
def principal() -> None:
|
||||||
|
"""Executa o registro pela linha de comando."""
|
||||||
|
argumentos = argparse.ArgumentParser()
|
||||||
|
argumentos.add_argument("pedido", type=Path)
|
||||||
|
argumentos.add_argument("--banco", type=Path, default=Path(".jhonny/analises.db"))
|
||||||
|
opcoes = argumentos.parse_args()
|
||||||
|
try:
|
||||||
|
identificador = registrar(opcoes.pedido, opcoes.banco)
|
||||||
|
print(json.dumps({"ok": True, "edicao_id": identificador}))
|
||||||
|
except (OSError, ValueError) as erro:
|
||||||
|
print(json.dumps({"ok": False, "erro": str(erro)}, ensure_ascii=False))
|
||||||
|
raise SystemExit(1) from erro
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
principal()
|
||||||
@@ -8,7 +8,9 @@ from pathlib import Path
|
|||||||
from engine.dominio import Clipe, Faixa, IntervaloDeTempo, Timeline
|
from engine.dominio import Clipe, Faixa, IntervaloDeTempo, Timeline
|
||||||
from engine.integracoes.midia.extracao_de_metadados import MetadadosDoArquivo
|
from engine.integracoes.midia.extracao_de_metadados import MetadadosDoArquivo
|
||||||
from engine.persistencia import (
|
from engine.persistencia import (
|
||||||
|
EdicaoDeVideo,
|
||||||
RepositorioDeAnalisesSQLite,
|
RepositorioDeAnalisesSQLite,
|
||||||
|
RepositorioDeEdicoes,
|
||||||
RepositorioDeRetakesSQLite,
|
RepositorioDeRetakesSQLite,
|
||||||
RepositorioDeTimelineSQLite,
|
RepositorioDeTimelineSQLite,
|
||||||
abrir_banco,
|
abrir_banco,
|
||||||
@@ -23,6 +25,7 @@ TABELAS_ESPERADAS = {
|
|||||||
"segmentos_de_transcricao", "palavras_de_transcricao",
|
"segmentos_de_transcricao", "palavras_de_transcricao",
|
||||||
"evidencias_visuais", "cenas", "grupos_de_retake",
|
"evidencias_visuais", "cenas", "grupos_de_retake",
|
||||||
"tomadas_de_retake", "evidencias_de_retake",
|
"tomadas_de_retake", "evidencias_de_retake",
|
||||||
|
"edicoes_de_video",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -129,6 +132,27 @@ class TesteRepositorioDeAnalisesSQLite(unittest.TestCase):
|
|||||||
self.assertEqual(linha["largura"], 1920)
|
self.assertEqual(linha["largura"], 1920)
|
||||||
self.assertEqual(linha["hash_do_conteudo"], "hash123")
|
self.assertEqual(linha["hash_do_conteudo"], "hash123")
|
||||||
|
|
||||||
|
|
||||||
|
class TesteRepositorioDeEdicoes(unittest.TestCase):
|
||||||
|
def test_registrar_configuracao_editorial_preserva_dados(self):
|
||||||
|
with tempfile.TemporaryDirectory() as pasta:
|
||||||
|
conexao = abrir_banco(Path(pasta) / "analises.db")
|
||||||
|
repositorio = RepositorioDeEdicoes(conexao)
|
||||||
|
identificador = repositorio.registrar(EdicaoDeVideo(
|
||||||
|
video_id="sequencia-1",
|
||||||
|
sequencia="Depoimento",
|
||||||
|
origem="/videos/depoimento.mp4",
|
||||||
|
tipo_de_video="depoimento",
|
||||||
|
configuracao={"legendas": {"habilitadas": True}},
|
||||||
|
))
|
||||||
|
linha = conexao.execute(
|
||||||
|
"SELECT * FROM edicoes_de_video WHERE id = ?", (identificador,),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
self.assertEqual(linha["video_id"], "sequencia-1")
|
||||||
|
self.assertEqual(linha["tipo_de_video"], "depoimento")
|
||||||
|
self.assertIn("habilitadas", linha["configuracao"])
|
||||||
|
|
||||||
def test_registrar_e_carregar_transcricoes_com_palavras(self):
|
def test_registrar_e_carregar_transcricoes_com_palavras(self):
|
||||||
with tempfile.TemporaryDirectory() as pasta:
|
with tempfile.TemporaryDirectory() as pasta:
|
||||||
repositorio = RepositorioDeAnalisesSQLite(Path(pasta) / "analises.db")
|
repositorio = RepositorioDeAnalisesSQLite(Path(pasta) / "analises.db")
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const painel = readFileSync(join(process.cwd(), "cep-plugin", "main.js"), "utf8");
|
||||||
|
|
||||||
|
describe("preview de falantes na tela Refinar", () => {
|
||||||
|
it("usa os intervalos convertidos para a origem do WAV", () => {
|
||||||
|
expect(painel).toContain("var intervalosDePreview = silenceConsultarPreviewDeFalantes(silenceState.sequenceId);");
|
||||||
|
expect(painel).toContain("start: Number(intervalo.inicio)");
|
||||||
|
expect(painel).toContain("end: Number(intervalo.fim)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("não controla a timeline do Premiere durante o preview", () => {
|
||||||
|
expect(painel).not.toContain("function toggleSpeakerTimeline");
|
||||||
|
expect(painel).not.toContain("setPlayerPosition");
|
||||||
|
expect(painel).not.toContain("startPlayback");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user