primocode 11.3.3 → 11.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/edit2-terceiros.md +31 -0
- package/LICENSES/models-dev-MIT.txt +25 -0
- package/README.md +2 -2
- package/editor/SKILL.md +24 -0
- package/editor/THIRD_PARTY_NOTICES.md +1 -0
- package/editor/assets/baixar.json +9 -1
- package/editor/helpers/df_limpar.py +29 -0
- package/editor/helpers/edit2.py +155 -0
- package/editor/helpers/enquadrar_camera.py +77 -0
- package/editor/helpers/face_layout.py +20 -0
- package/editor/helpers/face_mediapipe.py +147 -0
- package/editor/helpers/instalar_df.sh +7 -0
- package/editor/helpers/legenda_ass.py +124 -0
- package/editor/helpers/look_ffmpeg.py +105 -0
- package/editor/helpers/motor_palavras.py +157 -0
- package/editor/helpers/primeiro_passe.py +214 -0
- package/editor/helpers/som_edit2.py +119 -0
- package/editor/helpers/transcribe.py +31 -2
- package/editor/pyproject.toml +9 -0
- package/editor/references/edit2.md +22 -0
- package/editor/uv.lock +3342 -214
- package/kits/3d/LICENSES/README.md +11 -0
- package/kits/3d/animacoes.js +87 -0
- package/kits/3d/base.js +44 -0
- package/kits/3d/catalogo.js +38 -0
- package/kits/3d/cenario.js +162 -0
- package/kits/3d/exemplos/asfalto.mjs +91 -0
- package/kits/3d/maquinas.js +97 -0
- package/kits/3d/montador.js +142 -0
- package/kits/3d/package.json +1 -0
- package/kits/3d/personagem.js +86 -0
- package/kits/3d/remotion/Maquete.jsx +42 -0
- package/kits/3d/remotion/Root.jsx +10 -0
- package/kits/3d/render.mjs +73 -0
- package/lib/cli/comando-api.js +30 -8
- package/lib/cli/modelos-lista.js +58 -0
- package/lib/cli/sessao.js +11 -0
- package/lib/cli/subcomandos.js +3 -2
- package/lib/outra-pasta.js +1 -1
- package/lib/permissao-sistema.js +60 -0
- package/lib/permission/perigo.js +2 -1
- package/lib/permission/permissao.js +1 -0
- package/lib/permission/sem-perguntar.js +29 -0
- package/lib/plugin/desktop.js +5 -4
- package/lib/plugin/estudio/cota.js +4 -2
- package/lib/plugin/estudio/dialogo3d.js +71 -0
- package/lib/plugin/estudio/effectcraft.js +1 -1
- package/lib/plugin/estudio/glifos.js +2 -0
- package/lib/plugin/estudio/gsap-local.js +43 -0
- package/lib/plugin/estudio/index.js +63 -10
- package/lib/plugin/estudio/kit-ae/base.jsx +1 -1
- package/lib/plugin/estudio/kit-ae/cenas.jsx +7 -4
- package/lib/plugin/estudio/kit-ae/destrava.jsx +211 -0
- package/lib/plugin/estudio/maquina-diretor.js +1 -1
- package/lib/plugin/estudio/maquina-elementos.js +2 -2
- package/lib/plugin/estudio/maquina.js +26 -8
- package/lib/plugin/estudio/motion-plano/ajustar.js +139 -0
- package/lib/plugin/estudio/motion-plano/critico.js +59 -0
- package/lib/plugin/estudio/motion-plano/estrutura.js +111 -0
- package/lib/plugin/estudio/motion-plano/fatos.js +137 -0
- package/lib/plugin/estudio/motion-plano/index.js +75 -0
- package/lib/plugin/estudio/motion-plano/llm.js +59 -0
- package/lib/plugin/estudio/motion-plano/motor.js +50 -0
- package/lib/plugin/estudio/motion-plano/passos.js +38 -0
- package/lib/plugin/estudio/motion-plano/pele.js +36 -0
- package/lib/plugin/estudio/motion-plano/plano.js +152 -0
- package/lib/plugin/estudio/motion-plano/regua.js +100 -0
- package/lib/plugin/estudio/personagem.js +3 -1
- package/lib/plugin/estudio/referencias.js +1 -1
- package/lib/provider/catalogo.js +161 -0
- package/lib/provider/local/catalogo-embutido.json.gz +0 -0
- package/lib/provider/local/cerebro.json +1 -1
- package/lib/provider/local/entrada.js +10 -0
- package/lib/provider/local/motor.js +140 -27
- package/lib/provider/local/orcamento.js +193 -0
- package/lib/provider/local/provedor.js +127 -14
- package/lib/provider/local/recorte.js +170 -0
- package/lib/provider/local/revezamento.js +121 -0
- package/lib/session/runner/act.js +28 -3
- package/lib/skill/catalogo.js +9 -1
- package/lib/tool/reparar-entrada.js +1 -0
- package/lib/tool/tools.js +58 -1
- package/lib/tui/ui.js +1 -1
- package/package.json +2 -2
- package/vendor/fontes/ttf/ORIGEM.txt +1 -0
- package/vendor/fontes/ttf/inter-800.ttf +0 -0
- package/vendor/gsap/ORIGEM.txt +6 -0
- package/vendor/gsap/gsap.min.js +11 -0
- package/voz/primo_voz/chat.html +206 -5
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Edit 2.0: terceiros (licença e link)
|
|
2
|
+
|
|
3
|
+
Cada dependência nova do Edit 2.0 (`editor/pyproject.toml`, bloco "Edit 2.0") tem uma linha aqui. Licença conferida
|
|
4
|
+
em 13/11/2026 na pesquisa da equipe `[LI]` (arquivo LICENSE ou API do GitHub). Regra da casa: GPL/AGPL nunca
|
|
5
|
+
é importada; só poderia ser chamada por linha de comando, e preferimos não usar. Remotion fica de fora (licença paga).
|
|
6
|
+
|
|
7
|
+
| Peça | Licença | Como entra | Link |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| whisperx | BSD-2-Clause | importado (alinhamento palavra a palavra; diarização não usada) | https://github.com/m-bain/whisperX/blob/main/LICENSE |
|
|
10
|
+
| faster-whisper | MIT | via whisperx | https://github.com/SYSTRAN/faster-whisper/blob/master/LICENSE |
|
|
11
|
+
| wav2vec2 PT (modelo de alinhamento) | Apache-2.0 | baixado pelo whisperx | https://huggingface.co/jonatasgrosman/wav2vec2-large-xlsr-53-portuguese |
|
|
12
|
+
| whisper.cpp | MIT | só por linha de comando (`whisper-cli`), reserva do whisperx | https://github.com/ggml-org/whisper.cpp/blob/master/LICENSE |
|
|
13
|
+
| silero-vad | MIT | importado (fala vs silêncio) | https://github.com/snakers4/silero-vad/blob/master/LICENSE |
|
|
14
|
+
| auto-editor | Unlicense (domínio público) | linha de comando (1º passe do corte) | https://github.com/WyattBlue/auto-editor/blob/master/LICENSE |
|
|
15
|
+
| mediapipe | Apache-2.0 | importado (detector de rosto) | https://github.com/google-ai-edge/mediapipe/blob/master/LICENSE |
|
|
16
|
+
| BlazeFace short-range (`blaze_face_short_range.tflite`) | Apache-2.0 segundo o model card do Google `[VERIFICAR: cartão não relido nesta sessão]` | baixado na 1ª vez para `editor/assets/models/` (sha256 conferido, `assets/baixar.json`) | https://storage.googleapis.com/mediapipe-assets/MediaPipe%20BlazeFace%20Model%20Card%20(Short%20Range).pdf |
|
|
17
|
+
| pysubs2 | MIT | importado (legenda ASS) | https://github.com/tkarabela/pysubs2/blob/master/LICENSE.txt |
|
|
18
|
+
| libass (dentro do ffmpeg) | ISC | queima a legenda ASS | https://github.com/libass/libass/blob/master/COPYING |
|
|
19
|
+
| pyloudnorm | MIT | importado (mede LUFS) | https://github.com/csteinmetz1/pyloudnorm/blob/master/LICENSE |
|
|
20
|
+
| ffmpeg-normalize | MIT | importado/linha de comando (-14 LUFS) | https://github.com/slhck/ffmpeg-normalize/blob/master/LICENSE.md |
|
|
21
|
+
| DeepFilterNet | MIT ou Apache-2.0, à escolha | ambiente à parte (`~/.primocode/editor/df-venv`), chamado por processo | https://github.com/Rikorose/DeepFilterNet/blob/main/LICENSE |
|
|
22
|
+
| FFmpeg (`sidechaincompress`, `lut3d`, `noise`, `deshake`) | LGPL-2.1 (GPL se a build tiver `--enable-gpl`/libx264) | binário do sistema, por linha de comando | https://ffmpeg.org/legal.html |
|
|
23
|
+
| browser-use/video-use (só as regras do corte) | MIT | ideia, sem código novo; crédito em `editor/THIRD_PARTY_NOTICES.md` | https://github.com/browser-use/video-use/blob/main/LICENSE |
|
|
24
|
+
| pyautoflip (só a ideia da câmera suave) | MIT | nenhuma linha copiada | https://github.com/AhmedHisham1/pyautoflip |
|
|
25
|
+
|
|
26
|
+
## Fora de propósito
|
|
27
|
+
|
|
28
|
+
- Remotion: licença paga para a Paris Group. Motion e legenda especial seguem no HyperFrames.
|
|
29
|
+
- pyannote (diarização): modelos "gated" no Hugging Face. Fora por enquanto (vem como dependência do whisperx, não é chamado).
|
|
30
|
+
- vid.stab: exige build GPL do ffmpeg. Usamos `deshake`.
|
|
31
|
+
- pedalboard (GPL-3.0), ultralytics (AGPL-3.0), whisper-timestamped (AGPL-3.0), insightface (modelos não comerciais): nunca.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Catálogo de modelos lido do models.dev (https://github.com/anomalyco/models.dev), licença MIT.
|
|
2
|
+
Usado em: lib/provider/catalogo.js e lib/provider/local/catalogo-embutido.json.gz (recorte enxuto do api.json).
|
|
3
|
+
Ideia de camadas (rede, cache, snapshot embutido) vista no OpenCode (anomalyco/opencode, MIT). Código reescrito.
|
|
4
|
+
|
|
5
|
+
MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2025 models.dev
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v11.
|
|
1
|
+
# PrimoCode v11.5.0
|
|
2
2
|
|
|
3
3
|
Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
|
|
4
4
|
Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
|
|
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
|
|
|
9
9
|
está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
▐▛███▜▌ Primo Code v11.
|
|
12
|
+
▐▛███▜▌ Primo Code v11.5.0
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
package/editor/SKILL.md
CHANGED
|
@@ -67,6 +67,30 @@ aparece em cada momento e por quê. O jeito de trabalhar:
|
|
|
67
67
|
- Um render por vez na mesma pasta.
|
|
68
68
|
- Dinheiro só com o ok da pessoa: imagem, vídeo, trilha ou efeito gerado por IA, serviço pago.
|
|
69
69
|
|
|
70
|
+
## Edit 2.0: o que você decide e o que o helper mede
|
|
71
|
+
|
|
72
|
+
Para um vídeo falado de uma tomada só (ou poucas), o caminho curto é o `helpers/edit2.py` (`references/edit2.md`).
|
|
73
|
+
Ele mede; **você decide o que sai e conta em uma linha**:
|
|
74
|
+
|
|
75
|
+
- **Você decide a tomada e o erro.** Leia `fala_do_corte.md`/`takes_packed.md`, ache falso começo, palavra repetida
|
|
76
|
+
e frase dita duas vezes, e passe cada um como `--tirar a-b` (segundos da fonte). Fique com a última tomada inteira.
|
|
77
|
+
O que o `primeiro_passe.py` já resolve sozinho (VAD + onda): respiro, estalo, ruído. Nunca tire uma ressalva, número
|
|
78
|
+
ou condição que muda o sentido.
|
|
79
|
+
- **Regras que o helper garante** (do browser-use/video-use, MIT): nunca cortar dentro de palavra; folga de 30 a 200 ms
|
|
80
|
+
em cada borda; fade de 30 ms em cada emenda; pausa menor que 0,25 s não vira corte. Leia o `primeiro_passe.json`:
|
|
81
|
+
`cortes_dentro_de_palavra` tem que ser 0. Se não for, não entregue.
|
|
82
|
+
- **Palavra a palavra vem do WhisperX.** Se o relatório diz `motor: whisper.cpp` ou outro, o tempo é menos justo: diga.
|
|
83
|
+
Nome de marca ou sigla que o Whisper erra ("IA" virou "Iá"): rode de novo com `--prompt`.
|
|
84
|
+
- **Legenda simples** é ASS queimada (`legenda_ass.py`); sem libass no ffmpeg, o relatório diz que usou PNG. Legenda
|
|
85
|
+
especial e motion continuam no HyperFrames. Nada de Remotion (licença paga).
|
|
86
|
+
- **Som:** voz limpa (DeepFilterNet, se instalado), música abaixo da voz com `sidechaincompress`, tudo em -14 LUFS.
|
|
87
|
+
O número que vale é o medido (pyloudnorm) no arquivo final, não o pedido.
|
|
88
|
+
- **Rosto e cor:** o corte segue o rosto (MediaPipe, câmera suave); cor só com os filtros do ffmpeg e LUT própria
|
|
89
|
+
(`look_ffmpeg.py`). Estabilizar (`deshake`) só se a tomada tremer; nunca vid.stab.
|
|
90
|
+
- **Entrega honesta:** diga o que não rodou (DeepFilterNet ausente, libass ausente, motor de reserva) e o antes/depois
|
|
91
|
+
medido (duração, cortes, LUFS, % do rosto enquadrado). Quem aprova a qualidade é a pessoa, não você.
|
|
92
|
+
- Licença de tudo o que entra: `LICENSES/edit2-terceiros.md`. GPL/AGPL nunca é importada.
|
|
93
|
+
|
|
70
94
|
## Onde as coisas ficam
|
|
71
95
|
|
|
72
96
|
- `<SKILL>`: a pasta deste SKILL.md. Todo helper roda assim:
|
|
@@ -18,6 +18,7 @@ Arquivos deste projeto que têm código adaptado (todos começam com a linha
|
|
|
18
18
|
- `helpers/render_cut.py`
|
|
19
19
|
- `helpers/grade.py`
|
|
20
20
|
- `helpers/timeline_view.py`
|
|
21
|
+
- `helpers/primeiro_passe.py` (só as regras do corte: nunca cortar dentro de palavra, folga de 30 a 200 ms, fade de 30 ms)
|
|
21
22
|
|
|
22
23
|
Texto da licença, como publicado pelo projeto original:
|
|
23
24
|
|
|
@@ -594,6 +594,14 @@
|
|
|
594
594
|
"bytes": 16454537,
|
|
595
595
|
"grupo": "espelho",
|
|
596
596
|
"origem": "MediaPipe selfie_multiclass (Apache-2.0), convertido para ONNX pelo EDIT IA PRO"
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
"caminho": "assets/models/blaze_face_short_range.tflite",
|
|
600
|
+
"sha256": "b4578f35940bf5a1a655214a1cce5cab13eba73c1297cd78e1a04c2380b0152f",
|
|
601
|
+
"bytes": 229746,
|
|
602
|
+
"grupo": "essencial",
|
|
603
|
+
"url": "https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/1/blaze_face_short_range.tflite",
|
|
604
|
+
"origem": "Google MediaPipe BlazeFace short-range (Apache-2.0, conferir model card)"
|
|
597
605
|
}
|
|
598
606
|
]
|
|
599
|
-
}
|
|
607
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Roda DENTRO do ambiente do DeepFilterNet (~/.primocode/editor/df-venv): limpa a voz de um WAV.
|
|
2
|
+
|
|
3
|
+
Uso: <python do df-venv> df_limpar.py <entrada.wav> <saida.wav>
|
|
4
|
+
O DeepFilterNet (MIT ou Apache-2.0) trabalha a 48 kHz; entra e sai WAV mono/estéreo de 48 kHz.
|
|
5
|
+
Fica separado do ambiente do editor porque pede numpy<2 e torch antigo.
|
|
6
|
+
"""
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import soundfile as sf
|
|
11
|
+
import torch
|
|
12
|
+
from df.enhance import enhance, init_df
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main(entrada, saida):
|
|
16
|
+
modelo, estado, _ = init_df()
|
|
17
|
+
audio, sr = sf.read(entrada, dtype="float32", always_2d=True)
|
|
18
|
+
if sr != estado.sr():
|
|
19
|
+
raise SystemExit(f"esperava {estado.sr()} Hz, veio {sr}")
|
|
20
|
+
saidas = []
|
|
21
|
+
for c in range(audio.shape[1]):
|
|
22
|
+
x = torch.from_numpy(np.ascontiguousarray(audio[:, c])).unsqueeze(0)
|
|
23
|
+
saidas.append(enhance(modelo, estado, x).squeeze(0).numpy())
|
|
24
|
+
n = min(len(s) for s in saidas)
|
|
25
|
+
sf.write(saida, np.stack([s[:n] for s in saidas], axis=1), sr, subtype="PCM_16")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
main(sys.argv[1], sys.argv[2])
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Edit 2.0: o pipeline inteiro de um vídeo falado, numa chamada só (a IA decide o que tirar; o resto é medido).
|
|
2
|
+
|
|
3
|
+
1. WhisperX: palavra a palavra alinhada (whisper.cpp de reserva). motor_palavras.py
|
|
4
|
+
2. 1º passe: silero-vad + auto-editor, bordas presas nas palavras. primeiro_passe.py
|
|
5
|
+
3. A IA tira o que errou (`--tirar a-b`, em segundos da fonte) e fica com a última tomada.
|
|
6
|
+
4. Corte pela onda + render do corte (fade de 30 ms, folga), voz em -14 LUFS. corte_por_onda.py, render_cut.py
|
|
7
|
+
5. Voz limpa (DeepFilterNet, se instalado). som_edit2.py
|
|
8
|
+
6. Rosto (MediaPipe) -> câmera suavizada, cor (lut3d + grão), legenda (ASS ou PNG) em UMA passada de vídeo.
|
|
9
|
+
7. -14 LUFS (ffmpeg-normalize), LUFS medido (pyloudnorm), % do tempo com rosto enquadrado.
|
|
10
|
+
|
|
11
|
+
Uso:
|
|
12
|
+
uv run python helpers/edit2.py <video> <pasta_de_trabalho> -o <saida.mp4> [--tirar 8.09-8.59] [--look agencia]
|
|
13
|
+
[--grao 5] [--zoom 1.12] [--tamanho 1080x1920]
|
|
14
|
+
Gera <pasta>/edit/relatorio_edit2.json com o antes/depois (duração, cortes, LUFS, % enquadrado).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
import time
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
HELPERS = Path(__file__).resolve().parent
|
|
27
|
+
sys.path.insert(0, str(HELPERS))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _run(cmd: list[str]) -> None:
|
|
31
|
+
r = subprocess.run(cmd, capture_output=True, text=True)
|
|
32
|
+
if r.returncode != 0:
|
|
33
|
+
raise RuntimeError(f"{cmd[1] if len(cmd) > 1 else cmd[0]} falhou:\n{(r.stderr or r.stdout)[-600:]}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def medir_video(video: Path) -> dict:
|
|
37
|
+
import face_mediapipe # noqa: PLC0415
|
|
38
|
+
import som_edit2 # noqa: PLC0415
|
|
39
|
+
from common import probe_video # noqa: PLC0415
|
|
40
|
+
|
|
41
|
+
info = probe_video(video)
|
|
42
|
+
return {"duracao_s": round(float(info["duration"]), 2), "largura": info["width"], "altura": info["height"],
|
|
43
|
+
**som_edit2.medir_lufs(video), **face_mediapipe.percentual_enquadrado(video)}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def cues_das_palavras(palavras: list[dict]) -> dict:
|
|
47
|
+
import legenda_ass # noqa: PLC0415
|
|
48
|
+
|
|
49
|
+
return {"cues": [{"start": g[0]["start"], "end": g[-1]["end"], "words": g} for g in legenda_ass.blocos(palavras)]}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def render_final(cut: Path, palavras: list[dict], trilha: dict, saida: Path, edit: Path, look: str, grao: int, zoom: float,
|
|
53
|
+
largura: int, altura: int) -> dict:
|
|
54
|
+
import enquadrar_camera as ec # noqa: PLC0415
|
|
55
|
+
import legenda_ass # noqa: PLC0415
|
|
56
|
+
import look_ffmpeg # noqa: PLC0415
|
|
57
|
+
|
|
58
|
+
cam = ec.caminho(trilha, zoom)
|
|
59
|
+
send = ec.escrever_sendcmd(cam, edit / "camera.sendcmd")
|
|
60
|
+
base = ec.cadeia(cam, send, largura, altura)
|
|
61
|
+
cor = look_ffmpeg.cadeia(look, grao, False, edit)
|
|
62
|
+
if cor:
|
|
63
|
+
base += "," + cor
|
|
64
|
+
entradas = ["-i", str(cut)]
|
|
65
|
+
leg = {"metodo": ""}
|
|
66
|
+
if legenda_ass.tem_libass():
|
|
67
|
+
ass = edit / "legenda.ass"
|
|
68
|
+
legenda_ass.montar_ass(palavras, largura, altura).save(str(ass))
|
|
69
|
+
fc = f"[0:v]{base},{legenda_ass.filtro_ass(ass)}[v]"
|
|
70
|
+
leg = {"metodo": "ass (libass)", "arquivo": str(ass)}
|
|
71
|
+
else:
|
|
72
|
+
import text_render # noqa: PLC0415
|
|
73
|
+
|
|
74
|
+
pngs = edit / "legenda_png"
|
|
75
|
+
estados = text_render.render_caption_track(cues_das_palavras(palavras), "legenda_limpa", "#FFD700", str(pngs),
|
|
76
|
+
canvas=(largura, altura), fps=30)
|
|
77
|
+
lista = text_render.write_ffconcat(estados, str(edit / "legenda.ffconcat"), float(cut_duracao(cut)), str(pngs / "vazio.png"), fps=30)
|
|
78
|
+
entradas += ["-f", "concat", "-safe", "0", "-i", str(lista)]
|
|
79
|
+
fc = f"[0:v]{base}[b];[1:v]format=rgba[cap];[b][cap]overlay=0:0:eof_action=pass:format=auto[v]"
|
|
80
|
+
leg = {"metodo": "PNG por estado (text_render + overlay): este ffmpeg não tem libass", "estados": len(estados)}
|
|
81
|
+
_run(["ffmpeg", "-hide_banner", "-nostdin", "-v", "error", "-y", *entradas, "-filter_complex", fc, "-map", "[v]", "-map", "0:a?",
|
|
82
|
+
"-c:v", "libx264", "-preset", "medium", "-crf", "18", "-pix_fmt", "yuv420p", "-c:a", "copy", "-movflags", "+faststart", str(saida)])
|
|
83
|
+
return {"camera": ec.relatorio(cam), "cor": cor, "legenda": leg}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def cut_duracao(p: Path) -> float:
|
|
87
|
+
from common import probe_video # noqa: PLC0415
|
|
88
|
+
|
|
89
|
+
return float(probe_video(p)["duration"])
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def rodar(video: Path, trabalho: Path, saida: Path, tirar: list[list[float]], look: str = "agencia", grao: int = 5,
|
|
93
|
+
zoom: float = 1.12, tamanho: str = "1080x1920") -> dict:
|
|
94
|
+
import primeiro_passe # noqa: PLC0415
|
|
95
|
+
import rastrear_rosto # noqa: PLC0415
|
|
96
|
+
import som_edit2 # noqa: PLC0415
|
|
97
|
+
import transcribe # noqa: PLC0415
|
|
98
|
+
from common import load_json # noqa: PLC0415
|
|
99
|
+
|
|
100
|
+
t0 = time.time()
|
|
101
|
+
largura, altura = (int(x) for x in tamanho.split("x"))
|
|
102
|
+
edit = trabalho / "edit"
|
|
103
|
+
edit.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
rel: dict = {"fonte": str(video), "passos": {}}
|
|
105
|
+
rel["antes"] = medir_video(video)
|
|
106
|
+
rel["antes"]["cortes"] = 0
|
|
107
|
+
|
|
108
|
+
tp = transcribe.transcribe_one(video, edit_dir=edit, language="pt", model=transcribe.DEFAULT_MODEL, prompt=transcribe.build_prompt(None, True), verbose=False)
|
|
109
|
+
tr = load_json(tp)
|
|
110
|
+
rel["passos"]["transcricao"] = {"motor": tr.get("motor_usado"), "palavras": len(tr["words"]), "aviso": tr.get("aviso")}
|
|
111
|
+
|
|
112
|
+
rel["passos"]["primeiro_passe"] = primeiro_passe.montar(video, edit, tirar=tirar or None)
|
|
113
|
+
_run([sys.executable, str(HELPERS / "corte_por_onda.py"), str(edit / "edl_manual.json"), "-o", str(edit / "edl.json")])
|
|
114
|
+
_run([sys.executable, str(HELPERS / "render_cut.py"), str(edit / "edl.json")]) # render 1: o corte
|
|
115
|
+
cut = edit / "cut.mp4"
|
|
116
|
+
|
|
117
|
+
limpo = edit / "cut_limpo.mp4"
|
|
118
|
+
voz = som_edit2.limpar_voz(cut, limpo)
|
|
119
|
+
rel["passos"]["voz"] = voz
|
|
120
|
+
fonte_video = limpo if voz["limpou"] else cut
|
|
121
|
+
|
|
122
|
+
tpc = transcribe.transcribe_one(cut, edit_dir=edit, language="pt", model=transcribe.DEFAULT_MODEL, prompt=transcribe.build_prompt(None, True), verbose=False)
|
|
123
|
+
palavras = [{"text": w["text"], "start": w["start"], "end": w["end"]} for w in load_json(tpc)["words"]]
|
|
124
|
+
trilha = rastrear_rosto.construir(edit, forcar=True)
|
|
125
|
+
bruto = edit / "final_sem_normalizar.mp4"
|
|
126
|
+
rel["passos"]["acabamento"] = render_final(fonte_video, palavras, trilha, bruto, edit, look, grao, zoom, largura, altura) # render 2
|
|
127
|
+
saida.parent.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
rel["passos"]["som_final"] = som_edit2.normalizar(bruto, saida)
|
|
129
|
+
|
|
130
|
+
rel["depois"] = medir_video(saida)
|
|
131
|
+
rel["depois"]["cortes"] = len(load_json(edit / "edl.json")["ranges"]) - 1
|
|
132
|
+
rel["segundos"] = round(time.time() - t0, 1)
|
|
133
|
+
(edit / "relatorio_edit2.json").write_text(json.dumps(rel, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
134
|
+
return rel
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def main() -> int:
|
|
138
|
+
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
|
139
|
+
ap.add_argument("video", type=Path)
|
|
140
|
+
ap.add_argument("trabalho", type=Path)
|
|
141
|
+
ap.add_argument("-o", "--saida", type=Path, required=True)
|
|
142
|
+
ap.add_argument("--tirar", action="append", default=[], help="a-b em segundos da fonte (repetível): o que a IA decidiu tirar")
|
|
143
|
+
ap.add_argument("--look", default="agencia")
|
|
144
|
+
ap.add_argument("--grao", type=int, default=5)
|
|
145
|
+
ap.add_argument("--zoom", type=float, default=1.12)
|
|
146
|
+
ap.add_argument("--tamanho", default="1080x1920")
|
|
147
|
+
a = ap.parse_args()
|
|
148
|
+
tirar = [[float(x) for x in t.split("-")] for t in a.tirar]
|
|
149
|
+
rel = rodar(a.video.absolute(), a.trabalho.absolute(), a.saida.absolute(), tirar, a.look, a.grao, a.zoom, a.tamanho)
|
|
150
|
+
print(json.dumps({"antes": rel["antes"], "depois": rel["depois"]}, ensure_ascii=False, indent=1))
|
|
151
|
+
return 0
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
sys.exit(main())
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Edit 2.0, peça 7: o enquadramento que segue o rosto (câmera suavizada) direto no ffmpeg, sem HyperFrames.
|
|
2
|
+
|
|
3
|
+
Entrada: o `rosto_trilha.json` do rastrear_rosto.py (rosto medido 10 vezes por segundo pelo MediaPipe, mediana de 5,
|
|
4
|
+
zona morta de 3%, transição de 0,5 s: a suavização que já existia; a ideia vem do pyautoflip, MIT, sem código copiado).
|
|
5
|
+
Saída: um `sendcmd` que move o `crop` a cada 0,1 s e a cadeia `-vf` (crop com zoom fixo + scale para o quadro final).
|
|
6
|
+
|
|
7
|
+
Regras: o corte fica sempre dentro do quadro; o olho fica a ~40% da altura do corte (regra dos terços); tomada sem
|
|
8
|
+
rosto fica no enquadramento de antes (centro) e o relatório conta.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
ZOOM_PADRAO = 1.12
|
|
17
|
+
OLHO_NA_ALTURA = 0.42 # onde o centro do rosto cai dentro do corte (fração da altura do corte)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _par(n: float) -> int:
|
|
21
|
+
return int(n) // 2 * 2
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def caminho(trilha: dict, zoom: float = ZOOM_PADRAO) -> dict:
|
|
25
|
+
"""-> {"cw","ch","pontos":[(t,x,y)], "sem_rosto_s": segundos de tomada sem rosto}"""
|
|
26
|
+
W, H = int(trilha["largura"]), int(trilha["altura"])
|
|
27
|
+
cw, ch = _par(W / zoom), _par(H / zoom)
|
|
28
|
+
pts: list[tuple[float, int, int]] = []
|
|
29
|
+
sem = 0.0
|
|
30
|
+
for tom in trilha["tomadas"]:
|
|
31
|
+
pontos = tom.get("pontos") or []
|
|
32
|
+
if not pontos:
|
|
33
|
+
sem += float(tom["ate"]) - float(tom["de"])
|
|
34
|
+
for k in range(int((tom["ate"] - tom["de"]) / 0.1) + 1):
|
|
35
|
+
pts.append((round(tom["de"] + k * 0.1, 3), (W - cw) // 2, (H - ch) // 2))
|
|
36
|
+
continue
|
|
37
|
+
for t, cx, cy, w, h, medido in pontos:
|
|
38
|
+
x = min(max(cx * W - cw / 2, 0), W - cw)
|
|
39
|
+
y = min(max(cy * H - ch * OLHO_NA_ALTURA, 0), H - ch)
|
|
40
|
+
pts.append((float(t), int(round(x)), int(round(y))))
|
|
41
|
+
pts.sort()
|
|
42
|
+
return {"cw": cw, "ch": ch, "pontos": pts, "sem_rosto_s": round(sem, 2)}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def escrever_sendcmd(cam: dict, destino: Path, alvo: str = "cam") -> Path:
|
|
46
|
+
linhas = []
|
|
47
|
+
for t, x, y in cam["pontos"]:
|
|
48
|
+
linhas.append(f"{t:.3f} crop@{alvo} x {x}, crop@{alvo} y {y};")
|
|
49
|
+
destino.write_text("\n".join(linhas) + "\n", encoding="utf-8")
|
|
50
|
+
return destino
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _esc(p: Path) -> str:
|
|
54
|
+
return str(p).replace("\\", "/").replace(":", "\\:").replace("'", "\\'")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def cadeia(cam: dict, sendcmd: Path, saida_w: int, saida_h: int) -> str:
|
|
58
|
+
x0, y0 = (cam["pontos"][0][1], cam["pontos"][0][2]) if cam["pontos"] else (0, 0)
|
|
59
|
+
return (f"sendcmd=f='{_esc(sendcmd)}',crop@cam={cam['cw']}:{cam['ch']}:{x0}:{y0}:exact=1,"
|
|
60
|
+
f"scale={saida_w}:{saida_h}:flags=lanczos,setsar=1")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def relatorio(cam: dict) -> dict:
|
|
64
|
+
xs = [p[1] for p in cam["pontos"]]
|
|
65
|
+
ys = [p[2] for p in cam["pontos"]]
|
|
66
|
+
salto = max([abs(xs[i + 1] - xs[i]) for i in range(len(xs) - 1)] or [0])
|
|
67
|
+
return {"zoom": round(1080 / cam["cw"], 3), "pontos": len(cam["pontos"]), "x_min": min(xs, default=0), "x_max": max(xs, default=0),
|
|
68
|
+
"y_min": min(ys, default=0), "y_max": max(ys, default=0), "maior_salto_px_por_0_1s": salto,
|
|
69
|
+
"sem_rosto_s": cam["sem_rosto_s"]}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
import sys
|
|
74
|
+
|
|
75
|
+
tr = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
76
|
+
c = caminho(tr)
|
|
77
|
+
print(json.dumps(relatorio(c), ensure_ascii=False))
|
|
@@ -98,6 +98,26 @@ def _detector(w: int, h: int):
|
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
def detect_array(bgr) -> dict | None:
|
|
101
|
+
"""MediaPipe (Edit 2.0) when installed, YuNet otherwise or when MediaPipe sees nothing."""
|
|
102
|
+
usou_mp = False
|
|
103
|
+
try:
|
|
104
|
+
import face_mediapipe # noqa: PLC0415
|
|
105
|
+
if face_mediapipe.disponivel():
|
|
106
|
+
usou_mp = True
|
|
107
|
+
r = face_mediapipe.detect_array(bgr)
|
|
108
|
+
if r is not None:
|
|
109
|
+
return r
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
try:
|
|
113
|
+
return detect_array_yunet(bgr)
|
|
114
|
+
except FaceLayoutError:
|
|
115
|
+
if usou_mp: # o MediaPipe respondeu "sem rosto" e o YuNet não está instalado: sem rosto mesmo
|
|
116
|
+
return None
|
|
117
|
+
raise
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def detect_array_yunet(bgr) -> dict | None:
|
|
101
121
|
"""The speaker's face in one BGR frame (numpy HxWx3): the largest confident box.
|
|
102
122
|
|
|
103
123
|
-> {"x0", "y0", "x1", "y1", "eyes_y", "mouth_y", "score"} as fractions of the frame, or None.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Edit 2.0, peça 3: detector de rosto MediaPipe (Apache-2.0, BlazeFace short-range) para o enquadramento.
|
|
2
|
+
|
|
3
|
+
Mesmo retorno do `face_layout.detect_array` ({x0,y0,x1,y1,eyes_y,mouth_y,score}, frações do quadro), então a
|
|
4
|
+
câmera suavizada que já existe (rastrear_rosto.py: mediana, zona morta, transição de 0,5 s; ideia de câmera suave
|
|
5
|
+
do pyautoflip, MIT, sem código copiado) segue igual e só troca quem enxerga o rosto. Sem o MediaPipe, o
|
|
6
|
+
face_layout continua no YuNet.
|
|
7
|
+
|
|
8
|
+
Também mede "% do tempo com rosto enquadrado" de um vídeo (`enquadrado`): rosto inteiro dentro da zona segura.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import subprocess
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
MODELO = Path(os.environ.get("PRIMOCODE_ROSTO_MODELO") or Path(__file__).resolve().parent.parent / "assets" / "models" / "blaze_face_short_range.tflite")
|
|
18
|
+
SCORE_MIN = 0.6
|
|
19
|
+
MIN_FACE_H = 0.06
|
|
20
|
+
# zona onde um rosto "bem enquadrado" fica (frações do quadro): centro nos 2/3 do meio, rosto inteiro no quadro
|
|
21
|
+
ZONA_X = (0.25, 0.75)
|
|
22
|
+
ZONA_Y = (0.12, 0.60)
|
|
23
|
+
_DET = {}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
MODELO_URL = "https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/1/blaze_face_short_range.tflite"
|
|
27
|
+
MODELO_SHA256 = "b4578f35940bf5a1a655214a1cce5cab13eba73c1297cd78e1a04c2380b0152f" # o mesmo de assets/baixar.json
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def garantir_modelo() -> bool:
|
|
31
|
+
"""O modelo (230 KB) não vai no pacote: baixa na primeira vez (como o assets/baixar.json) e confere o sha256."""
|
|
32
|
+
if MODELO.exists():
|
|
33
|
+
return True
|
|
34
|
+
try:
|
|
35
|
+
import hashlib # noqa: PLC0415
|
|
36
|
+
import urllib.request # noqa: PLC0415
|
|
37
|
+
|
|
38
|
+
dados = urllib.request.urlopen(MODELO_URL, timeout=30).read()
|
|
39
|
+
if hashlib.sha256(dados).hexdigest() != MODELO_SHA256:
|
|
40
|
+
return False
|
|
41
|
+
MODELO.parent.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
MODELO.write_bytes(dados)
|
|
43
|
+
return True
|
|
44
|
+
except Exception:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def disponivel() -> bool:
|
|
49
|
+
if os.environ.get("PRIMOCODE_ROSTO") == "yunet" or not garantir_modelo():
|
|
50
|
+
return False
|
|
51
|
+
try:
|
|
52
|
+
import mediapipe # noqa: F401, PLC0415
|
|
53
|
+
return True
|
|
54
|
+
except Exception:
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _detector():
|
|
59
|
+
if "d" not in _DET:
|
|
60
|
+
import mediapipe as mp # noqa: PLC0415
|
|
61
|
+
from mediapipe.tasks import python as mpt # noqa: PLC0415
|
|
62
|
+
from mediapipe.tasks.python import vision # noqa: PLC0415
|
|
63
|
+
|
|
64
|
+
opts = vision.FaceDetectorOptions(base_options=mpt.BaseOptions(model_asset_path=str(MODELO)),
|
|
65
|
+
min_detection_confidence=SCORE_MIN)
|
|
66
|
+
_DET["d"] = (vision.FaceDetector.create_from_options(opts), mp)
|
|
67
|
+
return _DET["d"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def escolher(deteccoes: list[dict], w: int, h: int) -> dict | None:
|
|
71
|
+
"""A maior caixa confiável. `deteccoes`: [{"x","y","w","h","score","eyes_y","mouth_y"}] em pixels."""
|
|
72
|
+
melhor = None
|
|
73
|
+
for d in deteccoes:
|
|
74
|
+
if d["score"] < SCORE_MIN or d["h"] < MIN_FACE_H * h:
|
|
75
|
+
continue
|
|
76
|
+
if melhor is None or d["w"] * d["h"] > melhor["w"] * melhor["h"]:
|
|
77
|
+
melhor = d
|
|
78
|
+
if melhor is None:
|
|
79
|
+
return None
|
|
80
|
+
return {"x0": max(0.0, melhor["x"] / w), "y0": max(0.0, melhor["y"] / h),
|
|
81
|
+
"x1": min(1.0, (melhor["x"] + melhor["w"]) / w), "y1": min(1.0, (melhor["y"] + melhor["h"]) / h),
|
|
82
|
+
"eyes_y": melhor["eyes_y"] / h, "mouth_y": melhor["mouth_y"] / h, "score": melhor["score"]}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def detect_array(bgr) -> dict | None:
|
|
86
|
+
import numpy as np # noqa: PLC0415
|
|
87
|
+
|
|
88
|
+
det, mp = _detector()
|
|
89
|
+
h, w = bgr.shape[:2]
|
|
90
|
+
rgb = np.ascontiguousarray(bgr[:, :, ::-1])
|
|
91
|
+
res = det.detect(mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb))
|
|
92
|
+
out = []
|
|
93
|
+
for d in res.detections:
|
|
94
|
+
bb = d.bounding_box
|
|
95
|
+
kp = d.keypoints # olho dir., olho esq., nariz, centro da boca, orelhas
|
|
96
|
+
score = d.categories[0].score if d.categories else 0.0
|
|
97
|
+
if len(kp) >= 4:
|
|
98
|
+
eyes, mouth = (kp[0].y + kp[1].y) / 2 * h, kp[3].y * h
|
|
99
|
+
else:
|
|
100
|
+
eyes, mouth = bb.origin_y + bb.height * 0.4, bb.origin_y + bb.height * 0.8
|
|
101
|
+
out.append({"x": bb.origin_x, "y": bb.origin_y, "w": bb.width, "h": bb.height, "score": score,
|
|
102
|
+
"eyes_y": eyes, "mouth_y": mouth})
|
|
103
|
+
return escolher(out, w, h)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def enquadrado(rosto: dict | None) -> bool:
|
|
107
|
+
if not rosto:
|
|
108
|
+
return False
|
|
109
|
+
cx, cy = (rosto["x0"] + rosto["x1"]) / 2, (rosto["y0"] + rosto["y1"]) / 2
|
|
110
|
+
return ZONA_X[0] <= cx <= ZONA_X[1] and ZONA_Y[0] <= cy <= ZONA_Y[1] and rosto["x0"] > 0.0 and rosto["x1"] < 1.0 and rosto["y0"] > 0.0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def percentual_enquadrado(video: Path, por_segundo: float = 4.0, janelas: list[tuple[float, float]] | None = None) -> dict:
|
|
114
|
+
"""% dos quadros amostrados (4 por segundo) em que há rosto dentro da zona. `janelas`: só esses trechos."""
|
|
115
|
+
import numpy as np # noqa: PLC0415
|
|
116
|
+
|
|
117
|
+
from common import probe_video # noqa: PLC0415
|
|
118
|
+
|
|
119
|
+
info = probe_video(video)
|
|
120
|
+
w0, h0 = int(info["width"]), int(info["height"])
|
|
121
|
+
esc = min(1.0, 540 / min(w0, h0))
|
|
122
|
+
w, h = max(2, int(round(w0 * esc / 2)) * 2), max(2, int(round(h0 * esc / 2)) * 2)
|
|
123
|
+
cmd = ["ffmpeg", "-hide_banner", "-nostdin", "-v", "error", "-i", str(video), "-vf", f"fps={por_segundo},scale={w}:{h}",
|
|
124
|
+
"-an", "-f", "rawvideo", "-pix_fmt", "bgr24", "-"]
|
|
125
|
+
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
|
|
126
|
+
tam = w * h * 3
|
|
127
|
+
n = com = bem = 0
|
|
128
|
+
dx, dy = [], []
|
|
129
|
+
while True:
|
|
130
|
+
buf = p.stdout.read(tam)
|
|
131
|
+
if len(buf) < tam:
|
|
132
|
+
break
|
|
133
|
+
t = n / por_segundo
|
|
134
|
+
n += 1
|
|
135
|
+
if janelas and not any(a <= t <= b for a, b in janelas):
|
|
136
|
+
continue
|
|
137
|
+
r = detect_array(np.frombuffer(buf, np.uint8).reshape(h, w, 3))
|
|
138
|
+
com += r is not None
|
|
139
|
+
bem += enquadrado(r)
|
|
140
|
+
if r:
|
|
141
|
+
dx.append(abs((r["x0"] + r["x1"]) / 2 - 0.5) * 100)
|
|
142
|
+
dy.append(abs((r["y0"] + r["y1"]) / 2 - 0.40) * 100)
|
|
143
|
+
p.wait()
|
|
144
|
+
total = max(1, n if not janelas else sum(1 for k in range(n) if any(a <= k / por_segundo <= b for a, b in janelas)))
|
|
145
|
+
return {"quadros": total, "com_rosto_pct": round(100 * com / total, 1), "enquadrado_pct": round(100 * bem / total, 1),
|
|
146
|
+
"desvio_centro_x_pct": round(sum(dx) / len(dx), 1) if dx else None,
|
|
147
|
+
"desvio_altura_pct": round(sum(dy) / len(dy), 1) if dy else None}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Cria o ambiente do DeepFilterNet (MIT ou Apache-2.0) fora do ambiente do editor: ele pede numpy<2 e torch 2.5.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
DEST="${PRIMOCODE_DF_VENV:-$HOME/.primocode/editor/df-venv}"
|
|
5
|
+
uv venv "$DEST" --python 3.12
|
|
6
|
+
uv pip install --python "$DEST/bin/python" deepfilternet "torch<2.6" "torchaudio<2.6" "numpy<2" soundfile
|
|
7
|
+
echo "DeepFilterNet pronto em $DEST"
|