primocode 10.15.2 → 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 +74 -9
- 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 +38 -8
- package/lib/app-chat.js +97 -0
- package/lib/catalogo.js +7 -1
- 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/tela.html +1349 -0
- package/lib/estudio/gosto.js +305 -0
- package/lib/estudio/index.js +12 -1
- 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 +70 -12
- 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 +64 -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/headless.js +15 -3
- package/lib/local/cerebro.json +285 -7
- package/lib/local/motor.js +5 -2
- package/lib/memoria-usuario.js +164 -0
- package/lib/nuvem.js +33 -2
- package/lib/passagem.js +50 -0
- package/lib/perigo.js +19 -11
- package/lib/protecao.js +134 -0
- package/lib/repeticao.js +1 -1
- package/lib/sandbox.js +57 -9
- package/lib/titulo-conversa.js +46 -0
- package/lib/tools.js +23 -4
- package/lib/ui.js +2 -2
- 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/__main__.py +55 -40
- package/voz/primo_voz/app.py +500 -4
- package/voz/primo_voz/chat.html +1773 -0
- package/voz/primo_voz/conversa.py +0 -26
- package/voz/primo_voz/hud.html +8 -2
- package/voz/primo_voz/hud.py +129 -5
- package/voz/primo_voz/identidade.py +1 -1
- package/voz/primo_voz/painel_dados.py +134 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""The face inside each take, 10 times a second, smoothed: the path the framing follows (`acompanhar` blocks).
|
|
2
|
+
|
|
3
|
+
face_layout.json keeps one face per take, so the framing stands still inside a take. When the person leans,
|
|
4
|
+
walks or drifts inside a take and the block is zoomed (moldura, a punch-in, tela cheia at 1.2), the face slides
|
|
5
|
+
to the border or the forehead goes. This helper measures the face with the same YuNet detector as
|
|
6
|
+
face_layout.py, 10 times a second on the cut, and writes the smoothed path that modelo-projeto/build.py follows:
|
|
7
|
+
|
|
8
|
+
1. raw measure every 0.1 s (the largest confident face, as face_layout.py);
|
|
9
|
+
2. median of 5 measures in a row (a single wrong detection does not move anything);
|
|
10
|
+
3. lost face (hand in front, turned aside): the last good point is held. After a loss of 0.3 s or more, a new
|
|
11
|
+
detection is only trusted when 2 of the next 3 samples also find the face;
|
|
12
|
+
4. dead zone: the target moves only when the centre walks more than 3% of the video width (or the face size
|
|
13
|
+
changes more than 12%); micro movements and detector jitter never move the frame;
|
|
14
|
+
5. every change of target becomes a sine.inOut move of 0.5 s centred on the change (the path is continuous,
|
|
15
|
+
no jump);
|
|
16
|
+
6. everything restarts at each take of cut_timeline.json (one take's path never leaks into the next).
|
|
17
|
+
|
|
18
|
+
Output <edit>/rosto_trilha.json (fractions of the cut.mp4 frame; times in seconds of the cut):
|
|
19
|
+
{"versao": 1, "video": "cut.mp4", "video_sig": "size:mtime_ns", "largura", "altura", "fps", "passo_s": 0.1,
|
|
20
|
+
"parametros": {...},
|
|
21
|
+
"tomadas": [{"id", "de", "ate", "amostras", "medidos", "w_med", "h_med",
|
|
22
|
+
"pontos": [[t, cx, cy, w, h, medido], ...]}]} # medido: 1 = face found near this sample
|
|
23
|
+
A take with no face at all has "pontos": [] (the build keeps the take's framing and says so).
|
|
24
|
+
|
|
25
|
+
The build runs this by itself when a block asks for `"acompanhar": true` and the file is missing or older than the
|
|
26
|
+
cut. By hand:
|
|
27
|
+
uv run --directory '<SKILL>' python helpers/rastrear_rosto.py '<EDIT>' [--forcar]
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import argparse
|
|
33
|
+
import math
|
|
34
|
+
import subprocess
|
|
35
|
+
import sys
|
|
36
|
+
import time
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
40
|
+
from common import file_signature, load_json, now_iso, probe_video, require_binary, save_json # noqa: E402
|
|
41
|
+
|
|
42
|
+
VERSAO = 1
|
|
43
|
+
PASSO_S = 0.1 # 10 measures a second
|
|
44
|
+
JANELA = 5 # median of 5 measures in a row
|
|
45
|
+
ZONA_MORTA = 0.03 # the target moves when the centre walks more than 3% of the width
|
|
46
|
+
ZONA_TAMANHO = 0.12 # ... or the face width changes more than 12%
|
|
47
|
+
TRANSICAO_S = 0.5 # each move of the target: sine.inOut of 0.5 s, centred on the change
|
|
48
|
+
PERDA_LONGA = 3 # samples without a face that count as "lost" (0.3 s)
|
|
49
|
+
CONFIRMA = 2 # after a long loss: 2 of the next 3 samples must find the face
|
|
50
|
+
LADO_DETECCAO = 540 # frames are scaled to this short side before detection (same as face_layout.py)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TrilhaErro(Exception):
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------- smoothing (pure: tested without a video)
|
|
58
|
+
|
|
59
|
+
def _mediana(vals: list[float]) -> float:
|
|
60
|
+
v = sorted(vals)
|
|
61
|
+
n = len(v)
|
|
62
|
+
return v[n // 2] if n % 2 else (v[n // 2 - 1] + v[n // 2]) / 2.0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _suave(u: float) -> float:
|
|
66
|
+
"""sine.inOut from 0 to 1"""
|
|
67
|
+
if u <= 0:
|
|
68
|
+
return 0.0
|
|
69
|
+
if u >= 1:
|
|
70
|
+
return 1.0
|
|
71
|
+
return 0.5 - 0.5 * math.cos(math.pi * u)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
CHAVES = ("cx", "cy", "w", "h")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def suavizar(amostras: list, largura: int, altura: int, passo: float = PASSO_S) -> list | None:
|
|
78
|
+
"""The path of one take. `amostras`: [(t, {"cx", "cy", "w", "h"} or None)] in time order, fractions of the
|
|
79
|
+
frame. Returns [[t, cx, cy, w, h, medido]] (same times) or None when the take has no face at all."""
|
|
80
|
+
n = len(amostras)
|
|
81
|
+
if not n:
|
|
82
|
+
return None
|
|
83
|
+
brutos = [a[1] for a in amostras]
|
|
84
|
+
# 3a. after a long loss, a lone detection is not trusted yet
|
|
85
|
+
confiaveis = list(brutos)
|
|
86
|
+
perdidos = PERDA_LONGA # the start of a take counts as a loss (nothing to hold yet)
|
|
87
|
+
for k in range(n):
|
|
88
|
+
if brutos[k] is None:
|
|
89
|
+
perdidos += 1
|
|
90
|
+
continue
|
|
91
|
+
if perdidos >= PERDA_LONGA:
|
|
92
|
+
achados = sum(1 for j in range(k, min(n, k + 3)) if brutos[j] is not None)
|
|
93
|
+
if achados < min(CONFIRMA, n - k):
|
|
94
|
+
confiaveis[k] = None
|
|
95
|
+
continue
|
|
96
|
+
perdidos = 0
|
|
97
|
+
# 2. median of the trusted measures around each sample
|
|
98
|
+
med = []
|
|
99
|
+
meia = JANELA // 2
|
|
100
|
+
for k in range(n):
|
|
101
|
+
if confiaveis[k] is None:
|
|
102
|
+
med.append(None)
|
|
103
|
+
continue
|
|
104
|
+
viz = [confiaveis[j] for j in range(max(0, k - meia), min(n, k + meia + 1)) if confiaveis[j] is not None]
|
|
105
|
+
med.append({c: _mediana([v[c] for v in viz]) for c in CHAVES})
|
|
106
|
+
bons = [m for m in med if m is not None]
|
|
107
|
+
if not bons:
|
|
108
|
+
return None
|
|
109
|
+
# 3b. hold the last good point (and the first good one before the first detection)
|
|
110
|
+
seguro, ult = [], bons[0]
|
|
111
|
+
for m in med:
|
|
112
|
+
if m is not None:
|
|
113
|
+
ult = m
|
|
114
|
+
seguro.append(ult)
|
|
115
|
+
# 4. dead zone: the target stays until the face really moved
|
|
116
|
+
esc_y = altura / float(largura) # a vertical fraction in widths
|
|
117
|
+
alvo, anc = [], seguro[0]
|
|
118
|
+
for v in seguro:
|
|
119
|
+
if (abs(v["cx"] - anc["cx"]) > ZONA_MORTA or abs(v["cy"] - anc["cy"]) * esc_y > ZONA_MORTA
|
|
120
|
+
or abs(v["w"] / max(anc["w"], 1e-6) - 1) > ZONA_TAMANHO):
|
|
121
|
+
anc = v
|
|
122
|
+
alvo.append(anc)
|
|
123
|
+
# 5. each change of target is an eased move of TRANSICAO_S centred on it
|
|
124
|
+
ts = [a[0] for a in amostras]
|
|
125
|
+
mudancas = [(ts[k], {c: alvo[k][c] - alvo[k - 1][c] for c in CHAVES}) for k in range(1, n) if alvo[k] is not alvo[k - 1]]
|
|
126
|
+
saida = []
|
|
127
|
+
for k in range(n):
|
|
128
|
+
v = dict(alvo[0])
|
|
129
|
+
for tm, d in mudancas:
|
|
130
|
+
e = _suave((ts[k] - (tm - TRANSICAO_S / 2)) / TRANSICAO_S)
|
|
131
|
+
if e <= 0:
|
|
132
|
+
break
|
|
133
|
+
for c in CHAVES:
|
|
134
|
+
v[c] += d[c] * e
|
|
135
|
+
medido = any(brutos[j] is not None for j in range(max(0, k - 1), min(n, k + 2)))
|
|
136
|
+
saida.append([round(ts[k], 3), round(v["cx"], 4), round(v["cy"], 4), round(v["w"], 4), round(v["h"], 4),
|
|
137
|
+
1 if medido else 0])
|
|
138
|
+
return saida
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def tomadas_do_corte(timeline: dict | None, duracao: float) -> list[dict]:
|
|
142
|
+
segs = []
|
|
143
|
+
for i, s in enumerate((timeline or {}).get("segments", []) or []):
|
|
144
|
+
try:
|
|
145
|
+
a, b = float(s["out_start"]), float(s.get("out_end", 0) or 0)
|
|
146
|
+
except (KeyError, TypeError, ValueError):
|
|
147
|
+
continue
|
|
148
|
+
segs.append({"id": str(s.get("id") or f"t{i + 1}"), "de": a, "ate": b})
|
|
149
|
+
segs.sort(key=lambda s: s["de"])
|
|
150
|
+
for k, s in enumerate(segs): # a timeline without out_end: the next take (or the end) closes it
|
|
151
|
+
if s["ate"] <= s["de"]:
|
|
152
|
+
s["ate"] = segs[k + 1]["de"] if k + 1 < len(segs) else duracao
|
|
153
|
+
if not segs:
|
|
154
|
+
segs = [{"id": "t1", "de": 0.0, "ate": duracao}]
|
|
155
|
+
return segs
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def montar_tomadas(tomadas: list[dict], medidas: dict[float, dict | None], largura: int, altura: int) -> list[dict]:
|
|
159
|
+
"""medidas: {t: face or None} for every sample of the cut. One path per take."""
|
|
160
|
+
ts = sorted(medidas)
|
|
161
|
+
saida = []
|
|
162
|
+
for tm in tomadas:
|
|
163
|
+
# 2 ms of slack towards the new take, as the build does (a cut time rounded to 3 digits)
|
|
164
|
+
am = [(t, medidas[t]) for t in ts if tm["de"] - 2e-3 <= t < tm["ate"] - 2e-3]
|
|
165
|
+
pontos = suavizar(am, largura, altura) or []
|
|
166
|
+
achados = [m for _t, m in am if m is not None]
|
|
167
|
+
saida.append({"id": tm["id"], "de": round(tm["de"], 4), "ate": round(tm["ate"], 4), "amostras": len(am),
|
|
168
|
+
"medidos": len(achados),
|
|
169
|
+
"w_med": round(_mediana([m["w"] for m in achados]), 4) if achados else None,
|
|
170
|
+
"h_med": round(_mediana([m["h"] for m in achados]), 4) if achados else None,
|
|
171
|
+
"pontos": pontos})
|
|
172
|
+
return saida
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ---------------------------------------------------------------- measuring
|
|
176
|
+
|
|
177
|
+
def medir(video: Path, fps: float, largura: int, altura: int, progresso: bool = True) -> dict[float, dict | None]:
|
|
178
|
+
"""One decoding pass (every `step`-th frame, scaled to 540 on the short side), one detection per sample."""
|
|
179
|
+
import numpy as np # noqa: PLC0415
|
|
180
|
+
|
|
181
|
+
import face_layout # noqa: PLC0415
|
|
182
|
+
|
|
183
|
+
passo = max(1, int(round(fps * PASSO_S)))
|
|
184
|
+
esc = min(1.0, LADO_DETECCAO / max(1, min(largura, altura)))
|
|
185
|
+
dw, dh = max(2, int(round(largura * esc / 2)) * 2), max(2, int(round(altura * esc / 2)) * 2)
|
|
186
|
+
cmd = ["ffmpeg", "-hide_banner", "-nostdin", "-v", "error", "-i", str(video),
|
|
187
|
+
"-vf", f"select='not(mod(n\\,{passo}))',scale={dw}:{dh}:flags=area", "-fps_mode", "passthrough",
|
|
188
|
+
"-an", "-f", "rawvideo", "-pix_fmt", "bgr24", "-"]
|
|
189
|
+
tam = dw * dh * 3
|
|
190
|
+
out: dict[float, dict | None] = {}
|
|
191
|
+
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
192
|
+
k, inicio = 0, time.time()
|
|
193
|
+
try:
|
|
194
|
+
while True:
|
|
195
|
+
buf = proc.stdout.read(tam) if proc.stdout else b""
|
|
196
|
+
if len(buf) < tam:
|
|
197
|
+
break
|
|
198
|
+
img = np.frombuffer(buf, np.uint8).reshape(dh, dw, 3)
|
|
199
|
+
f = face_layout.detect_array(img)
|
|
200
|
+
t = round(k * passo / fps, 4)
|
|
201
|
+
out[t] = None if not f else {"cx": (f["x0"] + f["x1"]) / 2, "cy": (f["y0"] + f["y1"]) / 2,
|
|
202
|
+
"w": f["x1"] - f["x0"], "h": f["y1"] - f["y0"]}
|
|
203
|
+
k += 1
|
|
204
|
+
if progresso and k % 100 == 0:
|
|
205
|
+
print(f" {t:6.1f} s medidos ({time.time() - inicio:.0f} s)", flush=True)
|
|
206
|
+
finally:
|
|
207
|
+
if proc.poll() is None:
|
|
208
|
+
proc.kill()
|
|
209
|
+
err = proc.stderr.read().decode("utf-8", "replace") if proc.stderr else ""
|
|
210
|
+
proc.wait()
|
|
211
|
+
if not out:
|
|
212
|
+
raise TrilhaErro(f"não consegui ler os quadros de {video.name}: {' '.join(err.strip().splitlines()[-3:])}")
|
|
213
|
+
return out
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def construir(edit: Path, forcar: bool = False) -> dict:
|
|
217
|
+
edit = edit.resolve()
|
|
218
|
+
video = edit / "cut.mp4"
|
|
219
|
+
if not video.exists():
|
|
220
|
+
raise TrilhaErro(f"não achei {video}: termine o corte antes (render_cut.py)")
|
|
221
|
+
saida = edit / "rosto_trilha.json"
|
|
222
|
+
sig = file_signature(video)
|
|
223
|
+
velho = load_json(saida) if saida.exists() and not forcar else None
|
|
224
|
+
if isinstance(velho, dict) and velho.get("versao") == VERSAO and velho.get("video_sig") == sig:
|
|
225
|
+
velho["_de_novo"] = False
|
|
226
|
+
return velho
|
|
227
|
+
require_binary("ffmpeg")
|
|
228
|
+
info = probe_video(video)
|
|
229
|
+
largura = int(info.get("display_width") or info.get("width") or 0)
|
|
230
|
+
altura = int(info.get("display_height") or info.get("height") or 0)
|
|
231
|
+
fps = float(info.get("fps") or 30.0)
|
|
232
|
+
duracao = float(info.get("duration") or 0.0)
|
|
233
|
+
if largura <= 0 or altura <= 0:
|
|
234
|
+
raise TrilhaErro(f"não consegui ler o tamanho de {video.name}")
|
|
235
|
+
tl_p = edit / "cut_timeline.json"
|
|
236
|
+
timeline = load_json(tl_p) if tl_p.exists() else None
|
|
237
|
+
tomadas = tomadas_do_corte(timeline, duracao)
|
|
238
|
+
inicio = time.time()
|
|
239
|
+
medidas = medir(video, fps, largura, altura)
|
|
240
|
+
trilha = {
|
|
241
|
+
"versao": VERSAO, "video": "cut.mp4", "video_sig": sig, "largura": largura, "altura": altura,
|
|
242
|
+
"fps": round(fps, 3), "duracao": round(duracao, 3), "passo_s": PASSO_S,
|
|
243
|
+
"parametros": {"mediana": JANELA, "zona_morta": ZONA_MORTA, "zona_tamanho": ZONA_TAMANHO,
|
|
244
|
+
"transicao_s": TRANSICAO_S, "perda_longa_s": round(PERDA_LONGA * PASSO_S, 2),
|
|
245
|
+
"confirma": f"{CONFIRMA} de 3"},
|
|
246
|
+
"tomadas": montar_tomadas(tomadas, medidas, largura, altura),
|
|
247
|
+
"segundos": round(time.time() - inicio, 1),
|
|
248
|
+
"criado_em": now_iso(),
|
|
249
|
+
}
|
|
250
|
+
save_json(saida, trilha)
|
|
251
|
+
trilha["_de_novo"] = True
|
|
252
|
+
return trilha
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def resumo(trilha: dict) -> list[str]:
|
|
256
|
+
linhas = []
|
|
257
|
+
W = trilha["largura"]
|
|
258
|
+
for tm in trilha["tomadas"]:
|
|
259
|
+
pts = tm["pontos"]
|
|
260
|
+
if not pts:
|
|
261
|
+
linhas.append(f" {tm['id']:>5} {tm['de']:6.2f}-{tm['ate']:6.2f}s sem rosto ({tm['medidos']}/{tm['amostras']}): "
|
|
262
|
+
f"o enquadramento fica o da tomada")
|
|
263
|
+
continue
|
|
264
|
+
xs, ys = [p[1] for p in pts], [p[2] for p in pts]
|
|
265
|
+
anda = max(max(xs) - min(xs), (max(ys) - min(ys)) * trilha["altura"] / W)
|
|
266
|
+
linhas.append(f" {tm['id']:>5} {tm['de']:6.2f}-{tm['ate']:6.2f}s rosto {tm['medidos']}/{tm['amostras']} "
|
|
267
|
+
f"anda até {anda * W:.0f} px ({anda:.0%} da largura)")
|
|
268
|
+
return linhas
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def main(argv: list[str] | None = None) -> int:
|
|
272
|
+
ap = argparse.ArgumentParser(description="Rosto 10 vezes por segundo em cada tomada do corte, suavizado "
|
|
273
|
+
"(o caminho que o enquadramento segue nos blocos com acompanhar).")
|
|
274
|
+
ap.add_argument("edit", type=Path, help="pasta edit do corte (cut.mp4 e cut_timeline.json)")
|
|
275
|
+
ap.add_argument("--forcar", action="store_true", help="mede de novo mesmo com o rosto_trilha.json em dia")
|
|
276
|
+
a = ap.parse_args(argv)
|
|
277
|
+
edit = a.edit.expanduser()
|
|
278
|
+
if not edit.is_dir():
|
|
279
|
+
print(f"erro: pasta edit não encontrada: {edit}", file=sys.stderr)
|
|
280
|
+
return 2
|
|
281
|
+
try:
|
|
282
|
+
print("medindo o rosto 10 vezes por segundo no corte (uns segundos por minuto de vídeo)...", flush=True)
|
|
283
|
+
trilha = construir(edit, a.forcar)
|
|
284
|
+
except TrilhaErro as e:
|
|
285
|
+
print(f"erro: {e}", file=sys.stderr)
|
|
286
|
+
return 2
|
|
287
|
+
except Exception as e: # noqa: BLE001 (OpenCV or the model missing: say it plainly)
|
|
288
|
+
print(f"erro: {e}", file=sys.stderr)
|
|
289
|
+
return 2
|
|
290
|
+
for linha in resumo(trilha):
|
|
291
|
+
print(linha)
|
|
292
|
+
estado = "medido agora" if trilha.get("_de_novo") else "já estava em dia"
|
|
293
|
+
print(f"ok: {edit.resolve() / 'rosto_trilha.json'} ({estado})")
|
|
294
|
+
return 0
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
if __name__ == "__main__":
|
|
298
|
+
sys.exit(main())
|