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:
Executable
+210
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Aplica um plano de "actions" (cut/zoom/text/marker — formato da skill
|
||||
selecao-trechos) numa sequência do Premiere Pro, do mesmo jeito que foi
|
||||
feito manualmente nesta sessão: cria um backup da sequência, roda
|
||||
scripts/apply-editorial-actions.mjs (Node) e acompanha o progresso.
|
||||
|
||||
Por que isso chama Node em vez de falar com o Premiere direto em Python:
|
||||
o bridge até o Premiere (CEP + arquivos de comando/resposta, ver
|
||||
src/bridge/file-bridge.ts) só existe implementado em src/tools/*.ts. Este
|
||||
arquivo não duplica esse protocolo em Python — ele orquestra o mesmo
|
||||
pipeline Node já validado, exatamente como o painel CEP faz em
|
||||
cep-plugin/main.js:applyEditorialActions(). Se um dia fizer sentido remover
|
||||
a dependência de Node, isso significa reescrever o bridge inteiro, não só
|
||||
este script.
|
||||
|
||||
Uso:
|
||||
python3 apply_editorial_plan.py --plan <actions.json> --sequence "<nome>" \
|
||||
[--video-track 0] [--audio-track 0] [--dry-run] [--no-backup]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
CONFIG_FILE = Path.home() / ".premiere-mcp" / "config.json"
|
||||
# Mesmo fallback fixo de cep-plugin/main.js (EDITORIAL_ACTIONS_APPLY_SCRIPT_FALLBACK).
|
||||
FALLBACK_APPLY_SCRIPT = Path(
|
||||
"/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/scripts/apply-editorial-actions.mjs"
|
||||
)
|
||||
|
||||
|
||||
def load_repo_root() -> Optional[str]:
|
||||
"""Lê só a chave repoRoot — o config também guarda hfToken; nunca
|
||||
logamos o arquivo inteiro."""
|
||||
try:
|
||||
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return None
|
||||
repo_root = config.get("repoRoot")
|
||||
return repo_root if isinstance(repo_root, str) and repo_root.strip() else None
|
||||
|
||||
|
||||
def resolve_apply_script() -> Path:
|
||||
"""Mesma ordem de cep-plugin/main.js:resolveScript(): repoRoot do
|
||||
config > relativo a este arquivo (que já mora em code/scripts/) >
|
||||
caminho fixo de fallback."""
|
||||
candidates = []
|
||||
repo_root = load_repo_root()
|
||||
if repo_root:
|
||||
candidates.append(Path(repo_root) / "scripts" / "apply-editorial-actions.mjs")
|
||||
candidates.append(Path(__file__).resolve().parent / "apply-editorial-actions.mjs")
|
||||
candidates.append(FALLBACK_APPLY_SCRIPT)
|
||||
for candidate in candidates:
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
raise FileNotFoundError(
|
||||
"apply-editorial-actions.mjs não encontrado em nenhum dos caminhos conhecidos: "
|
||||
+ ", ".join(str(c) for c in candidates)
|
||||
+ ". Rode scripts/install-cep.sh para gravar o repoRoot em " + str(CONFIG_FILE) + "."
|
||||
)
|
||||
|
||||
|
||||
def resolve_node_bin() -> str:
|
||||
"""Mesma ordem de cep-plugin/main.js:resolveNodeBin()."""
|
||||
candidates = (
|
||||
["node.exe"]
|
||||
if os.name == "nt"
|
||||
else ["/opt/homebrew/bin/node", "/usr/local/bin/node", "/usr/bin/node", "node"]
|
||||
)
|
||||
for candidate in candidates:
|
||||
if "/" not in candidate or os.path.exists(candidate):
|
||||
return candidate
|
||||
return "node"
|
||||
|
||||
|
||||
def create_sequence_backup(node_bin: str, repo_root: Path, sequence: str) -> bool:
|
||||
"""Clona a sequência ativa antes de aplicar — mesma ideia do painel CEP
|
||||
(app.project.activeSequence.clone()), só que via um processo Node novo
|
||||
que importa dist/tools/sequence.js na hora, em vez de reusar um bridge
|
||||
já conectado. Isso evita depender de um servidor MCP que pode estar com
|
||||
o dist antigo em cache (foi exatamente esse tipo de cache que causou um
|
||||
zoom "fantasma" na sessão anterior)."""
|
||||
sequence_module = (repo_root / "dist" / "tools" / "sequence.js").as_posix()
|
||||
node_snippet = (
|
||||
f"import({json.dumps(sequence_module)}).then(async ({{ getSequenceTools }}) => {{"
|
||||
" const seq = getSequenceTools({});"
|
||||
f" const res = await seq.duplicate_sequence.handler({{ sequence_id: {json.dumps(sequence)} }});"
|
||||
" if (res && res.success === false) {"
|
||||
" console.error('Falha ao criar backup: ' + res.error);"
|
||||
" process.exit(1);"
|
||||
" }"
|
||||
" console.error('[apply_editorial_plan] Backup da sequência criado.');"
|
||||
"}).catch((err) => { console.error('Falha ao criar backup: ' + err.message); process.exit(1); });"
|
||||
)
|
||||
result = subprocess.run([node_bin, "-e", node_snippet], cwd=str(repo_root))
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def stream_apply(node_bin: str, apply_script: Path, args: argparse.Namespace) -> int:
|
||||
cmd = [
|
||||
node_bin,
|
||||
str(apply_script),
|
||||
"--plan", args.plan,
|
||||
"--sequence", args.sequence,
|
||||
"--video-track", str(args.video_track),
|
||||
"--audio-track", str(args.audio_track),
|
||||
]
|
||||
if args.dry_run:
|
||||
cmd.append("--dry-run")
|
||||
|
||||
repo_root = apply_script.parent.parent
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=str(repo_root),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
)
|
||||
assert process.stdout is not None
|
||||
progress_line_open = False
|
||||
for raw_line in process.stdout:
|
||||
line = raw_line.rstrip("\n")
|
||||
if line.startswith("@@PROGRESS "):
|
||||
try:
|
||||
payload = json.loads(line[len("@@PROGRESS "):])
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
stage = payload.get("stage", "")
|
||||
pct = payload.get("pct")
|
||||
detail = payload.get("detail", "")
|
||||
pct_str = f"{pct:5.1f}%" if isinstance(pct, (int, float)) else " ? "
|
||||
print(f"\r[{pct_str}] {stage} — {detail}".ljust(100), end="", flush=True)
|
||||
progress_line_open = True
|
||||
else:
|
||||
if progress_line_open:
|
||||
print()
|
||||
progress_line_open = False
|
||||
print(line)
|
||||
if progress_line_open:
|
||||
print()
|
||||
process.wait()
|
||||
return process.returncode
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Aplica um plano de edição (cut/zoom/text/marker) numa sequência do Premiere Pro."
|
||||
)
|
||||
parser.add_argument("--plan", required=True, help="Caminho do JSON do plano (actions).")
|
||||
parser.add_argument("--sequence", required=True, help="Nome ou ID da sequência no Premiere.")
|
||||
parser.add_argument("--video-track", type=int, default=0)
|
||||
parser.add_argument("--audio-track", type=int, default=0)
|
||||
parser.add_argument("--dry-run", action="store_true", help="Só valida e mostra o plano, não aplica nada.")
|
||||
parser.add_argument(
|
||||
"--no-backup", action="store_true", help="Pula o clone de segurança da sequência antes de aplicar."
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
plan_path = Path(args.plan)
|
||||
if not plan_path.exists():
|
||||
print(f"Arquivo do plano não encontrado: {plan_path}", file=sys.stderr)
|
||||
return 1
|
||||
try:
|
||||
plan = json.loads(plan_path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError as exc:
|
||||
print(f"O arquivo do plano não é um JSON válido: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
num_actions = len(plan.get("actions") or [])
|
||||
if not num_actions:
|
||||
print("O plano não tem nenhuma action.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
apply_script = resolve_apply_script()
|
||||
except FileNotFoundError as exc:
|
||||
print(str(exc), file=sys.stderr)
|
||||
return 1
|
||||
node_bin = resolve_node_bin()
|
||||
repo_root = apply_script.parent.parent
|
||||
|
||||
print(f"[apply_editorial_plan] {num_actions} ação(ões) no plano. Script: {apply_script}")
|
||||
|
||||
if not args.dry_run and not args.no_backup:
|
||||
print("[apply_editorial_plan] Criando backup da sequência...")
|
||||
if not create_sequence_backup(node_bin, repo_root, args.sequence):
|
||||
print(
|
||||
"[apply_editorial_plan] Não foi possível confirmar o backup automático. "
|
||||
"Ctrl+C agora para abortar, ou aguarde para aplicar mesmo assim.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
print("[apply_editorial_plan] Aplicando plano de edição na timeline...")
|
||||
return_code = stream_apply(node_bin, apply_script, args)
|
||||
if return_code != 0:
|
||||
print(f"[apply_editorial_plan] Falhou (código {return_code}).", file=sys.stderr)
|
||||
return return_code
|
||||
|
||||
print("[apply_editorial_plan] Plano de edição aplicado com sucesso!")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user