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
+5
View File
@@ -0,0 +1,5 @@
"""Leitura e validação de planos de edição."""
from .leitor_de_plano import LeitorDePlanoDeEdicao
__all__ = ["LeitorDePlanoDeEdicao"]
@@ -0,0 +1,88 @@
"""Leitura e validação do arquivo JSON de plano de edição.
Isola o formato externo (contrato ``kind``/``start``/``end``/``params``/
``reason`` das skills de seleção de trechos) do modelo interno em
:mod:`engine.editor.modelos`. Nenhuma outra parte do sistema deve interpretar
esse JSON diretamente.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from ..erros import ErroDePlanoInvalido
from ..modelos import AcaoDeEdicao, PlanoDeEdicao, TipoDeAcao
_CAMPOS_OBRIGATORIOS_DA_ACAO = ("kind", "start", "end", "reason")
class LeitorDePlanoDeEdicao:
"""Lê um arquivo de ações e devolve um :class:`PlanoDeEdicao` validado.
Não aplica nenhuma ação nem acessa o Premiere: apenas valida a estrutura
do arquivo e converte para os tipos internos do domínio.
"""
def ler(self, caminho: Path) -> PlanoDeEdicao:
"""Lê o arquivo em ``caminho`` e devolve o plano validado.
Levanta :class:`ErroDePlanoInvalido` se o arquivo não existir, não for
JSON válido ou não seguir o formato esperado.
"""
try:
texto = caminho.read_text(encoding="utf-8")
except OSError as erro:
raise ErroDePlanoInvalido(f"Não foi possível ler o plano de edição em {caminho}: {erro}") from erro
try:
dados = json.loads(texto)
except json.JSONDecodeError as erro:
raise ErroDePlanoInvalido(f"O plano de edição em {caminho} não é um JSON válido: {erro}") from erro
return self.ler_de_dicionario(dados)
def ler_de_dicionario(self, dados: Any) -> PlanoDeEdicao:
"""Converte um dicionário já carregado (ex.: de teste) em um plano validado."""
if not isinstance(dados, dict):
raise ErroDePlanoInvalido("O plano de edição precisa ser um objeto JSON no nível raiz.")
arquivo_de_origem = dados.get("source")
if not isinstance(arquivo_de_origem, str) or not arquivo_de_origem.strip():
raise ErroDePlanoInvalido("O plano de edição precisa de um campo 'source' com o nome do arquivo.")
acoes_brutas = dados.get("actions")
if not isinstance(acoes_brutas, list) or not acoes_brutas:
raise ErroDePlanoInvalido("O plano de edição precisa de uma lista não vazia em 'actions'.")
acoes = tuple(self._converter_acao(bruta, indice) for indice, bruta in enumerate(acoes_brutas))
try:
return PlanoDeEdicao(arquivo_de_origem=arquivo_de_origem, acoes=acoes)
except ValueError as erro:
raise ErroDePlanoInvalido(str(erro)) from erro
def _converter_acao(self, bruta: Any, indice: int) -> AcaoDeEdicao:
"""Converte um item de ``actions`` para :class:`AcaoDeEdicao`, com erros que apontam o índice."""
if not isinstance(bruta, dict):
raise ErroDePlanoInvalido(f"actions[{indice}] precisa ser um objeto JSON.")
faltando = [campo for campo in _CAMPOS_OBRIGATORIOS_DA_ACAO if campo not in bruta]
if faltando:
raise ErroDePlanoInvalido(f"actions[{indice}] está sem os campos obrigatórios: {', '.join(faltando)}.")
try:
tipo = TipoDeAcao(bruta["kind"])
except ValueError as erro:
tipos_validos = ", ".join(tipo.value for tipo in TipoDeAcao)
raise ErroDePlanoInvalido(
f"actions[{indice}].kind = {bruta['kind']!r} é desconhecido. Válidos: {tipos_validos}."
) from erro
try:
inicio = float(bruta["start"])
fim = float(bruta["end"])
except (TypeError, ValueError) as erro:
raise ErroDePlanoInvalido(f"actions[{indice}].start/end precisam ser numéricos.") from erro
parametros = bruta.get("params", {})
if not isinstance(parametros, dict):
raise ErroDePlanoInvalido(f"actions[{indice}].params precisa ser um objeto JSON quando presente.")
motivo = bruta["reason"]
if not isinstance(motivo, str):
raise ErroDePlanoInvalido(f"actions[{indice}].reason precisa ser texto.")
try:
return AcaoDeEdicao(tipo=tipo, inicio=inicio, fim=fim, motivo=motivo, parametros=dict(parametros))
except ValueError as erro:
raise ErroDePlanoInvalido(f"actions[{indice}]: {erro}") from erro