feat: criada a aba Análises no painel CEP para ler do banco (anali

- criada a aba Análises no painel CEP para ler do banco (analises.db) e copiar/abrir o J-SOM de som (transcrição por cena) e o de imagem (evidências visuais), com novo módulo engine/gerar_relatorio_de_analises.py e testes
- criado o módulo engine/editor (leitura do plano de edição em JSON, tradução de tempos de origem para a timeline ativa e aplicação de cortes/zooms/marcadores no Premiere via MCP) e o ponto de entrada engine/aplicar_plano_de_edicao.py, com testes; corrigido engine/integracoes/premiere/cliente_mcp.py para levantar os erros específicos já declarados em erros_mcp.py em vez de RuntimeError genérico

Resumo:
- 15 arquivos alterados
- 8 novos
- 5 modificados
- 2 removidos

 7 files changed, 172 insertions(+), 8 deletions(-)

Arquivos:
  - .jhonny/analises.db
  - .jhonny/analises.db-shm
  - .jhonny/analises.db-wal
  - code/cep-plugin/index.html
  - code/cep-plugin/main.js
  - code/cep-plugin/styles.css
  - code/engine/integracoes/premiere/cliente_mcp.py
  - code/engine/aplicar_plano_de_edicao.py
  - code/engine/editor/
  - code/engine/gerar_relatorio_de_analises.py
  - code/engine/testes/duplos_de_premiere.py
  - code/engine/testes/test_aplicador_de_plano_de_edicao.py
  - code/engine/testes/test_gerar_relatorio_de_analises.py
  - code/engine/testes/test_leitor_de_plano_de_edicao.py
  - code/engine/testes/test_mapeador_de_tempo.py
This commit is contained in:
João Henrique
2026-09-08 19:35:30 -04:00
parent 400c3c93dd
commit 4308c72441
24 changed files with 1591 additions and 8 deletions
@@ -0,0 +1,5 @@
"""Tradução de tempos de origem para posições na timeline ativa."""
from .mapeador_de_tempo import MapeadorDeTempoDeOrigemParaTimeline
__all__ = ["MapeadorDeTempoDeOrigemParaTimeline"]
@@ -0,0 +1,81 @@
"""Tradução de tempos do arquivo de origem para posições na timeline ativa.
O plano de edição descreve tudo em segundos do arquivo de origem (regra do
formato: nunca compensar para "depois do corte"). Só a sequência do Premiere,
lida no momento da aplicação, sabe onde cada trecho de origem está
atualmente na timeline — por isso este mapeador nunca usa tempos gravados
durante a análise (ex.: ``inicio_na_timeline`` do banco de análises), sempre
o clipe lido ao vivo via :class:`~engine.dominio.Clipe`.
"""
from __future__ import annotations
from ...dominio import Clipe, Faixa, Timeline
from ..erros import ErroDeMapeamentoDeTempo
_TOLERANCIA_EM_SEGUNDOS = 0.05
class MapeadorDeTempoDeOrigemParaTimeline:
"""Localiza clipes pelo tempo de origem e converte instantes para a timeline."""
def clipes_do_arquivo(self, faixa: Faixa, arquivo_de_origem: str) -> list[Clipe]:
"""Clipes da faixa que pertencem a ``arquivo_de_origem``, ordenados pelo início na origem.
A comparação ignora maiúsculas/minúsculas porque o Premiere e o plano
de edição podem registrar a extensão do arquivo em casos diferentes
(``.MP4`` vs. ``.mp4``).
"""
alvo = arquivo_de_origem.strip().lower()
candidatos = [
clipe for clipe in faixa.clipes
if clipe.intervalo_na_origem is not None and (clipe.nome or "").strip().lower() == alvo
]
return sorted(candidatos, key=lambda clipe: clipe.intervalo_na_origem.inicio)
def localizar_clipe_no_instante(self, clipes: list[Clipe], instante_de_origem: float) -> Clipe | None:
"""Devolve o clipe cujo intervalo de origem contém ``instante_de_origem``, ou ``None``.
Tolera pequenas diferenças de arredondamento (meio quadro) nas bordas
do intervalo.
"""
for clipe in clipes:
intervalo = clipe.intervalo_na_origem
if intervalo is None:
continue
if intervalo.inicio - _TOLERANCIA_EM_SEGUNDOS <= instante_de_origem <= intervalo.fim + _TOLERANCIA_EM_SEGUNDOS:
return clipe
return None
def instante_na_timeline(self, clipe: Clipe, instante_de_origem: float) -> float:
"""Converte um instante de origem, dentro do intervalo de origem de ``clipe``, para a timeline.
Levanta :class:`ErroDeMapeamentoDeTempo` se ``clipe`` não tiver
intervalo de origem conhecido — não há como calcular o deslocamento
sem ele.
"""
if clipe.intervalo_na_origem is None:
raise ErroDeMapeamentoDeTempo(
f"O clipe {clipe.identificador!r} não tem intervalo de origem conhecido; "
"não é possível traduzir o tempo para a timeline."
)
deslocamento = instante_de_origem - clipe.intervalo_na_origem.inicio
return clipe.intervalo_na_timeline.inicio + deslocamento
def instante_na_timeline_por_arquivo(
self, timeline: Timeline, faixa: Faixa, arquivo_de_origem: str, instante_de_origem: float
) -> float:
"""Atalho que localiza o clipe do instante em ``faixa`` e já devolve a posição na timeline.
Levanta :class:`ErroDeMapeamentoDeTempo` se nenhum clipe de
``arquivo_de_origem`` cobrir ``instante_de_origem`` nessa faixa.
"""
del timeline # mantido na assinatura para leitura clara no local de chamada
clipes = self.clipes_do_arquivo(faixa, arquivo_de_origem)
clipe = self.localizar_clipe_no_instante(clipes, instante_de_origem)
if clipe is None:
raise ErroDeMapeamentoDeTempo(
f"Nenhum clipe de {arquivo_de_origem!r} na faixa {faixa.identificador!r} "
f"cobre o instante {instante_de_origem}s de origem."
)
return self.instante_na_timeline(clipe, instante_de_origem)