primocode 10.16.0 → 10.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/edit-ia-pro.txt +32 -0
- package/LICENSES/fontes-OFL.txt +476 -0
- package/LICENSES/hyperframes-Apache-2.0.txt +205 -0
- package/LICENSES/permanent-marker-Apache-2.0.txt +204 -0
- package/LICENSES/selfie-multiclass-Apache-2.0.txt +227 -0
- package/LICENSES/three-MIT.txt +23 -0
- package/LICENSES/video-use-MIT.txt +24 -0
- package/LICENSES/woven-sfx-CC0.txt +16 -0
- package/LICENSES/yunet-MIT.txt +21 -0
- package/README.md +2 -2
- package/bin/primocode.js +11 -2
- package/editor/LICENSE.md +11 -0
- package/editor/README.md +321 -0
- package/editor/SKILL.md +307 -0
- package/editor/THIRD_PARTY_NOTICES.md +228 -0
- package/editor/assets/baixar.json +591 -0
- package/editor/assets/fonts/anton/OFL.txt +93 -0
- package/editor/assets/fonts/caveat/OFL.txt +93 -0
- package/editor/assets/fonts/figtree/OFL.txt +93 -0
- package/editor/assets/fonts/fonts.json +10 -0
- package/editor/assets/fonts/montserrat/OFL.txt +93 -0
- package/editor/assets/fonts/permanentmarker/LICENSE.txt +202 -0
- package/editor/assets/models/SELFIE_MULTICLASS_LICENSE.txt +227 -0
- package/editor/assets/models/YUNET_LICENSE.txt +21 -0
- package/editor/assets/sfx/licencas/bass_drop.txt +32 -0
- package/editor/assets/sfx/licencas/cash.txt +76 -0
- package/editor/assets/sfx/licencas/charge.txt +31 -0
- package/editor/assets/sfx/licencas/click.txt +46 -0
- package/editor/assets/sfx/licencas/coin.txt +50 -0
- package/editor/assets/sfx/licencas/count.txt +47 -0
- package/editor/assets/sfx/licencas/data.txt +31 -0
- package/editor/assets/sfx/licencas/error.txt +32 -0
- package/editor/assets/sfx/licencas/flash.txt +31 -0
- package/editor/assets/sfx/licencas/gears.txt +82 -0
- package/editor/assets/sfx/licencas/keyboard.txt +48 -0
- package/editor/assets/sfx/licencas/metal_hit.txt +62 -0
- package/editor/assets/sfx/licencas/metal_riser.txt +61 -0
- package/editor/assets/sfx/licencas/mouse_click.txt +50 -0
- package/editor/assets/sfx/licencas/notification.txt +54 -0
- package/editor/assets/sfx/licencas/pop.txt +38 -0
- package/editor/assets/sfx/licencas/rewind.txt +32 -0
- package/editor/assets/sfx/licencas/rise.txt +46 -0
- package/editor/assets/sfx/licencas/sinteticos.txt +21 -0
- package/editor/assets/sfx/licencas/snap.txt +50 -0
- package/editor/assets/sfx/licencas/success.txt +40 -0
- package/editor/assets/sfx/licencas/synth_hit.txt +32 -0
- package/editor/assets/sfx/licencas/tick.txt +14 -0
- package/editor/assets/sfx/licencas/whoosh.txt +47 -0
- package/editor/assets/sfx/licencas/wind.txt +31 -0
- package/editor/assets/sfx/sfx.json +832 -0
- package/editor/assets/sfx/sfx_sinteticos.json +317 -0
- package/editor/assets/sfx/sfx_woven.json +387 -0
- package/editor/assets/sfx/woven/LICENCA.md +16 -0
- package/editor/fichas/briefing-do-video.md +45 -0
- package/editor/fichas/conferencia-da-amostra.md +56 -0
- package/editor/fichas/matriz-de-ganchos.csv +3 -0
- package/editor/fichas/padrao.json +16 -0
- package/editor/fichas/planilha-de-cenas.csv +4 -0
- package/editor/fichas/planilha-de-versoes.csv +4 -0
- package/editor/fichas/plano-em-blocos.md +32 -0
- package/editor/fichas/registro-de-materiais.json +27 -0
- package/editor/helpers/assets_ocr/ocr_vision.swift +41 -0
- package/editor/helpers/bordas_precisas.py +272 -0
- package/editor/helpers/camera_viva.py +426 -0
- package/editor/helpers/captions.py +601 -0
- package/editor/helpers/common.py +308 -0
- package/editor/helpers/compose_final.py +3555 -0
- package/editor/helpers/conferir_corte.py +234 -0
- package/editor/helpers/conferir_dados.py +405 -0
- package/editor/helpers/contact_sheet.py +282 -0
- package/editor/helpers/corte_por_onda.py +433 -0
- package/editor/helpers/corte_seco.py +595 -0
- package/editor/helpers/denoise.py +971 -0
- package/editor/helpers/efeitos.py +408 -0
- package/editor/helpers/efeitos_gerar.py +159 -0
- package/editor/helpers/elevenlabs_api.py +442 -0
- package/editor/helpers/face_layout.py +570 -0
- package/editor/helpers/fetch_images.py +423 -0
- package/editor/helpers/ganho_por_trecho.py +214 -0
- package/editor/helpers/gen_image.py +312 -0
- package/editor/helpers/gen_music.py +316 -0
- package/editor/helpers/gen_sfx.py +148 -0
- package/editor/helpers/grade.py +307 -0
- package/editor/helpers/icloud.py +119 -0
- package/editor/helpers/make_sfx.py +940 -0
- package/editor/helpers/matte.py +1342 -0
- package/editor/helpers/medir_referencia.py +2232 -0
- package/editor/helpers/mixar_trilha.py +354 -0
- package/editor/helpers/montar_corte.py +295 -0
- package/editor/helpers/motion/__init__.py +13 -0
- package/editor/helpers/motion/engine.py +1915 -0
- package/editor/helpers/motion/registry.py +510 -0
- package/editor/helpers/motion/render.py +665 -0
- package/editor/helpers/motion/templates/barra_progresso.py +370 -0
- package/editor/helpers/motion/templates/carimbo.py +132 -0
- package/editor/helpers/motion/templates/checklist.py +271 -0
- package/editor/helpers/motion/templates/comentario_cta.py +380 -0
- package/editor/helpers/motion/templates/contador.py +222 -0
- package/editor/helpers/motion/templates/destaque.py +263 -0
- package/editor/helpers/motion/templates/etiqueta_preco.py +461 -0
- package/editor/helpers/motion/templates/grafico_barras.py +510 -0
- package/editor/helpers/motion/templates/grafico_linha.py +462 -0
- package/editor/helpers/motion/templates/notificacao.py +266 -0
- package/editor/helpers/motion/templates/palavra_impacto.py +410 -0
- package/editor/helpers/motion/templates/passos.py +321 -0
- package/editor/helpers/motion/templates/riscado.py +298 -0
- package/editor/helpers/motion/templates/selo_garantia.py +150 -0
- package/editor/helpers/motion_render.py +241 -0
- package/editor/helpers/motion_sugerir.py +961 -0
- package/editor/helpers/noise_check.py +134 -0
- package/editor/helpers/pack_transcripts.py +223 -0
- package/editor/helpers/palavras_na_onda.py +176 -0
- package/editor/helpers/plano_sfx.py +839 -0
- package/editor/helpers/portao.py +427 -0
- package/editor/helpers/preview_server.py +1177 -0
- package/editor/helpers/rastrear_rosto.py +298 -0
- package/editor/helpers/render_cut.py +1256 -0
- package/editor/helpers/respiros.py +89 -0
- package/editor/helpers/sincronizar_cameras.py +571 -0
- package/editor/helpers/speech_regions.py +423 -0
- package/editor/helpers/style_catalog.json +231 -0
- package/editor/helpers/style_samples.py +246 -0
- package/editor/helpers/tela_verde.py +2113 -0
- package/editor/helpers/text_render.py +2059 -0
- package/editor/helpers/texto_na_tela.py +366 -0
- package/editor/helpers/timeline_view.py +292 -0
- package/editor/helpers/tira_de_quadros.py +113 -0
- package/editor/helpers/transcribe.py +734 -0
- package/editor/helpers/transcribe_batch.py +129 -0
- package/editor/helpers/trilhas.py +1146 -0
- package/editor/helpers/trilhas_humores.json +66 -0
- package/editor/helpers/valores.py +187 -0
- package/editor/helpers/verify_cut.py +820 -0
- package/editor/helpers/voice_levels.py +233 -0
- package/editor/helpers/voz_check.py +386 -0
- package/editor/helpers/voz_limpa.py +81 -0
- package/editor/helpers/watch_preview.py +372 -0
- package/editor/modelo-projeto/LEIA.md +402 -0
- package/editor/modelo-projeto/assets/fontes/OFL-Caveat.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/OFL-JetBrainsMono.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/OFL-Montserrat.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/larguras.json +1 -0
- package/editor/modelo-projeto/assets/three/BufferGeometryUtils.js +1438 -0
- package/editor/modelo-projeto/assets/three/GLTFLoader.js +4895 -0
- package/editor/modelo-projeto/assets/three/LICENSE +21 -0
- package/editor/modelo-projeto/assets/three/ORIGEM.txt +17 -0
- package/editor/modelo-projeto/assets/three/RoomEnvironment.js +185 -0
- package/editor/modelo-projeto/assets/three/three.core.min.js +6 -0
- package/editor/modelo-projeto/assets/three/three.module.min.js +6 -0
- package/editor/modelo-projeto/build.py +4828 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_antes_depois.html +199 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_arquivo.html +232 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_busca.html +315 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_comando.html +304 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_conversa.html +217 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_esquema.html +289 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_medidor.html +249 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_notas.html +233 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_caminho.html +197 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_explodida.html +152 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_tela.html +167 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_volume.html +124 -0
- package/editor/modelo-projeto/compositions/cenas/3d/motor-three.js +728 -0
- package/editor/modelo-projeto/compositions/cenas/LEIA.md +177 -0
- package/editor/modelo-projeto/ferramentas/medir_fontes.py +65 -0
- package/editor/modelo-projeto/ferramentas/pessoa_recortada.py +176 -0
- package/editor/modelo-projeto/ferramentas/rastrear_maos.py +189 -0
- package/editor/modelo-projeto/hyperframes.json +12 -0
- package/editor/modelo-projeto/package.json +11 -0
- package/editor/modelo-projeto/projeto.json +16 -0
- package/editor/preview/app.css +1009 -0
- package/editor/preview/app.js +2122 -0
- package/editor/preview/index.html +307 -0
- package/editor/pyproject.toml +23 -0
- package/editor/references/3d.md +236 -0
- package/editor/references/animacao-catalogo.md +672 -0
- package/editor/references/animacao.md +498 -0
- package/editor/references/comandos.md +92 -0
- package/editor/references/corte-seco-como-funciona.md +50 -0
- package/editor/references/corte-seco.md +88 -0
- package/editor/references/corte.md +290 -0
- package/editor/references/direcao-por-blocos.md +218 -0
- package/editor/references/enquadramento.md +654 -0
- package/editor/references/estilos-prontos.md +355 -0
- package/editor/references/estudio.md +100 -0
- package/editor/references/exemplo-eventos.json +67 -0
- package/editor/references/lettering.md +252 -0
- package/editor/references/modelos.md +494 -0
- package/editor/references/phase2.md +1007 -0
- package/editor/references/referencia-aplicar.md +203 -0
- package/editor/references/referencia-medir.md +375 -0
- package/editor/references/revisao.md +335 -0
- package/editor/references/som-e-trilha.md +146 -0
- package/editor/references/tabela-de-sons.md +130 -0
- package/editor/references/variacoes.md +234 -0
- package/editor/uv.lock +1141 -0
- package/lib/act.js +36 -8
- package/lib/catalogo.js +4 -0
- package/lib/chamada-torta.js +81 -0
- package/lib/editor-pro.js +786 -0
- package/lib/entrega.js +58 -0
- package/lib/estudio/conferencia-visual.js +76 -0
- package/lib/estudio/diretor.js +6 -1
- package/lib/estudio/exportar.js +3 -1
- package/lib/estudio/gosto.js +45 -4
- package/lib/estudio/index.js +8 -5
- package/lib/estudio/kit-ae/base.jsx +119 -13
- package/lib/estudio/kit-ae/cenas.jsx +47 -15
- package/lib/estudio/kit-ae/guarda.jsx +274 -0
- package/lib/estudio/kit-ae/primitivas.jsx +180 -24
- package/lib/estudio/maquina-diretor.js +61 -7
- package/lib/estudio/maquina-foto.js +177 -0
- package/lib/estudio/maquina-guarda.js +207 -0
- package/lib/estudio/maquina-layout.js +22 -1
- package/lib/estudio/maquina.js +60 -11
- package/lib/estudio/referencias.js +1 -1
- package/lib/estudio/visao/visao.swift +97 -0
- package/lib/estudio/visao.js +60 -0
- package/lib/local/cerebro.json +192 -6
- package/lib/local/motor.js +5 -2
- package/lib/nuvem.js +11 -1
- package/lib/passagem.js +50 -0
- package/lib/perigo.js +19 -11
- package/lib/protecao.js +134 -0
- package/lib/titulo-conversa.js +46 -0
- package/lib/tools.js +3 -0
- package/lib/ui.js +1 -1
- package/package.json +3 -2
- package/skills/editar-meu-video/SKILL.md +6 -2
- package/skills/editar-video/SKILL.md +113 -0
- package/studio/pessoa.py +52 -0
- package/voz/primo_voz/app.py +43 -4
- package/voz/primo_voz/chat.html +17 -6
- package/voz/primo_voz/painel_dados.py +134 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""How much background noise each source has, and whether to offer the noise removal (audio.denoise).
|
|
2
|
+
|
|
3
|
+
For every video: the noise floor in its pauses (RMS of edit/regions/<stem>.json silences trimmed 60 ms,
|
|
4
|
+
measured now when the regions file is missing or belongs to another file), the speech level (RMS in the
|
|
5
|
+
speech regions) and their difference. With almost no pause (under 0.1 s) the noise is estimated on the
|
|
6
|
+
quietest 50 ms windows (+3 dB, they catch the dips of the noise). The channels are folded like render_cut.
|
|
7
|
+
|
|
8
|
+
difference >= 30 dB "limpo, não precisa"
|
|
9
|
+
20 to 30 dB "ruído leve" (offer Remover ruído de fundo: Leve)
|
|
10
|
+
under 20 dB "ruído forte" (offer Forte)
|
|
11
|
+
|
|
12
|
+
Prints one PT-BR line per video plus a recommendation, and writes edit/verify/noise_check.json.
|
|
13
|
+
|
|
14
|
+
It also measures, per video, saturation and room echo of the voice (helpers/voz_check.py): one "Voz" line per
|
|
15
|
+
video and the key "voz" in the JSON. Saturated voice does not come back whole; echo is treated lightly and judged
|
|
16
|
+
at the same perceived loudness (references/revisao.md, Voz). `--sem-voz` skips it.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
uv run python helpers/noise_check.py <videos_dir | video ...> [--edit-dir DIR] [--json]
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import json
|
|
26
|
+
import sys
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
30
|
+
from common import now_iso, require_binary, save_json # noqa: E402
|
|
31
|
+
import denoise # noqa: E402
|
|
32
|
+
import voz_check # noqa: E402
|
|
33
|
+
|
|
34
|
+
VIDEO_EXT = {".mov", ".mp4", ".m4v", ".mkv", ".webm"}
|
|
35
|
+
ORDER = {"limpo": 0, "leve": 1, "forte": 2}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def utf8_console() -> None:
|
|
39
|
+
for stream in (sys.stdout, sys.stderr):
|
|
40
|
+
try:
|
|
41
|
+
stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[attr-defined]
|
|
42
|
+
except (AttributeError, ValueError, OSError):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def collect(paths: list[Path]) -> list[Path]:
|
|
47
|
+
out: list[Path] = []
|
|
48
|
+
for p in paths:
|
|
49
|
+
if p.is_dir():
|
|
50
|
+
out += sorted(f for f in p.iterdir() if f.is_file() and f.suffix.lower() in VIDEO_EXT
|
|
51
|
+
and not f.name.startswith("."))
|
|
52
|
+
elif p.is_file():
|
|
53
|
+
out.append(p)
|
|
54
|
+
else:
|
|
55
|
+
raise FileNotFoundError(f"não encontrado: {p}")
|
|
56
|
+
return list(dict.fromkeys(out))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def line_for(r: dict) -> str:
|
|
60
|
+
name = r.get("stem", "?")
|
|
61
|
+
if r.get("error"):
|
|
62
|
+
return f" {name}: {r['error']}"
|
|
63
|
+
extra = " (quase sem pausa: fundo estimado nas partes mais quietas)" if r.get("noise_from") != "pausas" else ""
|
|
64
|
+
return (f" {name} · fundo {r['noise_db']:.1f} dB · voz {r['speech_db']:.1f} dB · diferença {r['snr_db']:.0f} dB · "
|
|
65
|
+
f"{r['advice']}{extra}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def recommendation(results: list[dict]) -> dict:
|
|
69
|
+
measured = [r for r in results if r.get("level")]
|
|
70
|
+
if not measured:
|
|
71
|
+
return {"suggest": None, "text": "Não deu para medir o ruído (sem áudio ou sem fala)."}
|
|
72
|
+
worst = max(measured, key=lambda r: (ORDER[r["level"]], -r["snr_db"]))
|
|
73
|
+
if worst["level"] == "limpo":
|
|
74
|
+
return {"suggest": "off", "worst": worst["stem"],
|
|
75
|
+
"text": "Recomendação: nenhum vídeo precisa de limpeza de ruído (deixe Desligado)."}
|
|
76
|
+
label = "Leve" if worst["level"] == "leve" else "Forte"
|
|
77
|
+
return {"suggest": worst["suggest"], "worst": worst["stem"],
|
|
78
|
+
"text": (f"Recomendação: ofereça Remover ruído de fundo {label} (o pior é {worst['stem']}, "
|
|
79
|
+
f"diferença de {worst['snr_db']:.0f} dB entre voz e fundo).")}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main() -> int:
|
|
83
|
+
ap = argparse.ArgumentParser(description="Mede o ruído de fundo de cada vídeo e diz se vale limpar a voz.")
|
|
84
|
+
ap.add_argument("paths", nargs="+", type=Path, help="pasta dos vídeos ou arquivos de vídeo")
|
|
85
|
+
ap.add_argument("--edit-dir", type=Path, default=None,
|
|
86
|
+
help="pasta edit (padrão: <pasta dos vídeos>/edit); usa regions/<nome>.json de lá")
|
|
87
|
+
ap.add_argument("--json", action="store_true", help="imprime o JSON em vez das linhas")
|
|
88
|
+
ap.add_argument("--sem-voz", action="store_true", help="não mede saturação nem eco da voz")
|
|
89
|
+
args = ap.parse_args()
|
|
90
|
+
utf8_console()
|
|
91
|
+
require_binary("ffmpeg")
|
|
92
|
+
require_binary("ffprobe")
|
|
93
|
+
paths = [p.expanduser().absolute() for p in args.paths]
|
|
94
|
+
try:
|
|
95
|
+
videos = collect(paths)
|
|
96
|
+
except FileNotFoundError as e:
|
|
97
|
+
print(f"erro: {e}", file=sys.stderr)
|
|
98
|
+
return 2
|
|
99
|
+
if not videos:
|
|
100
|
+
print("erro: nenhum vídeo encontrado (.mov, .mp4, .m4v, .mkv, .webm)", file=sys.stderr)
|
|
101
|
+
return 2
|
|
102
|
+
first = paths[0] if paths[0].is_dir() else videos[0].parent
|
|
103
|
+
edit_dir = (args.edit_dir.expanduser().absolute() if args.edit_dir else first / "edit")
|
|
104
|
+
results = [denoise.measure_source(v, edit_dir if edit_dir.is_dir() else None) for v in videos]
|
|
105
|
+
if not args.sem_voz:
|
|
106
|
+
for v, r in zip(videos, results):
|
|
107
|
+
try:
|
|
108
|
+
r["voz"] = voz_check.measure_voice(v)
|
|
109
|
+
except Exception as exc: # noqa: BLE001
|
|
110
|
+
r["voz"] = {"error": f"não consegui medir a voz ({exc.__class__.__name__})"}
|
|
111
|
+
rec = recommendation(results)
|
|
112
|
+
doc = {"created_at": now_iso(), "edit_dir": str(edit_dir), "videos": results, "recommendation": rec,
|
|
113
|
+
"thresholds_db": {"limpo": 30, "leve": 20}}
|
|
114
|
+
if edit_dir.is_dir():
|
|
115
|
+
try:
|
|
116
|
+
save_json(edit_dir / "verify" / "noise_check.json", doc)
|
|
117
|
+
except OSError:
|
|
118
|
+
pass
|
|
119
|
+
if args.json:
|
|
120
|
+
print(json.dumps(doc, ensure_ascii=False, indent=2))
|
|
121
|
+
return 0
|
|
122
|
+
print("Ruído de fundo (fundo = volume nas pausas, voz = volume da fala):")
|
|
123
|
+
for r in results:
|
|
124
|
+
print(line_for(r))
|
|
125
|
+
print(rec["text"])
|
|
126
|
+
if not args.sem_voz:
|
|
127
|
+
print("Voz (saturação e eco de sala, medidos na onda; confirme ouvindo os tempos citados):")
|
|
128
|
+
for r in results:
|
|
129
|
+
print(voz_check.line_for(r.get("stem", "?"), r.get("voz") or {"error": "sem medida"}))
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
sys.exit(main())
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# Adapted from browser-use/video-use (MIT, (c) 2026 Browser Use). See THIRD_PARTY_NOTICES.md.
|
|
2
|
+
"""Pack the word-level transcripts of <edit>/transcripts/ into one phrase-level reading view.
|
|
3
|
+
|
|
4
|
+
Words are grouped into phrases, breaking on any gap >= --silence seconds between words.
|
|
5
|
+
Each phrase gets a [start-end] prefix in source seconds. This is the file the agent reads
|
|
6
|
+
to pick cuts: a fraction of the tokens of the raw JSON, with word-boundary precision.
|
|
7
|
+
|
|
8
|
+
Only source transcripts are packed; cut.json (the transcript of the cut) is skipped. With `--corte`,
|
|
9
|
+
or when cut.json is the only transcript in the folder, the speech of the cut is packed instead: short
|
|
10
|
+
phrases (a pause of 0.3 s, a sentence end or 12 words) with the time each one starts in cut.mp4, to read
|
|
11
|
+
against the `quote` of every range.
|
|
12
|
+
|
|
13
|
+
Output: <edit>/takes_packed.md (the cut: <edit>/fala_do_corte.md)
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
uv run --directory '<SKILL>' python helpers/pack_transcripts.py --edit-dir <edit_dir> [--silence 0.5] [-o FILE]
|
|
17
|
+
uv run --directory '<SKILL>' python helpers/pack_transcripts.py --edit-dir <edit_dir> --corte
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
SKIP_STEMS = {"cut"}
|
|
28
|
+
SKILL_ROOT = Path(__file__).resolve().parent.parent
|
|
29
|
+
RODE_ANTES = (f"Rode antes: uv run --directory '{SKILL_ROOT}' python helpers/transcribe_batch.py '<pasta dos vídeos>' "
|
|
30
|
+
"--edit-dir '<EDIT>'")
|
|
31
|
+
CUT_STEM = "cut"
|
|
32
|
+
CUT_SILENCE_S = 0.3 # the cut has joints of ~0.12 s: a shorter pause breaks the phrase
|
|
33
|
+
CUT_MAX_WORDS = 12
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def format_time(seconds: float) -> str:
|
|
37
|
+
"""Seconds as zero-padded "NNN.NN" (fixed width for alignment)."""
|
|
38
|
+
return f"{max(0.0, seconds):06.2f}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def format_duration(seconds: float) -> str:
|
|
42
|
+
"""Seconds with one decimal, as in the contract ("70.8s")."""
|
|
43
|
+
return f"{seconds:.1f}s"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _plural(n: int, one: str, many: str) -> str:
|
|
47
|
+
return f"{n} {one if n == 1 else many}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_SENTENCE_END = (".", "!", "?", "…")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def group_into_phrases(words: list[dict], silence: float = 0.5, segment_starts: set[float] | None = None,
|
|
54
|
+
sentence_breaks: bool = True) -> list[dict]:
|
|
55
|
+
"""Break the word list into phrases. Returns [{start, end, text, words}].
|
|
56
|
+
|
|
57
|
+
Breaks on a gap >= silence between words. Fluent ad speech rarely pauses that long,
|
|
58
|
+
so it also breaks after a sentence end (. ! ? …) and where Whisper started a new
|
|
59
|
+
segment (segment_starts, times rounded to 3 decimals).
|
|
60
|
+
"""
|
|
61
|
+
phrases: list[dict] = []
|
|
62
|
+
current: list[dict] = []
|
|
63
|
+
prev_end: float | None = None
|
|
64
|
+
seg_starts = segment_starts or set()
|
|
65
|
+
|
|
66
|
+
def flush() -> None:
|
|
67
|
+
nonlocal current
|
|
68
|
+
parts = [(w.get("text") or "").strip() for w in current]
|
|
69
|
+
parts = [p for p in parts if p]
|
|
70
|
+
if parts:
|
|
71
|
+
phrases.append({
|
|
72
|
+
"start": float(current[0]["start"]),
|
|
73
|
+
"end": float(max(w.get("end", w["start"]) for w in current)),
|
|
74
|
+
"text": " ".join(parts),
|
|
75
|
+
"words": current,
|
|
76
|
+
})
|
|
77
|
+
current = []
|
|
78
|
+
|
|
79
|
+
for w in words:
|
|
80
|
+
start = w.get("start")
|
|
81
|
+
if start is None:
|
|
82
|
+
continue
|
|
83
|
+
if current and prev_end is not None:
|
|
84
|
+
last_text = (current[-1].get("text") or "").strip()
|
|
85
|
+
if (start - prev_end >= silence
|
|
86
|
+
or (sentence_breaks and last_text.endswith(_SENTENCE_END))
|
|
87
|
+
or round(float(start), 3) in seg_starts):
|
|
88
|
+
flush()
|
|
89
|
+
current.append(w)
|
|
90
|
+
prev_end = max(prev_end or 0.0, w.get("end", start))
|
|
91
|
+
flush()
|
|
92
|
+
return phrases
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def split_long(phrases: list[dict], max_words: int = CUT_MAX_WORDS) -> list[dict]:
|
|
96
|
+
"""Phrases of at most max_words, broken after a comma when one is near the limit (a line you read at once)."""
|
|
97
|
+
out: list[dict] = []
|
|
98
|
+
for p in phrases:
|
|
99
|
+
words = [w for w in p["words"] if (w.get("text") or "").strip()]
|
|
100
|
+
while len(words) > max_words:
|
|
101
|
+
cut = max_words
|
|
102
|
+
for k in range(max_words, max(1, max_words // 2) - 1, -1):
|
|
103
|
+
if (words[k - 1].get("text") or "").strip().endswith((",", ";", ":")):
|
|
104
|
+
cut = k
|
|
105
|
+
break
|
|
106
|
+
out.append(_phrase(words[:cut]))
|
|
107
|
+
words = words[cut:]
|
|
108
|
+
if words:
|
|
109
|
+
out.append(_phrase(words))
|
|
110
|
+
return out
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _phrase(words: list[dict]) -> dict:
|
|
114
|
+
return {"start": float(words[0]["start"]), "end": float(max(w.get("end", w["start"]) for w in words)),
|
|
115
|
+
"text": " ".join((w.get("text") or "").strip() for w in words), "words": words}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def cut_phrases(data: dict, silence: float = CUT_SILENCE_S, max_words: int = CUT_MAX_WORDS) -> list[dict]:
|
|
119
|
+
"""Short phrases of the transcript of the cut (used by --corte and by conferir_corte.py)."""
|
|
120
|
+
return split_long(phrases_for_transcript(data, silence), max_words)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def render_cut_markdown(data: dict, phrases: list[dict]) -> str:
|
|
124
|
+
n_words = len(data.get("words", []))
|
|
125
|
+
lines = [f"# Fala do corte · {Path(str(data.get('source') or 'cut.mp4')).name}, "
|
|
126
|
+
f"{format_duration(float(data.get('duration') or 0))}, {_plural(n_words, 'palavra', 'palavras')}",
|
|
127
|
+
"Tempo = começo da frase no corte (a transcrição adianta até 0,3 s; animação se mede na onda).", ""]
|
|
128
|
+
if not phrases:
|
|
129
|
+
lines.append(" _sem fala detectada_")
|
|
130
|
+
for p in phrases:
|
|
131
|
+
lines.append(f" [{format_time(p['start'])}] {p['text']}")
|
|
132
|
+
return "\n".join(lines) + "\n"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def source_transcripts(transcripts_dir: Path) -> list[Path]:
|
|
136
|
+
return sorted((p for p in transcripts_dir.glob("*.json")
|
|
137
|
+
if p.stem not in SKIP_STEMS and not p.name.startswith(".")),
|
|
138
|
+
key=lambda p: p.stem.lower())
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def phrases_for_transcript(data: dict, silence: float = 0.5) -> list[dict]:
|
|
142
|
+
"""Phrases of a Contract 1 transcript dict (used by pack and voice_levels)."""
|
|
143
|
+
words = sorted(data.get("words", []), key=lambda w: w.get("start", 0.0))
|
|
144
|
+
seg_starts = {round(float(s["start"]), 3) for s in data.get("segments", []) if "start" in s}
|
|
145
|
+
return group_into_phrases(words, silence, seg_starts)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def pack_one_file(json_path: Path, silence: float) -> tuple[str, float, list[dict]]:
|
|
149
|
+
"""(stem, source duration, phrases) for one transcript file."""
|
|
150
|
+
data = json.loads(json_path.read_text(encoding="utf-8"))
|
|
151
|
+
phrases = phrases_for_transcript(data, silence)
|
|
152
|
+
duration = float(data.get("duration") or (phrases[-1]["end"] if phrases else 0.0))
|
|
153
|
+
return data.get("stem") or json_path.stem, duration, phrases
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def render_markdown(entries: list[tuple[str, float, list[dict]]]) -> str:
|
|
157
|
+
total = sum(e[1] for e in entries)
|
|
158
|
+
lines = [f"# Tomadas · {_plural(len(entries), 'fonte', 'fontes')}, {format_duration(total)} no total", ""]
|
|
159
|
+
for name, duration, phrases in entries:
|
|
160
|
+
lines.append(f"## {name} ({format_duration(duration)}, {_plural(len(phrases), 'frase', 'frases')})")
|
|
161
|
+
if not phrases:
|
|
162
|
+
lines.append(" _sem fala detectada_")
|
|
163
|
+
for p in phrases:
|
|
164
|
+
lines.append(f" [{format_time(p['start'])}-{format_time(p['end'])}] {p['text']}")
|
|
165
|
+
lines.append("")
|
|
166
|
+
return "\n".join(lines)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def main() -> None:
|
|
170
|
+
ap = argparse.ArgumentParser(description="Junta as transcrições em takes_packed.md, uma frase por linha.")
|
|
171
|
+
ap.add_argument("--edit-dir", type=Path, required=True, help="pasta edit com transcripts/")
|
|
172
|
+
ap.add_argument("--silence", type=float, default=0.5,
|
|
173
|
+
help="quebra a frase em pausas maiores ou iguais a isso, em segundos (padrão 0.5)")
|
|
174
|
+
ap.add_argument("-o", "--output", type=Path, default=None, help="arquivo de saída (padrão: <edit-dir>/takes_packed.md)")
|
|
175
|
+
ap.add_argument("--corte", action="store_true",
|
|
176
|
+
help="junta a fala do corte (transcripts/cut.json) em fala_do_corte.md, com o tempo de cada frase")
|
|
177
|
+
args = ap.parse_args()
|
|
178
|
+
|
|
179
|
+
edit_dir = args.edit_dir.absolute()
|
|
180
|
+
transcripts_dir = edit_dir / "transcripts"
|
|
181
|
+
if not transcripts_dir.is_dir():
|
|
182
|
+
sys.exit(f"não achei a pasta de transcrições: {transcripts_dir}\n{RODE_ANTES}")
|
|
183
|
+
if args.silence <= 0:
|
|
184
|
+
sys.exit("--silence precisa ser maior que zero")
|
|
185
|
+
files = source_transcripts(transcripts_dir)
|
|
186
|
+
cut_json = transcripts_dir / f"{CUT_STEM}.json"
|
|
187
|
+
if args.corte or (not files and cut_json.is_file()):
|
|
188
|
+
if not cut_json.is_file():
|
|
189
|
+
sys.exit(f"não achei {cut_json}\nRode antes: uv run --directory '{SKILL_ROOT}' python "
|
|
190
|
+
f"helpers/conferir_corte.py '{edit_dir}'")
|
|
191
|
+
if not args.corte:
|
|
192
|
+
print("só tem a transcrição do corte nesta pasta: juntando a fala do corte (as fontes precisam do "
|
|
193
|
+
"transcribe_batch.py na pasta dos vídeos)", file=sys.stderr)
|
|
194
|
+
data = json.loads(cut_json.read_text(encoding="utf-8"))
|
|
195
|
+
silence = args.silence if args.silence != 0.5 else CUT_SILENCE_S
|
|
196
|
+
phrases = cut_phrases(data, silence)
|
|
197
|
+
out_path = (args.output or edit_dir / "fala_do_corte.md").absolute()
|
|
198
|
+
out_path.write_text(render_cut_markdown(data, phrases), encoding="utf-8")
|
|
199
|
+
print(f"fala do corte: {_plural(len(phrases), 'frase', 'frases')}, "
|
|
200
|
+
f"{format_duration(float(data.get('duration') or 0))} · {out_path}")
|
|
201
|
+
return
|
|
202
|
+
if not files:
|
|
203
|
+
sys.exit(f"nenhuma transcrição em {transcripts_dir}\n{RODE_ANTES}")
|
|
204
|
+
|
|
205
|
+
entries = []
|
|
206
|
+
for path in files:
|
|
207
|
+
try:
|
|
208
|
+
entries.append(pack_one_file(path, args.silence))
|
|
209
|
+
except (json.JSONDecodeError, OSError, KeyError, TypeError) as exc:
|
|
210
|
+
print(f"aviso: ignorei {path.name} (arquivo inválido: {exc})", file=sys.stderr)
|
|
211
|
+
if not entries:
|
|
212
|
+
sys.exit("nenhuma transcrição válida para juntar")
|
|
213
|
+
|
|
214
|
+
out_path = (args.output or edit_dir / "takes_packed.md").absolute()
|
|
215
|
+
out_path.write_text(render_markdown(entries), encoding="utf-8")
|
|
216
|
+
n_phrases = sum(len(e[2]) for e in entries)
|
|
217
|
+
total = sum(e[1] for e in entries)
|
|
218
|
+
print(f"{_plural(len(entries), 'fonte', 'fontes')}, {_plural(n_phrases, 'frase', 'frases')}, "
|
|
219
|
+
f"{format_duration(total)} no total · {out_path} ({out_path.stat().st_size / 1024:.1f} KB)")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
if __name__ == "__main__":
|
|
223
|
+
main()
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""When each word really starts, measured on the waveform (Whisper puts the start up to 0.3 s early).
|
|
2
|
+
|
|
3
|
+
The transcription of the cut marks word starts early, most of all after a joint or a pause: "éééé" came 0.22 s
|
|
4
|
+
early and "Eu" at 0.00 when the voice starts at 0.58 (exemplos, 29/09/2026). A caption can live with that; an
|
|
5
|
+
animation, a sound or a cut that must land ON the word cannot. This helper takes the cut (or any video) and its
|
|
6
|
+
transcript and, for every word, looks at the waveform around the Whisper start (0.10 s before to 0.40 s after,
|
|
7
|
+
never into the previous word):
|
|
8
|
+
|
|
9
|
+
pausa the voice comes back after a quiet run: the start is the first sustained sound after it (sharp,
|
|
10
|
+
~0.01 s). The usual case after a joint of the cut or a breath.
|
|
11
|
+
vale no pause (the words run together): the start is the rise after the deepest dip (10 dB or more) near
|
|
12
|
+
the Whisper start (about 0.03 s).
|
|
13
|
+
whisper nothing to measure (a dip too shallow): the Whisper time is kept.
|
|
14
|
+
|
|
15
|
+
Same measure as corte_por_onda.py (mono RMS in 10 ms windows; pause threshold from the floor of the file).
|
|
16
|
+
|
|
17
|
+
Output: a table of the words that moved 0.05 s or more (or the ones asked for with --palavra / --de --ate) and
|
|
18
|
+
<edit>/palavras_onda.json: [{"i", "text", "whisper", "start", "end", "dif", "como"}], times in seconds of the video.
|
|
19
|
+
|
|
20
|
+
Use: to fire an animation, a sound or a cut on a word, put the `start` of this file in the project as a time in
|
|
21
|
+
seconds (`"em": 3.42` instead of `"em": "éééé"`); the word name uses the Whisper time. Rerun after every new cut.
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
uv run --directory '<SKILL>' python helpers/palavras_na_onda.py '<EDIT>' [--palavra éééé] [--de 2 --ate 6] [--todas]
|
|
25
|
+
uv run --directory '<SKILL>' python helpers/palavras_na_onda.py --video X.mp4 --transcricao X.json [...]
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import argparse
|
|
31
|
+
import re
|
|
32
|
+
import sys
|
|
33
|
+
import unicodedata
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
import numpy as np
|
|
37
|
+
|
|
38
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
39
|
+
import corte_por_onda as onda # noqa: E402
|
|
40
|
+
from common import fmt_time, load_json, save_json # noqa: E402
|
|
41
|
+
|
|
42
|
+
ANTES_S = 0.10 # Whisper can also be a little late
|
|
43
|
+
DEPOIS_S = 0.40 # ... and is up to 0.3 s early
|
|
44
|
+
QUIETO_MIN_S = 0.03 # a quiet run this long before the word is a pause
|
|
45
|
+
SOM_MIN_S = 0.04 # sound this long after it is the word (not a click)
|
|
46
|
+
VALE_DB = 10.0 # a dip this deep between two words marks the start of the second
|
|
47
|
+
MOSTRAR_S = 0.05 # the table shows words that moved at least this much
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _norm(texto: str) -> str:
|
|
51
|
+
t = unicodedata.normalize("NFKD", (texto or "").lower())
|
|
52
|
+
return re.sub(r"[^\w]+", "", "".join(c for c in t if not unicodedata.combining(c)))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def medir(db: np.ndarray, th: float, whisper: float, depois_de: float, ate: float) -> tuple[float, str]:
|
|
56
|
+
"""(start, como) of one word, searched between `depois_de` (the previous word has started) and `ate` (where the
|
|
57
|
+
next word starts: its onset must not be taken for this one)."""
|
|
58
|
+
hop = onda.HOP_S
|
|
59
|
+
lo = max(depois_de, whisper - ANTES_S, 0.0)
|
|
60
|
+
hi = max(lo + hop, ate)
|
|
61
|
+
i0, i1 = int(round(lo / hop)), min(len(db), int(round(hi / hop)))
|
|
62
|
+
if i1 - i0 < 2:
|
|
63
|
+
return round(whisper, 3), "whisper"
|
|
64
|
+
seg = db[i0:i1]
|
|
65
|
+
quiet_need, som_need = int(round(QUIETO_MIN_S / hop)), int(round(SOM_MIN_S / hop))
|
|
66
|
+
# 1. pause -> sound: first sustained sound after a quiet run; the one nearest after the Whisper start wins
|
|
67
|
+
cands = []
|
|
68
|
+
run = 0
|
|
69
|
+
for k in range(len(seg)):
|
|
70
|
+
if seg[k] < th:
|
|
71
|
+
run += 1
|
|
72
|
+
continue
|
|
73
|
+
if run >= quiet_need and k + som_need <= len(seg) and bool((seg[k:k + som_need] >= th).all()):
|
|
74
|
+
cands.append(k)
|
|
75
|
+
run = 0
|
|
76
|
+
if cands:
|
|
77
|
+
w = int(round(whisper / hop)) - i0
|
|
78
|
+
depois = [k for k in cands if k >= w - int(round(0.05 / hop))]
|
|
79
|
+
k = depois[0] if depois else cands[-1]
|
|
80
|
+
return round((i0 + k) * hop, 3), "pausa"
|
|
81
|
+
# 2. no pause: the rise after the deepest dip near the Whisper start
|
|
82
|
+
j0 = max(0, int(round((whisper - 0.08) / hop)) - i0)
|
|
83
|
+
j1 = min(len(seg), int(round((whisper + 0.20) / hop)) - i0)
|
|
84
|
+
if j1 - j0 >= 3:
|
|
85
|
+
k = j0 + int(np.argmin(seg[j0:j1]))
|
|
86
|
+
fundo = float(seg[k])
|
|
87
|
+
adiante = seg[k:min(len(seg), k + int(round(0.15 / hop)))]
|
|
88
|
+
if len(adiante) and float(adiante.max()) - fundo >= VALE_DB:
|
|
89
|
+
meio = fundo + (float(adiante.max()) - fundo) / 2
|
|
90
|
+
sobe = int(np.argmax(adiante >= meio))
|
|
91
|
+
return round((i0 + k + sobe) * hop, 3), "vale"
|
|
92
|
+
return round(whisper, 3), "whisper"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def medir_palavras(db: np.ndarray, th: float, words: list[dict]) -> list[dict]:
|
|
96
|
+
out = []
|
|
97
|
+
ws = sorted(words, key=lambda w: float(w["start"]))
|
|
98
|
+
for i, w in enumerate(ws):
|
|
99
|
+
s, e = float(w["start"]), float(w.get("end", w["start"]))
|
|
100
|
+
depois_de = out[-1]["start"] + 0.05 if out else 0.0
|
|
101
|
+
# up to 0.4 s after the Whisper start (more when Whisper stretched the word over the silence before it,
|
|
102
|
+
# as the first word of a cut), never into the next word
|
|
103
|
+
ate = max(s + DEPOIS_S, e - 0.02)
|
|
104
|
+
if i + 1 < len(ws):
|
|
105
|
+
ate = min(ate, float(ws[i + 1]["start"]))
|
|
106
|
+
ate = max(ate, s + 0.05)
|
|
107
|
+
start, como = medir(db, th, s, depois_de, ate)
|
|
108
|
+
out.append({"i": i, "text": w.get("text", ""), "whisper": round(s, 3), "start": start,
|
|
109
|
+
"end": round(max(e, start), 3), "dif": round(start - s, 3), "como": como})
|
|
110
|
+
return out
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main(argv: list[str] | None = None) -> int:
|
|
114
|
+
ap = argparse.ArgumentParser(description="Começo de cada palavra medido na onda (o Whisper adianta até 0,3 s).")
|
|
115
|
+
ap.add_argument("edit", nargs="?", type=Path, help="pasta edit do corte (usa cut.mp4 e transcripts/cut.json)")
|
|
116
|
+
ap.add_argument("--video", type=Path, help="outro vídeo (com --transcricao)")
|
|
117
|
+
ap.add_argument("--transcricao", type=Path, help="transcrição do vídeo (transcripts/<nome>.json)")
|
|
118
|
+
ap.add_argument("--palavra", action="append", default=[], help="só esta palavra (pode repetir; sem acento conta)")
|
|
119
|
+
ap.add_argument("--de", type=float, help="só palavras a partir deste segundo")
|
|
120
|
+
ap.add_argument("--ate", type=float, help="só palavras até este segundo")
|
|
121
|
+
ap.add_argument("--todas", action="store_true", help="mostra todas as palavras, não só as que mudaram")
|
|
122
|
+
ap.add_argument("--limiar", type=float, help="dB abaixo do qual é pausa (padrão: medido, como no corte pela onda)")
|
|
123
|
+
ap.add_argument("-o", "--saida", type=Path, help="JSON de saída (padrão: <edit>/palavras_onda.json)")
|
|
124
|
+
a = ap.parse_args(argv)
|
|
125
|
+
|
|
126
|
+
if a.edit:
|
|
127
|
+
edit = a.edit.resolve()
|
|
128
|
+
video = a.video or edit / "cut.mp4"
|
|
129
|
+
tr = a.transcricao or edit / "transcripts" / f"{Path(video).stem}.json"
|
|
130
|
+
saida = a.saida or edit / "palavras_onda.json"
|
|
131
|
+
elif a.video and a.transcricao:
|
|
132
|
+
video, tr = a.video, a.transcricao
|
|
133
|
+
saida = a.saida or Path(tr).with_name(Path(tr).stem + "_onda.json")
|
|
134
|
+
else:
|
|
135
|
+
ap.error("passe a pasta edit, ou --video e --transcricao")
|
|
136
|
+
video, tr = Path(video), Path(tr)
|
|
137
|
+
for p in (video, tr):
|
|
138
|
+
if not p.is_file():
|
|
139
|
+
print(f"ERRO: não achei {p}" + (" (rode o conferir_corte.py antes: ele transcreve o corte)"
|
|
140
|
+
if p == tr else ""), file=sys.stderr)
|
|
141
|
+
return 2
|
|
142
|
+
data = load_json(tr)
|
|
143
|
+
words = data.get("words", []) if isinstance(data, dict) else []
|
|
144
|
+
if not words:
|
|
145
|
+
print(f"ERRO: {tr.name} não tem palavras", file=sys.stderr)
|
|
146
|
+
return 2
|
|
147
|
+
db = onda.envelope(video, None)
|
|
148
|
+
th = a.limiar if a.limiar is not None else onda.threshold(db)
|
|
149
|
+
res = medir_palavras(db, th, words)
|
|
150
|
+
save_json(saida, {"video": str(video), "transcricao": str(tr), "limiar_db": th, "palavras": res})
|
|
151
|
+
|
|
152
|
+
alvo = {_norm(x) for x in a.palavra}
|
|
153
|
+
def quer(r: dict) -> bool:
|
|
154
|
+
if alvo and _norm(r["text"]) not in alvo:
|
|
155
|
+
return False
|
|
156
|
+
if a.de is not None and r["start"] < a.de - 0.3:
|
|
157
|
+
return False
|
|
158
|
+
if a.ate is not None and r["start"] > a.ate:
|
|
159
|
+
return False
|
|
160
|
+
return a.todas or bool(alvo) or a.de is not None or a.ate is not None or abs(r["dif"]) >= MOSTRAR_S
|
|
161
|
+
linhas = [r for r in res if quer(r)]
|
|
162
|
+
n_mov = sum(1 for r in res if abs(r["dif"]) >= MOSTRAR_S)
|
|
163
|
+
print(f"{video.name}: {len(res)} palavras, {n_mov} com o começo 0,05 s ou mais longe do Whisper "
|
|
164
|
+
f"(pausa abaixo de {th:.0f} dB) · {saida}")
|
|
165
|
+
if linhas:
|
|
166
|
+
print(f" {'onda':>8s} {'whisper':>8s} {'dif':>6s} {'como':7s} palavra")
|
|
167
|
+
for r in linhas:
|
|
168
|
+
print(f" {fmt_time(r['start']):>8s} {fmt_time(r['whisper']):>8s} {r['dif']:+.2f} {r['como']:7s} {r['text']}")
|
|
169
|
+
if alvo and not linhas:
|
|
170
|
+
print(" nenhuma palavra com esse texto (confira a grafia na fala do corte)")
|
|
171
|
+
print(" para animação, som ou corte na palavra: use o tempo da coluna onda (\"em\": segundos) no projeto.")
|
|
172
|
+
return 0
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
raise SystemExit(main())
|