#!/usr/bin/env python3
"""pessoa.py — tirar você do fundo do seu próprio vídeo.

    "Recorte de pessoa: segmentar meu rosto/corpo e trocar o fundo por uma
     imagem relacionada ao que estou falando (ex.: falando sobre o olho →
     imagem do olho no fundo)."

── POR QUE ISTO NÃO É FEITO COM ffmpeg ──────────────────────────────────────
O ffmpeg sabe fazer chroma key: tirar um fundo VERDE. Ele não sabe olhar um
quadro e dizer onde termina a pessoa e começa a parede — isso é trabalho de
modelo de visão, quadro a quadro.

O MediaPipe faz exatamente isso, roda no computador da pessoa, é grátis e não
precisa de chave. O custo é o download do modelo na primeira vez.

── O QUE SAI DAQUI ──────────────────────────────────────────────────────────
Um .webm com CANAL ALFA: a pessoa recortada, e transparência no lugar do
fundo. Ele não é o resultado final — é a peça que o `video.sobreporAlfa`
costura sobre a imagem escolhida, ou que entra como elemento de vídeo numa
cena com qualquer coisa atrás.

Separar assim é de propósito: o recorte é caro (segundos por segundo de
vídeo) e o fundo muda de ideia. Recortar uma vez e testar cinco fundos custa
um recorte; recortar junto com o fundo custa cinco.

── A BORDA É O QUE DENUNCIA ─────────────────────────────────────────────────
Recorte automático erra na borda do cabelo, e o erro aparece como um contorno
duro, tipo adesivo. Duas defesas: suavizar a máscara (desfoque leve) e
encolhê-la um pouco, para o corte cair um fio DENTRO da pessoa em vez de
pegar um halo da parede.

Uso:
    python3 pessoa.py recortar --entrada gravacao.mp4 --saida pessoa.webm
    python3 pessoa.py conferir
"""

import argparse
import json
import shutil
import os
import subprocess
import sys


def achar_ffmpeg():
    """Onde está o ffmpeg — pelo PATH, e nos lugares onde ele costuma cair.

    O PATH de um processo aberto pela interface gráfica não é o PATH do
    terminal: no macOS um app aberto pelo ícone não enxerga /opt/homebrew/bin,
    onde o Homebrew instala. Procurar nos lugares conhecidos evita um "não
    achei" para quem tem o programa instalado.
    """
    achado = shutil.which("ffmpeg")
    if achado:
        return achado
    for c in ("/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg", "/usr/bin/ffmpeg",
              r"C:\Program Files\ffmpeg\bin\ffmpeg.exe"):
        if os.path.exists(c):
            return c
    return None


def faltando():
    """O que precisa estar instalado, e não está."""
    falta = []
    for modulo, pacote in (("cv2", "opencv-python"), ("mediapipe", "mediapipe"), ("numpy", "numpy")):
        try:
            __import__(modulo)
        except ImportError:
            falta.append(pacote)
    return falta


def instalar(pacotes):
    """Traz os pacotes, tentando na ordem da menos invasiva para a mais.

    Uma tentativa só não serve: o Python do Homebrew e o do Debian/Ubuntu
    recusam instalar no ambiente do sistema (PEP 668,
    "externally-managed-environment"), e nessas máquinas a instalação falharia
    sempre — aqui, calada, porque a saída é capturada e o except engole.
    """
    tentativas = (
        ["-m", "pip", "install", "--user", "--quiet", *pacotes],
        ["-m", "pip", "install", "--user", "--quiet", "--break-system-packages", *pacotes],
        ["-m", "pip", "install", "--quiet", *pacotes],
        ["-m", "pip", "install", "--quiet", "--break-system-packages", *pacotes],
    )
    for args in tentativas:
        try:
            r = subprocess.run([sys.executable] + args, capture_output=True, timeout=900)
            if r.returncode == 0 and not faltando():
                return True
        except Exception:
            continue
    return False


def recortar(entrada, saida, suavizar=7, encolher=2, limiar=0.6):
    import cv2
    import numpy as np
    import mediapipe as mp

    captura = cv2.VideoCapture(entrada)
    if not captura.isOpened():
        return {"ok": False, "error": f"não consegui abrir {entrada}"}

    fps = captura.get(cv2.CAP_PROP_FPS) or 30
    largura = int(captura.get(cv2.CAP_PROP_FRAME_WIDTH))
    altura = int(captura.get(cv2.CAP_PROP_FRAME_HEIGHT))
    total = int(captura.get(cv2.CAP_PROP_FRAME_COUNT)) or 0

    # VP9 num .webm é o formato de vídeo com transparência que o Chromium do
    # Remotion abre — e é o Chromium dele que vai desenhar isto.
    # O ffmpeg pelo nome cru estoura FileNotFoundError, e o que chega ao
    # usuário é o traceback do Python — que não diz o que instalar. Pior: aqui
    # o vídeo já foi ABERTO e a segmentação está prestes a começar, então a
    # falha aparece depois da espera, não antes dela.
    binario = achar_ffmpeg()
    if not binario:
        captura.release()
        return {"ok": False,
                "error": "o recorte de pessoa precisa do ffmpeg, e não achei nenhum nesta máquina.",
                "comoResolver": "macOS: brew install ffmpeg · Linux: sudo apt install ffmpeg · "
                                "Windows: winget install ffmpeg"}

    escritor = subprocess.Popen(
        [binario, "-hide_banner", "-loglevel", "error", "-y",
         "-f", "rawvideo", "-pix_fmt", "bgra",
         "-s", f"{largura}x{altura}", "-r", str(fps), "-i", "-",
         "-c:v", "libvpx-vp9", "-pix_fmt", "yuva420p", "-b:v", "3M",
         "-auto-alt-ref", "0", saida],
        stdin=subprocess.PIPE, stderr=subprocess.PIPE)

    segmentador = mp.solutions.selfie_segmentation.SelfieSegmentation(model_selection=1)
    nucleo = np.ones((3, 3), np.uint8)
    feitos = 0

    try:
        while True:
            ok, quadro = captura.read()
            if not ok:
                break
            resultado = segmentador.process(cv2.cvtColor(quadro, cv2.COLOR_BGR2RGB))
            mascara = (resultado.segmentation_mask > limiar).astype(np.uint8) * 255

            # Encolher: o corte cai um fio DENTRO da pessoa, em vez de levar
            # um halo da parede junto — é o halo que faz parecer adesivo.
            if encolher > 0:
                mascara = cv2.erode(mascara, nucleo, iterations=int(encolher))
            # Suavizar: borda dura no cabelo é o segundo sinal de recorte
            # automático. Ímpar porque o desfoque gaussiano exige.
            if suavizar > 0:
                k = int(suavizar) | 1
                mascara = cv2.GaussianBlur(mascara, (k, k), 0)

            bgra = cv2.cvtColor(quadro, cv2.COLOR_BGR2BGRA)
            bgra[:, :, 3] = mascara
            escritor.stdin.write(bgra.tobytes())
            feitos += 1
    finally:
        try:
            escritor.stdin.close()
        except Exception:
            pass
        captura.release()
        segmentador.close()

    escritor.wait()
    if escritor.returncode != 0:
        erro = (escritor.stderr.read() or b"").decode("utf-8", "ignore")[-400:]
        return {"ok": False, "error": f"o ffmpeg recusou o recorte: {erro}"}

    return {"ok": True, "arquivo": saida, "quadros": feitos,
            "segundos": round(feitos / fps, 2) if fps else None,
            "largura": largura, "altura": altura, "fps": round(fps, 2)}


def main():
    p = argparse.ArgumentParser(description="Recorte de pessoa do PrimoCode Studio")
    sub = p.add_subparsers(dest="comando", required=True)
    sub.add_parser("conferir")
    r = sub.add_parser("recortar")
    r.add_argument("--entrada", required=True)
    r.add_argument("--saida", required=True)
    r.add_argument("--suavizar", type=int, default=7)
    r.add_argument("--encolher", type=int, default=2)
    r.add_argument("--limiar", type=float, default=0.6)
    args = p.parse_args()

    falta = faltando()

    if args.comando == "conferir":
        saida = {"ok": not falta, "falta": falta,
                 "comoResolver": (f"{sys.executable} -m pip install --user " + " ".join(falta)
                                  + ' (se recusar com "externally-managed-environment",'
                                    " acrescente --break-system-packages)") if falta else None}
    else:
        if falta:
            # A instalação é pesada (o MediaPipe passa de 100 MB) e demora.
            # Tentar em silêncio seria uma tarefa parada sem explicação; por
            # isso o aviso sai ANTES, na saída de erro, que o agente mostra.
            print(f"instalando {', '.join(falta)} (uma vez, alguns minutos)…", file=sys.stderr)
            if not instalar(falta):
                saida = {"ok": False,
                         "error": "o recorte de pessoa precisa do MediaPipe, e não deu para instalá-lo.",
                         "comoResolver": f"{sys.executable} -m pip install " + " ".join(falta)}
                print(json.dumps(saida, ensure_ascii=False))
                sys.exit(1)
        saida = recortar(args.entrada, args.saida, args.suavizar, args.encolher, args.limiar)

    print(json.dumps(saida, ensure_ascii=False))
    sys.exit(0 if saida.get("ok") else 1)


if __name__ == "__main__":
    main()
