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,119 @@
|
|
|
1
|
+
"""Edit 2.0, peça 4: o som. Voz limpa (DeepFilterNet), -14 LUFS (ffmpeg-normalize), medida (pyloudnorm).
|
|
2
|
+
|
|
3
|
+
- `limpar_voz`: DeepFilterNet roda em ambiente à parte (~/.primocode/editor/df-venv, `instalar_df.sh`); sem ele
|
|
4
|
+
a função diz que não limpou (nunca finge) e o `denoise.py` de antes segue valendo.
|
|
5
|
+
- `normalizar`: ffmpeg-normalize (EBU R128, 2 passadas) em -14 LUFS, pico -1 dBTP, imagem copiada.
|
|
6
|
+
- `medir_lufs`: pyloudnorm (BS.1770) sobre o áudio do arquivo final; é o número do antes/depois.
|
|
7
|
+
- O ducking da trilha é o `sidechaincompress` do ffmpeg, que o `mixar_trilha.py` já usa (nada novo aqui).
|
|
8
|
+
|
|
9
|
+
Uso:
|
|
10
|
+
uv run python helpers/som_edit2.py medir <video>
|
|
11
|
+
uv run python helpers/som_edit2.py limpar <video> <saida.mp4>
|
|
12
|
+
uv run python helpers/som_edit2.py normalizar <video> <saida.mp4> [--alvo -14]
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
import tempfile
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
HELPERS = Path(__file__).resolve().parent
|
|
26
|
+
DF_PY = Path(os.environ.get("PRIMOCODE_DF_PYTHON") or Path.home() / ".primocode" / "editor" / "df-venv" / "bin" / "python")
|
|
27
|
+
ALVO_LUFS = -14.0
|
|
28
|
+
PICO_DBTP = -1.0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _ffmpeg(*args: str) -> None:
|
|
32
|
+
r = subprocess.run(["ffmpeg", "-hide_banner", "-nostdin", "-v", "error", "-y", *args], capture_output=True, text=True)
|
|
33
|
+
if r.returncode != 0:
|
|
34
|
+
raise RuntimeError(f"ffmpeg falhou: {r.stderr[-300:]}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def df_disponivel() -> bool:
|
|
38
|
+
return DF_PY.exists()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def medir_lufs(video: Path) -> dict:
|
|
42
|
+
"""LUFS integrado (pyloudnorm, BS.1770) e pico de amostra do áudio do arquivo."""
|
|
43
|
+
import numpy as np # noqa: PLC0415
|
|
44
|
+
import pyloudnorm as pyln # noqa: PLC0415
|
|
45
|
+
|
|
46
|
+
sr = 48000
|
|
47
|
+
r = subprocess.run(["ffmpeg", "-hide_banner", "-nostdin", "-v", "error", "-i", str(video), "-vn", "-ac", "2", "-ar", str(sr),
|
|
48
|
+
"-f", "f32le", "-"], capture_output=True)
|
|
49
|
+
if r.returncode != 0:
|
|
50
|
+
raise RuntimeError(f"ffmpeg falhou: {r.stderr.decode('utf-8', 'replace')[-300:]}")
|
|
51
|
+
dados = np.frombuffer(r.stdout, dtype="<f4").astype("float64").reshape(-1, 2)
|
|
52
|
+
if len(dados) < sr * 0.4:
|
|
53
|
+
return {"lufs": None, "pico_db": None, "motivo": "áudio curto demais para medir"}
|
|
54
|
+
lufs = pyln.Meter(sr).integrated_loudness(dados)
|
|
55
|
+
if not np.isfinite(lufs): # silêncio total: -inf não vira JSON
|
|
56
|
+
return {"lufs": None, "pico_db": round(20 * np.log10(max(float(np.max(np.abs(dados))), 1e-9)), 2), "motivo": "silêncio (abaixo do portão)"}
|
|
57
|
+
pico = float(np.max(np.abs(dados)))
|
|
58
|
+
return {"lufs": round(float(lufs), 2), "pico_db": round(20 * np.log10(max(pico, 1e-9)), 2)}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def limpar_voz(video: Path, saida: Path) -> dict:
|
|
62
|
+
"""Voz do vídeo passada pelo DeepFilterNet; imagem copiada. -> {"limpou": bool, "motivo": str}"""
|
|
63
|
+
if not df_disponivel():
|
|
64
|
+
return {"limpou": False, "motivo": f"DeepFilterNet não instalado ({DF_PY}); rode helpers/instalar_df.sh"}
|
|
65
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
66
|
+
ent, sai = Path(tmp) / "e.wav", Path(tmp) / "s.wav"
|
|
67
|
+
_ffmpeg("-i", str(video), "-vn", "-ac", "1", "-ar", "48000", str(ent))
|
|
68
|
+
r = subprocess.run([str(DF_PY), str(HELPERS / "df_limpar.py"), str(ent), str(sai)], capture_output=True, text=True)
|
|
69
|
+
if r.returncode != 0 or not sai.exists():
|
|
70
|
+
return {"limpou": False, "motivo": f"DeepFilterNet falhou: {r.stderr[-200:]}"}
|
|
71
|
+
_ffmpeg("-i", str(video), "-i", str(sai), "-map", "0:v:0", "-map", "1:a:0", "-c:v", "copy", "-c:a", "aac",
|
|
72
|
+
"-b:a", "192k", "-shortest", str(saida))
|
|
73
|
+
return {"limpou": True, "motivo": "DeepFilterNet"}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def normalizar(video: Path, saida: Path, alvo: float = ALVO_LUFS, pico: float = PICO_DBTP) -> dict:
|
|
77
|
+
"""ffmpeg-normalize em 2 passadas. Devolve o comando usado e a medida de depois."""
|
|
78
|
+
exe = Path(sys.executable).parent / "ffmpeg-normalize"
|
|
79
|
+
cmd = [str(exe) if exe.exists() else "ffmpeg-normalize", str(video), "-nt", "ebu", "-t", str(alvo), "-tp", str(pico),
|
|
80
|
+
"-c:v", "copy", "-c:a", "aac", "-b:a", "192k", "-ar", "48000", "-f", "-o", str(saida)]
|
|
81
|
+
r = subprocess.run(cmd, capture_output=True, text=True)
|
|
82
|
+
if r.returncode != 0 or not saida.exists():
|
|
83
|
+
raise RuntimeError(f"ffmpeg-normalize falhou: {(r.stderr or r.stdout)[-300:]}")
|
|
84
|
+
m = medir_lufs(saida)
|
|
85
|
+
corrigido = False
|
|
86
|
+
# o loudnorm do ffmpeg-normalize pode errar uns 0,5 LU depois do AAC: um acerto de ganho (só áudio, imagem copiada)
|
|
87
|
+
if m.get("lufs") is not None and abs(m["lufs"] - alvo) > 0.3 and (m["pico_db"] + (alvo - m["lufs"])) <= pico:
|
|
88
|
+
ajuste = alvo - m["lufs"]
|
|
89
|
+
tmp = saida.with_name(saida.stem + "_ajuste" + saida.suffix)
|
|
90
|
+
_ffmpeg("-i", str(saida), "-c:v", "copy", "-af", f"volume={ajuste:.2f}dB", "-c:a", "aac", "-b:a", "192k", str(tmp))
|
|
91
|
+
tmp.replace(saida)
|
|
92
|
+
m, corrigido = medir_lufs(saida), True
|
|
93
|
+
return {"alvo_lufs": alvo, "pico_dbtp": pico, "ajuste_de_ganho": corrigido, **m}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main() -> int:
|
|
97
|
+
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
|
98
|
+
sp = ap.add_subparsers(dest="cmd", required=True)
|
|
99
|
+
m = sp.add_parser("medir")
|
|
100
|
+
m.add_argument("video", type=Path)
|
|
101
|
+
for nome in ("limpar", "normalizar"):
|
|
102
|
+
p = sp.add_parser(nome)
|
|
103
|
+
p.add_argument("video", type=Path)
|
|
104
|
+
p.add_argument("saida", type=Path)
|
|
105
|
+
if nome == "normalizar":
|
|
106
|
+
p.add_argument("--alvo", type=float, default=ALVO_LUFS)
|
|
107
|
+
a = ap.parse_args()
|
|
108
|
+
if a.cmd == "medir":
|
|
109
|
+
out = medir_lufs(a.video)
|
|
110
|
+
elif a.cmd == "limpar":
|
|
111
|
+
out = limpar_voz(a.video, a.saida)
|
|
112
|
+
else:
|
|
113
|
+
out = normalizar(a.video, a.saida, a.alvo)
|
|
114
|
+
print(json.dumps(out, ensure_ascii=False))
|
|
115
|
+
return 0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
sys.exit(main())
|
|
@@ -135,12 +135,21 @@ def check_name_clash(out_path: Path, video: Path) -> None:
|
|
|
135
135
|
# ---------------------------------------------------------------- engines
|
|
136
136
|
|
|
137
137
|
def default_engine() -> str:
|
|
138
|
+
# Edit 2.0: WhisperX (palavra alinhada) quando instalado; PRIMOCODE_MOTOR_PALAVRAS=antigo volta ao de antes.
|
|
139
|
+
if os.environ.get("PRIMOCODE_MOTOR_PALAVRAS") != "antigo":
|
|
140
|
+
try:
|
|
141
|
+
import importlib.util
|
|
142
|
+
if importlib.util.find_spec("whisperx"):
|
|
143
|
+
return "whisperx"
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
|
138
146
|
return "mlx" if sys.platform == "darwin" and platform.machine() == "arm64" else "faster-whisper"
|
|
139
147
|
|
|
140
148
|
|
|
141
149
|
def engine_label(engine: str, model: str) -> str:
|
|
142
150
|
name = Path(model).name if ("/" in model or os.sep in model) else model
|
|
143
|
-
|
|
151
|
+
rotulo = {"mlx": "mlx-whisper", "whisperx": "whisperx", "whisper.cpp": "whisper.cpp"}.get(engine, "faster-whisper")
|
|
152
|
+
return f"{rotulo}/{name}"
|
|
144
153
|
|
|
145
154
|
|
|
146
155
|
def _mlx_model_path(model: str) -> str:
|
|
@@ -208,8 +217,27 @@ def _run_faster(audio: np.ndarray, model: str, language: str | None, prompt: str
|
|
|
208
217
|
return segments, getattr(info, "language", None)
|
|
209
218
|
|
|
210
219
|
|
|
220
|
+
MOTOR_USADO = [""] # o motor que de fato respondeu (o pedido pode ter caído no próximo)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _motor_antigo() -> str:
|
|
224
|
+
return "mlx" if sys.platform == "darwin" and platform.machine() == "arm64" else "faster-whisper"
|
|
225
|
+
|
|
226
|
+
|
|
211
227
|
def run_whisper(audio: np.ndarray, engine: str, model: str, language: str | None,
|
|
212
228
|
prompt: str | None) -> tuple[list[dict], str | None]:
|
|
229
|
+
if engine in ("whisperx", "whisper.cpp"):
|
|
230
|
+
import motor_palavras as mp # noqa: PLC0415
|
|
231
|
+
cadeia = [engine] + [e for e in ("whisper.cpp",) if e != engine]
|
|
232
|
+
for e in cadeia:
|
|
233
|
+
try:
|
|
234
|
+
fn = mp.run_whisperx if e == "whisperx" else mp.run_whisper_cpp
|
|
235
|
+
res = fn(audio, model, language, prompt)
|
|
236
|
+
MOTOR_USADO[0] = e
|
|
237
|
+
return res
|
|
238
|
+
except Exception as exc: # cai para o próximo e diz por quê, sem fingir
|
|
239
|
+
print(f"AVISO: motor {e} falhou ({str(exc)[:120]}); tentando o próximo", file=sys.stderr, flush=True)
|
|
240
|
+
engine = _motor_antigo()
|
|
213
241
|
if engine == "mlx":
|
|
214
242
|
return _run_mlx(audio, model, language, prompt)
|
|
215
243
|
return _run_faster(audio, model, language, prompt)
|
|
@@ -651,6 +679,7 @@ def transcribe_one(video: Path | str, edit_dir: Path | str | None = None, langua
|
|
|
651
679
|
"duration": round(duration, 3),
|
|
652
680
|
"language": detected_lang or language or "",
|
|
653
681
|
"engine": engine_label(engine, model),
|
|
682
|
+
"motor_usado": MOTOR_USADO[0] or engine,
|
|
654
683
|
"words": out_words,
|
|
655
684
|
"segments": segments,
|
|
656
685
|
"prompt": prompt,
|
|
@@ -707,7 +736,7 @@ def main() -> None:
|
|
|
707
736
|
ap.add_argument("--prompt", default=None,
|
|
708
737
|
help="palavras que aparecem no vídeo, para o Whisper acertar a grafia (ex.: \"Kiwify, Hubla\")")
|
|
709
738
|
ap.add_argument("--no-prompt", action="store_true", help="não usa o prompt padrão com vícios de fala")
|
|
710
|
-
ap.add_argument("--engine", choices=("auto", "mlx", "faster-whisper"), default="auto",
|
|
739
|
+
ap.add_argument("--engine", choices=("auto", "whisperx", "whisper.cpp", "mlx", "faster-whisper"), default="auto",
|
|
711
740
|
help="motor (padrão: mlx no Mac Apple Silicon, faster-whisper nos outros)")
|
|
712
741
|
args = ap.parse_args()
|
|
713
742
|
|
package/editor/pyproject.toml
CHANGED
|
@@ -17,7 +17,16 @@ dependencies = [
|
|
|
17
17
|
# onnxruntime (usado pelo faster-whisper e pela tela verde) parou de publicar pacote para Mac Intel e
|
|
18
18
|
# macOS 13 na 1.24.
|
|
19
19
|
"onnxruntime<1.24; sys_platform == 'darwin' and (platform_machine == 'x86_64' or platform_release < '23')",
|
|
20
|
+
# Edit 2.0 (licenças em LICENSES/edit2-terceiros.md)
|
|
21
|
+
"whisperx>=3.8", # BSD-2: palavra a palavra alinhada (diarização não é usada)
|
|
22
|
+
"silero-vad>=6.2", # MIT: fala vs silêncio
|
|
23
|
+
"auto-editor>=29", # Unlicense: 1º passe do corte (chamado por linha de comando)
|
|
24
|
+
"mediapipe>=0.10", # Apache-2.0: rosto
|
|
25
|
+
"pysubs2>=1.8", # MIT: legenda ASS
|
|
26
|
+
"pyloudnorm>=0.1", # MIT: mede LUFS
|
|
27
|
+
"ffmpeg-normalize>=1.30", # MIT: fecha em -14 LUFS
|
|
20
28
|
]
|
|
21
29
|
|
|
22
30
|
[tool.uv]
|
|
23
31
|
package = false
|
|
32
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Edit 2.0: o pipeline curto
|
|
2
|
+
|
|
3
|
+
Um comando, do bruto ao vídeo final (vertical 1080x1920 por padrão):
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
uv run --directory '<SKILL>' python helpers/edit2.py '<VIDEO>' '<PASTA_DE_TRABALHO>' -o '<SAIDA.mp4>' \
|
|
7
|
+
--tirar 8.09-8.59 [--look agencia] [--grao 5] [--zoom 1.12]
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Passos (cada um é um helper que dá para rodar sozinho): `transcribe.py` (WhisperX) → `primeiro_passe.py` (silero-vad +
|
|
11
|
+
auto-editor, bordas nas palavras) → `corte_por_onda.py` + `render_cut.py` → `som_edit2.py limpar` (DeepFilterNet) →
|
|
12
|
+
`rastrear_rosto.py` (MediaPipe) + `enquadrar_camera.py` + `look_ffmpeg.py` + legenda, numa passada de vídeo →
|
|
13
|
+
`som_edit2.py normalizar` (-14 LUFS) e `medir`.
|
|
14
|
+
|
|
15
|
+
O relatório fica em `<PASTA>/edit/relatorio_edit2.json`: `antes` e `depois` (duração, cortes, LUFS, % com rosto
|
|
16
|
+
enquadrado e desvio do centro). Nota: quando a fonte já é um rosto centrado, o % enquadrado dá 100 antes e depois;
|
|
17
|
+
o que mostra o ganho é o desvio do centro.
|
|
18
|
+
|
|
19
|
+
Instalação: `uv sync` na pasta da skill traz WhisperX, silero-vad, auto-editor, MediaPipe, pysubs2, pyloudnorm e
|
|
20
|
+
ffmpeg-normalize. O DeepFilterNet pede numpy<2 e torch antigo e fica à parte: `helpers/instalar_df.sh`
|
|
21
|
+
(cria `~/.primocode/editor/df-venv`). Sem ele a voz segue como veio e o relatório diz `limpou: false`.
|
|
22
|
+
`PRIMOCODE_MOTOR_PALAVRAS=antigo` volta ao motor de transcrição de antes; `PRIMOCODE_ROSTO=yunet` volta ao detector de rosto de antes.
|