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.12.0",
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",
@@ -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]))