feat: definido projeto com uma sequência e múltiplos arquivos de m
- definido projeto com uma sequência e múltiplos arquivos de mídia - criada persistência de projeto com uma sequência e seleções de faixas e clipes - criada guia Projeto para selecionar sequência, faixas e clipes do Premiere - adicionada gravação das seleções da guia Projeto no SQLite Resumo: - 10 arquivos alterados - 4 novos - 6 modificados - 0 removidos 6 files changed, 71 insertions(+), 2 deletions(-) Arquivos: - .jhonny/analises.db - CONTEXT.md - code/cep-plugin/index.html - code/cep-plugin/main.js - code/engine/persistencia/__init__.py - code/engine/persistencia/esquema.py - code/cep-plugin/abas/ - code/engine/persistencia/repositorio_de_projetos.py - code/engine/registrar_projeto_de_edicao.py - code/engine/testes/test_repositorio_de_projetos.py
This commit is contained in:
Binary file not shown.
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
- **Scanner**: caso de uso que coordena a leitura, análise, decisão, planejamento, aplicação e validação de uma edição.
|
- **Scanner**: caso de uso que coordena a leitura, análise, decisão, planejamento, aplicação e validação de uma edição.
|
||||||
- **Sequência**: timeline do Premiere e raiz operacional de um trabalho de edição; no banco é representada pelo vídeo identificado por `video_id` e nomeada pelo campo `sequencia`.
|
- **Sequência**: timeline do Premiere e raiz operacional de um trabalho de edição; no banco é representada pelo vídeo identificado por `video_id` e nomeada pelo campo `sequencia`.
|
||||||
|
- **Projeto de edição**: unidade de trabalho que contém exatamente uma sequência, suas escolhas de faixas e clipes, e os planos editoriais associados; uma sequência pode usar vários arquivos de mídia.
|
||||||
|
- **Arquivo de mídia**: arquivo físico de vídeo ou áudio usado por um ou mais clipes da sequência; não é sinônimo de projeto nem de sequência.
|
||||||
- **Edição de vídeo**: versão da configuração editorial concluída para uma sequência; registra tipo, instruções e origem, mas não representa ainda uma aplicação na timeline.
|
- **Edição de vídeo**: versão da configuração editorial concluída para uma sequência; registra tipo, instruções e origem, mas não representa ainda uma aplicação na timeline.
|
||||||
- **Conteúdo**: unidade analisável identificada por um `content_id`, com texto e metadados.
|
- **Conteúdo**: unidade analisável identificada por um `content_id`, com texto e metadados.
|
||||||
- **Análise**: observações estruturadas produzidas a partir do conteúdo.
|
- **Análise**: observações estruturadas produzidas a partir do conteúdo.
|
||||||
@@ -22,5 +24,6 @@
|
|||||||
- O domínio não conhece filesystem, banco de dados, SDK de IA ou plataforma de edição; essas integrações entram por interfaces e adaptadores.
|
- O domínio não conhece filesystem, banco de dados, SDK de IA ou plataforma de edição; essas integrações entram por interfaces e adaptadores.
|
||||||
- O primeiro fluxo é síncrono e determinístico, permitindo evolução posterior para operações assíncronas sem alterar o domínio.
|
- O primeiro fluxo é síncrono e determinístico, permitindo evolução posterior para operações assíncronas sem alterar o domínio.
|
||||||
- A sequência é a unidade de retomada: ao selecioná-la, o sistema deve localizar a edição concluída mais recente, reutilizar suas análises e oferecer o plano pendente para aprovação e aplicação.
|
- A sequência é a unidade de retomada: ao selecioná-la, o sistema deve localizar a edição concluída mais recente, reutilizar suas análises e oferecer o plano pendente para aprovação e aplicação.
|
||||||
|
- Cada projeto possui uma única sequência, mas pode reunir vários arquivos de mídia e vários clipes dessa sequência; a seleção do projeto nunca deve misturar timelines diferentes.
|
||||||
- Uma edição concluída, um plano gerado e uma aplicação realizada são estados distintos e devem permanecer auditáveis; concluir a configuração não significa que o Premiere já foi alterado.
|
- Uma edição concluída, um plano gerado e uma aplicação realizada são estados distintos e devem permanecer auditáveis; concluir a configuração não significa que o Premiere já foi alterado.
|
||||||
- O plano deve evoluir para referenciar diretamente a versão de edição que o originou, além do `video_id`, para impedir que uma nova configuração editorial seja aplicada por engano sobre um plano antigo.
|
- O plano deve evoluir para referenciar diretamente a versão de edição que o originou, além do `video_id`, para impedir que uma nova configuração editorial seja aplicada por engano sobre um plano antigo.
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
// A guia Projeto mantém a seleção da sequência, faixas e clipes fora das
|
||||||
|
// regras específicas de edição. O estado definitivo será persistido pelo
|
||||||
|
// repositório SQLite na próxima etapa da integração.
|
||||||
|
var projetoSequencias = [];
|
||||||
|
var projetoAtual = null;
|
||||||
|
|
||||||
|
function projetoListarSequencias() {
|
||||||
|
var botao = document.getElementById("btnProjetoCarregar");
|
||||||
|
if (botao) botao.disabled = true;
|
||||||
|
var select = document.getElementById("projetoSequenciaSelect");
|
||||||
|
var info = document.getElementById("projetoInfo");
|
||||||
|
info.textContent = "Lendo sequências, faixas e clipes do Premiere…";
|
||||||
|
cs.evalScript(
|
||||||
|
"(function(){try{" +
|
||||||
|
"if(!app.project)return JSON.stringify({ok:false,error:'Nenhum projeto aberto.'});" +
|
||||||
|
"var saida=[];" +
|
||||||
|
"for(var i=0;i<app.project.sequences.numSequences;i++){" +
|
||||||
|
"var s=app.project.sequences[i], faixas=[];" +
|
||||||
|
"for(var v=0;v<s.videoTracks.numTracks;v++){var tv=s.videoTracks[v],cv=[];for(var j=0;j<tv.clips.numItems;j++){var x=tv.clips[j];cv.push({id:String(x.nodeId),nome:String(x.name||''),inicio:Number(x.start.seconds),fim:Number(x.end.seconds)});}faixas.push({id:'video_'+v,nome:'Vídeo '+(v+1),tipo:'video',indice:v,clipes:cv});}" +
|
||||||
|
"for(var a=0;a<s.audioTracks.numTracks;a++){var ta=s.audioTracks[a],ca=[];for(var k=0;k<ta.clips.numItems;k++){var y=ta.clips[k];ca.push({id:String(y.nodeId),nome:String(y.name||''),inicio:Number(y.start.seconds),fim:Number(y.end.seconds)});}faixas.push({id:'audio_'+a,nome:'Áudio '+(a+1),tipo:'audio',indice:a,clipes:ca});}" +
|
||||||
|
"saida.push({id:String(s.sequenceID),nome:String(s.name||''),duracao:Number(s.end.seconds||0),largura:Number(s.frameSizeHorizontal||0),altura:Number(s.frameSizeVertical||0),ativa:!!(app.project.activeSequence&&s.sequenceID===app.project.activeSequence.sequenceID),faixas:faixas});" +
|
||||||
|
"}return JSON.stringify({ok:true,sequences:saida});}catch(e){return JSON.stringify({ok:false,error:String(e)});}}())",
|
||||||
|
function (raw) {
|
||||||
|
if (botao) botao.disabled = false;
|
||||||
|
var resposta;
|
||||||
|
try { resposta = JSON.parse(raw); } catch (erro) { resposta = {ok: false, error: "Resposta inválida do Premiere."}; }
|
||||||
|
if (!resposta.ok) { info.textContent = resposta.error; return; }
|
||||||
|
projetoSequencias = resposta.sequences || [];
|
||||||
|
select.innerHTML = "";
|
||||||
|
projetoSequencias.forEach(function (sequencia) {
|
||||||
|
var opcao = document.createElement("option");
|
||||||
|
opcao.value = sequencia.id;
|
||||||
|
opcao.textContent = sequencia.nome + (sequencia.ativa ? " · ativa" : "");
|
||||||
|
select.appendChild(opcao);
|
||||||
|
});
|
||||||
|
select.disabled = projetoSequencias.length === 0;
|
||||||
|
if (projetoSequencias.length) {
|
||||||
|
var ativa = projetoSequencias.findIndex(function (item) { return item.ativa; });
|
||||||
|
select.selectedIndex = ativa >= 0 ? ativa : 0;
|
||||||
|
projetoSelecionarSequencia();
|
||||||
|
} else {
|
||||||
|
info.textContent = "Nenhuma sequência encontrada no projeto do Premiere.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function projetoSelecionarSequencia() {
|
||||||
|
var select = document.getElementById("projetoSequenciaSelect");
|
||||||
|
projetoAtual = projetoSequencias.find(function (item) { return item.id === select.value; }) || null;
|
||||||
|
if (!projetoAtual) return;
|
||||||
|
document.getElementById("projetoTimelineCard").hidden = false;
|
||||||
|
document.getElementById("projetoStatus").textContent = "Sequência selecionada";
|
||||||
|
document.getElementById("projetoTimelineStatus").textContent = projetoAtual.nome;
|
||||||
|
document.getElementById("projetoDuracao").textContent = formatSequenceDuration(projetoAtual.duracao);
|
||||||
|
document.getElementById("projetoResolucao").textContent = projetoAtual.largura && projetoAtual.altura ? projetoAtual.largura + " × " + projetoAtual.altura : "Não informada";
|
||||||
|
document.getElementById("projetoTotalFaixas").textContent = projetoAtual.faixas.length;
|
||||||
|
document.getElementById("projetoTotalClipes").textContent = projetoAtual.faixas.reduce(function (total, faixa) { return total + faixa.clipes.length; }, 0);
|
||||||
|
var container = document.getElementById("projetoFaixas");
|
||||||
|
container.innerHTML = "";
|
||||||
|
projetoAtual.faixas.forEach(function (faixa) {
|
||||||
|
var bloco = document.createElement("div");
|
||||||
|
bloco.className = "scanner-subsection";
|
||||||
|
bloco.innerHTML = '<label class="checkbox-field"><input type="checkbox" checked data-projeto-faixa="' + faixa.id + '"><span>' + faixa.nome + " · " + faixa.clipes.length + " clipe(s)</span></label>";
|
||||||
|
faixa.clipes.forEach(function (clipe) {
|
||||||
|
bloco.innerHTML += '<label class="checkbox-field" style="padding-left:24px"><input type="checkbox" checked data-projeto-clipe="' + clipe.id + '"><span>' + clipe.nome + " · " + formatSequenceDuration(clipe.fim - clipe.inicio) + "</span></label>";
|
||||||
|
});
|
||||||
|
container.appendChild(bloco);
|
||||||
|
});
|
||||||
|
document.getElementById("btnProjetoContinuar").disabled = false;
|
||||||
|
document.getElementById("btnProjetoSalvar").disabled = false;
|
||||||
|
document.getElementById("projetoInfo").textContent = "A sequência foi carregada. Selecione as faixas e clipes que participarão do plano.";
|
||||||
|
}
|
||||||
|
|
||||||
|
function projetoSalvar() {
|
||||||
|
if (!projetoAtual) return;
|
||||||
|
var faixas = Array.prototype.slice.call(document.querySelectorAll("[data-projeto-faixa]:checked")).map(function (item) { return item.getAttribute("data-projeto-faixa"); });
|
||||||
|
var clipes = Array.prototype.slice.call(document.querySelectorAll("[data-projeto-clipe]:checked")).map(function (item) { return item.getAttribute("data-projeto-clipe"); });
|
||||||
|
var pedido = path.join(os.tmpdir(), "pedido-projeto-edicao-" + Date.now() + ".json");
|
||||||
|
fs.writeFileSync(pedido, JSON.stringify({
|
||||||
|
nome: projetoAtual.nome, video_id: projetoAtual.id, faixas: faixas, clipes: clipes,
|
||||||
|
}, null, 2), "utf-8");
|
||||||
|
setBusy("btnProjetoSalvar", true);
|
||||||
|
runStreamed(SCANNER_PYTHON_BIN, [REGISTRAR_PROJETO_SCRIPT, pedido, "--banco", ANALISES_DB_PATH],
|
||||||
|
function (linha) { silenceLog(linha); },
|
||||||
|
function (codigo, ultimoErro) {
|
||||||
|
setBusy("btnProjetoSalvar", false);
|
||||||
|
if (codigo !== 0) { showToast("err", ultimoErro || "Não foi possível salvar o projeto."); return; }
|
||||||
|
document.getElementById("projetoStatus").textContent = "Projeto salvo no banco";
|
||||||
|
showToast("ok", "Projeto salvo com " + faixas.length + " faixa(s) e " + clipes.length + " clipe(s).");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function projetoContinuarEdicao() {
|
||||||
|
if (!projetoAtual) return;
|
||||||
|
switchTab("silence");
|
||||||
|
silenceListSequences();
|
||||||
|
}
|
||||||
@@ -28,7 +28,10 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav class="tab-bar" role="tablist" aria-label="Painel">
|
<nav class="tab-bar" role="tablist" aria-label="Painel">
|
||||||
<button class="tab-button active" id="tabBtnScanner" role="tab" aria-selected="true" aria-controls="tabScanner" onclick="switchTab('scanner')" type="button">
|
<button class="tab-button active" id="tabBtnProjeto" role="tab" aria-selected="true" aria-controls="tabProjeto" onclick="switchTab('projeto')" type="button">
|
||||||
|
<span class="tab-icon" aria-hidden="true">▣</span>Projeto
|
||||||
|
</button>
|
||||||
|
<button class="tab-button" id="tabBtnScanner" role="tab" aria-selected="false" aria-controls="tabScanner" onclick="switchTab('scanner')" type="button">
|
||||||
<span class="tab-icon" aria-hidden="true">◉</span>Scanner
|
<span class="tab-icon" aria-hidden="true">◉</span>Scanner
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-button" id="tabBtnRefinar" role="tab" aria-selected="false" aria-controls="tabRefinar" onclick="switchTab('refinar')" type="button">
|
<button class="tab-button" id="tabBtnRefinar" role="tab" aria-selected="false" aria-controls="tabRefinar" onclick="switchTab('refinar')" type="button">
|
||||||
@@ -88,6 +91,35 @@
|
|||||||
<button type="button" class="toast-close" onclick="hideToast()" aria-label="Fechar aviso">✕</button>
|
<button type="button" class="toast-close" onclick="hideToast()" aria-label="Fechar aviso">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ================================ PROJETO ================================ -->
|
||||||
|
<div class="tab-panel" id="tabProjeto" role="tabpanel" aria-labelledby="tabBtnProjeto">
|
||||||
|
<p class="tab-intro">Um projeto contém uma única sequência e pode usar várias faixas, clipes e arquivos de mídia.</p>
|
||||||
|
<section class="card">
|
||||||
|
<div class="card-head"><h2>Sequência do projeto</h2><span class="card-kicker" id="projetoStatus">Aguardando seleção</span></div>
|
||||||
|
<button class="button button-primary" id="btnProjetoCarregar" onclick="projetoListarSequencias()" type="button">Carregar sequências do Premiere</button>
|
||||||
|
<label class="field-label" for="projetoSequenciaSelect">Sequência / timeline</label>
|
||||||
|
<select id="projetoSequenciaSelect" class="select-field" onchange="projetoSelecionarSequencia()" disabled>
|
||||||
|
<option value="">Nenhuma sequência carregada</option>
|
||||||
|
</select>
|
||||||
|
<p class="field-help" id="projetoInfo">Escolha a sequência que será a raiz deste projeto.</p>
|
||||||
|
</section>
|
||||||
|
<section class="card" id="projetoTimelineCard" hidden>
|
||||||
|
<div class="card-head"><h2>Informações da timeline</h2><span class="card-kicker" id="projetoTimelineStatus">—</span></div>
|
||||||
|
<dl class="kv">
|
||||||
|
<div><dt>Duração</dt><dd id="projetoDuracao">—</dd></div>
|
||||||
|
<div><dt>Resolução</dt><dd id="projetoResolucao">—</dd></div>
|
||||||
|
<div><dt>Faixas</dt><dd id="projetoTotalFaixas">—</dd></div>
|
||||||
|
<div><dt>Clipes</dt><dd id="projetoTotalClipes">—</dd></div>
|
||||||
|
</dl>
|
||||||
|
<h3>Faixas e clipes utilizados</h3>
|
||||||
|
<div id="projetoFaixas" class="scanner-track-list"></div>
|
||||||
|
<div class="button-row">
|
||||||
|
<button class="button button-primary" id="btnProjetoSalvar" onclick="projetoSalvar()" type="button" disabled>Salvar projeto no banco</button>
|
||||||
|
<button class="button button-primary" id="btnProjetoContinuar" onclick="projetoContinuarEdicao()" type="button" disabled>Continuar para editar vídeo</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ============================= 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>
|
||||||
|
|
||||||
@@ -675,6 +707,7 @@
|
|||||||
|
|
||||||
<script charset="utf-8" src="CSInterface.js"></script>
|
<script charset="utf-8" src="CSInterface.js"></script>
|
||||||
<script charset="utf-8" src="updater.cjs"></script>
|
<script charset="utf-8" src="updater.cjs"></script>
|
||||||
|
<script charset="utf-8" src="abas/projeto/projeto.js"></script>
|
||||||
<script charset="utf-8" src="main.js"></script>
|
<script charset="utf-8" src="main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ function stepState(n) {
|
|||||||
// ---- Tabs ----
|
// ---- Tabs ----
|
||||||
function switchTab(name) {
|
function switchTab(name) {
|
||||||
var tabs = [
|
var tabs = [
|
||||||
|
{ key: "projeto", panel: "tabProjeto", btn: "tabBtnProjeto" },
|
||||||
{ key: "scanner", panel: "tabScanner", btn: "tabBtnScanner" },
|
{ key: "scanner", panel: "tabScanner", btn: "tabBtnScanner" },
|
||||||
{ key: "refinar", panel: "tabRefinar", btn: "tabBtnRefinar" },
|
{ key: "refinar", panel: "tabRefinar", btn: "tabBtnRefinar" },
|
||||||
{ key: "silence", panel: "tabSilence", btn: "tabBtnSilence" },
|
{ key: "silence", panel: "tabSilence", btn: "tabBtnSilence" },
|
||||||
@@ -265,6 +266,7 @@ function switchTab(name) {
|
|||||||
btn.setAttribute("aria-selected", String(active));
|
btn.setAttribute("aria-selected", String(active));
|
||||||
});
|
});
|
||||||
if (name === "models") renderModelList();
|
if (name === "models") renderModelList();
|
||||||
|
if (name === "projeto") projetoListarSequencias();
|
||||||
if (name === "silence") { syncTranscribeModelSelect(); syncLanguageInfo(); renderizarTiposDeVideoParaEdicao(); }
|
if (name === "silence") { syncTranscribeModelSelect(); syncLanguageInfo(); renderizarTiposDeVideoParaEdicao(); }
|
||||||
if (name === "settings") renderEditorPersonalities();
|
if (name === "settings") renderEditorPersonalities();
|
||||||
if (name === "scanner") { renderScannerTranscriptionOptions(); loadScannerProfile(); renderScannerHuggingFaceStatus(); scannerListarAnalisesExistentes(); }
|
if (name === "scanner") { renderScannerTranscriptionOptions(); loadScannerProfile(); renderScannerHuggingFaceStatus(); scannerListarAnalisesExistentes(); }
|
||||||
@@ -947,6 +949,7 @@ 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 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 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 CARREGAR_PLANO_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/carregar_plano_de_edicao.py";
|
||||||
|
var REGISTRAR_PROJETO_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/registrar_projeto_de_edicao.py";
|
||||||
var planoEditorialIdCarregado = null;
|
var planoEditorialIdCarregado = null;
|
||||||
var retakesCasoAtual = null;
|
var retakesCasoAtual = null;
|
||||||
var retakesGrupos = [];
|
var retakesGrupos = [];
|
||||||
@@ -4129,7 +4132,7 @@ function downloadModel(model, btn, approxMB) {
|
|||||||
(function init() {
|
(function init() {
|
||||||
// A identificação de locutores pertence ao Scanner; abrir nele evita que o
|
// A identificação de locutores pertence ao Scanner; abrir nele evita que o
|
||||||
// painel pareça travado na etapa antiga de edição de vídeo.
|
// painel pareça travado na etapa antiga de edição de vídeo.
|
||||||
switchTab("scanner");
|
switchTab("projeto");
|
||||||
|
|
||||||
// Set the default temp dir in the input field
|
// Set the default temp dir in the input field
|
||||||
document.getElementById("tempDir").value = tempDir;
|
document.getElementById("tempDir").value = tempDir;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from .repositorio_de_planos import (
|
|||||||
RepositorioDePlanos,
|
RepositorioDePlanos,
|
||||||
plano_de_dict,
|
plano_de_dict,
|
||||||
)
|
)
|
||||||
|
from .repositorio_de_projetos import ErroDeProjeto, ProjetoDeEdicao, RepositorioDeProjetos
|
||||||
from .repositorio_de_edicoes import EdicaoDeVideo, ErroDeEdicao, RepositorioDeEdicoes, edicao_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
|
||||||
@@ -47,6 +48,9 @@ __all__ = [
|
|||||||
"PlanoDeEdicao",
|
"PlanoDeEdicao",
|
||||||
"RepositorioDeEnunciados",
|
"RepositorioDeEnunciados",
|
||||||
"RepositorioDePlanos",
|
"RepositorioDePlanos",
|
||||||
|
"RepositorioDeProjetos",
|
||||||
|
"ProjetoDeEdicao",
|
||||||
|
"ErroDeProjeto",
|
||||||
"RepositorioDeEdicoes",
|
"RepositorioDeEdicoes",
|
||||||
"ResultadoDeBusca",
|
"ResultadoDeBusca",
|
||||||
"abrir_banco",
|
"abrir_banco",
|
||||||
|
|||||||
@@ -285,6 +285,32 @@ 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);
|
||||||
|
|
||||||
|
-- Um projeto representa exatamente uma sequência, mas pode selecionar vários
|
||||||
|
-- arquivos, faixas e clipes pertencentes a ela.
|
||||||
|
CREATE TABLE IF NOT EXISTS projetos_de_edicao (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
video_id TEXT NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
|
||||||
|
nome TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'aberto',
|
||||||
|
criado_em TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
||||||
|
atualizado_em TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_projetos_video ON projetos_de_edicao(video_id, atualizado_em);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS faixas_do_projeto (
|
||||||
|
projeto_id INTEGER NOT NULL REFERENCES projetos_de_edicao(id) ON DELETE CASCADE,
|
||||||
|
faixa_id TEXT NOT NULL,
|
||||||
|
selecionada INTEGER NOT NULL DEFAULT 1,
|
||||||
|
PRIMARY KEY (projeto_id, faixa_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS clipes_do_projeto (
|
||||||
|
projeto_id INTEGER NOT NULL REFERENCES projetos_de_edicao(id) ON DELETE CASCADE,
|
||||||
|
clipe_id TEXT NOT NULL,
|
||||||
|
selecionado INTEGER NOT NULL DEFAULT 1,
|
||||||
|
PRIMARY KEY (projeto_id, clipe_id)
|
||||||
|
);
|
||||||
|
|
||||||
-- Configuração editorial escolhida no painel, antes de existir um plano da IA.
|
-- 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
|
-- 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.
|
-- somente a decisão do usuário e suas opções para a edição.
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"""Persistência dos projetos de edição e das seleções da guia Projeto."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
class ErroDeProjeto(ValueError):
|
||||||
|
"""Projeto inválido para persistência."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ProjetoDeEdicao:
|
||||||
|
"""Projeto associado a exatamente uma sequência do Premiere."""
|
||||||
|
|
||||||
|
nome: str
|
||||||
|
video_id: str
|
||||||
|
faixas: tuple[str, ...] = ()
|
||||||
|
clipes: tuple[str, ...] = ()
|
||||||
|
status: str = "aberto"
|
||||||
|
|
||||||
|
|
||||||
|
class RepositorioDeProjetos:
|
||||||
|
"""Grava e recupera projetos sem duplicar dados da timeline."""
|
||||||
|
|
||||||
|
def __init__(self, conexao: sqlite3.Connection) -> None:
|
||||||
|
"""Inicializa o repositório com uma conexão SQLite preparada."""
|
||||||
|
self.conexao = conexao
|
||||||
|
|
||||||
|
def registrar(self, projeto: ProjetoDeEdicao) -> int:
|
||||||
|
"""Cria um projeto e grava suas faixas e clipes selecionados."""
|
||||||
|
if not projeto.nome.strip():
|
||||||
|
raise ErroDeProjeto("O projeto precisa ter um nome.")
|
||||||
|
if not projeto.video_id.strip():
|
||||||
|
raise ErroDeProjeto("O projeto precisa ter uma sequência.")
|
||||||
|
with self.conexao:
|
||||||
|
cursor = self.conexao.execute(
|
||||||
|
"""INSERT INTO projetos_de_edicao (video_id, nome, status)
|
||||||
|
VALUES (?, ?, ?)""",
|
||||||
|
(projeto.video_id, projeto.nome, projeto.status),
|
||||||
|
)
|
||||||
|
projeto_id = int(cursor.lastrowid)
|
||||||
|
self.conexao.executemany(
|
||||||
|
"INSERT INTO faixas_do_projeto (projeto_id, faixa_id) VALUES (?, ?)",
|
||||||
|
[(projeto_id, faixa_id) for faixa_id in projeto.faixas],
|
||||||
|
)
|
||||||
|
self.conexao.executemany(
|
||||||
|
"INSERT INTO clipes_do_projeto (projeto_id, clipe_id) VALUES (?, ?)",
|
||||||
|
[(projeto_id, clipe_id) for clipe_id in projeto.clipes],
|
||||||
|
)
|
||||||
|
return projeto_id
|
||||||
|
|
||||||
|
def carregar(self, projeto_id: int) -> dict[str, object] | None:
|
||||||
|
"""Carrega um projeto com sua sequência, faixas e clipes selecionados."""
|
||||||
|
linha = self.conexao.execute(
|
||||||
|
"SELECT * FROM projetos_de_edicao WHERE id = ?", (projeto_id,),
|
||||||
|
).fetchone()
|
||||||
|
if linha is None:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"id": linha["id"], "nome": linha["nome"], "video_id": linha["video_id"],
|
||||||
|
"status": linha["status"], "faixas": [
|
||||||
|
r["faixa_id"] for r in self.conexao.execute(
|
||||||
|
"""SELECT faixa_id FROM faixas_do_projeto
|
||||||
|
WHERE projeto_id = ? AND selecionada = 1 ORDER BY rowid""",
|
||||||
|
(projeto_id,),
|
||||||
|
)
|
||||||
|
], "clipes": [
|
||||||
|
r["clipe_id"] for r in self.conexao.execute(
|
||||||
|
"""SELECT clipe_id FROM clipes_do_projeto
|
||||||
|
WHERE projeto_id = ? AND selecionado = 1 ORDER BY rowid""",
|
||||||
|
(projeto_id,),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["ErroDeProjeto", "ProjetoDeEdicao", "RepositorioDeProjetos"]
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"""Entrada de linha de comando para salvar um projeto de edição no SQLite."""
|
||||||
|
|
||||||
|
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_projetos import ProjetoDeEdicao, RepositorioDeProjetos
|
||||||
|
|
||||||
|
|
||||||
|
def registrar(caminho_do_pedido: Path, caminho_do_banco: Path) -> int:
|
||||||
|
"""Lê a seleção da guia Projeto e grava seu projeto no banco."""
|
||||||
|
dados = json.loads(caminho_do_pedido.read_text(encoding="utf-8"))
|
||||||
|
conexao = abrir_banco(caminho_do_banco)
|
||||||
|
try:
|
||||||
|
projeto = ProjetoDeEdicao(
|
||||||
|
nome=str(dados["nome"]), video_id=str(dados["video_id"]),
|
||||||
|
faixas=tuple(str(valor) for valor in dados.get("faixas", [])),
|
||||||
|
clipes=tuple(str(valor) for valor in dados.get("clipes", [])),
|
||||||
|
)
|
||||||
|
return RepositorioDeProjetos(conexao).registrar(projeto)
|
||||||
|
finally:
|
||||||
|
conexao.close()
|
||||||
|
|
||||||
|
|
||||||
|
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, "projeto_id": identificador}, ensure_ascii=False))
|
||||||
|
except (OSError, KeyError, ValueError) as erro:
|
||||||
|
print(json.dumps({"ok": False, "erro": str(erro)}, ensure_ascii=False))
|
||||||
|
raise SystemExit(1) from erro
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
principal()
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""Testes da persistência de projetos com uma sequência e várias seleções."""
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from engine.persistencia import ProjetoDeEdicao, RepositorioDeProjetos, abrir_banco
|
||||||
|
|
||||||
|
|
||||||
|
class TesteRepositorioDeProjetos(unittest.TestCase):
|
||||||
|
"""Garante a regra de uma sequência por projeto."""
|
||||||
|
|
||||||
|
def test_registra_um_projeto_com_varias_faixas_e_clipes(self):
|
||||||
|
with tempfile.TemporaryDirectory() as pasta:
|
||||||
|
conexao = abrir_banco(Path(pasta) / "analises.db")
|
||||||
|
conexao.execute("INSERT INTO videos (id, nome) VALUES ('video-1', 'Sequência')")
|
||||||
|
repositorio = RepositorioDeProjetos(conexao)
|
||||||
|
projeto_id = repositorio.registrar(ProjetoDeEdicao(
|
||||||
|
nome="Projeto depoimento", video_id="video-1",
|
||||||
|
faixas=("video_0", "audio_0"), clipes=("clipe-v", "clipe-a"),
|
||||||
|
))
|
||||||
|
projeto = repositorio.carregar(projeto_id)
|
||||||
|
conexao.close()
|
||||||
|
|
||||||
|
self.assertEqual(projeto["video_id"], "video-1")
|
||||||
|
self.assertEqual(projeto["faixas"], ["video_0", "audio_0"])
|
||||||
|
self.assertEqual(projeto["clipes"], ["clipe-v", "clipe-a"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user