feat: alinhada a remoção da engine Python ao fluxo ripple do execu

- alinhada a remoção da engine Python ao fluxo ripple do executor CEP para cortar vídeo e áudio
- criada remoção atômica para manter vídeo e áudio vinculados sincronizados
- criada aba de configuração de legendas offline e inclusão das preferências no pacote para a IA
- criado gerador offline de blocos de legenda sincronizados com palavras da transcrição e cortes aprovados
- criada uma versão base portátil da skill edit-video-by-voice para testes em outros agentes de IA
- incorporado perfil editorial opcional à skill portátil para orientar decisões de edição

Resumo:
- 14 arquivos alterados
- 4 novos
- 10 modificados
- 0 removidos

 10 files changed, 278 insertions(+), 33 deletions(-)

Arquivos:
  - code/cep-plugin/index.html
  - code/cep-plugin/main.js
  - code/docs/supported-actions.md
  - code/engine/editor/aplicador_de_plano_de_edicao.py
  - code/engine/editor/escrita/escrita_no_editor.py
  - code/engine/testes/duplos_de_premiere.py
  - code/engine/testes/test_aplicador_de_plano_de_edicao.py
  - code/src/tools/timeline.ts
  - code/tests/tools/structural-verification.test.ts
  - code/tests/tools/tool-modules.test.ts
  - code/engine/arquitetura/legendas.md
  - code/engine/editor/legendas/
  - code/engine/testes/test_gerador_de_legendas.py
  - skills/
This commit is contained in:
João Henrique
2026-09-09 13:05:07 -04:00
parent f3e356340b
commit 1249b3aeb7
18 changed files with 730 additions and 33 deletions
+5
View File
@@ -0,0 +1,5 @@
"""Geração determinística de blocos de legenda a partir da transcrição."""
from .gerador_de_legendas import ConfiguracaoDeLegendas, GeradorDeLegendas, LegendaGerada
__all__ = ["ConfiguracaoDeLegendas", "GeradorDeLegendas", "LegendaGerada"]
@@ -0,0 +1,141 @@
"""Geração offline de legendas sincronizadas com uma timeline pós-corte.
Este módulo transforma palavras já temporizadas em blocos curtos de texto.
Ele não chama IA, não lê arquivos e não altera o Premiere; essas integrações
devem permanecer nos adaptadores e no caso de uso que o coordena.
"""
from __future__ import annotations
from dataclasses import dataclass
from ...scanner.modelos import PalavraDeTranscricao
from ..modelos import AcaoDeEdicao, TipoDeAcao
@dataclass(frozen=True)
class ConfiguracaoDeLegendas:
"""Define os limites determinísticos usados para agrupar palavras."""
maximo_de_palavras: int = 5
maximo_de_caracteres: int = 32
pausa_para_quebrar_em_segundos: float = 0.65
def __post_init__(self) -> None:
if self.maximo_de_palavras < 1:
raise ValueError("O máximo de palavras deve ser maior que zero.")
if self.maximo_de_caracteres < 1:
raise ValueError("O máximo de caracteres deve ser maior que zero.")
if self.pausa_para_quebrar_em_segundos < 0:
raise ValueError("A pausa para quebra não pode ser negativa.")
@dataclass(frozen=True)
class LegendaGerada:
"""Representa um bloco de legenda já posicionado na timeline final."""
texto: str
inicio: float
fim: float
class GeradorDeLegendas:
"""Converte palavras da fonte em blocos de legenda pós-corte."""
def gerar(
self,
palavras: list[PalavraDeTranscricao],
cortes: list[AcaoDeEdicao],
duracao_da_fonte: float,
configuracao: ConfiguracaoDeLegendas | None = None,
) -> list[LegendaGerada]:
"""Gera legendas usando somente palavras que permanecem no vídeo.
Parâmetros:
palavras: Palavras com tempos em segundos da mídia original.
cortes: Ações ``cut`` em coordenadas da mídia original.
duracao_da_fonte: Duração total da mídia original.
configuracao: Limites de agrupamento; usa os padrões quando omitida.
Retorna:
Lista ordenada de blocos com tempos na timeline depois dos cortes.
"""
configuracao = configuracao or ConfiguracaoDeLegendas()
self._validar_duracao(duracao_da_fonte)
intervalos_de_corte = self._normalizar_cortes(cortes, duracao_da_fonte)
palavras_mantidas = [
palavra for palavra in sorted(palavras, key=lambda item: (item.inicio, item.fim))
if self._palavra_mantida(palavra, intervalos_de_corte)
]
palavras_reposicionadas = [
(palavra, self._reposicionar_tempo(palavra, intervalos_de_corte))
for palavra in palavras_mantidas
]
return self._agrupar(palavras_reposicionadas, configuracao)
@staticmethod
def _validar_duracao(duracao_da_fonte: float) -> None:
"""Valida a duração usada para limitar os intervalos."""
if duracao_da_fonte <= 0:
raise ValueError("A duração da fonte deve ser maior que zero.")
@staticmethod
def _normalizar_cortes(cortes: list[AcaoDeEdicao], duracao_da_fonte: float) -> list[tuple[float, float]]:
"""Ordena, limita e valida as ações de corte."""
intervalos = sorted(
(acao.inicio, min(acao.fim, duracao_da_fonte))
for acao in cortes
if acao.tipo is TipoDeAcao.CORTE and acao.inicio < duracao_da_fonte
)
anteriores: list[tuple[float, float]] = []
for inicio, fim in intervalos:
if fim <= inicio:
continue
if anteriores and inicio < anteriores[-1][1]:
raise ValueError("Os cortes das legendas não podem se sobrepor.")
anteriores.append((max(0.0, inicio), fim))
return anteriores
@staticmethod
def _palavra_mantida(palavra: PalavraDeTranscricao, cortes: list[tuple[float, float]]) -> bool:
"""Retorna falso quando a palavra está dentro ou atravessa um corte."""
return not any(palavra.inicio < fim and palavra.fim > inicio for inicio, fim in cortes)
@staticmethod
def _reposicionar_tempo(palavra: PalavraDeTranscricao, cortes: list[tuple[float, float]]) -> tuple[float, float]:
"""Subtrai os intervalos removidos anteriores à palavra."""
deslocamento = sum(fim - inicio for inicio, fim in cortes if fim <= palavra.inicio)
return palavra.inicio - deslocamento, palavra.fim - deslocamento
def _agrupar(
self,
palavras: list[tuple[PalavraDeTranscricao, tuple[float, float]]],
configuracao: ConfiguracaoDeLegendas,
) -> list[LegendaGerada]:
"""Agrupa palavras consecutivas respeitando os limites configurados."""
legendas: list[LegendaGerada] = []
bloco: list[tuple[PalavraDeTranscricao, tuple[float, float]]] = []
quantidade_de_caracteres = 0
for palavra, intervalo in palavras:
texto = palavra.texto.strip()
separador = 1 if bloco else 0
estoura_limites = (
len(bloco) >= configuracao.maximo_de_palavras
or quantidade_de_caracteres + separador + len(texto) > configuracao.maximo_de_caracteres
)
tem_pausa = bool(bloco) and intervalo[0] - bloco[-1][1][1] >= configuracao.pausa_para_quebrar_em_segundos
if bloco and (estoura_limites or tem_pausa):
legendas.append(self._criar_legenda(bloco))
bloco = []
quantidade_de_caracteres = 0
bloco.append((palavra, intervalo))
quantidade_de_caracteres += (1 if len(bloco) > 1 else 0) + len(texto)
if bloco:
legendas.append(self._criar_legenda(bloco))
return legendas
@staticmethod
def _criar_legenda(bloco: list[tuple[PalavraDeTranscricao, tuple[float, float]]]) -> LegendaGerada:
"""Materializa um bloco de palavras em uma legenda."""
texto = " ".join(palavra.texto.strip() for palavra, _ in bloco)
return LegendaGerada(texto=texto, inicio=bloco[0][1][0], fim=bloco[-1][1][1])