#!/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 --sequence "" \ [--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())