#!/usr/bin/env python3
"""Pessoa recortada (fundo transparente) no mesmo tempo do cut.mp4, para a tela verde.

Usa o recorte da skill editor-video (helpers/matte.py: modelo selfie_multiclass, fluxo óptico e filtro
guiado). O recorte é feito trecho por trecho do cut_timeline.json, então o filtro recomeça em cada corte e
a forma da tomada anterior não vaza para a próxima. Não use o `hyperframes remove-background` para isso:
ele deixa halo no cabelo e carrega a cor do fundo antigo na borda.

  python3 ferramentas/pessoa_recortada.py --edit '<EDIT>' [--de 28 --ate 32.3]
      [--skill PASTA_DA_SKILL] [--saida assets/pessoa.webm]

--skill: a pasta da skill editor-video. Sem ele, usa a skill anotada no projeto.json ("skill", que o
`build.py novo` escreve), depois a skill onde este modelo está (se rodar dentro dela), EDITOR_VIDEO_DIR (se
existir) e por fim ~/.claude/skills/editor-video e ~/.codex/skills/editor-video, nessa ordem. A primeira linha da
saída diz qual skill foi usada e de onde ela veio: confira que é a versão certa.

--de/--ate montam o vídeo só desse trecho (bem mais rápido; o recorte em si é feito no corte todo e fica
guardado). O pessoa.json anota o "inicio", e o build.py usa isso para alinhar com o cut.mp4.

Saídas: assets/pessoa.webm (VP9 com alfa, mesmo tamanho, fps e duração do cut.mp4) e assets/pessoa.json
(por trecho: onde a pessoa encosta na borda, usado pelo build.py para o lado cortado encostar na tela).
Leva uns 3 minutos por minuto de vídeo na primeira vez; depois o recorte fica guardado em <EDIT>/renders/matte.
Precisa do uv e da skill editor-video instalada. Só a biblioteca padrão do Python aqui.
"""
import argparse
import json
import os
import subprocess
import sys
import tempfile

AQUI = os.path.dirname(os.path.abspath(__file__))
PROJETO = os.path.dirname(AQUI)


def erro(msg, cod=2):
    print(f'erro: {msg}', file=sys.stderr)
    sys.exit(cod)


def contar_quadros(caminho):
    r = subprocess.run(['ffprobe', '-v', 'error', '-select_streams', 'v:0', '-count_packets', '-show_entries',
                        'stream=nb_read_packets', '-of', 'csv=p=0', caminho], capture_output=True, text=True)
    try:
        return int(r.stdout.strip().split(',')[0])
    except ValueError:
        return -1


def skill_padrao():
    """A skill editor-video instalada: EDITOR_VIDEO_DIR, depois Claude Code, depois Codex."""
    candidatas = [os.environ.get('EDITOR_VIDEO_DIR') or '',
                  os.path.join('~', '.claude', 'skills', 'editor-video'),
                  os.path.join('~', '.codex', 'skills', 'editor-video')]
    for c in candidatas:
        if c and os.path.exists(os.path.join(os.path.expanduser(c), 'helpers', 'matte.py')):
            return os.path.expanduser(c)
    return os.path.expanduser(candidatas[1])


def skill_do_projeto():
    """a skill anotada no projeto.json ("skill"), ou None"""
    try:
        with open(os.path.join(PROJETO, 'projeto.json'), encoding='utf-8') as f:
            s = str(json.load(f).get('skill') or '').strip()
    except (OSError, ValueError, AttributeError):
        return None
    return os.path.expanduser(s) if s else None


def achar_skill(explicita=None):
    """(pasta da skill, de onde veio): --skill, projeto.json, a skill onde o modelo está, a instalada"""
    if explicita:
        return os.path.abspath(os.path.expanduser(explicita)), '--skill'
    s = skill_do_projeto()
    if s:
        return os.path.abspath(s), 'projeto.json ("skill")'
    pai = os.path.dirname(PROJETO)
    if os.path.basename(PROJETO) == 'modelo-projeto' and os.path.exists(os.path.join(pai, 'helpers', 'matte.py')):
        return pai, 'a skill onde o modelo está'
    return os.path.abspath(skill_padrao()), 'a skill instalada (o projeto.json não diz qual; anote em "skill")'


def main():
    ap = argparse.ArgumentParser(description='pessoa recortada para a tela verde')
    ap.add_argument('--edit', required=True, help='pasta edit da editor-video (com cut.mp4 e cut_timeline.json)')
    ap.add_argument('--skill', default=None,
                    help='pasta da skill editor-video (padrão: o "skill" do projeto.json, a skill onde o modelo está, '
                         'EDITOR_VIDEO_DIR, ~/.claude/skills/editor-video ou ~/.codex/skills/editor-video)')
    ap.add_argument('--saida', default=os.path.join(PROJETO, 'assets', 'pessoa.webm'))
    ap.add_argument('--crf', type=int, default=20)
    ap.add_argument('--de', type=float, default=None, help='início do trecho (s do corte)')
    ap.add_argument('--ate', type=float, default=None, help='fim do trecho (s do corte)')
    a = ap.parse_args()
    edit = os.path.abspath(os.path.expanduser(a.edit))
    skill, origem = achar_skill(a.skill)
    print(f'skill: {skill} ({origem})', flush=True)
    cut = os.path.join(edit, 'cut.mp4')
    tl_p = os.path.join(edit, 'cut_timeline.json')
    for p in (cut, tl_p):
        if not os.path.exists(p):
            erro(f'falta {p}')
    if os.path.islink(cut):
        # o matte.py resolve o atalho, deixa de reconhecer o cut_timeline.json e recorta tudo como um trecho só
        # (o filtro não recomeça nos cortes e a tomada anterior vaza na seguinte)
        erro('o cut.mp4 da pasta edit é um atalho. Deixe o arquivo de verdade lá (a editor-video grava assim).')
    if not os.path.exists(os.path.join(skill, 'helpers', 'matte.py')):
        erro(f'não achei a skill editor-video em {skill} (use --skill, ou "skill" no projeto.json)')
    with open(tl_p, encoding='utf-8') as f:
        tl = json.load(f)
    fps, W, H = tl['fps'], tl['width'], tl['height']

    print('recortando a pessoa com a editor-video (a primeira vez leva uns 3 min por minuto de vídeo)...', flush=True)
    # o progresso sai na hora (a pessoa não fica olhando uma tela parada); o resumo JSON vem no fim
    # PrimoCode: o Python do editor (venv próprio) vem em PRIMOCODE_EDITOR_PYTHON; sem ele, o uv da skill
    py = os.environ.get('PRIMOCODE_EDITOR_PYTHON')
    base = [py] if py and os.path.isfile(py) else ['uv', 'run', '--directory', skill, 'python']
    proc = subprocess.Popen(base + ['helpers/matte.py', '--edit-dir', edit, '--json'], cwd=skill,
                            stdout=subprocess.PIPE, text=True, bufsize=1, env=dict(os.environ, PYTHONUNBUFFERED='1'))
    corpo = []
    for linha in proc.stdout:
        if corpo or linha.rstrip('\n') == '{':
            corpo.append(linha)
        else:
            print('  ' + linha.rstrip('\n'), flush=True)
    if proc.wait() != 0:
        erro('o recorte da editor-video falhou (veja a mensagem acima)', proc.returncode)
    if not corpo:
        erro('não entendi a resposta do matte.py')
    rep = json.loads(''.join(corpo))
    trechos = sorted(rep['ranges'], key=lambda x: x['start_frame'])
    for x, y in zip(trechos, trechos[1:]):
        if x['end_frame'] != y['start_frame']:
            erro(f'os trechos do recorte não emendam ({x["id"]} termina em {x["end_frame"]}, {y["id"]} começa em {y["start_frame"]})')

    total = trechos[-1]['end_frame'] - trechos[0]['start_frame']
    f_ini = max(0, int(round((a.de or 0) * fps)))
    f_fim = min(total, int(round(a.ate * fps)) if a.ate is not None else total)
    if f_fim <= f_ini:
        erro('--ate tem que ser maior que --de')
    os.makedirs(os.path.dirname(os.path.abspath(a.saida)), exist_ok=True)
    with tempfile.TemporaryDirectory() as tmp:
        lista = os.path.join(tmp, 'mattes.txt')
        with open(lista, 'w', encoding='utf-8') as f:
            for x in trechos:
                p = x['file'] if os.path.isabs(x['file']) else os.path.join(edit, x['file'])
                f.write("file '" + p.replace("'", "'\\''") + "'\n")
        # cor do cut.mp4 + alfa do recorte, quadro a quadro na mesma ordem (setpts alinha os dois pela contagem)
        corte = ''
        if a.de is not None or a.ate is not None:
            corte = f',trim=start_frame={f_ini}:end_frame={f_fim},setpts=N/({fps}*TB)'
        filtro = (f'[0:v]setpts=N/({fps}*TB){corte},format=rgba[c];'
                  f'[1:v]setpts=N/({fps}*TB){corte},format=gray,scale={W}:{H}:flags=area[m];'
                  f'[c][m]alphamerge,format=yuva420p[v]')
        cmd = ['ffmpeg', '-v', 'error', '-y', '-i', cut, '-f', 'concat', '-safe', '0', '-i', lista,
               '-filter_complex', filtro, '-map', '[v]', '-r', str(fps), '-c:v', 'libvpx-vp9', '-pix_fmt', 'yuva420p',
               '-b:v', '0', '-crf', str(a.crf), '-deadline', 'good', '-cpu-used', '4', '-row-mt', '1',
               '-auto-alt-ref', '0', '-metadata:s:v:0', 'alpha_mode=1', '-an', a.saida]
        print('montando o vídeo com fundo transparente...', flush=True)
        if subprocess.run(cmd).returncode != 0:
            erro('o ffmpeg falhou ao montar o pessoa.webm', 1)
    esperado = f_fim - f_ini
    feito = contar_quadros(a.saida)
    if abs(feito - esperado) > 1:
        print(f'aviso: o pessoa.webm tem {feito} quadros e o corte tem {esperado}', file=sys.stderr)
    info = dict(video=os.path.basename(a.saida), fps=fps, largura=W, altura=H, quadros=feito,
                inicio=round(f_ini / fps, 4), fim=round(f_fim / fps, 4),
                trechos=[dict(id=x['id'], de=round(x['start_frame'] / fps, 4), ate=round(x['end_frame'] / fps, 4),
                              stats=x.get('stats') or {}) for x in trechos])
    with open(os.path.splitext(a.saida)[0] + '.json', 'w', encoding='utf-8') as f:
        json.dump(info, f, ensure_ascii=False, indent=1)
    print(f'ok: {a.saida} ({feito} quadros, {len(trechos)} trechos)')


if __name__ == '__main__':
    main()
