primocode 10.16.0 → 10.17.1
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 +28 -8
- 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/mcp.js +105 -7
- package/lib/nuvem.js +11 -1
- package/lib/outra-pasta.js +1 -1
- package/lib/passagem.js +50 -0
- package/lib/perigo.js +29 -12
- package/lib/protecao.js +134 -0
- package/lib/sandbox.js +11 -4
- 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,246 @@
|
|
|
1
|
+
"""Sample PNGs of every caption and hook style on a real frame of the cut (Contract 7).
|
|
2
|
+
|
|
3
|
+
Writes edit/samples/caption_<id>.png and edit/samples/hook_<id>.png at 540x960 (960x540 for a
|
|
4
|
+
horizontal cut) plus edit/samples/index.json, and points state.json "samples" at the index.
|
|
5
|
+
|
|
6
|
+
The speaker's face is found on the frame (face_layout.py, YuNet) and each sample goes to the free
|
|
7
|
+
spot closest to its usual height, off the face, the way compose_final.py places the real text.
|
|
8
|
+
Without --time, when the frame at 25% shows no speaker (a screen, a product), the frame moves to the
|
|
9
|
+
middle of the nearest cut segment with a face (face_layout.json, which compose_final.py reuses); a
|
|
10
|
+
frame where the detector misses the face uses its segment's face zone. A
|
|
11
|
+
number in edit_data.json (hook.center_y, captions.center_y) is used as it is. index.json also
|
|
12
|
+
gets "face" (the zone found, fractions of the frame, or null), "positions" ({"caption": {id: px},
|
|
13
|
+
"hook": {id: px}} on the full-size canvas) and "suggest" ({"camera_focus_y": eyes line or null}).
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
uv run python helpers/style_samples.py --edit-dir DIR [--time T] [--accent HEX] [--text "..."]
|
|
17
|
+
[--hook-text "..."] [--video other.mp4]
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
import tempfile
|
|
26
|
+
import time
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from PIL import Image
|
|
30
|
+
|
|
31
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
32
|
+
from common import hex_to_rgb, load_json, load_style_catalog, now_iso, probe_video, require_binary, save_json # noqa: E402
|
|
33
|
+
import face_layout # noqa: E402
|
|
34
|
+
import text_render as tr # noqa: E402
|
|
35
|
+
|
|
36
|
+
DEFAULT_CAPTION_TEXT = "Olha só isso aqui"
|
|
37
|
+
DEFAULT_HOOK_TEXT = "Você está perdendo venda assim"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def grab_frame(video: Path, t: float) -> Image.Image:
|
|
41
|
+
with tempfile.TemporaryDirectory(prefix="samples-") as tmp:
|
|
42
|
+
out = Path(tmp) / "frame.png"
|
|
43
|
+
cmd = ["ffmpeg", "-v", "error", "-y", "-ss", f"{max(0.0, t):.3f}", "-i", str(video), "-frames:v", "1", str(out)]
|
|
44
|
+
proc = subprocess.run(cmd, capture_output=True, text=True)
|
|
45
|
+
if proc.returncode != 0 or not out.exists():
|
|
46
|
+
# seeking past the last frame gives nothing: fall back to the first frame
|
|
47
|
+
cmd[5] = "0"
|
|
48
|
+
proc = subprocess.run(cmd, capture_output=True, text=True)
|
|
49
|
+
if not out.exists():
|
|
50
|
+
raise RuntimeError(f"não consegui extrair um quadro de {video}: {proc.stderr[-400:]}")
|
|
51
|
+
with Image.open(out) as im:
|
|
52
|
+
return im.convert("RGB")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def caption_text_near(captions: dict | None, t: float) -> str | None:
|
|
56
|
+
"""A real cue around time t (prefers cues with 2+ words)."""
|
|
57
|
+
if not captions:
|
|
58
|
+
return None
|
|
59
|
+
cues = [c for c in captions.get("cues", []) if c.get("words")]
|
|
60
|
+
if not cues:
|
|
61
|
+
return None
|
|
62
|
+
multi = [c for c in cues if len(c["words"]) >= 2] or cues
|
|
63
|
+
best = min(multi, key=lambda c: abs((c["start"] + c["end"]) / 2.0 - t))
|
|
64
|
+
return " ".join(w["text"] for w in best["words"])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _load_dict(path: Path, notes: list[str]) -> dict:
|
|
68
|
+
"""A JSON object from the edit dir, or {} (with a note) when it is missing or unreadable."""
|
|
69
|
+
try:
|
|
70
|
+
data = load_json(path)
|
|
71
|
+
except (ValueError, OSError) as e:
|
|
72
|
+
notes.append(f"ignorei {path.name}: não consegui ler ({e})")
|
|
73
|
+
return {}
|
|
74
|
+
return data if isinstance(data, dict) else {}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _manual(sec: dict, key: str) -> float | None:
|
|
78
|
+
v = sec.get(key) if isinstance(sec, dict) else None
|
|
79
|
+
if v is None or (isinstance(v, str) and v.strip().lower() == "auto"):
|
|
80
|
+
return None
|
|
81
|
+
try:
|
|
82
|
+
return float(v)
|
|
83
|
+
except (TypeError, ValueError):
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def free_spot(img, pref: float, faces: list[tuple[float, float]], lay: dict) -> float:
|
|
88
|
+
"""Center for a text drawn at `pref` in `img` (ink box measured) that clears the faces."""
|
|
89
|
+
bb = tr.ink_bbox(img)
|
|
90
|
+
if not bb:
|
|
91
|
+
return pref
|
|
92
|
+
c = face_layout.free_center(pref, pref - bb[1], bb[3] - pref, faces, lay["top"], lay["bottom"])
|
|
93
|
+
return pref if c is None else c
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main(argv: list[str] | None = None) -> int:
|
|
97
|
+
ap = argparse.ArgumentParser(description="Gera amostras de todos os estilos de legenda e gancho sobre um quadro do corte.")
|
|
98
|
+
ap.add_argument("--edit-dir", required=True, help="pasta edit/ da sessão")
|
|
99
|
+
ap.add_argument("--time", type=float, help="segundo do quadro (padrão: 25%% do vídeo)")
|
|
100
|
+
ap.add_argument("--accent", help="cor de destaque em hex (padrão: a do edit_data, do preview ou do catálogo)")
|
|
101
|
+
ap.add_argument("--text", help="texto das amostras (legenda e gancho)")
|
|
102
|
+
ap.add_argument("--hook-text", help="texto só do gancho (padrão: --text, o gancho do edit_data ou um exemplo)")
|
|
103
|
+
ap.add_argument("--video", help="vídeo de onde tirar o quadro (padrão: edit/cut.mp4)")
|
|
104
|
+
args = ap.parse_args(argv)
|
|
105
|
+
|
|
106
|
+
require_binary("ffmpeg")
|
|
107
|
+
edit = Path(args.edit_dir).resolve()
|
|
108
|
+
if not edit.is_dir():
|
|
109
|
+
print(f"erro: pasta de edição não encontrada: {edit}", file=sys.stderr)
|
|
110
|
+
return 2
|
|
111
|
+
video = Path(args.video).resolve() if args.video else edit / "cut.mp4"
|
|
112
|
+
if not video.exists():
|
|
113
|
+
print(f"erro: vídeo não encontrado: {video}. Renderize o corte primeiro (render_cut.py) ou passe --video.",
|
|
114
|
+
file=sys.stderr)
|
|
115
|
+
return 2
|
|
116
|
+
|
|
117
|
+
catalog = load_style_catalog()
|
|
118
|
+
notes = []
|
|
119
|
+
edit_data = _load_dict(edit / "edit_data.json", notes)
|
|
120
|
+
picks = _load_dict(edit / "preview_style.json", notes)
|
|
121
|
+
captions = _load_dict(edit / "captions.json", notes) or None
|
|
122
|
+
|
|
123
|
+
accent = args.accent or edit_data.get("accent") or picks.get("accent") \
|
|
124
|
+
or (catalog.get("defaults") or {}).get("accent") or tr.DEFAULT_ACCENT
|
|
125
|
+
try:
|
|
126
|
+
hex_to_rgb(accent)
|
|
127
|
+
except ValueError:
|
|
128
|
+
print(f"erro: cor inválida: {accent!r} (use algo como #FFC400)", file=sys.stderr)
|
|
129
|
+
return 2
|
|
130
|
+
|
|
131
|
+
info = probe_video(video)
|
|
132
|
+
duration = float(info.get("duration") or 0.0)
|
|
133
|
+
t = args.time if args.time is not None else duration * 0.25
|
|
134
|
+
if duration > 0:
|
|
135
|
+
t = min(max(0.0, t), max(0.0, duration - 0.05))
|
|
136
|
+
layout_seg = None
|
|
137
|
+
if video == edit / "cut.mp4":
|
|
138
|
+
# the face of each cut segment (face_layout.json, cached for compose_final): the default frame
|
|
139
|
+
# is moved to the nearest segment where the speaker shows up
|
|
140
|
+
try:
|
|
141
|
+
segs = [s for s in face_layout.build(edit).get("segments") or [] if isinstance(s, dict)]
|
|
142
|
+
except Exception as e: # noqa: BLE001 (samples still work without the face)
|
|
143
|
+
segs = []
|
|
144
|
+
notes.append(f"não consegui procurar o rosto no corte ({e})")
|
|
145
|
+
here = next((s for s in segs if s["start"] <= t < s["end"]), None)
|
|
146
|
+
with_face = [s for s in segs if s.get("face")]
|
|
147
|
+
if args.time is None and with_face and not (here and here.get("face")):
|
|
148
|
+
here = min(with_face, key=lambda s: min(abs(t - s["start"]), abs(t - s["end"])))
|
|
149
|
+
t = round((here["start"] + here["end"]) / 2.0, 3)
|
|
150
|
+
layout_seg = here if here and here.get("face") else None
|
|
151
|
+
|
|
152
|
+
caption_text = args.text or caption_text_near(captions, t) or DEFAULT_CAPTION_TEXT
|
|
153
|
+
hook_text = args.hook_text or args.text or (edit_data.get("hook") if isinstance(edit_data.get("hook"), dict) else {}).get("text") \
|
|
154
|
+
or picks.get("hook_text") or DEFAULT_HOOK_TEXT
|
|
155
|
+
|
|
156
|
+
started = time.time()
|
|
157
|
+
frame = grab_frame(video, t)
|
|
158
|
+
vertical = frame.height >= frame.width
|
|
159
|
+
size = (540, 960) if vertical else (960, 540)
|
|
160
|
+
# compose on the full-size canvas, then shrink: sharper than drawing at 540 wide
|
|
161
|
+
background = tr.cover_image(frame, (1080, 1920) if vertical else (1920, 1080))
|
|
162
|
+
|
|
163
|
+
samples = edit / "samples"
|
|
164
|
+
samples.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
cap_ids = [s["id"] for s in catalog.get("caption_styles", [])] or tr.caption_style_ids()
|
|
166
|
+
hook_ids = [s["id"] for s in catalog.get("hook_styles", [])] or tr.hook_style_ids()
|
|
167
|
+
|
|
168
|
+
canvas = tr.sample_canvas(background.size)
|
|
169
|
+
lay = tr.layout_defaults(canvas)
|
|
170
|
+
face = zone = None
|
|
171
|
+
try:
|
|
172
|
+
face = face_layout.detect_image(background)
|
|
173
|
+
except Exception as e: # noqa: BLE001 (samples still work without the face)
|
|
174
|
+
notes.append(f"não consegui procurar o rosto ({e}); amostras nas alturas padrão")
|
|
175
|
+
faces: list[tuple[float, float]] = []
|
|
176
|
+
if not face and layout_seg is not None:
|
|
177
|
+
face = layout_seg["face"] # not found on this very frame: the segment's face
|
|
178
|
+
if face:
|
|
179
|
+
_, zone, _ = face_layout._summarize([face])
|
|
180
|
+
if layout_seg is not None and face is layout_seg["face"] and layout_seg.get("zone"):
|
|
181
|
+
zone = layout_seg["zone"]
|
|
182
|
+
gap = face_layout.GAP_FRAC * canvas[1]
|
|
183
|
+
faces = [(zone["y0"] * canvas[1] - gap, zone["y1"] * canvas[1] + gap)]
|
|
184
|
+
manual = {"caption": _manual(edit_data.get("captions"), "center_y"), "hook": _manual(edit_data.get("hook"), "center_y")}
|
|
185
|
+
out_cfg = edit_data.get("output") if isinstance(edit_data.get("output"), dict) else {}
|
|
186
|
+
try:
|
|
187
|
+
scale_y = canvas[1] / float(out_cfg.get("height") or canvas[1])
|
|
188
|
+
except (TypeError, ValueError, ZeroDivisionError):
|
|
189
|
+
scale_y = 1.0
|
|
190
|
+
|
|
191
|
+
def position(kind: str, sid: str, text: str) -> float:
|
|
192
|
+
if manual[kind] is not None:
|
|
193
|
+
return manual[kind] * scale_y
|
|
194
|
+
if kind == "caption":
|
|
195
|
+
words, active = tr.sample_caption_words(text, sid)
|
|
196
|
+
probe = tr.render_caption_image(words, sid, accent, canvas, active=active, center_y=lay["caption"])
|
|
197
|
+
return free_spot(probe, lay["caption"], faces, lay) if faces else lay["caption"]
|
|
198
|
+
probe = tr.render_hook(text, sid, accent, canvas, center_y=int(round(lay["hook"])))
|
|
199
|
+
return free_spot(probe, lay["hook"], faces, lay) if faces else lay["hook"]
|
|
200
|
+
|
|
201
|
+
index = {"frame_time": round(t, 3), "accent": accent, "caption": {}, "hook": {},
|
|
202
|
+
"caption_text": caption_text, "hook_text": hook_text, "size": list(size),
|
|
203
|
+
"face": zone, "positions": {"caption": {}, "hook": {}},
|
|
204
|
+
"suggest": {"camera_focus_y": round(face["eyes_y"], 3) if face else None},
|
|
205
|
+
"created_at": now_iso()}
|
|
206
|
+
skipped = []
|
|
207
|
+
for kind, ids, text in (("caption", cap_ids, caption_text), ("hook", hook_ids, hook_text)):
|
|
208
|
+
for sid in ids:
|
|
209
|
+
try:
|
|
210
|
+
cy = position(kind, sid, text)
|
|
211
|
+
img = tr.render_sample_frame(background, kind, sid, accent, text, center_y=cy).resize(size, Image.LANCZOS)
|
|
212
|
+
except ValueError as e:
|
|
213
|
+
skipped.append(f"{kind} {sid}: {e}")
|
|
214
|
+
continue
|
|
215
|
+
index["positions"][kind][sid] = int(round(cy))
|
|
216
|
+
name = f"{kind}_{sid}.png"
|
|
217
|
+
tmp = samples / f".tmp-{name}"
|
|
218
|
+
img.save(tmp, "PNG", optimize=False, compress_level=6)
|
|
219
|
+
tmp.replace(samples / name)
|
|
220
|
+
index[kind][sid] = name
|
|
221
|
+
save_json(samples / "index.json", index)
|
|
222
|
+
|
|
223
|
+
state_path = edit / "state.json"
|
|
224
|
+
state = load_json(state_path, default={}) or {}
|
|
225
|
+
state["samples"] = "samples/index.json"
|
|
226
|
+
state["updated_at"] = now_iso()
|
|
227
|
+
save_json(state_path, state)
|
|
228
|
+
|
|
229
|
+
for msg in skipped:
|
|
230
|
+
print(f"aviso: estilo ignorado ({msg})", file=sys.stderr)
|
|
231
|
+
for msg in notes + tr.text_issues("hook", hook_text, hook_ids[0] if hook_ids else "manchete"):
|
|
232
|
+
print(f"aviso: {msg}", file=sys.stderr)
|
|
233
|
+
where = ""
|
|
234
|
+
if face:
|
|
235
|
+
caps = sorted(set(index["positions"]["caption"].values()))
|
|
236
|
+
hooks = sorted(set(index["positions"]["hook"].values()))
|
|
237
|
+
span = lambda v: f"{v[0]}" if len(v) == 1 else f"{v[0]} a {v[-1]}" # noqa: E731
|
|
238
|
+
where = (f", rosto em {zone['y0']:.2f}-{zone['y1']:.2f} da altura: legenda em {span(caps)} px, "
|
|
239
|
+
f"gancho em {span(hooks)} px, camera.focus_y sugerido {index['suggest']['camera_focus_y']}")
|
|
240
|
+
print(f"ok: {len(index['caption'])} amostras de legenda e {len(index['hook'])} de gancho "
|
|
241
|
+
f"(quadro em {t:.2f}s, cor {accent}, {time.time() - started:.1f}s{where}) -> {samples / 'index.json'}")
|
|
242
|
+
return 0
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
if __name__ == "__main__":
|
|
246
|
+
sys.exit(main())
|