- Adicionado estrutura completa do projeto - Configurado MCP server para Premiere Pro - Adicionado documentação e skills - Configurado Gitignore para o projeto
211 lines
8.2 KiB
Python
Executable File
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())
|