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:
João Henrique
2026-09-08 09:59:31 -04:00
commit b541f502ba
1507 changed files with 387650 additions and 0 deletions
@@ -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()