Automatizando o Pipeline com FFmpeg
Com Python + FFmpeg você cria um pipeline que processa, converte e organiza tudo automaticamente. Humanos só são necessários no início e no fim.
Como o pipeline fica
after renderiza ProRes |Script detecta novo arquivo |FFmpeg converte para múltiplos formatos |Arquivos organizados por plataforma |Notificação enviada (email, Slack)Pipeline completo: render para entrega
import subprocess, os, glob, jsonfrom pathlib import Path
class RenderPipeline: def __init__(self, project_name, output_dir): self.project = project_name self.output_dir = Path(output_dir) self.output_dir.mkdir(parents=True, exist_ok=True)
def sequence_to_prores(self, pattern, fps=24): output = self.output_dir / f"{self.project}_master.mov" subprocess.run([ 'ffmpeg', '-framerate', str(fps), '-i', pattern, '-c:v', 'prores_ks', '-profile:v', '3', '-c:a', 'pcm_s16le', str(output) ], check=True) return output
def export_youtube(self, master): output = self.output_dir / f"{self.project}_youtube.mp4" subprocess.run([ 'ffmpeg', '-i', str(master), '-c:v', 'libx264', '-crf', '18', '-preset', 'slow', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-b:a', '192k', '-movflags', '+faststart', str(output) ], check=True) return output
def export_instagram(self, master): output = self.output_dir / f"{self.project}_instagram.mp4" subprocess.run([ 'ffmpeg', '-i', str(master), '-vf', 'crop=1080:1920:420:0', '-c:v', 'libx264', '-crf', '18', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-b:a', '192k', str(output) ], check=True) return output
def run(self, frame_pattern, fps=24): print(f"Pipeline: {self.project}")
print("1/3 Convertendo sequência para ProRes...") master = self.sequence_to_prores(frame_pattern, fps)
print("2/3 Exportando YouTube...") yt = self.export_youtube(master)
print("3/3 Exportando Instagram...") ig = self.export_instagram(master)
for label, path in [("YouTube", yt), ("Instagram", ig)]: size = os.path.getsize(path) / (1024*1024) print(f" {label}: {path.name} ({size:.1f}MB)")
# Usopipeline = RenderPipeline("hero_animation_v03", "/delivery/hero_animation")pipeline.run("/renders/hero_animation/frame_%04d.png", fps=24)Watch folder: processamento automático
import time, os, glob, subprocess
def watch_and_process(watch_dir, output_dir, interval=10): processed = set() print(f"Monitorando: {watch_dir}")
while True: files = set(glob.glob(os.path.join(watch_dir, '*.mov'))) new_files = files - processed
for filepath in new_files: size1 = os.path.getsize(filepath) time.sleep(2) size2 = os.path.getsize(filepath)
if size1 == size2: # arquivo estável (não está sendo gravado) print(f"Novo: {os.path.basename(filepath)}") output = os.path.join(output_dir, os.path.basename(filepath).replace('.mov', '.mp4')) subprocess.run([ 'ffmpeg', '-i', filepath, '-c:v', 'libx264', '-crf', '18', '-pix_fmt', 'yuv420p', '-movflags', '+faststart', output ]) processed.add(filepath)
time.sleep(interval)
watch_and_process('/renders/watch_folder', '/delivery/auto')Script mestre com notificação Slack
import subprocess, os, glob, time, requests, json
WEBHOOK_SLACK = 'https://hooks.slack.com/services/XXX/YYY/ZZZ'PASTA_RENDERS = '/renders/prores'PASTA_ENTREGA = '/entregas'
def converter(entrada, saida, preset): os.makedirs(os.path.dirname(saida), exist_ok=True) subprocess.run(['ffmpeg', '-i', entrada, '-y'] + preset + [saida], check=True)
def processar_arquivo(arquivo): nome = os.path.splitext(os.path.basename(arquivo))[0]
print(f'Processando: {nome}')
converter(arquivo, f'{PASTA_ENTREGA}/youtube/{nome}_youtube.mp4', ['-c:v', 'libx264', '-crf', '18', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-b:a', '192k'] )
converter(arquivo, f'{PASTA_ENTREGA}/instagram/{nome}_square.mp4', ['-vf', 'scale=1080:1080:force_original_aspect_ratio=decrease,pad=1080:1080:(ow-iw)/2:(oh-ih)/2:black', '-c:v', 'libx264', '-crf', '20', '-pix_fmt', 'yuv420p'] )
notificar(f'Entrega concluída: {nome}')
def notificar(mensagem): payload = {'text': mensagem} requests.post(WEBHOOK_SLACK, data=json.dumps(payload), headers={'Content-Type': 'application/json'})
processados = set()
print(f'Monitorando {PASTA_RENDERS}...')while True: arquivos = set(glob.glob(os.path.join(PASTA_RENDERS, '*.mov'))) novos = arquivos - processados
for arquivo in novos: processar_arquivo(arquivo) processados.add(arquivo)
time.sleep(30)Verificação de qualidade automática
import subprocess, json
def verificar_arquivo(arquivo): cmd = ['ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_streams', '-show_format', arquivo] result = subprocess.run(cmd, capture_output=True, text=True) data = json.loads(result.stdout)
video = next(s for s in data['streams'] if s['codec_type'] == 'video')
checks = { 'resolucao': f"{video['width']}x{video['height']}", 'fps': eval(video['r_frame_rate']), 'codec': video['codec_name'], 'duracao': float(data['format']['duration']) }
return checks
info = verificar_arquivo('/entregas/youtube/projeto_youtube.mp4')print(f"Codec: {info['codec']}")print(f"Resolução: {info['resolucao']}")print(f"FPS: {info['fps']:.2f}")print(f"Duração: {info['duracao']:.1f}s")Tip
O truque do size1 == size2 no watch folder é importante: garante que o arquivo já terminou de ser gravado antes de tentar processar. Sem isso, você tenta converter um ProRes que o after ainda está escrevendo.