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:
@@ -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
|
||||
Reference in New Issue
Block a user