feat: criado protótipo local de montagem incremental com gancho e
- criado protótipo local de montagem incremental com gancho e trechos preservados - adicionado checkbox no painel para executar planos pelo modo de montagem incremental Resumo: - 8 arquivos alterados - 4 novos - 4 modificados - 0 removidos 4 files changed, 230 insertions(+) Arquivos: - .jhonny/analises.db - code/cep-plugin/index.html - code/cep-plugin/main.js - code/src/tools/timeline.ts - code/engine/aplicar_montagem_incremental_prototipo.py - code/engine/editor/pedido_montagem_incremental_exemplo.json - code/engine/editor/plano_montagem_incremental_6.json - code/engine/editor/prototipo_montagem_incremental.py
This commit is contained in:
Binary file not shown.
@@ -216,6 +216,8 @@
|
|||||||
<strong>Sem desfazer</strong>
|
<strong>Sem desfazer</strong>
|
||||||
<p>Esta versão do Premiere não expõe undo para o painel. Um backup da sequência é criado automaticamente antes de aplicar.</p>
|
<p>Esta versão do Premiere não expõe undo para o painel. Um backup da sequência é criado automaticamente antes de aplicar.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<label class="checkbox-field"><input id="editorialIncrementalMode" type="checkbox"><span>Usar montagem incremental (teste)</span></label>
|
||||||
|
<p class="field-help">Marcado: cria uma sequência nova, limpa somente a cópia e monta nela os trechos preservados. A sequência original não é cortada.</p>
|
||||||
<button id="btnApplyEditorialActions" class="button button-danger" onclick="applyEditorialActions()" type="button" disabled>Aplicar plano na timeline</button>
|
<button id="btnApplyEditorialActions" class="button button-danger" onclick="applyEditorialActions()" type="button" disabled>Aplicar plano na timeline</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -419,6 +419,7 @@ function inserirLegendasNaTimeline() {
|
|||||||
|
|
||||||
// ---- 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";
|
||||||
|
var MONTAGEM_INCREMENTAL_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/aplicar_montagem_incremental_prototipo.py";
|
||||||
|
|
||||||
function testarAcoesCarregarExemplo() {
|
function testarAcoesCarregarExemplo() {
|
||||||
var exemplo = {
|
var exemplo = {
|
||||||
@@ -3805,6 +3806,11 @@ function applyEditorialActions() {
|
|||||||
}
|
}
|
||||||
var numActions = plan.actions.length;
|
var numActions = plan.actions.length;
|
||||||
|
|
||||||
|
if (document.getElementById("editorialIncrementalMode") && document.getElementById("editorialIncrementalMode").checked) {
|
||||||
|
aplicarMontagemIncremental(plan, numActions);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var summary =
|
var summary =
|
||||||
numActions + " ação(ões) no plano (cortes, ganchos, zooms, textos e/ou marcadores).\n\n" +
|
numActions + " ação(ões) no plano (cortes, ganchos, zooms, textos e/ou marcadores).\n\n" +
|
||||||
"Um backup da sequência atual será criado antes de aplicar. Confirmar?";
|
"Um backup da sequência atual será criado antes de aplicar. Confirmar?";
|
||||||
@@ -3867,6 +3873,50 @@ function applyEditorialActions() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function aplicarMontagemIncremental(plan, numActions) {
|
||||||
|
var summary =
|
||||||
|
numActions + " ação(ões) serão convertidas em trechos preservados.\n\n" +
|
||||||
|
"Será criada uma nova sequência clonada; a sequência original não será cortada. Confirmar?";
|
||||||
|
if (typeof window.confirm === "function" && !window.confirm(summary)) return;
|
||||||
|
setBusy("btnApplyEditorialActions", true);
|
||||||
|
setStep(3, "running", "Montando cópia…");
|
||||||
|
taskStart("Montagem incremental (teste)", "Clonando a sequência e preparando os trechos");
|
||||||
|
var caminhoDoPlano = path.join(silenceState.outputDir || os.tmpdir(), "plano-incremental-" + 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 incremental.", erroEscrita.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var processoIncremental = null;
|
||||||
|
taskStart("Montagem incremental (teste)", "Executando na sequência clonada", null, function () {
|
||||||
|
if (processoIncremental) processoIncremental.kill("SIGTERM");
|
||||||
|
});
|
||||||
|
processoIncremental = runStreamed(
|
||||||
|
SCANNER_PYTHON_BIN,
|
||||||
|
[MONTAGEM_INCREMENTAL_ENGINE_SCRIPT, caminhoDoPlano],
|
||||||
|
function (line) { silenceLog(line); },
|
||||||
|
function (code, lastErr, lastOutput) {
|
||||||
|
processoIncremental = null;
|
||||||
|
setBusy("btnApplyEditorialActions", false);
|
||||||
|
if (code !== 0) {
|
||||||
|
setStep(3, "error", "Falhou");
|
||||||
|
var detalhe = lastErr || "A montagem incremental não retornou detalhes.";
|
||||||
|
try { detalhe = JSON.parse(lastOutput || "{}").erro || detalhe; } catch (_) {}
|
||||||
|
taskEnd(false, "A montagem incremental falhou.", detalhe);
|
||||||
|
silenceLog("Montagem incremental não aplicada: " + detalhe, "err");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStep(3, "done", "Cópia montada");
|
||||||
|
var resultado = null;
|
||||||
|
try { resultado = JSON.parse(lastOutput || "{}").resultado || null; } catch (_) {}
|
||||||
|
var nome = resultado && resultado.destinationSequenceName ? resultado.destinationSequenceName : "nova sequência";
|
||||||
|
taskEnd(true, "Montagem incremental concluída em " + nome + ".");
|
||||||
|
silenceLog("Montagem incremental concluída em " + nome + ".", "ok");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Abre o seletor de arquivos nativo do CEP em vez de exigir que a pessoa cole
|
// Abre o seletor de arquivos nativo do CEP em vez de exigir que a pessoa cole
|
||||||
// um caminho absoluto à mão.
|
// um caminho absoluto à mão.
|
||||||
function pickEditorialPlanFile() {
|
function pickEditorialPlanFile() {
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
"""Executa o protótipo de montagem incremental a partir de um plano editorial.
|
||||||
|
|
||||||
|
O script lê um plano ``cut``/``hook`` já carregado pelo painel, calcula o
|
||||||
|
complemento dos cortes e chama uma ferramenta experimental que clona a
|
||||||
|
sequência ativa e monta os intervalos preservados na cópia.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
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.editor.prototipo_montagem_incremental import AcaoDoPlano, PlanejadorDeMontagemIncremental
|
||||||
|
from engine.integracoes.premiere.cliente_mcp import ClienteMCPPorStdio
|
||||||
|
|
||||||
|
|
||||||
|
class ErroDeMontagemIncremental(RuntimeError):
|
||||||
|
"""Indica que o plano não pôde ser preparado para a montagem experimental."""
|
||||||
|
|
||||||
|
|
||||||
|
def carregar_plano(caminho: Path) -> dict[str, object]:
|
||||||
|
"""Lê o plano editorial JSON fornecido pelo painel."""
|
||||||
|
try:
|
||||||
|
dados = json.loads(caminho.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, json.JSONDecodeError) as erro:
|
||||||
|
raise ErroDeMontagemIncremental(f"Não foi possível ler o plano: {erro}") from erro
|
||||||
|
if not isinstance(dados, dict) or not isinstance(dados.get("actions"), list):
|
||||||
|
raise ErroDeMontagemIncremental("O plano precisa conter uma lista actions válida.")
|
||||||
|
return dados
|
||||||
|
|
||||||
|
|
||||||
|
def preparar_intervalos(
|
||||||
|
plano: dict[str, object], duracao_da_midia: float
|
||||||
|
) -> list[dict[str, object]]:
|
||||||
|
"""Converte cortes e gancho em intervalos contíguos de montagem."""
|
||||||
|
acoes = [
|
||||||
|
AcaoDoPlano(
|
||||||
|
tipo=str(acao["kind"]),
|
||||||
|
inicio=float(acao["start"]),
|
||||||
|
fim=float(acao["end"]),
|
||||||
|
motivo=str(acao.get("reason", "")),
|
||||||
|
)
|
||||||
|
for acao in plano["actions"]
|
||||||
|
if isinstance(acao, dict) and acao.get("kind") in {"cut", "hook"}
|
||||||
|
]
|
||||||
|
insercoes = PlanejadorDeMontagemIncremental(duracao_da_midia).planejar(acoes)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"in_seconds": insercao.origem.inicio,
|
||||||
|
"out_seconds": insercao.origem.fim,
|
||||||
|
"destination_seconds": insercao.destino_inicio,
|
||||||
|
"label": insercao.tipo.value,
|
||||||
|
}
|
||||||
|
for insercao in insercoes
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def executar(caminho_do_plano: Path) -> dict[str, object]:
|
||||||
|
"""Clona a sequência ativa e monta o plano incremental na nova sequência."""
|
||||||
|
plano = carregar_plano(caminho_do_plano)
|
||||||
|
cliente = ClienteMCPPorStdio([resolver_node(), str(CAMINHO_DO_CODIGO / "dist" / "index.js")], tempo_limite=60)
|
||||||
|
cliente.conectar()
|
||||||
|
try:
|
||||||
|
sequencia = cliente.chamar("get_active_sequence")
|
||||||
|
if not isinstance(sequencia, dict) or not sequencia.get("id"):
|
||||||
|
raise ErroDeMontagemIncremental("Nenhuma sequência ativa foi encontrada no Premiere.")
|
||||||
|
clipe = _primeiro_clipe_de_video(sequencia)
|
||||||
|
intervalos = preparar_intervalos(plano, float(sequencia["end"]))
|
||||||
|
resultado = cliente.chamar(
|
||||||
|
"assemble_incremental_edit_prototype",
|
||||||
|
{
|
||||||
|
"source_sequence_id": str(sequencia["id"]),
|
||||||
|
"source_item_id": str(clipe["sourceProjectItemId"]),
|
||||||
|
"target_name": _nome_da_sequencia(str(sequencia["name"])),
|
||||||
|
"ranges": intervalos,
|
||||||
|
"confirm_prototype": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return {"ok": True, "plano_incremental": True, "resultado": resultado}
|
||||||
|
finally:
|
||||||
|
cliente.desconectar()
|
||||||
|
|
||||||
|
|
||||||
|
def _primeiro_clipe_de_video(sequencia: dict[str, object]) -> dict[str, object]:
|
||||||
|
"""Obtém a mídia do primeiro clipe da faixa de vídeo principal."""
|
||||||
|
faixas = sequencia.get("videoTracks", [])
|
||||||
|
if not isinstance(faixas, list) or not faixas or not isinstance(faixas[0], dict):
|
||||||
|
raise ErroDeMontagemIncremental("A sequência ativa não possui faixa de vídeo.")
|
||||||
|
clipes = faixas[0].get("clips", [])
|
||||||
|
if not isinstance(clipes, list) or not clipes or not isinstance(clipes[0], dict):
|
||||||
|
raise ErroDeMontagemIncremental("A faixa de vídeo principal está vazia.")
|
||||||
|
if not clipes[0].get("sourceProjectItemId"):
|
||||||
|
raise ErroDeMontagemIncremental("Não foi possível identificar a mídia do primeiro clipe.")
|
||||||
|
return clipes[0]
|
||||||
|
|
||||||
|
|
||||||
|
def _nome_da_sequencia(nome_da_origem: str) -> str:
|
||||||
|
"""Cria um nome identificável e único para a sequência experimental."""
|
||||||
|
carimbo = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||||
|
return f"PROTÓTIPO incremental - {nome_da_origem} - {carimbo}"[:255]
|
||||||
|
|
||||||
|
|
||||||
|
def resolver_node() -> str:
|
||||||
|
"""Resolve o Node usado pelo servidor MCP local."""
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
caminho = shutil.which("node")
|
||||||
|
if caminho:
|
||||||
|
return caminho
|
||||||
|
raise ErroDeMontagemIncremental("Node.js não foi encontrado no PATH do painel.")
|
||||||
|
|
||||||
|
|
||||||
|
def principal() -> None:
|
||||||
|
"""Executa o script e imprime um resultado JSON para o painel."""
|
||||||
|
argumentos = argparse.ArgumentParser()
|
||||||
|
argumentos.add_argument("plano", type=Path)
|
||||||
|
opcoes = argumentos.parse_args()
|
||||||
|
try:
|
||||||
|
print(json.dumps(executar(opcoes.plano), ensure_ascii=False))
|
||||||
|
except (ErroDeMontagemIncremental, OSError, 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,23 @@
|
|||||||
|
{
|
||||||
|
"duracao_da_midia": 180,
|
||||||
|
"acoes": [
|
||||||
|
{
|
||||||
|
"tipo": "hook",
|
||||||
|
"inicio": 141.2,
|
||||||
|
"fim": 147.8,
|
||||||
|
"motivo": "frase completa para abrir o vídeo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tipo": "cut",
|
||||||
|
"inicio": 32.5,
|
||||||
|
"fim": 48.0,
|
||||||
|
"motivo": "remover repetição"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tipo": "cut",
|
||||||
|
"inicio": 95.4,
|
||||||
|
"fim": 110.0,
|
||||||
|
"motivo": "remover pausa longa"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"modo_de_execucao": "montagem_incremental",
|
||||||
|
"plano_de_origem": 6,
|
||||||
|
"source_sequence_id": "12669c9c-038d-4a3f-99f9-fe6f6827b5f4",
|
||||||
|
"source_item_id": "000f4289",
|
||||||
|
"target_name": "PROTÓTIPO - plano 6 - montagem incremental",
|
||||||
|
"duracao_da_midia": 1018.22554166667,
|
||||||
|
"ranges": [
|
||||||
|
{
|
||||||
|
"in_seconds": 255.98,
|
||||||
|
"out_seconds": 274.96,
|
||||||
|
"destination_seconds": 0,
|
||||||
|
"label": "gancho de abertura"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in_seconds": 124.47,
|
||||||
|
"out_seconds": 215.06,
|
||||||
|
"destination_seconds": 18.98,
|
||||||
|
"label": "corpo 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in_seconds": 255.98,
|
||||||
|
"out_seconds": 427.32,
|
||||||
|
"destination_seconds": 109.57,
|
||||||
|
"label": "corpo 2 - ocorrência original do gancho"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in_seconds": 469.24,
|
||||||
|
"out_seconds": 552.28,
|
||||||
|
"destination_seconds": 280.91,
|
||||||
|
"label": "corpo 3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in_seconds": 848.42,
|
||||||
|
"out_seconds": 879.89,
|
||||||
|
"destination_seconds": 363.95,
|
||||||
|
"label": "corpo 4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in_seconds": 962.78,
|
||||||
|
"out_seconds": 1014.99,
|
||||||
|
"label": "corpo 5",
|
||||||
|
"destination_seconds": 395.42
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
"""Protótipo descartável da montagem incremental de uma edição.
|
||||||
|
|
||||||
|
Este módulo não se conecta ao Premiere. Ele responde à pergunta:
|
||||||
|
"Dado um plano em intervalos da mídia original, qual sequência de inserções
|
||||||
|
seria feita em uma sequência de destino inicialmente vazia?"
|
||||||
|
|
||||||
|
O protótipo mantém a sequência original intacta e transforma ações de corte
|
||||||
|
em trechos preservados. O gancho, quando existir, é colocado primeiro.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class TipoDeTrecho(str, Enum):
|
||||||
|
"""Origem editorial de um trecho que será inserido na montagem."""
|
||||||
|
|
||||||
|
GANCHO = "gancho"
|
||||||
|
CORPO = "corpo"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class IntervaloDeOrigem:
|
||||||
|
"""Representa um intervalo contínuo em segundos na mídia original."""
|
||||||
|
|
||||||
|
inicio: float
|
||||||
|
fim: float
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if self.inicio < 0 or self.fim <= self.inicio:
|
||||||
|
raise ValueError("Intervalo inválido: o fim precisa ser maior que o início.")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def duracao(self) -> float:
|
||||||
|
"""Devolve a duração do intervalo em segundos."""
|
||||||
|
return self.fim - self.inicio
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AcaoDoPlano:
|
||||||
|
"""Ação mínima usada pelo protótipo para representar um plano editorial."""
|
||||||
|
|
||||||
|
tipo: str
|
||||||
|
inicio: float
|
||||||
|
fim: float
|
||||||
|
motivo: str = ""
|
||||||
|
|
||||||
|
def intervalo(self) -> IntervaloDeOrigem:
|
||||||
|
"""Converte a ação em intervalo na mídia original."""
|
||||||
|
return IntervaloDeOrigem(self.inicio, self.fim)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class InsercaoDeMontagem:
|
||||||
|
"""Uma inserção A/V planejada para a sequência de destino."""
|
||||||
|
|
||||||
|
ordem: int
|
||||||
|
tipo: TipoDeTrecho
|
||||||
|
origem: IntervaloDeOrigem
|
||||||
|
destino_inicio: float
|
||||||
|
destino_fim: float
|
||||||
|
motivo: str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def duracao(self) -> float:
|
||||||
|
"""Devolve a duração que será inserida na montagem."""
|
||||||
|
return self.destino_fim - self.destino_inicio
|
||||||
|
|
||||||
|
def como_dicionario(self) -> dict[str, Any]:
|
||||||
|
"""Serializa a inserção para inspeção humana ou integração futura."""
|
||||||
|
return {
|
||||||
|
"ordem": self.ordem,
|
||||||
|
"tipo": self.tipo.value,
|
||||||
|
"origem": {"inicio": self.origem.inicio, "fim": self.origem.fim},
|
||||||
|
"destino": {"inicio": self.destino_inicio, "fim": self.destino_fim},
|
||||||
|
"duracao": self.duracao,
|
||||||
|
"motivo": self.motivo,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PlanejadorDeMontagemIncremental:
|
||||||
|
"""Converte cortes e gancho em uma montagem crescente e sem ripple de origem."""
|
||||||
|
|
||||||
|
def __init__(self, duracao_da_midia: float) -> None:
|
||||||
|
if duracao_da_midia <= 0:
|
||||||
|
raise ValueError("A duração da mídia precisa ser positiva.")
|
||||||
|
self.duracao_da_midia = duracao_da_midia
|
||||||
|
|
||||||
|
def planejar(self, acoes: list[AcaoDoPlano]) -> list[InsercaoDeMontagem]:
|
||||||
|
"""Gera as inserções ordenadas que reconstruiriam a edição final."""
|
||||||
|
cortes = self._normalizar_intervalos(
|
||||||
|
acao.intervalo() for acao in acoes if acao.tipo == "cut"
|
||||||
|
)
|
||||||
|
gancho = next((acao for acao in acoes if acao.tipo == "hook"), None)
|
||||||
|
if sum(acao.tipo == "hook" for acao in acoes) > 1:
|
||||||
|
raise ValueError("O protótipo aceita no máximo um gancho.")
|
||||||
|
|
||||||
|
trechos = self._complemento_da_midia(cortes)
|
||||||
|
insercoes: list[InsercaoDeMontagem] = []
|
||||||
|
proximo_destino = 0.0
|
||||||
|
|
||||||
|
if gancho is not None:
|
||||||
|
intervalo_do_gancho = gancho.intervalo()
|
||||||
|
self._validar_intervalo_do_gancho(intervalo_do_gancho, trechos)
|
||||||
|
insercoes.append(
|
||||||
|
self._criar_insercao(
|
||||||
|
len(insercoes) + 1,
|
||||||
|
TipoDeTrecho.GANCHO,
|
||||||
|
intervalo_do_gancho,
|
||||||
|
proximo_destino,
|
||||||
|
gancho.motivo or "gancho de abertura",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
proximo_destino += intervalo_do_gancho.duracao
|
||||||
|
|
||||||
|
for trecho in trechos:
|
||||||
|
insercoes.append(
|
||||||
|
self._criar_insercao(
|
||||||
|
len(insercoes) + 1,
|
||||||
|
TipoDeTrecho.CORPO,
|
||||||
|
trecho,
|
||||||
|
proximo_destino,
|
||||||
|
"trecho preservado após os cortes",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
proximo_destino += trecho.duracao
|
||||||
|
return insercoes
|
||||||
|
|
||||||
|
def _normalizar_intervalos(self, intervalos: Any) -> list[IntervaloDeOrigem]:
|
||||||
|
"""Ordena, limita e rejeita cortes sobrepostos."""
|
||||||
|
normalizados = []
|
||||||
|
for intervalo in sorted(intervalos, key=lambda item: item.inicio):
|
||||||
|
inicio = max(0.0, intervalo.inicio)
|
||||||
|
fim = min(self.duracao_da_midia, intervalo.fim)
|
||||||
|
if fim <= inicio:
|
||||||
|
continue
|
||||||
|
atual = IntervaloDeOrigem(inicio, fim)
|
||||||
|
if normalizados and atual.inicio < normalizados[-1].fim:
|
||||||
|
raise ValueError("Os cortes do protótipo não podem se sobrepor.")
|
||||||
|
normalizados.append(atual)
|
||||||
|
return normalizados
|
||||||
|
|
||||||
|
def _complemento_da_midia(self, cortes: list[IntervaloDeOrigem]) -> list[IntervaloDeOrigem]:
|
||||||
|
"""Calcula os trechos que permanecem depois dos cortes."""
|
||||||
|
trechos: list[IntervaloDeOrigem] = []
|
||||||
|
cursor = 0.0
|
||||||
|
for corte in cortes:
|
||||||
|
if corte.inicio > cursor:
|
||||||
|
trechos.append(IntervaloDeOrigem(cursor, corte.inicio))
|
||||||
|
cursor = corte.fim
|
||||||
|
if cursor < self.duracao_da_midia:
|
||||||
|
trechos.append(IntervaloDeOrigem(cursor, self.duracao_da_midia))
|
||||||
|
return trechos
|
||||||
|
|
||||||
|
def _validar_intervalo_do_gancho(
|
||||||
|
self, gancho: IntervaloDeOrigem, trechos: list[IntervaloDeOrigem]
|
||||||
|
) -> None:
|
||||||
|
"""Confirma que o gancho não foi removido pelo conjunto de cortes."""
|
||||||
|
if not any(self._contem_intervalo(trecho, gancho) for trecho in trechos):
|
||||||
|
raise ValueError("O gancho precisa estar em um trecho preservado da mídia.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _contem_intervalo(
|
||||||
|
recipiente: IntervaloDeOrigem, intervalo: IntervaloDeOrigem
|
||||||
|
) -> bool:
|
||||||
|
"""Informa se um intervalo está completamente dentro de outro."""
|
||||||
|
return recipiente.inicio <= intervalo.inicio and intervalo.fim <= recipiente.fim
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _criar_insercao(
|
||||||
|
ordem: int,
|
||||||
|
tipo: TipoDeTrecho,
|
||||||
|
origem: IntervaloDeOrigem,
|
||||||
|
destino_inicio: float,
|
||||||
|
motivo: str,
|
||||||
|
) -> InsercaoDeMontagem:
|
||||||
|
"""Cria uma inserção com destino contíguo na sequência de montagem."""
|
||||||
|
return InsercaoDeMontagem(
|
||||||
|
ordem=ordem,
|
||||||
|
tipo=tipo,
|
||||||
|
origem=origem,
|
||||||
|
destino_inicio=destino_inicio,
|
||||||
|
destino_fim=destino_inicio + origem.duracao,
|
||||||
|
motivo=motivo,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def carregar_pedido(caminho: Path) -> tuple[float, list[AcaoDoPlano]]:
|
||||||
|
"""Carrega um pedido JSON pequeno para execução local do protótipo."""
|
||||||
|
dados = json.loads(caminho.read_text(encoding="utf-8"))
|
||||||
|
acoes = [AcaoDoPlano(**acao) for acao in dados["acoes"]]
|
||||||
|
return float(dados["duracao_da_midia"]), acoes
|
||||||
|
|
||||||
|
|
||||||
|
def executar(caminho: Path) -> None:
|
||||||
|
"""Executa o protótipo e imprime o plano de montagem resultante."""
|
||||||
|
duracao, acoes = carregar_pedido(caminho)
|
||||||
|
insercoes = PlanejadorDeMontagemIncremental(duracao).planejar(acoes)
|
||||||
|
print(json.dumps({"sequencia_de_destino": insercoes}, default=lambda item: item.como_dicionario(), ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
raise SystemExit("Uso: python3 prototipo_montagem_incremental.py pedido.json")
|
||||||
|
executar(Path(sys.argv[1]))
|
||||||
@@ -3,6 +3,184 @@ import { sendCommand, BridgeOptions } from "../bridge/file-bridge.js";
|
|||||||
|
|
||||||
export function getTimelineTools(bridgeOptions: BridgeOptions) {
|
export function getTimelineTools(bridgeOptions: BridgeOptions) {
|
||||||
return {
|
return {
|
||||||
|
assemble_incremental_edit_prototype: {
|
||||||
|
description:
|
||||||
|
"PROTOTYPE: clone a source sequence, clear only the clone, and assemble one project item's A/V source ranges in order. The source sequence is not edited.",
|
||||||
|
parameters: {
|
||||||
|
type: "object" as const,
|
||||||
|
additionalProperties: false,
|
||||||
|
properties: {
|
||||||
|
source_sequence_id: {
|
||||||
|
type: "string",
|
||||||
|
minLength: 1,
|
||||||
|
description: "Source sequence name or ID to clone",
|
||||||
|
},
|
||||||
|
source_item_id: {
|
||||||
|
type: "string",
|
||||||
|
minLength: 1,
|
||||||
|
description: "Project item name or node ID used by every source range",
|
||||||
|
},
|
||||||
|
target_name: {
|
||||||
|
type: "string",
|
||||||
|
minLength: 1,
|
||||||
|
maxLength: 255,
|
||||||
|
description: "Name for the cloned destination sequence",
|
||||||
|
},
|
||||||
|
ranges: {
|
||||||
|
type: "array",
|
||||||
|
minItems: 1,
|
||||||
|
maxItems: 64,
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
properties: {
|
||||||
|
in_seconds: { type: "number", minimum: 0 },
|
||||||
|
out_seconds: { type: "number", minimum: 0 },
|
||||||
|
destination_seconds: { type: "number", minimum: 0 },
|
||||||
|
label: { type: "string", maxLength: 255 },
|
||||||
|
},
|
||||||
|
required: ["in_seconds", "out_seconds", "destination_seconds"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
confirm_prototype: {
|
||||||
|
type: "boolean",
|
||||||
|
description: "Must be true because this creates and mutates a Premiere sequence",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["source_sequence_id", "source_item_id", "target_name", "ranges", "confirm_prototype"],
|
||||||
|
},
|
||||||
|
handler: async (args: {
|
||||||
|
source_sequence_id: string;
|
||||||
|
source_item_id: string;
|
||||||
|
target_name: string;
|
||||||
|
ranges: Array<{
|
||||||
|
in_seconds: number;
|
||||||
|
out_seconds: number;
|
||||||
|
destination_seconds: number;
|
||||||
|
label?: string;
|
||||||
|
}>;
|
||||||
|
confirm_prototype: boolean;
|
||||||
|
}) => {
|
||||||
|
if (args.confirm_prototype !== true) {
|
||||||
|
return { success: false, error: "confirm_prototype must be true for the experimental sequence assembly." };
|
||||||
|
}
|
||||||
|
if (!Array.isArray(args.ranges) || args.ranges.length === 0 || args.ranges.length > 64) {
|
||||||
|
return { success: false, error: "ranges must contain between 1 and 64 source ranges." };
|
||||||
|
}
|
||||||
|
for (const [index, range] of args.ranges.entries()) {
|
||||||
|
if (!Number.isFinite(range.in_seconds) || !Number.isFinite(range.out_seconds) || !Number.isFinite(range.destination_seconds) ||
|
||||||
|
range.in_seconds < 0 || range.out_seconds <= range.in_seconds || range.destination_seconds < 0) {
|
||||||
|
return { success: false, error: `ranges[${index}] has invalid source or destination timing.` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceSequenceId = escapeForExtendScript(args.source_sequence_id);
|
||||||
|
const sourceItemId = escapeForExtendScript(args.source_item_id);
|
||||||
|
const targetName = escapeForExtendScript(args.target_name);
|
||||||
|
const ranges = args.ranges.map((range) => ({
|
||||||
|
inSeconds: range.in_seconds,
|
||||||
|
outSeconds: range.out_seconds,
|
||||||
|
destinationSeconds: range.destination_seconds,
|
||||||
|
label: range.label || "",
|
||||||
|
}));
|
||||||
|
const rangeJson = JSON.stringify(ranges);
|
||||||
|
|
||||||
|
const script = buildToolScript(`
|
||||||
|
var sourceSequence = __findSequence("${sourceSequenceId}");
|
||||||
|
if (!sourceSequence) return __error("Source sequence not found; no prototype sequence was created.");
|
||||||
|
var item = __findProjectItem("${sourceItemId}");
|
||||||
|
if (!item) return __error("Source project item not found; no prototype sequence was created.");
|
||||||
|
|
||||||
|
var beforeIds = {};
|
||||||
|
var sequenceIndex;
|
||||||
|
for (sequenceIndex = 0; sequenceIndex < app.project.sequences.numSequences; sequenceIndex++) {
|
||||||
|
beforeIds[String(app.project.sequences[sequenceIndex].sequenceID)] = true;
|
||||||
|
}
|
||||||
|
sourceSequence.clone();
|
||||||
|
|
||||||
|
var destinationSequence = null;
|
||||||
|
for (sequenceIndex = 0; sequenceIndex < app.project.sequences.numSequences; sequenceIndex++) {
|
||||||
|
var candidate = app.project.sequences[sequenceIndex];
|
||||||
|
if (!beforeIds[String(candidate.sequenceID)]) {
|
||||||
|
destinationSequence = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!destinationSequence) return __error("Premiere did not expose the cloned destination sequence.");
|
||||||
|
destinationSequence.name = "${targetName}";
|
||||||
|
|
||||||
|
var trackGroup;
|
||||||
|
var trackIndex;
|
||||||
|
var clipIndex;
|
||||||
|
var trackGroups = [destinationSequence.videoTracks, destinationSequence.audioTracks];
|
||||||
|
for (trackGroup = 0; trackGroup < trackGroups.length; trackGroup++) {
|
||||||
|
for (trackIndex = 0; trackIndex < trackGroups[trackGroup].numTracks; trackIndex++) {
|
||||||
|
var track = trackGroups[trackGroup][trackIndex];
|
||||||
|
for (clipIndex = track.clips.numItems - 1; clipIndex >= 0; clipIndex--) {
|
||||||
|
track.clips[clipIndex].remove(false, false);
|
||||||
|
}
|
||||||
|
if (track.clips.numItems !== 0) return __error("Could not clear the cloned destination sequence.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!destinationSequence.videoTracks.numTracks || !destinationSequence.audioTracks.numTracks) {
|
||||||
|
return __error("The cloned destination sequence must have at least one video and one audio track.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var ranges = ${rangeJson};
|
||||||
|
var oldIn = item.getInPoint(4);
|
||||||
|
var oldOut = item.getOutPoint(4);
|
||||||
|
var oldInTicks = oldIn ? oldIn.ticks : null;
|
||||||
|
var oldOutTicks = oldOut ? oldOut.ticks : null;
|
||||||
|
var placements = [];
|
||||||
|
var insertionError = null;
|
||||||
|
try {
|
||||||
|
for (var rangeIndex = 0; rangeIndex < ranges.length; rangeIndex++) {
|
||||||
|
var range = ranges[rangeIndex];
|
||||||
|
if (range.outSeconds <= range.inSeconds) return __error("A prototype source range has non-positive duration.");
|
||||||
|
item.setInPoint(__secondsToTicks(range.inSeconds).toString(), 4);
|
||||||
|
item.setOutPoint(__secondsToTicks(range.outSeconds).toString(), 4);
|
||||||
|
destinationSequence.insertClip(item, __secondsToTicks(range.destinationSeconds).toString(), 0, 0);
|
||||||
|
placements.push({
|
||||||
|
label: range.label,
|
||||||
|
sourceInSeconds: range.inSeconds,
|
||||||
|
sourceOutSeconds: range.outSeconds,
|
||||||
|
destinationSeconds: range.destinationSeconds,
|
||||||
|
durationSeconds: range.outSeconds - range.inSeconds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (insertError) {
|
||||||
|
insertionError = insertError.toString();
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
if (oldInTicks !== null) item.setInPoint(String(oldInTicks), 4); else item.clearInPoint();
|
||||||
|
if (oldOutTicks !== null) item.setOutPoint(String(oldOutTicks), 4); else item.clearOutPoint();
|
||||||
|
} catch (restoreError) {
|
||||||
|
return __error("The source item's in/out could not be restored: " + restoreError.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (insertionError !== null) return __error("Premiere rejected a prototype source-range insertion: " + insertionError);
|
||||||
|
|
||||||
|
var videoCount = destinationSequence.videoTracks[0].clips.numItems;
|
||||||
|
var audioCount = destinationSequence.audioTracks[0].clips.numItems;
|
||||||
|
if (videoCount !== ranges.length || audioCount !== ranges.length) {
|
||||||
|
return __error("The prototype destination did not receive exactly one video and one audio clip per range.");
|
||||||
|
}
|
||||||
|
return __result({
|
||||||
|
prototype: true,
|
||||||
|
verified: true,
|
||||||
|
sourceSequenceId: String(sourceSequence.sequenceID),
|
||||||
|
destinationSequenceId: String(destinationSequence.sequenceID),
|
||||||
|
destinationSequenceName: destinationSequence.name,
|
||||||
|
videoClipCount: videoCount,
|
||||||
|
audioClipCount: audioCount,
|
||||||
|
placements: placements,
|
||||||
|
note: "The source sequence was not edited. Review the cloned sequence manually before keeping it."
|
||||||
|
});
|
||||||
|
`);
|
||||||
|
return sendCommand(script, bridgeOptions);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
add_to_timeline: {
|
add_to_timeline: {
|
||||||
description:
|
description:
|
||||||
"Insert a project item at a timeline position and verify Premiere added no unexpected same-track fragments.",
|
"Insert a project item at a timeline position and verify Premiere added no unexpected same-track fragments.",
|
||||||
|
|||||||
Reference in New Issue
Block a user