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,433 @@
|
|
|
1
|
+
"""Dry cut by the waveform: split every EDL range at its inner breaths, leaving ~0.12 s at each joint.
|
|
2
|
+
|
|
3
|
+
This is the cut André approved on the DJI reels and the hook/body tests (27-28/09/2026). The agent
|
|
4
|
+
writes rough ranges, one per sentence and always the LAST good take, into `<EDIT>/edl_manual.json`
|
|
5
|
+
(same format as edl.json). This helper measures each source on the waveform (mono RMS in 10 ms
|
|
6
|
+
windows, cached in `<EDIT>/regions/onda_<name>.npy`) and, for every range:
|
|
7
|
+
|
|
8
|
+
1. starts PAD_S before the first sound that lasts ONSET_S (a mouth click before the sentence does not
|
|
9
|
+
count as its start), then walks back while a weak first consonant still sounds (the S of "Sem"),
|
|
10
|
+
and trims the silence at the end down to PAD_S, however short it is (the written end has 0.1 to
|
|
11
|
+
0.3 s of slack: left there, it made joints of 0.29 s with the head of the next range);
|
|
12
|
+
2. splits the range at every pause of at least PAUSE_MIN_S, keeping PAD_S of silence on each side,
|
|
13
|
+
so a joint ends up with about 2 * PAD_S. Shorter pauses stay: splitting a micro-pause inside a
|
|
14
|
+
sentence bites the consonant next to it ("token" came out as "Tolkien");
|
|
15
|
+
3. walks the end of each piece out, up to TAIL_MAX_S, while a weak final consonant still sounds
|
|
16
|
+
(the S of "demais", the E of "YouTube"), stopping if the sound rises again (click, breath, word)
|
|
17
|
+
or falls TAIL_UNDER_TH_DB under the threshold (on a phone with noise suppression the room decays
|
|
18
|
+
slowly toward -72 dB, and the walk used to go through 0.07 s of that near-silence). When the last
|
|
19
|
+
sentence is glued to the next word ("computador e", no pause between them) the walk stops where the
|
|
20
|
+
next word rises; the end then goes back to the valley between the two words (the lowest 10 ms of the
|
|
21
|
+
walked decay, after the final consonant), and the range gets "fim_no_vale": true. A valley shallower
|
|
22
|
+
than VALLEY_DB is reported: the end may carry the start of the next word;
|
|
23
|
+
4. drops what is left of a breath or a click: pieces shorter than 0.08 s, and pieces shorter than
|
|
24
|
+
0.35 s that never get PEAK_OVER_TH_DB above the threshold.
|
|
25
|
+
|
|
26
|
+
Ranges with "dry": false stay whole, with the edges exactly as written (a video playing on screen, sound
|
|
27
|
+
coming out of a phone, the last frames held after the last word). The tail of a range never walks into the
|
|
28
|
+
next range of the same source (a take split in two, or a "dry": false range right after it): that audio would
|
|
29
|
+
play twice.
|
|
30
|
+
Threshold per source: noise floor (10th percentile) + FLOOR_TO_TH_DB, never lower than SPEECH_TO_TH_MAX_DB
|
|
31
|
+
under the loud speech (90th percentile: a phone with noise suppression has a near-digital floor and breaths
|
|
32
|
+
far above it) and at least SPEECH_TO_TH_DB under it. `--limiar` forces one value for every source (the DJI mic without a
|
|
33
|
+
noise gate used -38; a quiet OBS mic needed -44).
|
|
34
|
+
|
|
35
|
+
A manual edge that sits on speech is reported: it may cut a word. Run `bordas_precisas.py --apply`
|
|
36
|
+
on edl_manual.json first, or move the edge to a pause.
|
|
37
|
+
|
|
38
|
+
Camera switch ("imagem_de", see render_cut.py): ranges in a row from the same source that touch (the next start
|
|
39
|
+
within 0.02 s of the end) and change the picture are measured as ONE range on the source's waveform, then split
|
|
40
|
+
at the switch. A switch that falls in a pause of PAUSE_MIN_S or more just goes with the pause that is cut; one in
|
|
41
|
+
the middle of speech keeps both pieces touching, so render_cut plays the voice straight through and only the
|
|
42
|
+
picture changes. A piece shorter than MIN_SWITCH_S on either side of a switch goes to the neighbour (no flash of
|
|
43
|
+
the other camera). A switch on speech is listed, not reported as an edge that may cut a word.
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
uv run python helpers/corte_por_onda.py <edit>/edl_manual.json -o <edit>/edl.json [--limiar -38]
|
|
47
|
+
Then render with render_cut.py as usual.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
import argparse
|
|
53
|
+
import copy
|
|
54
|
+
import subprocess
|
|
55
|
+
import sys
|
|
56
|
+
from pathlib import Path
|
|
57
|
+
|
|
58
|
+
import numpy as np
|
|
59
|
+
|
|
60
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
61
|
+
from common import ffprobe_json, load_json, require_binary, save_json # noqa: E402
|
|
62
|
+
from icloud import SoNaNuvem, garantir_local # noqa: E402
|
|
63
|
+
|
|
64
|
+
HOP_S = 0.01
|
|
65
|
+
PAUSE_MIN_S = 0.22
|
|
66
|
+
PAD_S = 0.06
|
|
67
|
+
TAIL_MAX_S = 0.20
|
|
68
|
+
FLOOR_TO_TH_DB = 14.0
|
|
69
|
+
SPEECH_TO_TH_DB = 20.0
|
|
70
|
+
SPEECH_TO_TH_MAX_DB = 28.0
|
|
71
|
+
TAIL_FLOOR_DB = 6.0
|
|
72
|
+
TAIL_UNDER_TH_DB = 12.0
|
|
73
|
+
TAIL_RISE_DB = 3.0
|
|
74
|
+
MIN_PIECE_S = 0.08
|
|
75
|
+
WEAK_PIECE_S = 0.35
|
|
76
|
+
PEAK_OVER_TH_DB = 13.0
|
|
77
|
+
ONSET_S = 0.04
|
|
78
|
+
JOIN_S = 0.02
|
|
79
|
+
MIN_SWITCH_S = 0.30
|
|
80
|
+
VALLEY_DB = 6.0 # the valley between two glued words is at least this far under both of them
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def envelope(path: Path, cache: Path | None) -> np.ndarray:
|
|
84
|
+
"""dB of the mono mix in 10 ms windows, index 0 = time 0 of the file."""
|
|
85
|
+
if cache and cache.is_file() and cache.stat().st_mtime >= path.stat().st_mtime:
|
|
86
|
+
return np.load(cache)
|
|
87
|
+
ffmpeg = require_binary("ffmpeg")
|
|
88
|
+
raw = subprocess.run([ffmpeg, "-v", "error", "-nostdin", "-i", str(path), "-vn", "-ac", "1", "-ar", "48000",
|
|
89
|
+
"-f", "f32le", "-"], capture_output=True).stdout
|
|
90
|
+
x = np.frombuffer(raw, dtype=np.float32).astype(np.float64)
|
|
91
|
+
w = int(48000 * HOP_S)
|
|
92
|
+
n = len(x) // w
|
|
93
|
+
if n == 0:
|
|
94
|
+
raise SystemExit(f"ERRO: {path.name} não tem áudio que dê pra medir")
|
|
95
|
+
db = 10 * np.log10((x[: n * w].reshape(n, w) ** 2).mean(axis=1) + 1e-12)
|
|
96
|
+
offset = audio_offset(path)
|
|
97
|
+
if offset > 0: # audio starts after the video: the first windows are silence
|
|
98
|
+
db = np.concatenate([np.full(int(round(offset / HOP_S)), -120.0), db])
|
|
99
|
+
db = db.astype(np.float32)
|
|
100
|
+
if cache:
|
|
101
|
+
cache.parent.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
np.save(cache, db)
|
|
103
|
+
return db
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def audio_offset(path: Path) -> float:
|
|
107
|
+
info = ffprobe_json(path)
|
|
108
|
+
starts = {s.get("codec_type"): float(s.get("start_time") or 0) for s in info.get("streams", [])
|
|
109
|
+
if s.get("codec_type") in ("audio", "video")}
|
|
110
|
+
return max(0.0, starts.get("audio", 0.0) - starts.get("video", 0.0))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def threshold(db: np.ndarray) -> float:
|
|
114
|
+
floor = float(np.percentile(db, 10))
|
|
115
|
+
loud = float(np.percentile(db, 90))
|
|
116
|
+
# a phone with noise suppression leaves the gaps near digital silence (-72 dB) and the breaths at -45:
|
|
117
|
+
# the floor alone would count a breath as speech, so the threshold never goes lower than loud - 28
|
|
118
|
+
return round(min(max(floor + FLOOR_TO_TH_DB, loud - SPEECH_TO_TH_MAX_DB), loud - SPEECH_TO_TH_DB), 1)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class Source:
|
|
122
|
+
def __init__(self, db: np.ndarray, th: float):
|
|
123
|
+
self.db, self.th = db, th
|
|
124
|
+
self.floor = float(np.percentile(db, 10))
|
|
125
|
+
# a weak consonant still sounds above this; below it is the room (or the decay of a noise gate)
|
|
126
|
+
self.weak = max(self.floor + TAIL_FLOOR_DB, th - TAIL_UNDER_TH_DB)
|
|
127
|
+
|
|
128
|
+
def at(self, t: float) -> float:
|
|
129
|
+
# the epsilon: 11.45 + 4 hops is 11.489999..., which read the window before and walked one hop too far
|
|
130
|
+
return float(self.db[min(len(self.db) - 1, max(0, int(t / HOP_S + 1e-6)))])
|
|
131
|
+
|
|
132
|
+
def pauses(self, a: float, b: float) -> list[tuple[float, float]]:
|
|
133
|
+
out, run = [], None
|
|
134
|
+
i0, i1 = int(round(a / HOP_S)), min(len(self.db), int(round(b / HOP_S)))
|
|
135
|
+
for i in range(max(0, i0), i1):
|
|
136
|
+
quiet = self.db[i] < self.th
|
|
137
|
+
if quiet and run is None:
|
|
138
|
+
run = i
|
|
139
|
+
if not quiet and run is not None:
|
|
140
|
+
if (i - run) * HOP_S >= PAUSE_MIN_S:
|
|
141
|
+
out.append((run * HOP_S, i * HOP_S))
|
|
142
|
+
run = None
|
|
143
|
+
if run is not None and (i1 - run) * HOP_S >= PAUSE_MIN_S:
|
|
144
|
+
out.append((run * HOP_S, i1 * HOP_S))
|
|
145
|
+
return out
|
|
146
|
+
|
|
147
|
+
def first_sustained(self, a: float, b: float) -> float | None:
|
|
148
|
+
need = int(round(ONSET_S / HOP_S))
|
|
149
|
+
above = self.db[max(0, int(round(a / HOP_S))): min(len(self.db), int(round(b / HOP_S)))] >= self.th
|
|
150
|
+
run = 0
|
|
151
|
+
for k, up in enumerate(above):
|
|
152
|
+
run = run + 1 if up else 0
|
|
153
|
+
if run >= need:
|
|
154
|
+
return (int(round(a / HOP_S)) + k - need + 1) * HOP_S
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
def last_sustained_end(self, a: float, b: float) -> float | None:
|
|
158
|
+
"""end of the last sound that lasts ONSET_S inside [a, b] (None: no sound)"""
|
|
159
|
+
need = int(round(ONSET_S / HOP_S))
|
|
160
|
+
i0, i1 = max(0, int(round(a / HOP_S))), min(len(self.db), int(round(b / HOP_S)))
|
|
161
|
+
run = 0
|
|
162
|
+
for i in range(i1 - 1, i0 - 1, -1):
|
|
163
|
+
run = run + 1 if self.db[i] >= self.th else 0
|
|
164
|
+
if run >= need:
|
|
165
|
+
return (i + need) * HOP_S
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
def extend_head(self, x0: float, limit: float) -> float:
|
|
169
|
+
"""weak first consonant (the S of "Sem"): walk back while it still sounds, never into a click"""
|
|
170
|
+
x, ref = x0, self.at(x0)
|
|
171
|
+
while x - HOP_S > max(limit, x0 - TAIL_MAX_S):
|
|
172
|
+
v = self.at(x - HOP_S)
|
|
173
|
+
if v <= self.weak or v > ref + TAIL_RISE_DB:
|
|
174
|
+
break
|
|
175
|
+
ref = min(ref, v)
|
|
176
|
+
x -= HOP_S
|
|
177
|
+
return round(x, 3)
|
|
178
|
+
|
|
179
|
+
def extend_tail(self, x1: float, limit: float) -> float:
|
|
180
|
+
return self.extend_tail_why(x1, limit)[0]
|
|
181
|
+
|
|
182
|
+
def extend_tail_why(self, x1: float, limit: float) -> tuple[float, str]:
|
|
183
|
+
"""(where the walk stopped, why): "fraco" (the room), "subiu" (a click, a breath or the next word),
|
|
184
|
+
"limite" (TAIL_MAX_S or the next range)"""
|
|
185
|
+
x, ref = x1, self.at(x1)
|
|
186
|
+
while x < min(limit, x1 + TAIL_MAX_S):
|
|
187
|
+
v = self.at(x)
|
|
188
|
+
if v <= self.weak:
|
|
189
|
+
return round(x, 3), "fraco"
|
|
190
|
+
if v > ref + TAIL_RISE_DB:
|
|
191
|
+
return round(x, 3), "subiu"
|
|
192
|
+
ref = min(ref, v)
|
|
193
|
+
x += HOP_S
|
|
194
|
+
return round(x, 3), "limite"
|
|
195
|
+
|
|
196
|
+
def sustained_from(self, t: float, within: float = 0.03) -> bool:
|
|
197
|
+
"""a sound of ONSET_S or more (a word, not a click) starts within `within` s after t"""
|
|
198
|
+
onset = self.first_sustained(t, t + within + ONSET_S + HOP_S)
|
|
199
|
+
return onset is not None and onset <= t + within + 1e-6
|
|
200
|
+
|
|
201
|
+
def valley(self, a: float, b: float) -> tuple[float, float] | None:
|
|
202
|
+
"""(end time, depth in dB) of the lowest 10 ms of [a, b): the end goes right after that window. The depth is
|
|
203
|
+
how far it is under the louder of the two sides (0.1 s before and 0.1 s after), taking the quieter side."""
|
|
204
|
+
i0 = max(1, int(a / HOP_S + 1e-6))
|
|
205
|
+
i1 = min(len(self.db) - 1, int(b / HOP_S + 1e-6))
|
|
206
|
+
if i1 <= i0:
|
|
207
|
+
i0 = max(1, i1 - 1)
|
|
208
|
+
if i1 <= i0:
|
|
209
|
+
return None
|
|
210
|
+
seg = self.db[i0:i1]
|
|
211
|
+
k = i0 + int(len(seg) - 1 - np.argmin(seg[::-1])) # the lowest; on a tie, the later one
|
|
212
|
+
left = float(self.db[max(0, k - 10):k].max())
|
|
213
|
+
right = float(self.db[k + 1:k + 11].max()) if k + 1 < len(self.db) else float(self.db[k])
|
|
214
|
+
return round((k + 1) * HOP_S, 3), round(min(left, right) - float(self.db[k]), 1)
|
|
215
|
+
|
|
216
|
+
def peak(self, a: float, b: float) -> float:
|
|
217
|
+
seg = self.db[max(0, int(a / HOP_S)): int(b / HOP_S)]
|
|
218
|
+
return float(seg.max()) if len(seg) else -120.0
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def next_in_source(ranges: list[dict], i: int) -> float | None:
|
|
222
|
+
"""Earliest start of another range of the same source that begins at or after the end of range i."""
|
|
223
|
+
r = ranges[i]
|
|
224
|
+
end = float(r["end"])
|
|
225
|
+
starts = [float(o["start"]) for k, o in enumerate(ranges)
|
|
226
|
+
if k != i and o.get("source") == r.get("source") and float(o["start"]) >= end - 0.05]
|
|
227
|
+
return min(starts) if starts else None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def split_range(r: dict, src: Source, limit: float | None = None, notes: list | None = None) -> list[dict]:
|
|
231
|
+
"""limit: the tail never goes past it (the start of the next range of the same source). notes: the end glued to
|
|
232
|
+
the next word is reported there."""
|
|
233
|
+
a, b = float(r["start"]), float(r["end"])
|
|
234
|
+
onset = src.first_sustained(a, b)
|
|
235
|
+
if onset is not None and onset - PAD_S > a:
|
|
236
|
+
a = src.extend_head(round(onset - PAD_S, 3), a)
|
|
237
|
+
cur, pieces = a, []
|
|
238
|
+
for s0, s1 in src.pauses(a, b):
|
|
239
|
+
if s0 <= a + 0.02: # silence at the start: trim it
|
|
240
|
+
cur = max(cur, s1 - PAD_S)
|
|
241
|
+
continue
|
|
242
|
+
if s1 >= b - 0.02: # silence at the end: trim it
|
|
243
|
+
b = min(b, s0 + PAD_S)
|
|
244
|
+
break
|
|
245
|
+
pieces.append((cur, s0 + PAD_S))
|
|
246
|
+
cur = s1 - PAD_S
|
|
247
|
+
last = src.last_sustained_end(cur, b) # a trailing silence shorter than a pause is trimmed too
|
|
248
|
+
if last is not None and b - last > PAD_S + 0.02:
|
|
249
|
+
b = round(last + PAD_S, 3)
|
|
250
|
+
pieces.append((cur, b))
|
|
251
|
+
out = []
|
|
252
|
+
for k, (x0, x1) in enumerate(pieces):
|
|
253
|
+
last_piece = k + 1 == len(pieces)
|
|
254
|
+
nxt = pieces[k + 1][0] if not last_piece else x1 + TAIL_MAX_S
|
|
255
|
+
if limit is not None and last_piece:
|
|
256
|
+
nxt = min(nxt, max(x1, limit))
|
|
257
|
+
walked, why = src.extend_tail_why(x1, nxt)
|
|
258
|
+
walked = min(walked, max(x1, round(nxt, 3))) # the 10 ms walk may step past the limit
|
|
259
|
+
glued = None
|
|
260
|
+
if last_piece and why == "subiu" and src.sustained_from(walked):
|
|
261
|
+
# the sentence ends glued to the next word: stop in the valley between them, after the final consonant
|
|
262
|
+
# (the walked decay only goes down, so its lowest point is past the consonant and before the next word)
|
|
263
|
+
v = src.valley(x1, walked)
|
|
264
|
+
if v is not None and x0 + MIN_PIECE_S < v[0] <= walked + 1e-6:
|
|
265
|
+
walked, glued = v[0], v[1]
|
|
266
|
+
x1 = walked
|
|
267
|
+
if x1 - x0 < MIN_PIECE_S:
|
|
268
|
+
continue
|
|
269
|
+
if x1 - x0 < WEAK_PIECE_S and src.peak(x0, x1) < src.th + PEAK_OVER_TH_DB:
|
|
270
|
+
continue
|
|
271
|
+
n = copy.deepcopy(r)
|
|
272
|
+
n.update(start=round(x0, 3), end=round(x1, 3), reason=(r.get("reason") or "").strip() + " + corte por onda")
|
|
273
|
+
if glued is not None:
|
|
274
|
+
n["fim_no_vale"] = True
|
|
275
|
+
quem = f"{r.get('id')} ({(r.get('quote') or '')[:40]!r})"
|
|
276
|
+
if notes is not None and glued >= VALLEY_DB:
|
|
277
|
+
notes.append(f"{quem}: o fim estava colado na palavra seguinte; parou no vale entre as duas em {x1:.2f} s "
|
|
278
|
+
f"({glued:.0f} dB abaixo delas), depois da consoante final. O verify_cut ainda marca "
|
|
279
|
+
f"EDGE-IN-SPEECH nessa borda (a palavra seguinte começa logo depois): é esperado, o fade de "
|
|
280
|
+
f"30 ms do render cobre; confira ouvindo")
|
|
281
|
+
elif notes is not None:
|
|
282
|
+
notes.append(f"{quem}: o fim está colado na palavra seguinte e não há vale entre as duas (só {glued:.0f} "
|
|
283
|
+
f"dB): ficou em {x1:.2f} s, no ponto mais baixo, e pode levar o começo da palavra seguinte. "
|
|
284
|
+
f"Use outra tomada, ou termine o trecho uma palavra depois")
|
|
285
|
+
out.append(n)
|
|
286
|
+
return out
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def picture_of(r: dict) -> str:
|
|
290
|
+
return r.get("imagem_de") or r.get("source")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def switch_groups(ranges: list[dict]) -> list[list[int]]:
|
|
294
|
+
"""Indices of the ranges, grouped where a camera switch continues the same speech (same source, touching,
|
|
295
|
+
picture changes, none of them "dry": false)."""
|
|
296
|
+
groups: list[list[int]] = []
|
|
297
|
+
for i, r in enumerate(ranges):
|
|
298
|
+
if groups:
|
|
299
|
+
prev = ranges[groups[-1][-1]]
|
|
300
|
+
if (prev.get("source") == r.get("source") and prev.get("dry", True) and r.get("dry", True)
|
|
301
|
+
and abs(float(r["start"]) - float(prev["end"])) <= JOIN_S and picture_of(prev) != picture_of(r)):
|
|
302
|
+
groups[-1].append(i)
|
|
303
|
+
continue
|
|
304
|
+
groups.append([i])
|
|
305
|
+
return groups
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def split_at_switches(pieces: list[dict], group: list[dict]) -> list[dict]:
|
|
309
|
+
"""Pieces of a merged range, back to the range each part belongs to, split at the switch times."""
|
|
310
|
+
seams = [float(r["start"]) for r in group[1:]]
|
|
311
|
+
out = []
|
|
312
|
+
for piece in pieces:
|
|
313
|
+
x0, x1 = float(piece["start"]), float(piece["end"])
|
|
314
|
+
owner = sum(1 for t in seams if t <= x0 + 1e-9)
|
|
315
|
+
parts = []
|
|
316
|
+
a = x0
|
|
317
|
+
for k, t in enumerate(seams):
|
|
318
|
+
if x0 < t < x1:
|
|
319
|
+
parts.append([a, t, owner])
|
|
320
|
+
a, owner = t, k + 1
|
|
321
|
+
parts.append([a, x1, owner])
|
|
322
|
+
while len(parts) > 1:
|
|
323
|
+
short = min(range(len(parts)), key=lambda j: parts[j][1] - parts[j][0])
|
|
324
|
+
if parts[short][1] - parts[short][0] >= MIN_SWITCH_S:
|
|
325
|
+
break
|
|
326
|
+
if short == 0:
|
|
327
|
+
parts[1][0] = parts[0][0]
|
|
328
|
+
elif short == len(parts) - 1:
|
|
329
|
+
parts[-2][1] = parts[-1][1]
|
|
330
|
+
else:
|
|
331
|
+
left, right = parts[short - 1], parts[short + 1]
|
|
332
|
+
if left[1] - left[0] >= right[1] - right[0]:
|
|
333
|
+
left[1] = parts[short][1]
|
|
334
|
+
else:
|
|
335
|
+
right[0] = parts[short][0]
|
|
336
|
+
del parts[short]
|
|
337
|
+
for x, y, k in parts:
|
|
338
|
+
n = copy.deepcopy(group[k])
|
|
339
|
+
n.update(start=round(x, 3), end=round(y, 3),
|
|
340
|
+
reason=(group[k].get("reason") or "").strip() + " + corte por onda")
|
|
341
|
+
out.append(n)
|
|
342
|
+
return out
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def main(argv: list[str] | None = None) -> int:
|
|
346
|
+
ap = argparse.ArgumentParser(description="Corte seco pela onda: parte cada trecho nos respiros de dentro.")
|
|
347
|
+
ap.add_argument("edl_manual", type=Path, help="EDL com um trecho por frase (a última tomada boa)")
|
|
348
|
+
ap.add_argument("-o", "--saida", type=Path, help="EDL cortado (padrão: edl.json na mesma pasta)")
|
|
349
|
+
ap.add_argument("--limiar", type=float, help="dB abaixo do qual é pausa, pra todas as fontes (padrão: medido)")
|
|
350
|
+
args = ap.parse_args(argv)
|
|
351
|
+
|
|
352
|
+
edl = load_json(args.edl_manual)
|
|
353
|
+
if not edl or not edl.get("ranges"):
|
|
354
|
+
print(f"ERRO: {args.edl_manual} não tem trechos", file=sys.stderr)
|
|
355
|
+
return 2
|
|
356
|
+
saida = args.saida or args.edl_manual.with_name("edl.json")
|
|
357
|
+
if saida.resolve() == args.edl_manual.resolve():
|
|
358
|
+
print("ERRO: a saída não pode ser o próprio edl_manual.json", file=sys.stderr)
|
|
359
|
+
return 2
|
|
360
|
+
edit = args.edl_manual.resolve().parent
|
|
361
|
+
try:
|
|
362
|
+
garantir_local([Path(p) for p in edl.get("sources", {}).values()])
|
|
363
|
+
except SoNaNuvem as exc:
|
|
364
|
+
print(f"ERRO: {exc}", file=sys.stderr)
|
|
365
|
+
return 2
|
|
366
|
+
sources: dict[str, Source] = {}
|
|
367
|
+
for name, p in edl.get("sources", {}).items():
|
|
368
|
+
path = Path(p)
|
|
369
|
+
if not path.is_file():
|
|
370
|
+
print(f"ERRO: a fonte {name} não existe: {path}", file=sys.stderr)
|
|
371
|
+
return 2
|
|
372
|
+
db = envelope(path, edit / "regions" / f"onda_{name}.npy")
|
|
373
|
+
th = args.limiar if args.limiar is not None else threshold(db)
|
|
374
|
+
sources[name] = Source(db, th)
|
|
375
|
+
print(f"{name}: chão {sources[name].floor:.0f} dB, fala forte {np.percentile(db, 90):.0f} dB, "
|
|
376
|
+
f"pausa abaixo de {th:.0f} dB")
|
|
377
|
+
|
|
378
|
+
new, avisos, inteiros, trocas, vales = [], [], [], [], []
|
|
379
|
+
for r in edl["ranges"]:
|
|
380
|
+
if r.get("source") not in sources:
|
|
381
|
+
print(f"ERRO: o trecho {r.get('id')} usa a fonte {r.get('source')!r}, que não está em sources",
|
|
382
|
+
file=sys.stderr)
|
|
383
|
+
return 2
|
|
384
|
+
if r.get("imagem_de") and r.get("imagem_de") not in sources:
|
|
385
|
+
print(f"ERRO: o trecho {r.get('id')} tira a imagem de {r.get('imagem_de')!r}, que não está em sources",
|
|
386
|
+
file=sys.stderr)
|
|
387
|
+
return 2
|
|
388
|
+
for group in switch_groups(edl["ranges"]):
|
|
389
|
+
rs = [edl["ranges"][i] for i in group]
|
|
390
|
+
src = sources[rs[0]["source"]]
|
|
391
|
+
for k, r in enumerate(rs):
|
|
392
|
+
sides = (("começo", r["start"], k > 0), ("fim", r["end"], k < len(rs) - 1))
|
|
393
|
+
for lado, t, troca in sides:
|
|
394
|
+
if troca:
|
|
395
|
+
if lado == "começo" and src.at(t) >= src.th:
|
|
396
|
+
trocas.append(f"{rs[k - 1].get('id')} -> {r.get('id')} em {float(t):.2f} s: a imagem passa "
|
|
397
|
+
f"de {picture_of(rs[k - 1])} para {picture_of(r)} no meio da fala, a voz segue")
|
|
398
|
+
continue
|
|
399
|
+
if src.at(t) >= src.th + 6:
|
|
400
|
+
avisos.append((r.get("id"), lado, f"{r.get('id')} ({r.get('quote', '')[:40]!r}): o {lado} em "
|
|
401
|
+
f"{t:.2f} s está em cima de fala; pode cortar palavra"))
|
|
402
|
+
r = rs[0]
|
|
403
|
+
if not r.get("dry", True):
|
|
404
|
+
new.append(copy.deepcopy(r))
|
|
405
|
+
inteiros.append(f"{r.get('id')} (\"dry\": false): {float(r['start']):.2f} a {float(r['end']):.2f} s, "
|
|
406
|
+
"bordas como escritas")
|
|
407
|
+
continue
|
|
408
|
+
limit = next_in_source(edl["ranges"], group[-1])
|
|
409
|
+
if len(rs) == 1:
|
|
410
|
+
new.extend(split_range(r, src, limit, vales))
|
|
411
|
+
continue
|
|
412
|
+
merged = dict(copy.deepcopy(r), end=rs[-1]["end"])
|
|
413
|
+
new.extend(split_at_switches(split_range(merged, src, limit, vales), rs))
|
|
414
|
+
|
|
415
|
+
no_vale = {n.get("id") for n in new if n.get("fim_no_vale")} # the valley note says it better
|
|
416
|
+
avisos = [txt for rid, lado, txt in avisos if not (lado == "fim" and rid in no_vale)]
|
|
417
|
+
for i, n in enumerate(new):
|
|
418
|
+
n["id"] = f"r{i + 1:03d}"
|
|
419
|
+
edl = dict(edl, ranges=new)
|
|
420
|
+
save_json(saida, edl)
|
|
421
|
+
total = sum(n["end"] - n["start"] for n in new)
|
|
422
|
+
print(f"{len(new)} trechos, {total:.2f} s → {saida}")
|
|
423
|
+
for t in inteiros:
|
|
424
|
+
print(" inteiro: " + t)
|
|
425
|
+
for t in trocas:
|
|
426
|
+
print(" troca de câmera: " + t)
|
|
427
|
+
for a in avisos + vales:
|
|
428
|
+
print(" aviso: " + a)
|
|
429
|
+
return 0
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
if __name__ == "__main__":
|
|
433
|
+
raise SystemExit(main())
|