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:
João Henrique
2026-09-10 19:21:07 -04:00
parent 024b004938
commit 59f1358f18
8 changed files with 643 additions and 0 deletions
@@ -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]))