primocode 10.16.0 → 10.17.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/edit-ia-pro.txt +32 -0
- package/LICENSES/fontes-OFL.txt +476 -0
- package/LICENSES/hyperframes-Apache-2.0.txt +205 -0
- package/LICENSES/permanent-marker-Apache-2.0.txt +204 -0
- package/LICENSES/selfie-multiclass-Apache-2.0.txt +227 -0
- package/LICENSES/three-MIT.txt +23 -0
- package/LICENSES/video-use-MIT.txt +24 -0
- package/LICENSES/woven-sfx-CC0.txt +16 -0
- package/LICENSES/yunet-MIT.txt +21 -0
- package/README.md +2 -2
- package/bin/primocode.js +11 -2
- package/editor/LICENSE.md +11 -0
- package/editor/README.md +321 -0
- package/editor/SKILL.md +307 -0
- package/editor/THIRD_PARTY_NOTICES.md +228 -0
- package/editor/assets/baixar.json +591 -0
- package/editor/assets/fonts/anton/OFL.txt +93 -0
- package/editor/assets/fonts/caveat/OFL.txt +93 -0
- package/editor/assets/fonts/figtree/OFL.txt +93 -0
- package/editor/assets/fonts/fonts.json +10 -0
- package/editor/assets/fonts/montserrat/OFL.txt +93 -0
- package/editor/assets/fonts/permanentmarker/LICENSE.txt +202 -0
- package/editor/assets/models/SELFIE_MULTICLASS_LICENSE.txt +227 -0
- package/editor/assets/models/YUNET_LICENSE.txt +21 -0
- package/editor/assets/sfx/licencas/bass_drop.txt +32 -0
- package/editor/assets/sfx/licencas/cash.txt +76 -0
- package/editor/assets/sfx/licencas/charge.txt +31 -0
- package/editor/assets/sfx/licencas/click.txt +46 -0
- package/editor/assets/sfx/licencas/coin.txt +50 -0
- package/editor/assets/sfx/licencas/count.txt +47 -0
- package/editor/assets/sfx/licencas/data.txt +31 -0
- package/editor/assets/sfx/licencas/error.txt +32 -0
- package/editor/assets/sfx/licencas/flash.txt +31 -0
- package/editor/assets/sfx/licencas/gears.txt +82 -0
- package/editor/assets/sfx/licencas/keyboard.txt +48 -0
- package/editor/assets/sfx/licencas/metal_hit.txt +62 -0
- package/editor/assets/sfx/licencas/metal_riser.txt +61 -0
- package/editor/assets/sfx/licencas/mouse_click.txt +50 -0
- package/editor/assets/sfx/licencas/notification.txt +54 -0
- package/editor/assets/sfx/licencas/pop.txt +38 -0
- package/editor/assets/sfx/licencas/rewind.txt +32 -0
- package/editor/assets/sfx/licencas/rise.txt +46 -0
- package/editor/assets/sfx/licencas/sinteticos.txt +21 -0
- package/editor/assets/sfx/licencas/snap.txt +50 -0
- package/editor/assets/sfx/licencas/success.txt +40 -0
- package/editor/assets/sfx/licencas/synth_hit.txt +32 -0
- package/editor/assets/sfx/licencas/tick.txt +14 -0
- package/editor/assets/sfx/licencas/whoosh.txt +47 -0
- package/editor/assets/sfx/licencas/wind.txt +31 -0
- package/editor/assets/sfx/sfx.json +832 -0
- package/editor/assets/sfx/sfx_sinteticos.json +317 -0
- package/editor/assets/sfx/sfx_woven.json +387 -0
- package/editor/assets/sfx/woven/LICENCA.md +16 -0
- package/editor/fichas/briefing-do-video.md +45 -0
- package/editor/fichas/conferencia-da-amostra.md +56 -0
- package/editor/fichas/matriz-de-ganchos.csv +3 -0
- package/editor/fichas/padrao.json +16 -0
- package/editor/fichas/planilha-de-cenas.csv +4 -0
- package/editor/fichas/planilha-de-versoes.csv +4 -0
- package/editor/fichas/plano-em-blocos.md +32 -0
- package/editor/fichas/registro-de-materiais.json +27 -0
- package/editor/helpers/assets_ocr/ocr_vision.swift +41 -0
- package/editor/helpers/bordas_precisas.py +272 -0
- package/editor/helpers/camera_viva.py +426 -0
- package/editor/helpers/captions.py +601 -0
- package/editor/helpers/common.py +308 -0
- package/editor/helpers/compose_final.py +3555 -0
- package/editor/helpers/conferir_corte.py +234 -0
- package/editor/helpers/conferir_dados.py +405 -0
- package/editor/helpers/contact_sheet.py +282 -0
- package/editor/helpers/corte_por_onda.py +433 -0
- package/editor/helpers/corte_seco.py +595 -0
- package/editor/helpers/denoise.py +971 -0
- package/editor/helpers/efeitos.py +408 -0
- package/editor/helpers/efeitos_gerar.py +159 -0
- package/editor/helpers/elevenlabs_api.py +442 -0
- package/editor/helpers/face_layout.py +570 -0
- package/editor/helpers/fetch_images.py +423 -0
- package/editor/helpers/ganho_por_trecho.py +214 -0
- package/editor/helpers/gen_image.py +312 -0
- package/editor/helpers/gen_music.py +316 -0
- package/editor/helpers/gen_sfx.py +148 -0
- package/editor/helpers/grade.py +307 -0
- package/editor/helpers/icloud.py +119 -0
- package/editor/helpers/make_sfx.py +940 -0
- package/editor/helpers/matte.py +1342 -0
- package/editor/helpers/medir_referencia.py +2232 -0
- package/editor/helpers/mixar_trilha.py +354 -0
- package/editor/helpers/montar_corte.py +295 -0
- package/editor/helpers/motion/__init__.py +13 -0
- package/editor/helpers/motion/engine.py +1915 -0
- package/editor/helpers/motion/registry.py +510 -0
- package/editor/helpers/motion/render.py +665 -0
- package/editor/helpers/motion/templates/barra_progresso.py +370 -0
- package/editor/helpers/motion/templates/carimbo.py +132 -0
- package/editor/helpers/motion/templates/checklist.py +271 -0
- package/editor/helpers/motion/templates/comentario_cta.py +380 -0
- package/editor/helpers/motion/templates/contador.py +222 -0
- package/editor/helpers/motion/templates/destaque.py +263 -0
- package/editor/helpers/motion/templates/etiqueta_preco.py +461 -0
- package/editor/helpers/motion/templates/grafico_barras.py +510 -0
- package/editor/helpers/motion/templates/grafico_linha.py +462 -0
- package/editor/helpers/motion/templates/notificacao.py +266 -0
- package/editor/helpers/motion/templates/palavra_impacto.py +410 -0
- package/editor/helpers/motion/templates/passos.py +321 -0
- package/editor/helpers/motion/templates/riscado.py +298 -0
- package/editor/helpers/motion/templates/selo_garantia.py +150 -0
- package/editor/helpers/motion_render.py +241 -0
- package/editor/helpers/motion_sugerir.py +961 -0
- package/editor/helpers/noise_check.py +134 -0
- package/editor/helpers/pack_transcripts.py +223 -0
- package/editor/helpers/palavras_na_onda.py +176 -0
- package/editor/helpers/plano_sfx.py +839 -0
- package/editor/helpers/portao.py +427 -0
- package/editor/helpers/preview_server.py +1177 -0
- package/editor/helpers/rastrear_rosto.py +298 -0
- package/editor/helpers/render_cut.py +1256 -0
- package/editor/helpers/respiros.py +89 -0
- package/editor/helpers/sincronizar_cameras.py +571 -0
- package/editor/helpers/speech_regions.py +423 -0
- package/editor/helpers/style_catalog.json +231 -0
- package/editor/helpers/style_samples.py +246 -0
- package/editor/helpers/tela_verde.py +2113 -0
- package/editor/helpers/text_render.py +2059 -0
- package/editor/helpers/texto_na_tela.py +366 -0
- package/editor/helpers/timeline_view.py +292 -0
- package/editor/helpers/tira_de_quadros.py +113 -0
- package/editor/helpers/transcribe.py +734 -0
- package/editor/helpers/transcribe_batch.py +129 -0
- package/editor/helpers/trilhas.py +1146 -0
- package/editor/helpers/trilhas_humores.json +66 -0
- package/editor/helpers/valores.py +187 -0
- package/editor/helpers/verify_cut.py +820 -0
- package/editor/helpers/voice_levels.py +233 -0
- package/editor/helpers/voz_check.py +386 -0
- package/editor/helpers/voz_limpa.py +81 -0
- package/editor/helpers/watch_preview.py +372 -0
- package/editor/modelo-projeto/LEIA.md +402 -0
- package/editor/modelo-projeto/assets/fontes/OFL-Caveat.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/OFL-JetBrainsMono.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/OFL-Montserrat.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/larguras.json +1 -0
- package/editor/modelo-projeto/assets/three/BufferGeometryUtils.js +1438 -0
- package/editor/modelo-projeto/assets/three/GLTFLoader.js +4895 -0
- package/editor/modelo-projeto/assets/three/LICENSE +21 -0
- package/editor/modelo-projeto/assets/three/ORIGEM.txt +17 -0
- package/editor/modelo-projeto/assets/three/RoomEnvironment.js +185 -0
- package/editor/modelo-projeto/assets/three/three.core.min.js +6 -0
- package/editor/modelo-projeto/assets/three/three.module.min.js +6 -0
- package/editor/modelo-projeto/build.py +4828 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_antes_depois.html +199 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_arquivo.html +232 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_busca.html +315 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_comando.html +304 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_conversa.html +217 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_esquema.html +289 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_medidor.html +249 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_notas.html +233 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_caminho.html +197 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_explodida.html +152 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_tela.html +167 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_volume.html +124 -0
- package/editor/modelo-projeto/compositions/cenas/3d/motor-three.js +728 -0
- package/editor/modelo-projeto/compositions/cenas/LEIA.md +177 -0
- package/editor/modelo-projeto/ferramentas/medir_fontes.py +65 -0
- package/editor/modelo-projeto/ferramentas/pessoa_recortada.py +176 -0
- package/editor/modelo-projeto/ferramentas/rastrear_maos.py +189 -0
- package/editor/modelo-projeto/hyperframes.json +12 -0
- package/editor/modelo-projeto/package.json +11 -0
- package/editor/modelo-projeto/projeto.json +16 -0
- package/editor/preview/app.css +1009 -0
- package/editor/preview/app.js +2122 -0
- package/editor/preview/index.html +307 -0
- package/editor/pyproject.toml +23 -0
- package/editor/references/3d.md +236 -0
- package/editor/references/animacao-catalogo.md +672 -0
- package/editor/references/animacao.md +498 -0
- package/editor/references/comandos.md +92 -0
- package/editor/references/corte-seco-como-funciona.md +50 -0
- package/editor/references/corte-seco.md +88 -0
- package/editor/references/corte.md +290 -0
- package/editor/references/direcao-por-blocos.md +218 -0
- package/editor/references/enquadramento.md +654 -0
- package/editor/references/estilos-prontos.md +355 -0
- package/editor/references/estudio.md +100 -0
- package/editor/references/exemplo-eventos.json +67 -0
- package/editor/references/lettering.md +252 -0
- package/editor/references/modelos.md +494 -0
- package/editor/references/phase2.md +1007 -0
- package/editor/references/referencia-aplicar.md +203 -0
- package/editor/references/referencia-medir.md +375 -0
- package/editor/references/revisao.md +335 -0
- package/editor/references/som-e-trilha.md +146 -0
- package/editor/references/tabela-de-sons.md +130 -0
- package/editor/references/variacoes.md +234 -0
- package/editor/uv.lock +1141 -0
- package/lib/act.js +36 -8
- package/lib/catalogo.js +4 -0
- package/lib/chamada-torta.js +81 -0
- package/lib/editor-pro.js +786 -0
- package/lib/entrega.js +58 -0
- package/lib/estudio/conferencia-visual.js +76 -0
- package/lib/estudio/diretor.js +6 -1
- package/lib/estudio/exportar.js +3 -1
- package/lib/estudio/gosto.js +45 -4
- package/lib/estudio/index.js +8 -5
- package/lib/estudio/kit-ae/base.jsx +119 -13
- package/lib/estudio/kit-ae/cenas.jsx +47 -15
- package/lib/estudio/kit-ae/guarda.jsx +274 -0
- package/lib/estudio/kit-ae/primitivas.jsx +180 -24
- package/lib/estudio/maquina-diretor.js +61 -7
- package/lib/estudio/maquina-foto.js +177 -0
- package/lib/estudio/maquina-guarda.js +207 -0
- package/lib/estudio/maquina-layout.js +22 -1
- package/lib/estudio/maquina.js +60 -11
- package/lib/estudio/referencias.js +1 -1
- package/lib/estudio/visao/visao.swift +97 -0
- package/lib/estudio/visao.js +60 -0
- package/lib/local/cerebro.json +192 -6
- package/lib/local/motor.js +5 -2
- package/lib/nuvem.js +11 -1
- package/lib/passagem.js +50 -0
- package/lib/perigo.js +19 -11
- package/lib/protecao.js +134 -0
- package/lib/titulo-conversa.js +46 -0
- package/lib/tools.js +3 -0
- package/lib/ui.js +1 -1
- package/package.json +3 -2
- package/skills/editar-meu-video/SKILL.md +6 -2
- package/skills/editar-video/SKILL.md +113 -0
- package/studio/pessoa.py +52 -0
- package/voz/primo_voz/app.py +43 -4
- package/voz/primo_voz/chat.html +17 -6
- package/voz/primo_voz/painel_dados.py +134 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Limpa o ruído da voz do corte (cut.mp4) e põe a voz limpa no lugar, sem mexer na imagem nem no tempo.
|
|
2
|
+
|
|
3
|
+
Escrito pelo PrimoCode em cima do EDIT IA PRO (SHK Soluções Digitais, com autorização por escrito, 19/10/2026).
|
|
4
|
+
No caminho do HyperFrames a voz vem do cut.mp4, e a limpeza do denoise.py só rodava dentro do compose_final (motor
|
|
5
|
+
antigo). Aqui ela roda depois do render_cut: a voz é refeita das fontes, trecho a trecho (limpeza antes do ganho, do
|
|
6
|
+
limitador e dos fades, igual ao render_cut), normalizada em -14 LUFS e trocada no cut.mp4. O original fica em
|
|
7
|
+
<edit>/cut_sem_limpeza.mp4 e o relatório em <edit>/verify/voz_limpa.json (com o modo do loudnorm).
|
|
8
|
+
|
|
9
|
+
Uso:
|
|
10
|
+
python helpers/voz_limpa.py <edit> [--modo leve|forte]
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
22
|
+
import denoise # noqa: E402
|
|
23
|
+
from common import load_json # noqa: E402
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main() -> int:
|
|
27
|
+
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
|
28
|
+
ap.add_argument("edit", type=Path)
|
|
29
|
+
ap.add_argument("--modo", default="leve", choices=["leve", "forte"])
|
|
30
|
+
a = ap.parse_args()
|
|
31
|
+
edit = a.edit.resolve()
|
|
32
|
+
cut, tl_path = edit / "cut.mp4", edit / "cut_timeline.json"
|
|
33
|
+
if not cut.exists() or not tl_path.exists():
|
|
34
|
+
print("erro: falta o cut.mp4 ou o cut_timeline.json; rode o render_cut antes", file=sys.stderr)
|
|
35
|
+
return 2
|
|
36
|
+
tl = load_json(tl_path)
|
|
37
|
+
fps = int(round(tl.get("fps") or 30))
|
|
38
|
+
frames = int(tl["frames"])
|
|
39
|
+
total = frames * 48000 // fps
|
|
40
|
+
original = edit / "cut_sem_limpeza.mp4"
|
|
41
|
+
marca = edit / "verify" / "voz_limpa.json"
|
|
42
|
+
assinatura = lambda p: [p.stat().st_size, int(p.stat().st_mtime)] # noqa: E731
|
|
43
|
+
# rodar de novo parte do corte SEM limpeza (senão limparia duas vezes): o cut.mp4 que esta ferramenta gravou
|
|
44
|
+
# tem a assinatura anotada; um render_cut novo troca a assinatura e vira o novo original
|
|
45
|
+
ja_limpo = False
|
|
46
|
+
try:
|
|
47
|
+
ja_limpo = original.exists() and load_json(marca).get("cut_assinatura") == assinatura(cut)
|
|
48
|
+
except Exception: # noqa: BLE001
|
|
49
|
+
ja_limpo = False
|
|
50
|
+
if not ja_limpo:
|
|
51
|
+
shutil.copy2(cut, original)
|
|
52
|
+
base = original
|
|
53
|
+
work = edit / "clips" / "voz_limpa"
|
|
54
|
+
work.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
res = denoise.build_voice(edit, base, tl, fps, total, a.modo, work, keep_temp=False)
|
|
56
|
+
rel = {"modo": a.modo, "voz": str(res.path) if res.path else None, "relatorio": res.report}
|
|
57
|
+
if not res.path or res.report.get("applied") in (None, "off"):
|
|
58
|
+
rel["aviso"] = "a limpeza não se aplicou (sem ruído medível ou sem fonte); o corte ficou como estava"
|
|
59
|
+
(edit / "verify").mkdir(exist_ok=True)
|
|
60
|
+
(edit / "verify" / "voz_limpa.json").write_text(json.dumps(rel, ensure_ascii=False, indent=1, default=str))
|
|
61
|
+
print(rel["aviso"])
|
|
62
|
+
return 0
|
|
63
|
+
dur = float(tl["duration"])
|
|
64
|
+
tmp = edit / ".cut_limpo.part.mp4"
|
|
65
|
+
r = subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", str(base), "-i", str(res.path), "-map", "0:v:0", "-map",
|
|
66
|
+
"1:a:0", "-c:v", "copy", "-c:a", "aac", "-b:a", "192k", "-ar", "48000", "-ac", "2", "-t",
|
|
67
|
+
f"{dur:.6f}", "-movflags", "+faststart", str(tmp)], capture_output=True, text=True)
|
|
68
|
+
if r.returncode:
|
|
69
|
+
print("erro: o ffmpeg falhou ao trocar a voz:\n" + r.stderr[-800:], file=sys.stderr)
|
|
70
|
+
return 1
|
|
71
|
+
tmp.replace(cut)
|
|
72
|
+
rel["cut_assinatura"] = assinatura(cut)
|
|
73
|
+
(edit / "verify").mkdir(exist_ok=True)
|
|
74
|
+
marca.write_text(json.dumps(rel, ensure_ascii=False, indent=1, default=str))
|
|
75
|
+
modo_ln = res.report.get("loudnorm_modo")
|
|
76
|
+
print(f"voz limpa ({a.modo}) no cut.mp4; loudnorm {modo_ln}; original em {original.name}")
|
|
77
|
+
return 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
sys.exit(main())
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""Print what the user did in the preview, one flushed line per new event (SPEC Contract 9).
|
|
2
|
+
|
|
3
|
+
Reads edit/preview_marks.json, edit/preview_trims.json, edit/preview_approval.json and
|
|
4
|
+
edit/preview_style.json.
|
|
5
|
+
Remembers what it already reported (by `saved_at`) in edit/preview_cache/watch_seen.json,
|
|
6
|
+
so running it again never repeats an event. Every line ends with the time the person saved it
|
|
7
|
+
("· salvo às 14:30:14"), so a resumed session can compare it with the last render.
|
|
8
|
+
|
|
9
|
+
When an approval arrives while marks for the same video were saved after its last render, an extra
|
|
10
|
+
`[preview] atenção: ...` line follows. The loop mode writes preview_cache/watcher.json every few
|
|
11
|
+
seconds, so the preview page knows a live watcher is listening (Codex has none).
|
|
12
|
+
|
|
13
|
+
Output is UTF-8 and never stops on a character the console cannot show.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
uv run python helpers/watch_preview.py --edit-dir DIR [--once] [--interval 1.0]
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
from datetime import datetime
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
30
|
+
|
|
31
|
+
from common import fmt_time, load_json, load_style_catalog, save_json # noqa: E402
|
|
32
|
+
|
|
33
|
+
SOURCES = (
|
|
34
|
+
("approval", "preview_approval.json"),
|
|
35
|
+
("marks", "preview_marks.json"),
|
|
36
|
+
("trims", "preview_trims.json"),
|
|
37
|
+
("style", "preview_style.json"),
|
|
38
|
+
)
|
|
39
|
+
PHASE_NAME = {"cut": "corte", "final": "vídeo final"}
|
|
40
|
+
PHASE_IN = {"cut": "no corte", "final": "no vídeo final"}
|
|
41
|
+
SEEN_LIMIT = 50
|
|
42
|
+
HEARTBEAT_S = 3.0
|
|
43
|
+
DEFAULT_MEDIA = {"cut": "cut.mp4", "final": "final.mp4"}
|
|
44
|
+
MEDIA_KEY = {"cut": "cut_video", "final": "final_video"}
|
|
45
|
+
DENOISE_LABEL = {"off": "desligado", "leve": "leve", "forte": "forte"} # preview_style.json "denoise"
|
|
46
|
+
# preview_style.json "music_mood": "off" (Sem trilha), "arquivo" (+ music_file) or a library mood id
|
|
47
|
+
# preview_style.json "tela_verde_posicao" and "tela_verde_inserts_fundo" (with "edit_type": "tela_verde")
|
|
48
|
+
TV_POSITION_LABEL = {"esquerda": "canto esquerdo", "direita": "canto direito", "centro": "meio"}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def utf8_console() -> None:
|
|
52
|
+
"""Windows pipes (Monitor, Codex) default to the ANSI code page: an emoji in a note must not crash."""
|
|
53
|
+
for stream in (sys.stdout, sys.stderr):
|
|
54
|
+
try:
|
|
55
|
+
stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[attr-defined]
|
|
56
|
+
except (AttributeError, ValueError, OSError):
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def saved_clock(saved_at: str) -> str:
|
|
61
|
+
try:
|
|
62
|
+
return datetime.fromisoformat(str(saved_at)).strftime("%H:%M:%S")
|
|
63
|
+
except ValueError:
|
|
64
|
+
return str(saved_at)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def quote(text: str) -> str:
|
|
68
|
+
"""One-line, double-quoted note."""
|
|
69
|
+
clean = " ".join(str(text or "").split())
|
|
70
|
+
return '"' + clean.replace('"', "'") + '"'
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def format_approval(rec: dict) -> str:
|
|
74
|
+
phase = PHASE_NAME.get(rec.get("phase"), str(rec.get("phase")))
|
|
75
|
+
line = f"[preview] {phase} APROVADO"
|
|
76
|
+
note = (rec.get("note") or "").strip()
|
|
77
|
+
if note:
|
|
78
|
+
line += f" · nota: {quote(note)}"
|
|
79
|
+
return line
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def format_marks(rec: dict) -> str:
|
|
83
|
+
marks = rec.get("marks") or []
|
|
84
|
+
where = PHASE_IN.get(rec.get("phase"), f"em {rec.get('phase')}")
|
|
85
|
+
if not marks:
|
|
86
|
+
return f"[preview] marcações {where} apagadas (0 marcações)"
|
|
87
|
+
parts = []
|
|
88
|
+
for m in marks:
|
|
89
|
+
note = (m.get("note") or "").strip()
|
|
90
|
+
if m.get("kind") == "motion":
|
|
91
|
+
piece = f"MOTION AQUI em {fmt_time(float(m.get('in', 0)))}"
|
|
92
|
+
parts.append(f"{piece} {quote(note)}" if note else f"{piece} (o agente escolhe o modelo pela fala)")
|
|
93
|
+
continue
|
|
94
|
+
piece = f"{fmt_time(float(m.get('in', 0)))} a {fmt_time(float(m.get('out', 0)))}"
|
|
95
|
+
parts.append(f"{piece} {quote(note)}" if note else f"{piece} (sem nota)")
|
|
96
|
+
label = "marcação" if len(marks) == 1 else "marcações"
|
|
97
|
+
return f"[preview] {len(marks)} {label} {where}: " + "; ".join(parts)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def format_trims(rec: dict) -> str:
|
|
101
|
+
"""Pedidos de esticar/encolher a borda de um trecho, em segundos do EDL."""
|
|
102
|
+
trims = rec.get("trims") or []
|
|
103
|
+
where = PHASE_IN.get(rec.get("phase"), f"em {rec.get('phase')}")
|
|
104
|
+
if not trims:
|
|
105
|
+
return f"[preview] ajustes de trecho {where} apagados (0 ajustes)"
|
|
106
|
+
parts = []
|
|
107
|
+
for tr in trims:
|
|
108
|
+
rid = str(tr.get("id", "?"))
|
|
109
|
+
bits = []
|
|
110
|
+
for key, onde in (("start_delta", "no começo"), ("end_delta", "no fim")):
|
|
111
|
+
delta = float(tr.get(key) or 0.0)
|
|
112
|
+
if not delta:
|
|
113
|
+
continue
|
|
114
|
+
verbo = "estica" if (delta < 0 if key == "start_delta" else delta > 0) else "encolhe"
|
|
115
|
+
bits.append(f"{verbo} {abs(delta):.2f}s {onde}")
|
|
116
|
+
quote_txt = (tr.get("quote") or "").strip()
|
|
117
|
+
peca = f"{rid}: " + " e ".join(bits or ["sem mudança"])
|
|
118
|
+
parts.append(f"{peca} {quote(quote_txt)}" if quote_txt else peca)
|
|
119
|
+
label = "ajuste de trecho" if len(trims) == 1 else "ajustes de trecho"
|
|
120
|
+
return (f"[preview] {len(trims)} {label} {where}: " + "; ".join(parts)
|
|
121
|
+
+ " · aplique no edl.json (start_delta soma no start, end_delta soma no end) e renderize de novo")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def format_style(rec: dict, catalog: dict) -> str:
|
|
125
|
+
elements = rec.get("elements") or {}
|
|
126
|
+
order = [e.get("id") for e in (catalog.get("elements") or []) if e.get("id")]
|
|
127
|
+
order += [k for k in elements if k not in order]
|
|
128
|
+
on = [k for k in order if elements.get(k) is True]
|
|
129
|
+
off = [k for k in order if k in elements and elements.get(k) is False]
|
|
130
|
+
edit_type = rec.get("edit_type") or "-"
|
|
131
|
+
if edit_type == "tela_verde":
|
|
132
|
+
pos = rec.get("tela_verde_posicao") or "direita"
|
|
133
|
+
moved = rec.get("tela_verde_inserts_fundo")
|
|
134
|
+
if isinstance(moved, int) and not isinstance(moved, bool) and moved > 0:
|
|
135
|
+
what = ("o insert de imagem em cima ou embaixo vai" if moved == 1 else
|
|
136
|
+
f"os {moved} inserts de imagem em cima ou embaixo vão")
|
|
137
|
+
what += " para o fundo, sem tela dividida"
|
|
138
|
+
else:
|
|
139
|
+
what = "imagens e vídeos dos inserts vão para o fundo, sem tela dividida"
|
|
140
|
+
edit_type += f" ({TV_POSITION_LABEL.get(pos, pos)}: tela_verde.posicao \"{pos}\"; {what})"
|
|
141
|
+
fields = [
|
|
142
|
+
f"[preview] estilo salvo: {edit_type}",
|
|
143
|
+
f"legenda {rec.get('caption_style') or '-'}",
|
|
144
|
+
f"gancho {rec.get('hook_style') or '-'}",
|
|
145
|
+
f"cor {rec.get('accent') or '-'}",
|
|
146
|
+
"elementos: " + (", ".join(on) if on else "nenhum"),
|
|
147
|
+
"fora: " + (", ".join(off) if off else "nenhum"),
|
|
148
|
+
]
|
|
149
|
+
if isinstance(elements.get("motion"), bool):
|
|
150
|
+
if elements["motion"] and rec.get("motion_qtd"):
|
|
151
|
+
n = {"poucas": 3, "algumas": 5}.get(rec["motion_qtd"], 3)
|
|
152
|
+
tema = rec.get("motion_tema") or "papelaria"
|
|
153
|
+
fields.append(f"animações: ligadas, {rec['motion_qtd']} (motion.quantidade {n}), visual {tema} "
|
|
154
|
+
f"(motion.tema \"{tema}\")")
|
|
155
|
+
else:
|
|
156
|
+
fields.append(f"animações: {'ligadas' if elements['motion'] else 'desligadas'}")
|
|
157
|
+
if isinstance(rec.get("denoise"), str) and rec["denoise"] in DENOISE_LABEL:
|
|
158
|
+
fields.append(f"ruído: {DENOISE_LABEL[rec['denoise']]}")
|
|
159
|
+
mood = rec.get("music_mood")
|
|
160
|
+
if isinstance(mood, str) and mood:
|
|
161
|
+
if mood == "off":
|
|
162
|
+
fields.append("trilha: sem")
|
|
163
|
+
elif mood == "arquivo":
|
|
164
|
+
fields.append(f"trilha: arquivo da pasta ({rec.get('music_file') or '?'})")
|
|
165
|
+
else:
|
|
166
|
+
fields.append(f"trilha: {mood}")
|
|
167
|
+
if (rec.get("hook_text") or "").strip():
|
|
168
|
+
fields.append(f"texto do gancho: {quote(rec['hook_text'])}")
|
|
169
|
+
if (rec.get("cta_text") or "").strip():
|
|
170
|
+
fields.append(f"chamada: {quote(rec['cta_text'])}")
|
|
171
|
+
if (rec.get("note") or "").strip():
|
|
172
|
+
fields.append(f"nota: {quote(rec['note'])}")
|
|
173
|
+
return " · ".join(fields)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def read_record(path: Path) -> dict | None:
|
|
177
|
+
for attempt in range(3):
|
|
178
|
+
try:
|
|
179
|
+
data = load_json(path)
|
|
180
|
+
return data if isinstance(data, dict) else None
|
|
181
|
+
except json.JSONDecodeError:
|
|
182
|
+
time.sleep(0.05)
|
|
183
|
+
except OSError:
|
|
184
|
+
return None
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def load_seen(path: Path) -> dict[str, list[str]]:
|
|
189
|
+
try:
|
|
190
|
+
data = load_json(path, default={}) or {}
|
|
191
|
+
except (json.JSONDecodeError, OSError):
|
|
192
|
+
data = {}
|
|
193
|
+
seen: dict[str, list[str]] = {}
|
|
194
|
+
for kind, _ in SOURCES:
|
|
195
|
+
value = data.get(kind)
|
|
196
|
+
if isinstance(value, str):
|
|
197
|
+
value = [value]
|
|
198
|
+
seen[kind] = [v for v in (value or []) if isinstance(v, str)]
|
|
199
|
+
return seen
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def collect_new(edit_dir: Path, seen: dict[str, list[str]]) -> list[tuple[str, str, dict]]:
|
|
203
|
+
events = []
|
|
204
|
+
for kind, filename in SOURCES:
|
|
205
|
+
path = edit_dir / filename
|
|
206
|
+
if not path.is_file():
|
|
207
|
+
continue
|
|
208
|
+
rec = read_record(path)
|
|
209
|
+
if not rec:
|
|
210
|
+
continue
|
|
211
|
+
saved_at = rec.get("saved_at")
|
|
212
|
+
if not isinstance(saved_at, str) or not saved_at:
|
|
213
|
+
continue
|
|
214
|
+
if saved_at in seen.get(kind, []):
|
|
215
|
+
continue
|
|
216
|
+
events.append((saved_at, kind, rec))
|
|
217
|
+
events.sort(key=lambda e: e[0])
|
|
218
|
+
return events
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def media_mtime(edit_dir: Path, phase: str) -> float | None:
|
|
222
|
+
"""mtime of the video the person was reviewing for this phase."""
|
|
223
|
+
rel = DEFAULT_MEDIA.get(phase)
|
|
224
|
+
try:
|
|
225
|
+
state = load_json(edit_dir / "state.json") or {}
|
|
226
|
+
if isinstance(state, dict) and isinstance(state.get(MEDIA_KEY.get(phase, "")), str):
|
|
227
|
+
rel = state[MEDIA_KEY[phase]]
|
|
228
|
+
except (json.JSONDecodeError, OSError):
|
|
229
|
+
pass
|
|
230
|
+
try:
|
|
231
|
+
return (edit_dir / rel).stat().st_mtime if rel else None
|
|
232
|
+
except OSError:
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def iso_epoch(saved_at: str) -> float | None:
|
|
237
|
+
try:
|
|
238
|
+
return datetime.fromisoformat(str(saved_at)).timestamp()
|
|
239
|
+
except ValueError:
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def pending_marks_note(edit_dir: Path, approval: dict) -> str | None:
|
|
244
|
+
"""Marks for the same video saved after its render and before the approval: the agent must ask."""
|
|
245
|
+
phase = approval.get("phase")
|
|
246
|
+
marks = read_record(edit_dir / "preview_marks.json")
|
|
247
|
+
if not marks or marks.get("phase") != phase or not marks.get("marks"):
|
|
248
|
+
return None
|
|
249
|
+
marks_t, approval_t = iso_epoch(marks.get("saved_at", "")), iso_epoch(approval.get("saved_at", ""))
|
|
250
|
+
render_t = media_mtime(edit_dir, str(phase))
|
|
251
|
+
if marks_t is None or approval_t is None or marks_t > approval_t + 0.001:
|
|
252
|
+
return None
|
|
253
|
+
if render_t is not None and marks_t < render_t:
|
|
254
|
+
return None # those marks were for an older version, already handled
|
|
255
|
+
n = len(marks["marks"])
|
|
256
|
+
where = PHASE_IN.get(phase, f"em {phase}")
|
|
257
|
+
label = "marcação" if n == 1 else "marcações"
|
|
258
|
+
return (f"[preview] atenção: a aprovação veio junto com {n} {label} {where} salvas depois do último render. "
|
|
259
|
+
"Pergunte se é para ajustar antes de seguir.")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def stale_approval_note(edit_dir: Path, approval: dict) -> str | None:
|
|
263
|
+
"""The approval names the file version the person watched; a newer render needs a new approval."""
|
|
264
|
+
rel, mtime, size = approval.get("file"), approval.get("file_mtime"), approval.get("file_size")
|
|
265
|
+
if not isinstance(rel, str) or mtime is None:
|
|
266
|
+
return None
|
|
267
|
+
try:
|
|
268
|
+
st = (edit_dir / rel).stat()
|
|
269
|
+
except OSError:
|
|
270
|
+
return None
|
|
271
|
+
if abs(st.st_mtime - float(mtime)) <= 0.01 and (size is None or int(size) == st.st_size):
|
|
272
|
+
return None
|
|
273
|
+
name = PHASE_NAME.get(approval.get("phase"), str(approval.get("phase")))
|
|
274
|
+
return (f"[preview] atenção: essa aprovação é de uma versão anterior do {name} ({rel} mudou depois). "
|
|
275
|
+
"Peça para a pessoa assistir e aprovar de novo.")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def emit(line: str) -> None:
|
|
279
|
+
try:
|
|
280
|
+
print(line, flush=True)
|
|
281
|
+
except (UnicodeError, OSError):
|
|
282
|
+
try:
|
|
283
|
+
sys.stdout.buffer.write((line + "\n").encode("utf-8", "replace"))
|
|
284
|
+
sys.stdout.flush()
|
|
285
|
+
except (AttributeError, OSError):
|
|
286
|
+
pass
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def report(edit_dir: Path, seen_path: Path, seen: dict[str, list[str]], catalog: dict) -> int:
|
|
290
|
+
events = collect_new(edit_dir, seen)
|
|
291
|
+
if not events:
|
|
292
|
+
return 0
|
|
293
|
+
for saved_at, kind, rec in events:
|
|
294
|
+
# mark it seen first: one line that fails to print must never block every later event
|
|
295
|
+
seen[kind] = (seen.get(kind, []) + [saved_at])[-SEEN_LIMIT:]
|
|
296
|
+
try:
|
|
297
|
+
if kind == "approval":
|
|
298
|
+
line = format_approval(rec)
|
|
299
|
+
elif kind == "marks":
|
|
300
|
+
line = format_marks(rec)
|
|
301
|
+
elif kind == "trims":
|
|
302
|
+
line = format_trims(rec)
|
|
303
|
+
else:
|
|
304
|
+
line = format_style(rec, catalog)
|
|
305
|
+
except (TypeError, ValueError, AttributeError) as exc:
|
|
306
|
+
line = f"[preview] {kind}: não consegui ler o registro ({exc}); abra o arquivo preview_{kind}.json"
|
|
307
|
+
emit(f"{line} · salvo às {saved_clock(saved_at)}")
|
|
308
|
+
if kind == "approval":
|
|
309
|
+
for note in (stale_approval_note(edit_dir, rec), pending_marks_note(edit_dir, rec)):
|
|
310
|
+
if note:
|
|
311
|
+
emit(note)
|
|
312
|
+
try:
|
|
313
|
+
save_json(seen_path, seen)
|
|
314
|
+
except OSError as exc:
|
|
315
|
+
print(f"Aviso: não consegui gravar {seen_path}: {exc}", file=sys.stderr, flush=True)
|
|
316
|
+
return len(events)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def heartbeat(edit_dir: Path) -> None:
|
|
320
|
+
try:
|
|
321
|
+
save_json(edit_dir / "preview_cache" / "watcher.json", {"mode": "loop", "pid": os.getpid(), "updated_at": time.time()})
|
|
322
|
+
except OSError:
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def main() -> int:
|
|
327
|
+
parser = argparse.ArgumentParser(
|
|
328
|
+
description="Mostra, uma linha por evento, o que a pessoa fez no preview (aprovação, marcações, estilo).",
|
|
329
|
+
)
|
|
330
|
+
parser.add_argument("--edit-dir", required=True, help="pasta edit/ da sessão")
|
|
331
|
+
parser.add_argument("--once", action="store_true", help="verifica uma vez e sai")
|
|
332
|
+
parser.add_argument("--interval", type=float, default=1.0, help="segundos entre verificações (padrão 1.0)")
|
|
333
|
+
args = parser.parse_args()
|
|
334
|
+
utf8_console()
|
|
335
|
+
|
|
336
|
+
edit_dir = Path(args.edit_dir).expanduser().resolve()
|
|
337
|
+
if not edit_dir.is_dir():
|
|
338
|
+
print(f"Erro: a pasta de edição não existe: {edit_dir}", file=sys.stderr)
|
|
339
|
+
return 2
|
|
340
|
+
if args.interval <= 0:
|
|
341
|
+
print("Erro: --interval precisa ser maior que zero.", file=sys.stderr)
|
|
342
|
+
return 2
|
|
343
|
+
seen_path = edit_dir / "preview_cache" / "watch_seen.json"
|
|
344
|
+
seen = load_seen(seen_path)
|
|
345
|
+
catalog = load_style_catalog()
|
|
346
|
+
|
|
347
|
+
if args.once:
|
|
348
|
+
if report(edit_dir, seen_path, seen, catalog) == 0:
|
|
349
|
+
print("[preview] nada novo", file=sys.stderr, flush=True)
|
|
350
|
+
return 0
|
|
351
|
+
|
|
352
|
+
print("Esperando ações no preview. Ctrl+C para parar.", file=sys.stderr, flush=True)
|
|
353
|
+
last_beat = 0.0
|
|
354
|
+
try:
|
|
355
|
+
while True:
|
|
356
|
+
if not catalog:
|
|
357
|
+
catalog = load_style_catalog()
|
|
358
|
+
if time.time() - last_beat >= HEARTBEAT_S:
|
|
359
|
+
heartbeat(edit_dir)
|
|
360
|
+
last_beat = time.time()
|
|
361
|
+
report(edit_dir, seen_path, seen, catalog)
|
|
362
|
+
time.sleep(args.interval)
|
|
363
|
+
except KeyboardInterrupt:
|
|
364
|
+
try:
|
|
365
|
+
(edit_dir / "preview_cache" / "watcher.json").unlink()
|
|
366
|
+
except OSError:
|
|
367
|
+
pass
|
|
368
|
+
return 0
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
if __name__ == "__main__":
|
|
372
|
+
sys.exit(main())
|