Files
jhonny-editor/code/scripts/apply_editorial_plan.py
T
João Henrique b541f502ba 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
2026-09-08 09:59:31 -04:00

211 lines
8.2 KiB
Python
Executable File

#!/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())