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.
Files changed (89) hide show
  1. package/LICENSES/edit2-terceiros.md +31 -0
  2. package/LICENSES/models-dev-MIT.txt +25 -0
  3. package/README.md +2 -2
  4. package/editor/SKILL.md +24 -0
  5. package/editor/THIRD_PARTY_NOTICES.md +1 -0
  6. package/editor/assets/baixar.json +9 -1
  7. package/editor/helpers/df_limpar.py +29 -0
  8. package/editor/helpers/edit2.py +155 -0
  9. package/editor/helpers/enquadrar_camera.py +77 -0
  10. package/editor/helpers/face_layout.py +20 -0
  11. package/editor/helpers/face_mediapipe.py +147 -0
  12. package/editor/helpers/instalar_df.sh +7 -0
  13. package/editor/helpers/legenda_ass.py +124 -0
  14. package/editor/helpers/look_ffmpeg.py +105 -0
  15. package/editor/helpers/motor_palavras.py +157 -0
  16. package/editor/helpers/primeiro_passe.py +214 -0
  17. package/editor/helpers/som_edit2.py +119 -0
  18. package/editor/helpers/transcribe.py +31 -2
  19. package/editor/pyproject.toml +9 -0
  20. package/editor/references/edit2.md +22 -0
  21. package/editor/uv.lock +3342 -214
  22. package/kits/3d/LICENSES/README.md +11 -0
  23. package/kits/3d/animacoes.js +87 -0
  24. package/kits/3d/base.js +44 -0
  25. package/kits/3d/catalogo.js +38 -0
  26. package/kits/3d/cenario.js +162 -0
  27. package/kits/3d/exemplos/asfalto.mjs +91 -0
  28. package/kits/3d/maquinas.js +97 -0
  29. package/kits/3d/montador.js +142 -0
  30. package/kits/3d/package.json +1 -0
  31. package/kits/3d/personagem.js +86 -0
  32. package/kits/3d/remotion/Maquete.jsx +42 -0
  33. package/kits/3d/remotion/Root.jsx +10 -0
  34. package/kits/3d/render.mjs +73 -0
  35. package/lib/cli/comando-api.js +30 -8
  36. package/lib/cli/modelos-lista.js +58 -0
  37. package/lib/cli/sessao.js +11 -0
  38. package/lib/cli/subcomandos.js +3 -2
  39. package/lib/outra-pasta.js +1 -1
  40. package/lib/permissao-sistema.js +60 -0
  41. package/lib/permission/perigo.js +2 -1
  42. package/lib/permission/permissao.js +1 -0
  43. package/lib/permission/sem-perguntar.js +29 -0
  44. package/lib/plugin/desktop.js +5 -4
  45. package/lib/plugin/estudio/cota.js +4 -2
  46. package/lib/plugin/estudio/dialogo3d.js +71 -0
  47. package/lib/plugin/estudio/effectcraft.js +1 -1
  48. package/lib/plugin/estudio/glifos.js +2 -0
  49. package/lib/plugin/estudio/gsap-local.js +43 -0
  50. package/lib/plugin/estudio/index.js +63 -10
  51. package/lib/plugin/estudio/kit-ae/base.jsx +1 -1
  52. package/lib/plugin/estudio/kit-ae/cenas.jsx +7 -4
  53. package/lib/plugin/estudio/kit-ae/destrava.jsx +211 -0
  54. package/lib/plugin/estudio/maquina-diretor.js +1 -1
  55. package/lib/plugin/estudio/maquina-elementos.js +2 -2
  56. package/lib/plugin/estudio/maquina.js +26 -8
  57. package/lib/plugin/estudio/motion-plano/ajustar.js +139 -0
  58. package/lib/plugin/estudio/motion-plano/critico.js +59 -0
  59. package/lib/plugin/estudio/motion-plano/estrutura.js +111 -0
  60. package/lib/plugin/estudio/motion-plano/fatos.js +137 -0
  61. package/lib/plugin/estudio/motion-plano/index.js +75 -0
  62. package/lib/plugin/estudio/motion-plano/llm.js +59 -0
  63. package/lib/plugin/estudio/motion-plano/motor.js +50 -0
  64. package/lib/plugin/estudio/motion-plano/passos.js +38 -0
  65. package/lib/plugin/estudio/motion-plano/pele.js +36 -0
  66. package/lib/plugin/estudio/motion-plano/plano.js +152 -0
  67. package/lib/plugin/estudio/motion-plano/regua.js +100 -0
  68. package/lib/plugin/estudio/personagem.js +3 -1
  69. package/lib/plugin/estudio/referencias.js +1 -1
  70. package/lib/provider/catalogo.js +161 -0
  71. package/lib/provider/local/catalogo-embutido.json.gz +0 -0
  72. package/lib/provider/local/cerebro.json +1 -1
  73. package/lib/provider/local/entrada.js +10 -0
  74. package/lib/provider/local/motor.js +140 -27
  75. package/lib/provider/local/orcamento.js +193 -0
  76. package/lib/provider/local/provedor.js +127 -14
  77. package/lib/provider/local/recorte.js +170 -0
  78. package/lib/provider/local/revezamento.js +121 -0
  79. package/lib/session/runner/act.js +28 -3
  80. package/lib/skill/catalogo.js +9 -1
  81. package/lib/tool/reparar-entrada.js +1 -0
  82. package/lib/tool/tools.js +58 -1
  83. package/lib/tui/ui.js +1 -1
  84. package/package.json +2 -2
  85. package/vendor/fontes/ttf/ORIGEM.txt +1 -0
  86. package/vendor/fontes/ttf/inter-800.ttf +0 -0
  87. package/vendor/gsap/ORIGEM.txt +6 -0
  88. package/vendor/gsap/gsap.min.js +11 -0
  89. 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
- return f"{'mlx-whisper' if engine == 'mlx' else 'faster-whisper'}/{name}"
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
 
@@ -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.