feat: initial commit - Jhonny Editor
- Adicionado estrutura completa do projeto - Configurado MCP server para Premiere Pro - Adicionado documentação e skills - Configurado Gitignore para o projeto
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
from .cliente_mcp import ClienteMCP, ClienteMCPPorStdio
|
||||
from .erros_mcp import ErroMCP, ErroDeConexaoMCP, ErroDeRespostaMCP, ErroDeFerramentaMCP
|
||||
|
||||
__all__ = ["ClienteMCP", "ClienteMCPPorStdio", "ErroMCP", "ErroDeConexaoMCP", "ErroDeRespostaMCP", "ErroDeFerramentaMCP"]
|
||||
from .sessao_mcp import SessaoMCP
|
||||
|
||||
__all__ = ["SessaoMCP"]
|
||||
@@ -0,0 +1,71 @@
|
||||
from abc import ABC, abstractmethod
|
||||
import json
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
|
||||
class ClienteMCP(ABC):
|
||||
"""Define a comunicação técnica com o servidor MCP."""
|
||||
|
||||
@abstractmethod
|
||||
def conectar(self) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
def desconectar(self) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
def esta_conectado(self) -> bool: ...
|
||||
|
||||
@abstractmethod
|
||||
def chamar(self, nome_da_ferramenta: str, argumentos: dict[str, Any] | None = None) -> dict[str, Any]: ...
|
||||
|
||||
|
||||
class ClienteMCPPorStdio(ClienteMCP):
|
||||
"""Cliente MCP para o servidor local do Premiere via stdio."""
|
||||
|
||||
def __init__(self, comando: list[str], tempo_limite: float = 30.0) -> None:
|
||||
self.comando = comando
|
||||
self.tempo_limite = tempo_limite
|
||||
self._processo: subprocess.Popen[str] | None = None
|
||||
self._proximo_id = 1
|
||||
|
||||
def conectar(self) -> None:
|
||||
if self._processo is not None:
|
||||
return
|
||||
self._processo = subprocess.Popen(
|
||||
self.comando,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
)
|
||||
self._enviar("initialize", {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "engine", "version": "0.1.0"}})
|
||||
|
||||
def desconectar(self) -> None:
|
||||
if self._processo is not None:
|
||||
self._processo.terminate()
|
||||
self._processo = None
|
||||
|
||||
def esta_conectado(self) -> bool:
|
||||
return self._processo is not None and self._processo.poll() is None
|
||||
|
||||
def chamar(self, nome_da_ferramenta: str, argumentos: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
if not self.esta_conectado():
|
||||
raise RuntimeError("Cliente MCP não está conectado.")
|
||||
resposta = self._enviar("tools/call", {"name": nome_da_ferramenta, "arguments": argumentos or {}})
|
||||
if resposta.get("error"):
|
||||
raise RuntimeError(f"Erro MCP: {resposta['error']}")
|
||||
return resposta.get("result", {})
|
||||
|
||||
def _enviar(self, metodo: str, parametros: dict[str, Any]) -> dict[str, Any]:
|
||||
if self._processo is None or self._processo.stdin is None or self._processo.stdout is None:
|
||||
raise RuntimeError("Processo MCP indisponível.")
|
||||
identificador = self._proximo_id
|
||||
self._proximo_id += 1
|
||||
self._processo.stdin.write(json.dumps({"jsonrpc": "2.0", "id": identificador, "method": metodo, "params": parametros}) + "\n")
|
||||
self._processo.stdin.flush()
|
||||
linha = self._processo.stdout.readline()
|
||||
if not linha:
|
||||
raise RuntimeError("O MCP encerrou sem retornar resposta.")
|
||||
return json.loads(linha)
|
||||
@@ -0,0 +1,5 @@
|
||||
from .conversor_de_timeline import ConversorDeTimeline
|
||||
from .conversor_de_faixas import ConversorDeFaixas
|
||||
from .conversor_de_clipes import ConversorDeClipes
|
||||
|
||||
__all__ = ["ConversorDeTimeline", "ConversorDeFaixas", "ConversorDeClipes"]
|
||||
@@ -0,0 +1,26 @@
|
||||
from typing import Any
|
||||
|
||||
from ....dominio import Clipe, IntervaloDeTempo
|
||||
from ....scanner.modelos import ErroDeAnalise
|
||||
|
||||
|
||||
class ConversorDeClipes:
|
||||
def converter(self, dados: dict[str, Any], identificador_da_faixa: str, indice: int = 0) -> Clipe:
|
||||
inicio = dados.get("start", dados.get("timeline_start"))
|
||||
fim = dados.get("end", dados.get("timeline_end"))
|
||||
identificador = dados.get("nodeId", dados.get("id"))
|
||||
if identificador is None or inicio is None or fim is None:
|
||||
raise ErroDeAnalise("resposta_incompleta", "Clipe sem identificador ou intervalo obrigatório.",
|
||||
f"clipes[{indice}]", {"campos": list(dados)})
|
||||
try:
|
||||
intervalo = IntervaloDeTempo(float(inicio), float(fim))
|
||||
origem_inicio = dados.get("inPoint", dados.get("source_start"))
|
||||
origem_fim = dados.get("outPoint", dados.get("source_end"))
|
||||
origem = (IntervaloDeTempo(float(origem_inicio), float(origem_fim))
|
||||
if origem_inicio is not None and origem_fim is not None else None)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ErroDeAnalise("intervalo_invalido", str(exc), f"clipes[{indice}]") from exc
|
||||
return Clipe(str(identificador), str(dados.get("name", "")), intervalo, origem,
|
||||
dados.get("sourceFile"), identificador_da_faixa,
|
||||
bool(dados.get("offline", False)), dict(dados.get("metadata", {})))
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from typing import Any
|
||||
from ....dominio import Faixa
|
||||
from .conversor_de_clipes import ConversorDeClipes
|
||||
|
||||
|
||||
class ConversorDeFaixas:
|
||||
def __init__(self, conversor_de_clipes: ConversorDeClipes | None = None) -> None:
|
||||
self.conversor_de_clipes = conversor_de_clipes or ConversorDeClipes()
|
||||
|
||||
def converter(self, dados: dict[str, Any], tipo: str, indice: int) -> Faixa:
|
||||
identificador = str(dados.get("id", f"{tipo}_{indice}"))
|
||||
faixa = Faixa(identificador, str(dados.get("name", "")), tipo,
|
||||
int(dados.get("index", indice)))
|
||||
faixa.clipes = [self.conversor_de_clipes.converter(clipe, identificador, i)
|
||||
for i, clipe in enumerate(dados.get("clips", []))]
|
||||
return faixa
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
from typing import Any
|
||||
|
||||
from ....dominio import Timeline
|
||||
from .conversor_de_faixas import ConversorDeFaixas
|
||||
|
||||
|
||||
class ConversorDeTimeline:
|
||||
"""Converte a resposta externa do MCP em objetos do domínio."""
|
||||
|
||||
def __init__(self, conversor_de_faixas: ConversorDeFaixas | None = None) -> None:
|
||||
self.conversor_de_faixas = conversor_de_faixas or ConversorDeFaixas()
|
||||
|
||||
def converter(self, dados_brutos: dict[str, Any]) -> Timeline:
|
||||
if not isinstance(dados_brutos, dict) or dados_brutos.get("id") is None:
|
||||
from ....scanner.modelos import ErroDeAnalise
|
||||
raise ErroDeAnalise("timeline_invalida", "A timeline precisa de um identificador.", "timeline.id")
|
||||
timeline = Timeline(
|
||||
identificador=str(dados_brutos["id"]),
|
||||
nome=str(dados_brutos.get("name", "")),
|
||||
duracao=dados_brutos.get("duration"),
|
||||
taxa_de_quadros=dados_brutos.get("frameRate"),
|
||||
largura=dados_brutos.get("frameSizeHorizontal"),
|
||||
altura=dados_brutos.get("frameSizeVertical"),
|
||||
)
|
||||
faixas_de_video = dados_brutos.get("videoTracks", dados_brutos.get("video_tracks", []))
|
||||
faixas_de_audio = dados_brutos.get("audioTracks", dados_brutos.get("audio_tracks", []))
|
||||
for tipo, faixas in (("video", faixas_de_video), ("audio", faixas_de_audio)):
|
||||
timeline.faixas.extend(self.conversor_de_faixas.converter(f, tipo, i)
|
||||
for i, f in enumerate(faixas))
|
||||
return timeline
|
||||
@@ -0,0 +1,14 @@
|
||||
class ErroMCP(Exception):
|
||||
"""Erro geral da integração com o MCP."""
|
||||
|
||||
|
||||
class ErroDeConexaoMCP(ErroMCP):
|
||||
"""Erro ao conectar ao MCP."""
|
||||
|
||||
|
||||
class ErroDeFerramentaMCP(ErroMCP):
|
||||
"""Erro ao executar uma ferramenta MCP."""
|
||||
|
||||
|
||||
class ErroDeRespostaMCP(ErroMCP):
|
||||
"""Erro quando a resposta do MCP é inválida."""
|
||||
@@ -0,0 +1,3 @@
|
||||
from .acesso_ao_editor import AcessoAClipes, AcessoAoEditor, AcessoAFaixas, AcessoASequencia, AcessoATimeline
|
||||
|
||||
__all__ = ["AcessoAoEditor", "AcessoATimeline", "AcessoASequencia", "AcessoAFaixas", "AcessoAClipes"]
|
||||
@@ -0,0 +1,47 @@
|
||||
from typing import Any
|
||||
|
||||
from ..cliente_mcp import ClienteMCP
|
||||
|
||||
|
||||
class AcessoATimeline:
|
||||
"""Realiza consultas técnicas da timeline por meio do MCP."""
|
||||
|
||||
def __init__(self, cliente_mcp: ClienteMCP, nome_da_ferramenta: str = "get_active_sequence") -> None:
|
||||
self.cliente_mcp = cliente_mcp
|
||||
self.nome_da_ferramenta = nome_da_ferramenta
|
||||
|
||||
def obter_timeline_ativa(self) -> dict[str, Any]:
|
||||
resposta = self.cliente_mcp.chamar(self.nome_da_ferramenta, {})
|
||||
conteudo_estruturado = resposta.get("structuredContent", {})
|
||||
return conteudo_estruturado.get("data", resposta)
|
||||
|
||||
|
||||
class AcessoASequencia:
|
||||
"""Consulta somente os dados da sequência ativa."""
|
||||
|
||||
def __init__(self, acesso_a_timeline: AcessoATimeline) -> None:
|
||||
self.acesso_a_timeline = acesso_a_timeline
|
||||
|
||||
def obter_sequencia_ativa(self) -> dict[str, Any]:
|
||||
return self.acesso_a_timeline.obter_timeline_ativa()
|
||||
|
||||
|
||||
class AcessoAFaixas:
|
||||
def obter_faixas(self, sequencia: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
return list(sequencia.get("videoTracks", sequencia.get("video_tracks", []))) + list(
|
||||
sequencia.get("audioTracks", sequencia.get("audio_tracks", [])))
|
||||
|
||||
|
||||
class AcessoAClipes:
|
||||
def obter_clipes(self, faixa: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
return list(faixa.get("clips", []))
|
||||
|
||||
|
||||
class AcessoAoEditor:
|
||||
"""Expõe operações de leitura compreensíveis pelo domínio."""
|
||||
|
||||
def __init__(self, acesso_a_timeline: AcessoATimeline) -> None:
|
||||
self.acesso_a_timeline = acesso_a_timeline
|
||||
|
||||
def obter_timeline_ativa(self) -> dict[str, Any]:
|
||||
return self.acesso_a_timeline.obter_timeline_ativa()
|
||||
@@ -0,0 +1,17 @@
|
||||
from contextlib import AbstractContextManager
|
||||
from .cliente_mcp import ClienteMCP
|
||||
|
||||
|
||||
class SessaoMCP(AbstractContextManager["SessaoMCP"]):
|
||||
"""Garante o ciclo de vida de uma conexão MCP durante uma leitura."""
|
||||
|
||||
def __init__(self, cliente: ClienteMCP) -> None:
|
||||
self.cliente = cliente
|
||||
|
||||
def __enter__(self) -> "SessaoMCP":
|
||||
self.cliente.conectar()
|
||||
return self
|
||||
|
||||
def __exit__(self, tipo, valor, traceback) -> None:
|
||||
self.cliente.desconectar()
|
||||
|
||||
Reference in New Issue
Block a user