Recorte de Ralis de Tênis (tennis-rally-cut)
Criado a partir de uma edição real em 08/11/2026: dois vídeos de treino contra parede, gerando 14 ralis. Ideia central: ouvir o som para encontrar os ralis, ver as imagens para conferir.
O que o usuário quer (regras rígidas)
- Um rali = da primeira batida até a bola morrer. O rali inteiro em um único segmento.
- Não cortar um rali em pedaços. Se houve 10 batidas seguidas contra a parede, é um segmento único, não pode ser dividido em vários de 2 ou 3 batidas.
- Pegar bola, andar, pausas, pegar o celular, guardar o equipamento: tudo removido.
- O vídeo original não é alterado. Os arquivos finais ficam na pasta
Recortes/no mesmo diretório do vídeo original.
Por que usar som e não imagem
- Cada batida produz dois sons nítidos: "raquete na bola" e "bola na parede/chão". Pegar bola ou andar não tem esses sons.
- Movimento na imagem não funciona: pegar bola gera mais variação de imagem do que o jogo.
- A imagem é usada apenas para verificação pontual.
Ferramentas
Script: scripts/rally_cut.py. Depende apenas de ffmpeg, ffprobe e numpy.
| Subcomando | Função |
|---|---|
probe | Ver resolução, bitrate, orientação; avisa se foi comprimido por app de mensagem |
analyze | Encontrar sons de batida, agrupar em ralis, gerar <nome>.segments.json |
sheets | Criar uma imagem de quadro para início e fim de cada rali, para verificação |
cut | Cortar cada rali em mp4 independente, gerar Lista de Segmentos.txt |
align | Quando trocar para a fonte HD original, calcular o deslocamento temporal entre as versões |
Daqui em diante, RC é o caminho absoluto do script. Os arquivos intermediários ficam no diretório temporário da sessão, não no diretório de vídeo do usuário.
Fluxo
1. Primeiro, verifique a qualidade
python3 -I $RC probe <vídeo...>
- Se aparecer "possivelmente comprimido por app de mensagem", avise o usuário primeiro. O corte em si quase não perde qualidade; se o original estiver borrado, o resultado também estará.
- Vídeos enviados pelo WeChat geralmente ficam no cache com sufixo
_raw:~/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/<conta>/msg/video/<ano-mês>/<nome>_raw.mp4Só baixa quando o usuário clica em "Ver original" no WeChat. Usemdfindpara buscar por data de adição:mdfind -onlyin ~ 'kMDItemContentTypeTree == "public.movie" && kMDItemDateAdded >= $time.iso(<data>)' - Depois de encontrar o original HD, use
alignpara confirmar deslocamento zero, e então poderá reaproveitar os cortes já calculados.
2. Encontrar ralis
python3 -I $RC analyze <vídeo...> --work <dir_temporario>
Ele imprime uma barra temporal de sons de batida por segundo e a lista de ralis. Parâmetros padrão:
| Parâmetro | Padrão | Significado |
|---|---|---|
--k | 2.5 | Quantas vezes mais alto que o fundo para ser considerado som. Aumente em ambiente barulhento |
--gap | 6 | Se dois sons tiverem mais de 6s de intervalo, o rali termina. Essencial para não picotar |
--min-hits | 6 | Menos de 6 sons consecutivos são descartados como ruído (passos, pegar celular) |
--pre / --post | 1 / 1.5 | Segundos extras antes/depois do rali para garantir a batida e a queda da bola |
Compensação do --gap:
- 4s dividiria "bola morta e reinício imediato" em dois segmentos.
- 6s mantém juntos. O usuário pede para não picotar, então padrão é 6.
- Só mude para 4 se o usuário disser explicitamente "cada reinício deve ser um segmento separado".
3. Verifique visualmente (não pule)
python3 -I $RC sheets <vídeo> --segments <json> --work <dir_temporario>/sheets
Veja cada imagem, focando em:
- Último rali do vídeo: Pode ter alguém andando em direção à câmera, pegando celular, e o som pode ser interpretado como batida. Se vir, ajuste
endpara cerca de 1,5s após a última batida real. - Ralis curtos com poucas batidas: Podem ser ruído. Se ninguém está batendo na imagem, remova.
- Lacunas de ~5s entre ralis: Confirme se é fim de rali e reinício, não uma batida perdida.
Edite o array segments no JSON: altere start/end, ou remova o segmento inteiro.
4. Corte
python3 -I $RC cut <vídeo> --segments <json> --out <diretório_do_vídeo>/Recortes --label A
- Vários vídeos de origem usam
--labeldiferentes (A, B...). Nomes dos arquivos:VideoA_03_2m54s-3m22s.mp4, com tempo no original. - Precisa reencodificar (padrão H.264, CRF 17). Quadros-chave do original costumam ter 2~3s de intervalo, copiar direto cortaria errado.
- Vídeos na vertical: a orientação é tratada automaticamente pelo ffmpeg, o resultado fica correto.
- Se a pasta de destino já tiver resultados antigos, renomeie para
Recortes_antigocomo backup, não sobrescreva nem delete. - Vídeos 1080p 60fps codificam devagar; execute em segundo plano.
5. Verifique e reporte
- Use ffprobe para conferir duração, resolução, taxa de quadros de cada resultado.
- Tire um quadro de um ou dois resultados: orientação correta, imagem nítida.
- No relatório: número de segmentos, faixa de duração de cada um, diretório dos resultados (com link
file://que o Finder possa abrir; caminhos com caracteres chineses devem ser codificados em percentual). - Indique segmentos duvidosos (ex.: ralis curtos com poucas batidas) e deixe o usuário decidir se remove.
- Deixe claro que apenas conferiu visualmente alguns trechos, não assistiu cada segmento do início ao fim.
Armadilhas já enfrentadas
| Armadilha | Consequência | Solução |
|---|---|---|
Chamar ffmpeg em loop sem -nostdin | ffmpeg engolia linhas seguintes do script, causando cortes errados e crash | Adicionado ao script |
| Converter tempo como "quadros × 10ms" | Cada quadro na verdade tem 220 amostras; 7 minutos desvia 1 segundo | Converter por amostras |
| Usar movimento da imagem para detectar jogo | Pegar bola mexia mais que o jogo | Usar apenas som |
| Usar vídeo de encaminhamento comprimido | Resultado borrado, usuário achou que foi erro de corte | Passo 1: probe primeiro |