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,426 @@
|
|
|
1
|
+
"""Live camera by default: writes in projeto.json the base zoom that alternates sentence by sentence and the
|
|
2
|
+
punch-ins on the words that carry the emphasis.
|
|
3
|
+
|
|
4
|
+
Reads the words of the cut (`<EDIT>/transcripts/cut.json`, with the start measured on the wave when
|
|
5
|
+
`palavras_onda.json` exists), the cuts (`cut_timeline.json`) and the loudness of the audio. Then, in the stretches
|
|
6
|
+
of the video that have no block yet (or only a plain `tela_cheia`):
|
|
7
|
+
|
|
8
|
+
- one `tela_cheia` block per sentence, base zoom alternating between 1.0 and 1.12 (a long sentence is split at a
|
|
9
|
+
comma or at a cut, so something changes on screen every 2 to 4 s);
|
|
10
|
+
- one `zooms` punch-in on the face (1.25 over the 1.0 base, 1.3 over the 1.12 base) on the emphasis word of the
|
|
11
|
+
sentence: a number, a negation, or the loudest word. At least MIN_GAP_S between two punch-ins.
|
|
12
|
+
|
|
13
|
+
A scene of `cenas_proprias` with `"sobre_a_pessoa": true` (a small card above the head of the person, who stays on
|
|
14
|
+
screen) marks a stretch where the camera does NOT come closer: there the base zoom stays 1.0 and no punch-in is
|
|
15
|
+
written, so the head cannot rise under the card. Its "de" and "ate" are seconds or a word ("ate" missing = to the end).
|
|
16
|
+
(PrimoCode) The pieces that sit over the head (`lettering`, `rotulo`, `carimbo`) get the same calm stretch: with the
|
|
17
|
+
1.12 base zoom the head rose under the title and the text ended up glued to the forehead. Under a big title on top
|
|
18
|
+
(a `lettering` that is not "base"/"meio", with the captions out of the stretch) the block is 1.12 with
|
|
19
|
+
`"foco_y": 0.62`: the face goes DOWN in the frame and the top opens for the text.
|
|
20
|
+
|
|
21
|
+
It only uses what the build already does (`zooms` in `tela_cheia`, references/enquadramento.md). Blocks written by
|
|
22
|
+
hand that are not a plain `tela_cheia` (`prova`, `contida`, `moldura`, `tela_verde`, `comparar`, or a `tela_cheia`
|
|
23
|
+
with its own `zooms`, `acompanhar` or `"camera_viva": false`) are left exactly as they are. Everything written here
|
|
24
|
+
carries `"auto": "camera_viva"`: running again replaces only that, and `--desfazer` removes it.
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
uv run python helpers/camera_viva.py <PROJ> [--desfazer] [--min-gap 2.5] [--json]
|
|
28
|
+
Exit code 0 when projeto.json was written (or nothing had to change), 2 on error.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import argparse
|
|
34
|
+
import json
|
|
35
|
+
import re
|
|
36
|
+
import sys
|
|
37
|
+
import unicodedata
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
MARCA = "camera_viva"
|
|
41
|
+
ZOOM_A, ZOOM_B = 1.0, 1.12
|
|
42
|
+
PUNCH_A, PUNCH_B = 1.25, 1.3
|
|
43
|
+
MIN_GAP_S = 2.5
|
|
44
|
+
LONGA_S = 4.5 # a sentence longer than this is split in two
|
|
45
|
+
CURTA_S = 1.2 # a sentence shorter than this joins the next one
|
|
46
|
+
PUNCH_FICA_S = 0.6 # how long the punch-in stays after the word ends
|
|
47
|
+
SNAP_S = 0.25 # a sentence start this close to a cut moves to the cut
|
|
48
|
+
FOCO_TITULO = 0.62 # (PrimoCode) under a big title on top: the face center goes this low in the frame (build: foco_y)
|
|
49
|
+
|
|
50
|
+
NEGACOES = {"nao", "nunca", "nem", "nada", "ninguem", "jamais", "nenhum", "nenhuma", "sem", "zero"}
|
|
51
|
+
NUMEROS = {"um", "uma", "dois", "duas", "tres", "quatro", "cinco", "seis", "sete", "oito", "nove", "dez", "onze",
|
|
52
|
+
"doze", "treze", "catorze", "quatorze", "quinze", "vinte", "trinta", "quarenta", "cinquenta", "sessenta",
|
|
53
|
+
"setenta", "oitenta", "noventa", "cem", "cento", "duzentos", "trezentos", "quinhentos", "mil", "milhao",
|
|
54
|
+
"milhoes", "bilhao", "bilhoes", "dobro", "triplo", "metade", "primeiro", "primeira", "segundo", "segunda",
|
|
55
|
+
"terceiro", "terceira"}
|
|
56
|
+
ARTIGOS_NUM = {"um", "uma"} # "um" and "uma" are almost always the article: they only count with energy
|
|
57
|
+
FRACAS = {"a", "o", "as", "os", "de", "da", "do", "das", "dos", "e", "que", "em", "no", "na", "nos", "nas", "um",
|
|
58
|
+
"uma", "por", "pra", "para", "com", "se", "eu", "ele", "ela", "voce", "me", "te", "foi", "era", "isso",
|
|
59
|
+
"esse", "essa", "este", "esta", "meu", "minha", "seu", "sua", "la", "ja", "mas", "ou", "como", "so", "ai",
|
|
60
|
+
"ne", "tem", "ter", "vai", "vou", "ao", "aos", "quando", "porque", "quem", "tudo", "cada", "mesma", "mesmo"}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Erro(Exception):
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def norm(s: str) -> str:
|
|
68
|
+
s = unicodedata.normalize("NFD", str(s).lower())
|
|
69
|
+
return re.sub(r"[^a-z0-9%$]", "", "".join(c for c in s if unicodedata.category(c) != "Mn"))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def ler_json(p: Path):
|
|
73
|
+
with open(p, encoding="utf-8") as f:
|
|
74
|
+
return json.load(f)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def palavras_do_corte(edit: Path) -> list[dict]:
|
|
78
|
+
"""Words of the cut with the start measured on the wave when palavras_onda.json matches the transcript."""
|
|
79
|
+
p = edit / "transcripts" / "cut.json"
|
|
80
|
+
if not p.is_file():
|
|
81
|
+
raise Erro(f"falta {p}: termine o corte primeiro (render_cut.py e a transcrição do cut.mp4)")
|
|
82
|
+
ws = [dict(text=w["text"].strip(), start=float(w["start"]), end=float(w["end"]))
|
|
83
|
+
for w in ler_json(p)["words"] if w["text"].strip()]
|
|
84
|
+
po = edit / "palavras_onda.json"
|
|
85
|
+
if po.is_file():
|
|
86
|
+
try:
|
|
87
|
+
lista = ler_json(po).get("palavras") or []
|
|
88
|
+
except (ValueError, AttributeError):
|
|
89
|
+
lista = []
|
|
90
|
+
por_hora: dict[float, list[dict]] = {}
|
|
91
|
+
for r in lista:
|
|
92
|
+
try:
|
|
93
|
+
por_hora.setdefault(round(float(r["whisper"]), 2), []).append(r)
|
|
94
|
+
except (KeyError, TypeError, ValueError):
|
|
95
|
+
continue
|
|
96
|
+
for w in ws:
|
|
97
|
+
for r in por_hora.get(round(w["start"], 2), []):
|
|
98
|
+
if norm(r.get("text", "")) == norm(w["text"]) and abs(float(r["start"]) - w["start"]) <= 0.45:
|
|
99
|
+
w["start"] = round(float(r["start"]), 3)
|
|
100
|
+
w["end"] = round(max(w["end"], w["start"] + 0.05), 3)
|
|
101
|
+
break
|
|
102
|
+
return ws
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def frases(ws: list[dict], cortes: list[float]) -> list[list[int]]:
|
|
106
|
+
"""Indexes of the words of each sentence: split on . ! ? and a long sentence split again at a comma or a cut."""
|
|
107
|
+
out, atual = [], []
|
|
108
|
+
for i, w in enumerate(ws):
|
|
109
|
+
atual.append(i)
|
|
110
|
+
if re.search(r"[.!?…]$", w["text"]):
|
|
111
|
+
out.append(atual)
|
|
112
|
+
atual = []
|
|
113
|
+
if atual:
|
|
114
|
+
out.append(atual)
|
|
115
|
+
|
|
116
|
+
def dur(f):
|
|
117
|
+
return ws[f[-1]]["end"] - ws[f[0]]["start"]
|
|
118
|
+
|
|
119
|
+
partidas = []
|
|
120
|
+
for f in out:
|
|
121
|
+
fila = [f]
|
|
122
|
+
while fila:
|
|
123
|
+
g = fila.pop(0)
|
|
124
|
+
if dur(g) <= LONGA_S or len(g) < 4:
|
|
125
|
+
partidas.append(g)
|
|
126
|
+
continue
|
|
127
|
+
meio = (ws[g[0]]["start"] + ws[g[-1]]["end"]) / 2
|
|
128
|
+
cand = [k for k in range(1, len(g) - 1) if ws[g[k - 1]]["text"].endswith((",", ";", ":"))]
|
|
129
|
+
cand += [k for k in range(1, len(g) - 1)
|
|
130
|
+
if any(abs(c - ws[g[k]]["start"]) <= SNAP_S for c in cortes)]
|
|
131
|
+
cand = [k for k in cand if ws[g[k]]["start"] - ws[g[0]]["start"] >= CURTA_S
|
|
132
|
+
and ws[g[-1]]["end"] - ws[g[k]]["start"] >= CURTA_S]
|
|
133
|
+
if not cand:
|
|
134
|
+
partidas.append(g)
|
|
135
|
+
continue
|
|
136
|
+
k = min(cand, key=lambda k: abs(ws[g[k]]["start"] - meio))
|
|
137
|
+
fila = [g[:k], g[k:]] + fila
|
|
138
|
+
juntas: list[list[int]] = []
|
|
139
|
+
for f in partidas: # a very short sentence rides with the one before it
|
|
140
|
+
if juntas and (dur(f) < CURTA_S or dur(juntas[-1]) < CURTA_S) and dur(juntas[-1]) + dur(f) <= LONGA_S:
|
|
141
|
+
juntas[-1] = juntas[-1] + f
|
|
142
|
+
else:
|
|
143
|
+
juntas.append(f)
|
|
144
|
+
return juntas
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def energia(db, w: dict) -> float:
|
|
148
|
+
"""Loudness of a word: the 90th percentile of the wave (10 ms windows) between its start and its end."""
|
|
149
|
+
if db is None:
|
|
150
|
+
return 0.0
|
|
151
|
+
import numpy as np # noqa: PLC0415
|
|
152
|
+
a, b = int(w["start"] * 100), max(int(w["end"] * 100), int(w["start"] * 100) + 1)
|
|
153
|
+
seg = db[a:b]
|
|
154
|
+
return float(np.percentile(seg, 90)) if len(seg) else -99.0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def enfase(ws: list[dict], f: list[int], db) -> tuple[int, str] | None:
|
|
158
|
+
"""(index, reason) of the emphasis word of a sentence: number, negation, or the loudest content word."""
|
|
159
|
+
en = {i: energia(db, ws[i]) for i in f}
|
|
160
|
+
nums = [i for i in f if re.search(r"\d", ws[i]["text"]) or "%" in ws[i]["text"] or "$" in ws[i]["text"]
|
|
161
|
+
or (norm(ws[i]["text"]) in NUMEROS and norm(ws[i]["text"]) not in ARTIGOS_NUM)]
|
|
162
|
+
if nums:
|
|
163
|
+
return max(nums, key=lambda i: en[i]), "número"
|
|
164
|
+
negs = [i for i in f if norm(ws[i]["text"]) in NEGACOES]
|
|
165
|
+
if negs:
|
|
166
|
+
return max(negs, key=lambda i: en[i]), "negação"
|
|
167
|
+
fortes = [i for i in f if norm(ws[i]["text"]) not in FRACAS and len(norm(ws[i]["text"])) >= 3]
|
|
168
|
+
if not fortes:
|
|
169
|
+
return None
|
|
170
|
+
return max(fortes, key=lambda i: en[i]), "palavra mais forte"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def tempo_do_campo(v, ws: list[dict], depois: float = 0.0) -> float | None:
|
|
174
|
+
"""Seconds of a "de"/"ate" of a hand-written block: a number, or the first time the word(s) are said."""
|
|
175
|
+
if isinstance(v, (int, float)):
|
|
176
|
+
return float(v)
|
|
177
|
+
alvo = [norm(x) for x in str(v).split() if norm(x)]
|
|
178
|
+
if not alvo:
|
|
179
|
+
return None
|
|
180
|
+
for i in range(len(ws) - len(alvo) + 1):
|
|
181
|
+
if ws[i]["start"] >= depois - 1e-3 and [norm(w["text"]) for w in ws[i:i + len(alvo)]] == alvo:
|
|
182
|
+
return ws[i]["start"]
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def trechos_sobre_a_pessoa(cfg: dict, ws: list[dict], vdur: float) -> list[tuple[float, float]]:
|
|
187
|
+
"""Stretches (de, ate) of the `cenas_proprias` marked "sobre_a_pessoa": true, merged. "de" and "ate" are seconds or a
|
|
188
|
+
word (`de_depois_de` / `ate_depois_de` choose the repetition); no "ate" = to the end of the video."""
|
|
189
|
+
out = []
|
|
190
|
+
for i, c in enumerate(cfg.get("cenas_proprias") or []):
|
|
191
|
+
if not isinstance(c, dict) or c.get("sobre_a_pessoa") is not True:
|
|
192
|
+
continue
|
|
193
|
+
nome = c.get("arquivo") or f"cena própria {i + 1}"
|
|
194
|
+
de = tempo_do_campo(c.get("de", 0.0), ws, float(c.get("de_depois_de", c.get("depois_de", 0.0)) or 0.0))
|
|
195
|
+
if de is None:
|
|
196
|
+
raise Erro(f'{nome}: não achei a palavra "{c.get("de")}" (do "de") na fala; escreva em segundos')
|
|
197
|
+
ate = vdur
|
|
198
|
+
if c.get("ate") is not None:
|
|
199
|
+
ate = tempo_do_campo(c["ate"], ws, max(de, float(c.get("ate_depois_de", 0.0) or 0.0)))
|
|
200
|
+
if ate is None:
|
|
201
|
+
raise Erro(f'{nome}: não achei a palavra "{c["ate"]}" (do "ate") na fala; escreva em segundos')
|
|
202
|
+
de, ate = max(0.0, de), min(vdur, ate)
|
|
203
|
+
if ate > de + 0.05:
|
|
204
|
+
out.append((de, ate))
|
|
205
|
+
out.sort()
|
|
206
|
+
juntos: list[tuple[float, float]] = []
|
|
207
|
+
for de, ate in out:
|
|
208
|
+
if juntos and de <= juntos[-1][1] + 0.05:
|
|
209
|
+
juntos[-1] = (juntos[-1][0], max(juntos[-1][1], ate))
|
|
210
|
+
else:
|
|
211
|
+
juntos.append((de, ate))
|
|
212
|
+
return juntos
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def mesclar(trechos: list[tuple[float, float]]) -> list[tuple[float, float]]:
|
|
216
|
+
"""Ranges sorted and merged where they touch."""
|
|
217
|
+
out: list[tuple[float, float]] = []
|
|
218
|
+
for a, b in sorted(trechos):
|
|
219
|
+
if out and a <= out[-1][1] + 0.05:
|
|
220
|
+
out[-1] = (out[-1][0], max(out[-1][1], b))
|
|
221
|
+
else:
|
|
222
|
+
out.append((a, b))
|
|
223
|
+
return out
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def plano(cfg: dict, ws: list[dict], cortes: list[float], vdur: float, db=None, min_gap: float = MIN_GAP_S) -> dict:
|
|
227
|
+
"""The new "blocos" of the project and a short report. Pure: reads nothing from disk."""
|
|
228
|
+
manuais, simples = [], []
|
|
229
|
+
for b in cfg.get("blocos") or []:
|
|
230
|
+
if not isinstance(b, dict) or b.get("auto") == MARCA:
|
|
231
|
+
continue
|
|
232
|
+
b = dict(b)
|
|
233
|
+
if isinstance(b.get("zooms"), list): # punch-ins from a previous run inside a hand-written block
|
|
234
|
+
b["zooms"] = [z for z in b["zooms"] if not (isinstance(z, dict) and z.get("auto") == MARCA)]
|
|
235
|
+
if not b["zooms"]:
|
|
236
|
+
del b["zooms"]
|
|
237
|
+
if b.pop("_zoom_auto", None):
|
|
238
|
+
b.pop("zoom", None)
|
|
239
|
+
de = tempo_do_campo(b.get("de", 0.0), ws)
|
|
240
|
+
ate = tempo_do_campo(b["ate"], ws, de or 0.0) if "ate" in b else vdur
|
|
241
|
+
if de is None or ate is None:
|
|
242
|
+
raise Erro(f'bloco {b.get("cena")}: não achei a palavra de "de"/"ate" na fala; escreva em segundos')
|
|
243
|
+
plain = (b.get("cena") == "tela_cheia" and not b.get("zooms") and not b.get("acompanhar")
|
|
244
|
+
and b.get("camera_viva", True) is not False and not b.get("borrar"))
|
|
245
|
+
(simples if plain else manuais).append((de, min(ate, vdur), b))
|
|
246
|
+
manuais.sort(key=lambda x: x[0])
|
|
247
|
+
|
|
248
|
+
# the stretches that are free for the live camera: everything outside the hand-written blocks that stay
|
|
249
|
+
livres, t = [], 0.0
|
|
250
|
+
for de, ate, _ in manuais:
|
|
251
|
+
if de > t + 0.05:
|
|
252
|
+
livres.append((t, de))
|
|
253
|
+
t = max(t, ate)
|
|
254
|
+
if t < vdur - 0.05:
|
|
255
|
+
livres.append((t, vdur))
|
|
256
|
+
|
|
257
|
+
# lettering, rótulo e carimbo ficam em cima da cabeça: ali a aproximação no rosto empurraria a cabeça para baixo do texto
|
|
258
|
+
ocupado: list[tuple[float, float]] = []
|
|
259
|
+
titulos: list[tuple[float, float]] = []
|
|
260
|
+
for an in cfg.get("animacoes") or []:
|
|
261
|
+
if not isinstance(an, dict) or an.get("tipo") not in ("lettering", "rotulo", "carimbo"):
|
|
262
|
+
continue
|
|
263
|
+
ini = tempo_do_campo(an["em"], ws, float(an.get("depois_de") or 0.0)) if an.get("em") is not None else None
|
|
264
|
+
if ini is None:
|
|
265
|
+
continue
|
|
266
|
+
fim = tempo_do_campo(an["sai"], ws, ini) if an.get("sai") is not None else None
|
|
267
|
+
ocupado.append((ini - 0.1, fim if fim is not None else ini + 2.5))
|
|
268
|
+
# o título grande no alto (lettering que não pediu "base"/"meio", com a legenda fora do trecho): a câmera abre
|
|
269
|
+
# espaço em cima, com o rosto mais baixo no quadro (zoom 1,12 com o foco embaixo), em vez de subir a cabeça
|
|
270
|
+
leg = cfg.get("legenda") if isinstance(cfg.get("legenda"), dict) else {}
|
|
271
|
+
junto = an.get("legenda_junto")
|
|
272
|
+
if junto is None:
|
|
273
|
+
junto = leg.get("estilo") == "discreta"
|
|
274
|
+
if an.get("tipo") == "lettering" and an.get("posicao") not in ("base", "meio") and not junto:
|
|
275
|
+
titulos.append(ocupado[-1])
|
|
276
|
+
|
|
277
|
+
fs = frases(ws, cortes)
|
|
278
|
+
limites = [] # where each sentence starts on screen
|
|
279
|
+
for f in fs:
|
|
280
|
+
s = ws[f[0]]["start"]
|
|
281
|
+
c = min(cortes, key=lambda c: abs(c - s), default=None)
|
|
282
|
+
limites.append(round(c if c is not None and abs(c - s) <= SNAP_S else max(0.0, s - 0.04), 3))
|
|
283
|
+
if limites:
|
|
284
|
+
limites[0] = 0.0
|
|
285
|
+
|
|
286
|
+
sobre = trechos_sobre_a_pessoa(cfg, ws, vdur) # the camera stays at 1.0 and does not come closer here
|
|
287
|
+
# PrimoCode: o mesmo vale para o lettering, o rótulo e o carimbo: o zoom de base 1,12 também sobe a cabeça (o título
|
|
288
|
+
# ficava colado na testa). Nesses trechos a câmera fica em 1,0 e parada; a peça é o que muda na tela.
|
|
289
|
+
calmos = mesclar(sobre + [(max(0.0, a), min(vdur, b)) for a, b in ocupado if b - a > 0.05])
|
|
290
|
+
avisos = []
|
|
291
|
+
for de, ate, b in manuais:
|
|
292
|
+
for sd, sa in sobre:
|
|
293
|
+
if b.get("cena") == "tela_cheia" and de < sa - 0.05 and ate > sd + 0.05:
|
|
294
|
+
avisos.append(f'tela_cheia seu de {de:.1f} a {ate:.1f} s cruza a cena sobre_a_pessoa de {sd:.1f} a {sa:.1f} s: '
|
|
295
|
+
f'tire o zoom dali, senão a cabeça sobe sob o cartão')
|
|
296
|
+
|
|
297
|
+
def pedacos(la: float, lb: float) -> list[tuple[float, float, bool]]:
|
|
298
|
+
"""The free stretch cut at the edges of the calm stretches (sobre_a_pessoa, lettering): [(de, ate, calmo)]"""
|
|
299
|
+
out, t = [], la
|
|
300
|
+
for sd, sa in calmos:
|
|
301
|
+
lo, hi = max(sd, la), min(sa, lb)
|
|
302
|
+
if hi - lo <= 0.05:
|
|
303
|
+
continue
|
|
304
|
+
if lo - t > 0.05:
|
|
305
|
+
out.append((t, lo, False))
|
|
306
|
+
t = lo
|
|
307
|
+
out.append((t, hi, True))
|
|
308
|
+
t = hi
|
|
309
|
+
if lb - t > 0.05:
|
|
310
|
+
out.append((t, lb, False))
|
|
311
|
+
elif out:
|
|
312
|
+
out[-1] = (out[-1][0], lb, out[-1][2])
|
|
313
|
+
return out
|
|
314
|
+
|
|
315
|
+
autos, punches, ultima_base, ultimo_punch = [], [], ZOOM_B, -99.0
|
|
316
|
+
for la, lb in livres:
|
|
317
|
+
for pa, pb, sob in pedacos(la, lb):
|
|
318
|
+
pontos = [pa] + ([] if sob else [x for x in limites if pa + CURTA_S * 0.8 <= x <= pb - CURTA_S * 0.8]) + [pb]
|
|
319
|
+
if sob: # o trecho calmo troca de enquadramento só onde começa ou acaba um título no alto
|
|
320
|
+
pontos = sorted(set(pontos + [round(x, 3) for t in titulos for x in t if pa + 0.3 < x < pb - 0.3]))
|
|
321
|
+
for a, b in zip(pontos, pontos[1:]):
|
|
322
|
+
titulo = sob and any(x - 0.01 <= (a + b) / 2 <= y + 0.01 for x, y in titulos) and \
|
|
323
|
+
not any(x < b - 0.05 and a + 0.05 < y for x, y in sobre)
|
|
324
|
+
base = ZOOM_B if titulo else (ZOOM_A if sob or ultima_base == ZOOM_B else ZOOM_B)
|
|
325
|
+
ultima_base = base
|
|
326
|
+
bloco = dict(cena="tela_cheia", de=round(a, 3), ate=round(b, 3), zoom=base, auto=MARCA)
|
|
327
|
+
if titulo:
|
|
328
|
+
bloco["foco_y"] = FOCO_TITULO
|
|
329
|
+
dentro = [f for f in fs if a - 0.05 <= ws[f[0]]["start"] < b - 0.05]
|
|
330
|
+
cands = [] if sob else [e for e in (enfase(ws, f, db) for f in dentro) if e]
|
|
331
|
+
ordem = {"número": 0, "negação": 1, "palavra mais forte": 2}
|
|
332
|
+
cands.sort(key=lambda e: (ordem[e[1]], -energia(db, ws[e[0]])))
|
|
333
|
+
for i, motivo in cands:
|
|
334
|
+
t0 = ws[i]["start"]
|
|
335
|
+
if t0 - ultimo_punch < min_gap or t0 < a + 0.45 or b - t0 < PUNCH_FICA_S + 0.25:
|
|
336
|
+
continue
|
|
337
|
+
t1 = round(min(ws[i]["end"] + PUNCH_FICA_S, b - 0.05), 3)
|
|
338
|
+
if any(x < t1 and t0 - 0.1 < y for x, y in ocupado):
|
|
339
|
+
continue
|
|
340
|
+
bloco["zooms"] = [dict(em=round(t0, 3), ate=t1, escala=PUNCH_A if base == ZOOM_A else PUNCH_B,
|
|
341
|
+
som=False, auto=MARCA)]
|
|
342
|
+
punches.append(dict(em=round(t0, 2), palavra=ws[i]["text"], motivo=motivo))
|
|
343
|
+
ultimo_punch = t0
|
|
344
|
+
break
|
|
345
|
+
autos.append((a, bloco))
|
|
346
|
+
if autos and autos[-1][1]["ate"] >= vdur - 0.05:
|
|
347
|
+
del autos[-1][1]["ate"] # the last block goes to the end (and holds on the still end)
|
|
348
|
+
|
|
349
|
+
blocos = [b for _, _, b in manuais]
|
|
350
|
+
todos = sorted([(de, b) for de, _, b in manuais] + autos, key=lambda x: x[0])
|
|
351
|
+
# the card of a sobre_a_pessoa scene coming and going is also something that changes on screen
|
|
352
|
+
cartoes = {round(x, 2) for d, a in calmos for x in (d, a) if 0.05 < x < vdur - 0.05}
|
|
353
|
+
trocas = sorted({round(de, 2) for de, _ in todos} | {p["em"] for p in punches} | cartoes)
|
|
354
|
+
# inside a sobre_a_pessoa stretch the camera stays still on purpose: the card is what changes there (main() says how long)
|
|
355
|
+
dentro = lambda x, y: any(x >= sd - 0.01 and y <= sa + 0.01 for sd, sa in calmos) # noqa: E731
|
|
356
|
+
maior = max((y - x for x, y in zip([0.0] + trocas, trocas + [vdur]) if not dentro(x, y)), default=0.0)
|
|
357
|
+
return dict(blocos=[b for _, b in todos], frases=len(fs), blocos_auto=len(autos), punches=punches,
|
|
358
|
+
mantidos=len(blocos), simples_trocados=len(simples), maior_intervalo=round(maior, 2),
|
|
359
|
+
sobre_a_pessoa=[[round(d, 2), round(a, 2)] for d, a in sobre], avisos=avisos)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def main(argv: list[str] | None = None) -> int:
|
|
363
|
+
ap = argparse.ArgumentParser(description="Câmera viva: zoom de base alternando por frase e aproximação no rosto "
|
|
364
|
+
"nas palavras de ênfase, escritos no projeto.json.")
|
|
365
|
+
ap.add_argument("projeto", type=Path, help="a pasta do projeto (<PROJ>)")
|
|
366
|
+
ap.add_argument("--desfazer", action="store_true", help="tira tudo o que a câmera viva escreveu (vídeo calmo)")
|
|
367
|
+
ap.add_argument("--min-gap", type=float, default=MIN_GAP_S, help="segundos entre duas aproximações (2,5)")
|
|
368
|
+
ap.add_argument("--json", action="store_true")
|
|
369
|
+
a = ap.parse_args(argv)
|
|
370
|
+
try:
|
|
371
|
+
pj = a.projeto / "projeto.json"
|
|
372
|
+
if not pj.is_file():
|
|
373
|
+
raise Erro(f"não achei {pj}: crie o projeto com o build.py novo")
|
|
374
|
+
cfg = ler_json(pj)
|
|
375
|
+
if a.desfazer:
|
|
376
|
+
antes = cfg.get("blocos") or []
|
|
377
|
+
cfg["blocos"] = [b for b in antes if not (isinstance(b, dict) and b.get("auto") == MARCA)]
|
|
378
|
+
for b in cfg["blocos"]:
|
|
379
|
+
if isinstance(b, dict) and isinstance(b.get("zooms"), list):
|
|
380
|
+
b["zooms"] = [z for z in b["zooms"] if not (isinstance(z, dict) and z.get("auto") == MARCA)]
|
|
381
|
+
pj.write_text(json.dumps(cfg, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
382
|
+
print(f"câmera viva: tirei {len(antes) - len(cfg['blocos'])} bloco(s). Rode o build de novo.")
|
|
383
|
+
return 0
|
|
384
|
+
edit = (a.projeto / cfg.get("edit", "../edit")).resolve()
|
|
385
|
+
tl = edit / "cut_timeline.json"
|
|
386
|
+
if not tl.is_file():
|
|
387
|
+
raise Erro(f"falta {tl}: termine o corte primeiro")
|
|
388
|
+
tl = ler_json(tl)
|
|
389
|
+
vdur = float(tl["duration"])
|
|
390
|
+
cortes = sorted(float(s["out_start"]) for s in tl["segments"] if s["out_start"] > 0.01)
|
|
391
|
+
ws = palavras_do_corte(edit)
|
|
392
|
+
db = None
|
|
393
|
+
video = edit / cfg.get("video", "cut.mp4")
|
|
394
|
+
if video.is_file():
|
|
395
|
+
try:
|
|
396
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
397
|
+
import corte_por_onda as onda # noqa: E402, PLC0415
|
|
398
|
+
db = onda.envelope(video, None)
|
|
399
|
+
except Exception as e: # noqa: BLE001 (without the wave the emphasis is number, negation, first strong word)
|
|
400
|
+
print(f"aviso: não medi a onda ({e}); a ênfase fica por número e negação", file=sys.stderr)
|
|
401
|
+
r = plano(cfg, ws, cortes, vdur, db, a.min_gap)
|
|
402
|
+
cfg["blocos"] = r["blocos"]
|
|
403
|
+
pj.write_text(json.dumps(cfg, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
404
|
+
except Erro as e:
|
|
405
|
+
print(f"ERRO: {e}", file=sys.stderr)
|
|
406
|
+
return 2
|
|
407
|
+
if a.json:
|
|
408
|
+
print(json.dumps({k: v for k, v in r.items() if k != "blocos"}, ensure_ascii=False))
|
|
409
|
+
return 0
|
|
410
|
+
print(f"câmera viva: {r['blocos_auto']} trecho(s) de tela cheia alternando {ZOOM_A:g} e {ZOOM_B:g}, "
|
|
411
|
+
f"{len(r['punches'])} aproximação(ões) no rosto; {r['mantidos']} bloco(s) seu(s) mantido(s).")
|
|
412
|
+
for p in r["punches"]:
|
|
413
|
+
print(f" {p['em']:6.2f} s \"{p['palavra']}\" ({p['motivo']})")
|
|
414
|
+
for d, a in r["sobre_a_pessoa"]:
|
|
415
|
+
print(f" {d:6.2f} a {a:.2f} s sobre a pessoa: zoom 1,0 e sem aproximação (a cabeça não sobe sob o cartão)"
|
|
416
|
+
+ (f"; são {a - d:.1f} s com a câmera parada: o cartão precisa mudar de estado a cada 2 a 4 s" if a - d > 4.0 else ""))
|
|
417
|
+
for aviso in r["avisos"]:
|
|
418
|
+
print(f" aviso: {aviso}")
|
|
419
|
+
print(f" maior intervalo sem mudança de câmera: {r['maior_intervalo']:.1f} s"
|
|
420
|
+
+ (" (acima de 4 s: ponha uma peça ou cena nesse trecho)" if r["maior_intervalo"] > 4.0 else ""))
|
|
421
|
+
print(" rode o build de novo (amostra ou completo) para ver no estúdio.")
|
|
422
|
+
return 0
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
if __name__ == "__main__":
|
|
426
|
+
raise SystemExit(main())
|