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,308 @@
|
|
|
1
|
+
"""Shared utilities for the editor-video skill helpers.
|
|
2
|
+
|
|
3
|
+
Every helper imports this module as `from common import ...` (helpers run with
|
|
4
|
+
`uv run python helpers/<name>.py`, so the helpers directory is on sys.path).
|
|
5
|
+
|
|
6
|
+
This file is the contract other helpers rely on: keep signatures stable.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from fractions import Fraction
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
SKILL_ROOT = Path(__file__).resolve().parent.parent
|
|
23
|
+
HELPERS_DIR = SKILL_ROOT / "helpers"
|
|
24
|
+
ASSETS_DIR = SKILL_ROOT / "assets"
|
|
25
|
+
FONTS_DIR = ASSETS_DIR / "fonts"
|
|
26
|
+
SFX_DIR = ASSETS_DIR / "sfx"
|
|
27
|
+
PREVIEW_DIR = SKILL_ROOT / "preview"
|
|
28
|
+
STYLE_CATALOG_PATH = HELPERS_DIR / "style_catalog.json"
|
|
29
|
+
|
|
30
|
+
OUTPUT_FORMATS: dict[str, tuple[int, int]] = {
|
|
31
|
+
"vertical": (1080, 1920),
|
|
32
|
+
"horizontal": (1920, 1080),
|
|
33
|
+
"square": (1080, 1080),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Vertical 9:16 safe zones (pixels on a 1080x1920 canvas), validated on real
|
|
37
|
+
# Reels: platform UI covers the top ~210px, the bottom ~370px and a right rail.
|
|
38
|
+
SAFE_ZONE_VERTICAL = {
|
|
39
|
+
"top": 210,
|
|
40
|
+
"bottom": 1550, # nothing important below this line
|
|
41
|
+
"right_rail": 140, # like/comment/share column on Reels and TikTok
|
|
42
|
+
"caption_center_y": 1400,
|
|
43
|
+
"disclaimer_y": 1548,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
EDIT_SUBDIRS = ("transcripts", "regions", "clips", "verify", "samples", "assets", "preview_cache", "renders")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------- processes
|
|
50
|
+
|
|
51
|
+
def run(cmd: list[str], quiet: bool = False, check: bool = True, capture: bool = False) -> subprocess.CompletedProcess:
|
|
52
|
+
"""Run a command. quiet hides stdout/stderr unless it fails."""
|
|
53
|
+
if not quiet:
|
|
54
|
+
print("$ " + " ".join(str(c) for c in cmd), file=sys.stderr)
|
|
55
|
+
proc = subprocess.run(
|
|
56
|
+
[str(c) for c in cmd],
|
|
57
|
+
capture_output=quiet or capture,
|
|
58
|
+
text=True,
|
|
59
|
+
)
|
|
60
|
+
if check and proc.returncode != 0:
|
|
61
|
+
tail = (proc.stderr or "")[-2000:] if (quiet or capture) else ""
|
|
62
|
+
raise RuntimeError(f"command failed ({proc.returncode}): {' '.join(str(c) for c in cmd)}\n{tail}")
|
|
63
|
+
return proc
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# O instalador do EDIT IA PRO baixa ffmpeg, ffprobe e Node para ~/.editor-video (sem senha de
|
|
67
|
+
# administrador). Essas pastas entram na frente do PATH para todo helper achar os programas.
|
|
68
|
+
_LOCAL = Path.home() / ".editor-video"
|
|
69
|
+
for _pasta in (_LOCAL / "node" / "bin", _LOCAL / "node", _LOCAL / "bin"):
|
|
70
|
+
if _pasta.is_dir() and str(_pasta) not in os.environ.get("PATH", "").split(os.pathsep):
|
|
71
|
+
os.environ["PATH"] = str(_pasta) + os.pathsep + os.environ.get("PATH", "")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def require_binary(name: str) -> str:
|
|
75
|
+
path = shutil.which(name)
|
|
76
|
+
if not path:
|
|
77
|
+
hint = {
|
|
78
|
+
"ffmpeg": "rode de novo o instalador do EDIT IA PRO (ele baixa o ffmpeg sem senha) · ou macOS: brew install ffmpeg · Windows: winget install Gyan.FFmpeg",
|
|
79
|
+
"ffprobe": "vem junto com o ffmpeg",
|
|
80
|
+
"node": "macOS: brew install node · Windows: winget install OpenJS.NodeJS.LTS",
|
|
81
|
+
}.get(name, "")
|
|
82
|
+
sys.exit(f"'{name}' não encontrado no PATH. {hint}")
|
|
83
|
+
return path
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def has_ffmpeg_filter(name: str) -> bool:
|
|
87
|
+
proc = subprocess.run(["ffmpeg", "-hide_banner", "-filters"], capture_output=True, text=True)
|
|
88
|
+
for line in proc.stdout.splitlines():
|
|
89
|
+
parts = line.split()
|
|
90
|
+
if len(parts) >= 2 and parts[1] == name:
|
|
91
|
+
return True
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------- probing
|
|
96
|
+
|
|
97
|
+
def ffprobe_json(path: Path | str) -> dict:
|
|
98
|
+
proc = run(
|
|
99
|
+
["ffprobe", "-v", "error", "-print_format", "json", "-show_format", "-show_streams", str(path)],
|
|
100
|
+
quiet=True,
|
|
101
|
+
)
|
|
102
|
+
return json.loads(proc.stdout or "{}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _rotation_of(stream: dict) -> int:
|
|
106
|
+
rot = 0
|
|
107
|
+
for sd in stream.get("side_data_list", []) or []:
|
|
108
|
+
if "rotation" in sd:
|
|
109
|
+
try:
|
|
110
|
+
rot = int(round(float(sd["rotation"])))
|
|
111
|
+
except (TypeError, ValueError):
|
|
112
|
+
pass
|
|
113
|
+
tags = stream.get("tags", {}) or {}
|
|
114
|
+
if not rot and "rotate" in tags:
|
|
115
|
+
try:
|
|
116
|
+
rot = -int(tags["rotate"])
|
|
117
|
+
except ValueError:
|
|
118
|
+
pass
|
|
119
|
+
return rot % 360 if rot else 0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def probe_video(path: Path | str) -> dict:
|
|
123
|
+
"""Normalized facts about a media file.
|
|
124
|
+
|
|
125
|
+
Returns: duration, width, height (coded), display_width, display_height
|
|
126
|
+
(after rotation), rotation (0/90/180/270), fps (float), fps_str ("30000/1001"),
|
|
127
|
+
codec, pix_fmt, bit_depth, color_transfer, color_primaries, color_space,
|
|
128
|
+
is_hdr, has_video, has_audio, audio_tracks, audio_channels, sample_rate.
|
|
129
|
+
"""
|
|
130
|
+
info = ffprobe_json(path)
|
|
131
|
+
streams = info.get("streams", [])
|
|
132
|
+
v = next((s for s in streams if s.get("codec_type") == "video" and not s.get("disposition", {}).get("attached_pic")), None)
|
|
133
|
+
audios = [s for s in streams if s.get("codec_type") == "audio"]
|
|
134
|
+
fmt = info.get("format", {})
|
|
135
|
+
duration = float(fmt.get("duration") or (v or {}).get("duration") or 0.0)
|
|
136
|
+
out: dict[str, Any] = {
|
|
137
|
+
"path": str(path),
|
|
138
|
+
"duration": duration,
|
|
139
|
+
"has_video": v is not None,
|
|
140
|
+
"has_audio": bool(audios),
|
|
141
|
+
"audio_tracks": len(audios),
|
|
142
|
+
"audio_channels": int(audios[0].get("channels", 0)) if audios else 0,
|
|
143
|
+
"sample_rate": int(audios[0].get("sample_rate", 0) or 0) if audios else 0,
|
|
144
|
+
}
|
|
145
|
+
if v is not None:
|
|
146
|
+
width, height = int(v.get("width", 0)), int(v.get("height", 0))
|
|
147
|
+
rotation = _rotation_of(v)
|
|
148
|
+
if rotation in (90, 270):
|
|
149
|
+
dw, dh = height, width
|
|
150
|
+
else:
|
|
151
|
+
dw, dh = width, height
|
|
152
|
+
fps_str = v.get("avg_frame_rate") or v.get("r_frame_rate") or "0/1"
|
|
153
|
+
if fps_str in ("0/0", "0/1"):
|
|
154
|
+
fps_str = v.get("r_frame_rate") or "30/1"
|
|
155
|
+
try:
|
|
156
|
+
fps = float(Fraction(fps_str))
|
|
157
|
+
except (ValueError, ZeroDivisionError):
|
|
158
|
+
fps, fps_str = 30.0, "30/1"
|
|
159
|
+
pix_fmt = v.get("pix_fmt", "")
|
|
160
|
+
bit_depth = 10 if ("10" in pix_fmt or "p010" in pix_fmt) else 12 if "12" in pix_fmt else 8
|
|
161
|
+
transfer = v.get("color_transfer", "") or ""
|
|
162
|
+
out.update(
|
|
163
|
+
width=width,
|
|
164
|
+
height=height,
|
|
165
|
+
display_width=dw,
|
|
166
|
+
display_height=dh,
|
|
167
|
+
rotation=rotation,
|
|
168
|
+
fps=fps,
|
|
169
|
+
fps_str=fps_str,
|
|
170
|
+
codec=v.get("codec_name", ""),
|
|
171
|
+
pix_fmt=pix_fmt,
|
|
172
|
+
bit_depth=bit_depth,
|
|
173
|
+
color_transfer=transfer,
|
|
174
|
+
color_primaries=v.get("color_primaries", "") or "",
|
|
175
|
+
color_space=v.get("color_space", "") or "",
|
|
176
|
+
is_hdr=transfer in ("arib-std-b67", "smpte2084"),
|
|
177
|
+
)
|
|
178
|
+
return out
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
_COMMON_RATES = (23.976, 24.0, 25.0, 29.97, 30.0, 48.0, 50.0, 59.94, 60.0, 120.0)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def nominal_fps(fps: float) -> float:
|
|
185
|
+
"""Snap a measured average fps (e.g. 59.9971) to the nearest common rate."""
|
|
186
|
+
return min(_COMMON_RATES, key=lambda r: abs(r - fps)) if fps > 0 else 30.0
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def output_fps_for(source_fps: float) -> int:
|
|
190
|
+
"""Delivery fps for short-form: 30 when the source is ~30 or faster, else 24/25."""
|
|
191
|
+
n = nominal_fps(source_fps)
|
|
192
|
+
if n >= 29.0:
|
|
193
|
+
return 30
|
|
194
|
+
return 25 if abs(n - 25.0) < 0.5 else 24
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def file_signature(path: Path | str) -> str:
|
|
198
|
+
"""Cheap change detector for caches: size + mtime_ns."""
|
|
199
|
+
st = os.stat(path)
|
|
200
|
+
return f"{st.st_size}:{st.st_mtime_ns}"
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ---------------------------------------------------------------- paths & json
|
|
204
|
+
|
|
205
|
+
def edit_dir_for(videos_dir: Path | str) -> Path:
|
|
206
|
+
return Path(videos_dir).resolve() / "edit"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def ensure_edit_dirs(edit_dir: Path | str) -> Path:
|
|
210
|
+
edit = Path(edit_dir)
|
|
211
|
+
edit.mkdir(parents=True, exist_ok=True)
|
|
212
|
+
for sub in EDIT_SUBDIRS:
|
|
213
|
+
(edit / sub).mkdir(exist_ok=True)
|
|
214
|
+
return edit
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def load_json(path: Path | str, default: Any = None) -> Any:
|
|
218
|
+
p = Path(path)
|
|
219
|
+
if not p.exists():
|
|
220
|
+
return default
|
|
221
|
+
with p.open("r", encoding="utf-8") as fh:
|
|
222
|
+
return json.load(fh)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def save_json(path: Path | str, obj: Any) -> Path:
|
|
226
|
+
"""Atomic write (temp file + replace) so the preview never reads half a file."""
|
|
227
|
+
p = Path(path)
|
|
228
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
229
|
+
fd, tmp = tempfile.mkstemp(prefix=".tmp-", suffix=".json", dir=str(p.parent))
|
|
230
|
+
try:
|
|
231
|
+
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
232
|
+
json.dump(obj, fh, ensure_ascii=False, indent=2)
|
|
233
|
+
os.replace(tmp, p)
|
|
234
|
+
finally:
|
|
235
|
+
if os.path.exists(tmp):
|
|
236
|
+
os.unlink(tmp)
|
|
237
|
+
return p
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def now_iso() -> str:
|
|
241
|
+
return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def fmt_time(seconds: float) -> str:
|
|
245
|
+
"""12.345 -> '00:12.34' (mm:ss.cc)."""
|
|
246
|
+
if seconds is None:
|
|
247
|
+
return "--:--"
|
|
248
|
+
s = max(0.0, float(seconds))
|
|
249
|
+
m = int(s // 60)
|
|
250
|
+
return f"{m:02d}:{s - 60 * m:05.2f}"
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def hex_to_rgb(value: str) -> tuple[int, int, int]:
|
|
254
|
+
h = value.strip().lstrip("#")
|
|
255
|
+
if len(h) == 3:
|
|
256
|
+
h = "".join(c * 2 for c in h)
|
|
257
|
+
if len(h) != 6:
|
|
258
|
+
raise ValueError(f"cor inválida: {value!r}")
|
|
259
|
+
return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ---------------------------------------------------------------- fonts
|
|
263
|
+
|
|
264
|
+
def font_path(key: str) -> Path:
|
|
265
|
+
"""Resolve a bundled font by key using assets/fonts/fonts.json.
|
|
266
|
+
|
|
267
|
+
fonts.json maps key -> {"file": "<file in assets/fonts>", "variation": <int|null>}.
|
|
268
|
+
Raises FileNotFoundError with the list of known keys when missing.
|
|
269
|
+
"""
|
|
270
|
+
manifest = load_json(FONTS_DIR / "fonts.json", default={}) or {}
|
|
271
|
+
entry = manifest.get(key)
|
|
272
|
+
if not entry:
|
|
273
|
+
raise FileNotFoundError(f"fonte '{key}' não existe em assets/fonts/fonts.json (tem: {sorted(manifest)})")
|
|
274
|
+
path = FONTS_DIR / entry["file"]
|
|
275
|
+
if not path.exists():
|
|
276
|
+
raise FileNotFoundError(f"arquivo da fonte '{key}' não encontrado: {path}")
|
|
277
|
+
return path
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def load_font(key: str, size: int):
|
|
281
|
+
"""PIL ImageFont for a bundled font key, applying the variable-font weight if declared."""
|
|
282
|
+
from PIL import ImageFont
|
|
283
|
+
|
|
284
|
+
manifest = load_json(FONTS_DIR / "fonts.json", default={}) or {}
|
|
285
|
+
entry = manifest.get(key) or {}
|
|
286
|
+
font = ImageFont.truetype(str(font_path(key)), size=size)
|
|
287
|
+
variation = entry.get("variation")
|
|
288
|
+
if variation is not None:
|
|
289
|
+
try:
|
|
290
|
+
font.set_variation_by_axes([variation])
|
|
291
|
+
except Exception:
|
|
292
|
+
pass
|
|
293
|
+
return font
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# ---------------------------------------------------------------- style catalog
|
|
297
|
+
|
|
298
|
+
def load_style_catalog() -> dict:
|
|
299
|
+
return load_json(STYLE_CATALOG_PATH, default={}) or {}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
if __name__ == "__main__":
|
|
303
|
+
import pprint
|
|
304
|
+
|
|
305
|
+
if len(sys.argv) > 1:
|
|
306
|
+
pprint.pprint(probe_video(sys.argv[1]))
|
|
307
|
+
else:
|
|
308
|
+
print("uso: uv run python helpers/common.py <video>")
|