wizz-method 1.12.0 → 1.13.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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "wizz-method",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.13.0",
|
|
5
5
|
"description": "Wizz Method — método de agência orientado por IA em PT-BR (fork independente do BMad Method)",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agile",
|
package/skills-registry.yaml
CHANGED
|
@@ -198,6 +198,12 @@ areas:
|
|
|
198
198
|
# sistema específica (é um app desktop autocontido que expõe um
|
|
199
199
|
# endpoint MCP local) — nada de concreto pra checar sem inventar um
|
|
200
200
|
# requisito que o registry não afirma.
|
|
201
|
+
- id: ctc-align
|
|
202
|
+
when: "Timestamps palavra a palavra CONFIÁVEIS de narração TTS (texto conhecido): sincronizar legenda, corte de cena, animação e elemento gráfico com a fala. Fonte única de timing em vídeo — substitui timestamps do Whisper e estimativa manual. Skill da lib: ctc-align (instalação + helper align.py + regras de âncora monotônica e duração mínima). Roda em CPU, sem CUDA."
|
|
203
|
+
platform: [darwin, linux]
|
|
204
|
+
check: "test -x ./tools/ctc-align/.venv/bin/ctc-forced-aligner"
|
|
205
|
+
install: 'mkdir -p tools/ctc-align && uv venv --python 3.12 tools/ctc-align/.venv && uv pip install --python tools/ctc-align/.venv/bin/python "git+https://github.com/MahmoudAshraf97/ctc-forced-aligner.git"'
|
|
206
|
+
verify: "command -v ffmpeg >/dev/null 2>&1 && command -v uv >/dev/null 2>&1"
|
|
201
207
|
references:
|
|
202
208
|
- "references/premium-agency-rules.md (regras $150k-tier, importadas por impeccable/huashu no modo high-end)"
|
|
203
209
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ctc-align
|
|
3
|
+
description: Timestamps palavra a palavra confiáveis para narração TTS via alinhamento forçado (ctc-forced-aligner + âncora nos silêncios reais). Usar SEMPRE antes de definir tempo de legenda, corte de cena, animação ou elemento sincronizado com fala em qualquer vídeo (HyperFrames ou outro editor). Substitui o Whisper como fonte de timing quando o texto do roteiro é conhecido. Gatilhos - sincronizar legenda, timing de cena, alinhar áudio e texto, palavra a palavra, forced alignment, legenda karaokê, corrigir sync de vídeo.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ctc-align — timing de fala como fonte única de verdade
|
|
7
|
+
|
|
8
|
+
## Quando usar
|
|
9
|
+
|
|
10
|
+
Sempre que um vídeo tiver narração cujo TEXTO é conhecido (roteiro de TTS, VSL,
|
|
11
|
+
explainer) e for preciso decidir QUANDO algo aparece: legenda, corte de cena,
|
|
12
|
+
animação, texto na tela.
|
|
13
|
+
|
|
14
|
+
**Regra do método: nunca estimar tempo à mão e nunca confiar no timestamp do Whisper.**
|
|
15
|
+
O Whisper adivinha as palavras e deriva (medido: até 6s); interpolação proporcional
|
|
16
|
+
entre silêncios desloca palavras para o bloco vizinho. O alinhamento CTC recebe o texto
|
|
17
|
+
exato e só procura onde cada palavra está: precisão de dezenas de ms.
|
|
18
|
+
|
|
19
|
+
## Instalação (uma vez por projeto)
|
|
20
|
+
|
|
21
|
+
mkdir -p tools/ctc-align && cd tools/ctc-align
|
|
22
|
+
uv venv --python 3.12 .venv
|
|
23
|
+
uv pip install --python .venv/bin/python "git+https://github.com/MahmoudAshraf97/ctc-forced-aligner.git"
|
|
24
|
+
|
|
25
|
+
Copiar o `scripts/align.py` desta skill para `tools/ctc-align/align.py`.
|
|
26
|
+
Roda em CPU (Mac ARM ok, sem CUDA). Primeiro uso baixa o modelo MMS (~1GB).
|
|
27
|
+
|
|
28
|
+
## Uso
|
|
29
|
+
|
|
30
|
+
1. Gerar um .txt com o roteiro EXATO que o TTS falou (texto corrido; se o TTS falou
|
|
31
|
+
diferente do escrito, usar o que foi FALADO).
|
|
32
|
+
2. Rodar:
|
|
33
|
+
|
|
34
|
+
tools/ctc-align/.venv/bin/python tools/ctc-align/align.py narracao.mp3 roteiro.txt transcript.json
|
|
35
|
+
|
|
36
|
+
Saída: JSON [{"text","start","end","score"}] por palavra, bordas já encostadas nos
|
|
37
|
+
silêncios reais (silencedetect embutido). Score perto de 0 = confiável; o helper avisa
|
|
38
|
+
palavras com score < -1.0 (conferir de ouvido).
|
|
39
|
+
|
|
40
|
+
## Como converter em tempos de cena/legenda
|
|
41
|
+
|
|
42
|
+
- Legenda: usar start/end da palavra direto.
|
|
43
|
+
- Corte de cena/elemento: âncora = start da PRIMEIRA palavra da frase que o elemento
|
|
44
|
+
ilustra. Buscar a palavra de forma MONOTÔNICA (cada âncora procurada a partir da
|
|
45
|
+
anterior), nunca busca global — palavra repetida ("10", "bónus") pega ocorrência errada.
|
|
46
|
+
- Duração mínima de cena: 1,2s; se a frase for mais curta, empurrar a próxima cena
|
|
47
|
+
dentro da própria frase dela.
|
|
48
|
+
- Nada aparece antes de ser mencionado; nada continua depois que o assunto mudou.
|
|
49
|
+
|
|
50
|
+
## QA obrigatório antes do render
|
|
51
|
+
|
|
52
|
+
Nenhuma palavra pode sobrepor o miolo de um silêncio real, e cada fim de frase do
|
|
53
|
+
roteiro deve terminar num silêncio. Medir isso, não o desvio de âncoras que o próprio
|
|
54
|
+
método criou. Áudio concatenado de blocos: alinhar bloco a bloco ou conferir fronteiras.
|
|
55
|
+
|
|
56
|
+
## Limitações honestas
|
|
57
|
+
|
|
58
|
+
- Precisa do texto exato; não transcreve. Áudio sem roteiro: transcrever com Whisper,
|
|
59
|
+
revisar o texto à mão e SÓ ENTÃO alinhar com este fluxo.
|
|
60
|
+
- Acentos exigem a flag --romanize (o helper já passa).
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""align.py — timestamps palavra a palavra CONFIÁVEIS para narração TTS.
|
|
3
|
+
|
|
4
|
+
Uso:
|
|
5
|
+
.venv/bin/python align.py <audio.mp3> <texto.txt> [saida.json]
|
|
6
|
+
|
|
7
|
+
1. Roda o ctc-forced-aligner (MMS/CTC) com o TEXTO EXATO do roteiro.
|
|
8
|
+
2. Encosta as bordas das palavras nos silêncios reais (ffmpeg silencedetect):
|
|
9
|
+
palavra que termina dentro de um silêncio termina onde o silêncio começa;
|
|
10
|
+
palavra que começa dentro de um silêncio começa onde o silêncio termina.
|
|
11
|
+
3. Grava JSON [{"text","start","end","score"}].
|
|
12
|
+
"""
|
|
13
|
+
import json, re, subprocess, sys, os
|
|
14
|
+
|
|
15
|
+
def silencios(audio, noise="-35dB", dur=0.30):
|
|
16
|
+
out = subprocess.run(["ffmpeg","-hide_banner","-i",audio,
|
|
17
|
+
"-af",f"silencedetect=noise={noise}:d={dur}","-f","null","-"],
|
|
18
|
+
capture_output=True, text=True).stderr
|
|
19
|
+
starts = [float(x) for x in re.findall(r"silence_start: ([\d.]+)", out)]
|
|
20
|
+
ends = [float(x) for x in re.findall(r"silence_end: ([\d.]+)", out)]
|
|
21
|
+
return list(zip(starts, ends))
|
|
22
|
+
|
|
23
|
+
def alinhar(audio, texto_path):
|
|
24
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
25
|
+
cli = os.path.join(here, ".venv", "bin", "ctc-forced-aligner")
|
|
26
|
+
r = subprocess.run([cli, "--audio_path", audio, "--text_path", texto_path,
|
|
27
|
+
"--language", "por", "--split_size", "word", "--romanize",
|
|
28
|
+
"--device", "cpu"], capture_output=True, text=True)
|
|
29
|
+
saida = os.path.splitext(audio)[0] + ".json"
|
|
30
|
+
if not os.path.exists(saida):
|
|
31
|
+
sys.exit(f"aligner falhou:\n{r.stderr[-1500:]}")
|
|
32
|
+
d = json.load(open(saida))
|
|
33
|
+
segs = d if isinstance(d, list) else d["segments"]
|
|
34
|
+
for ext in (".json", ".txt"):
|
|
35
|
+
p = os.path.splitext(audio)[0] + ext
|
|
36
|
+
if os.path.exists(p): os.remove(p)
|
|
37
|
+
return segs
|
|
38
|
+
|
|
39
|
+
def encostar(segs, sils):
|
|
40
|
+
novo = []
|
|
41
|
+
for w in segs:
|
|
42
|
+
s, e = w["start"], w["end"]
|
|
43
|
+
for ss, se in sils:
|
|
44
|
+
if ss <= s <= se: s = se
|
|
45
|
+
if ss <= e <= se: e = ss
|
|
46
|
+
if s < ss and e > se: e = ss
|
|
47
|
+
if e <= s: e = s + 0.05
|
|
48
|
+
novo.append({"text": w["text"], "start": round(s,3), "end": round(e,3),
|
|
49
|
+
"score": round(w.get("score",0),4)})
|
|
50
|
+
return novo
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
if len(sys.argv) < 3:
|
|
54
|
+
sys.exit(__doc__)
|
|
55
|
+
audio, texto = sys.argv[1], sys.argv[2]
|
|
56
|
+
destino = sys.argv[3] if len(sys.argv) > 3 else os.path.splitext(audio)[0] + ".alinhado.json"
|
|
57
|
+
segs = alinhar(audio, texto)
|
|
58
|
+
sils = silencios(audio)
|
|
59
|
+
novo = encostar(segs, sils)
|
|
60
|
+
json.dump(novo, open(destino,"w",encoding="utf-8"), ensure_ascii=False, indent=1)
|
|
61
|
+
ruins = [w for w in novo if w["score"] < -1.0]
|
|
62
|
+
print(f"{len(novo)} palavras -> {destino} | {len(sils)} silencios usados como ancora")
|
|
63
|
+
if ruins:
|
|
64
|
+
print(f"atencao: {len(ruins)} palavras com score baixo (conferir): " +
|
|
65
|
+
", ".join(f"{w['text']}@{w['start']:.1f}" for w in ruins[:8]))
|