eubottura commited on
Commit
6430b1d
·
verified ·
1 Parent(s): c0403be

E assim, consegue respeitar essas regras? Deve ser para qualquer tipo de idioma, por favor, só analisa precisamente linha por linha

Browse files

# scope.md — SRT + Áudio CapCut‑ready (precisão máxima)

> Projeto: **Recortar e Transcrever** — alinhamento fino de SRT (CapCut), áudio “atropelado” natural com **gating por dBFS**, e **divisão de roteiro** com regras editoriais rígidas.
> Ambiente do cliente: macOS (path com acentos), execução via `python3 main.py`.

---

## 1) Objetivo e Resultado

**Objetivo**: Entregar pipeline **determinístico e robusto** para gerar **SRT perfeito para CapCut** (1 linha = 1 bloco) e **áudio natural** (atropelo só quando correto), evitando cortar consoantes iniciais e impedindo que **palavras curtas (2–3 letras)** fiquem **no final** dos blocos de legenda.

**Resultado**:
- SRT calibrado (advance positivo) + colagem sem buracos.
- Áudio com **overlap dB-aware** (não atropela início “alto” e reduz overlap em transições “quentes”).
- Split de texto **CapCut-ready** (11 caracteres **sem espaços**, “E/QUE” no início, anti‑corte de 2–3 letras).
- Correções estruturais: remoção de duplicatas, conserto de indentação/escopo e `SyntaxError`s, padrão de calibração atualizado.

---

## 2) Escopo entregue (arquivo por arquivo)

### 2.1 `main.py`
- **Calibração padrão atualizada**:
`--advance_ms` **70** (positivo) → “adiantar global” para cravar no CapCut (antes havia default negativo e/ou help duplicado).
- **Log e split**:
Mensagem deixa claro: **11 chars SEM espaços**, “**E/QUE**” começam bloco, e **nenhum bloco termina com palavra 2–3 letras**.
- **Fluxo** (inalterado conceitualmente, revisado):
1) Gera/seleciona áudio → 2) Pré‑processa roteiro → 3) Split rígido → 4) Edição manual opcional → 5) Transcrição (palavra a palavra) + duração → 6) Alinhamento sequencial por âncoras (fim/início) → 7) **Calibração** (advance/preroll/postroll/quantize) → 8) **Colagem** (join_gap=0) → 9) Guard‑rails e validação → 10) Export SRT.

### 2.2 `modules/joiner.py`
- **Novas funções**:
- `first_join_timing(...)`: **se o começo do primeiro chunk está “alto”** (pico > −12 dBFS na cabeça), **não** faz overlap inicial (usa gap curto); senão mantém overlap forçado do primeiro encaixe.
- `calculate_smart_timing_dbaware(...)`: mede **pico no fim do chunk anterior** e **início do atual**; se “alto” (ex.: > −8 dBFS), **reduz** overlap (≈30% do base) ou **zera** com pequeno gap, conforme severidade. Se “normal”, delega à sua `calculate_smart_timing(...)` (pausas/respirações).
- **Loop de montagem revisado**:
- `i == 1` → usa `first_join_timing(...)`.
- demais → usa `calculate_smart_timing_dbaware(...)`.
- Transição aplicada dentro do loop (`apply_natural_transition`), diagnóstico preservado.
- **Loudness**: normalização LUFS/true-peak final **inalterada**; somente melhorados os critérios de “quando” sobrepor.

### 2.3 `modules/text_utils.py`
- **Pré‑processamento**: vírgulas viram **“!” colada** à palavra anterior (sem espaço antes da pontuação), normalização de espaços.
- **Split CapCut‑ready**:
- **11 caracteres SEM espaços** por bloco (exceção: bloco de **uma** palavra pode exceder).
- **“E/QUE”** (qualquer caixa) **sempre** começam bloco quando ocorrem no meio.
- **Anti‑corte**: **nenhum bloco termina** com **tabu** nem com **palavra de 2–3 letras** → a palavra curta é **movida para o início do próximo bloco**.
- Fusão final se a última linha ficar fraca **e couber** na anterior (sem quebrar o limite).
- **Correções estruturais**: funções auxiliares (`core`, `is_strong`, etc.) **dentro** do escopo da função; um único `return out` (sem sobras), removendo a causa do `IndentationError` e `return outside function`.

### 2.4 `modules/config.py`
- **Coerência**: `MAX_BLOCK_LEN = 11` (para refletir o limite real adotado no split).

### 2.5 Demais módulos (incluídos no pacote)
- `modules/audio_utils.py`: duração (pydub) e transcrição `faster-whisper` com **word timestamps** + smoothing/fallback por caracteres.
- `modules/srt_utils.py`: **alinhamento sequencial por âncoras** (primeira/última forte) com bônus por “última sílaba”, **calibração CapCut** (advance/preroll/postroll/quantize), **colar_blocos** e **validar_cobertura_temporal**.
- `modules/silence.py`, `modules/conversion.py`, `modules/utils.py`, `modules/vad.py`: utilitários preservados.

> Observação: mantivemos o pipeline original e **fortalecemos** somente onde havia risco concreto de erro/artefato perceptível no CapCut ou no áudio.

---

## 3) Bugs/erros reais encontrados e correções

1) **`main.py` help duplicado / default negativo**
- Sintoma: linha com `help` repetido após `")"`; e/ou `default=-40` incoerente com recomendação “(60–90)”.
- **Correção**: help limpo e `default=70`.

2) **`joiner.py` — `SyntaxError` (`else:` órfão)**
- Causa: patch parcial deixou `else:` sem `if` correspondente na região do loop de montagem.
- **Correção**: reescrevemos **o loop completo**, substituindo `calculate_smart_timing(...)` por `..._dbaware(...)` e `first_join_timing(...)` no primeiro encaixe.

3) **`text_utils.py` — `IndentationError` / `return outside function`**
- Causa: duplicação de blocos e `return out` fora do escopo da função.
- **Correção**: função **unificada** e **fechada**; auxiliares indentadas dentro; remoção de trechos remanescentes; regra explícita de 2–3 letras no fim.

---

## 4) Regras editoriais do split (linha por linha)

- **Sempre** corta em `! ? .` (a próxima palavra começa o bloco seguinte).
- **“E/QUE”** no meio **iniciam** bloco (ficam no começo da linha seguinte).
- **Limite**: **11 caracteres sem espaços** (contagem do texto “colado”).
- **Proibido terminar** com **tabu** (preps/conjunções/pronomes listados) **ou** com **palavra de 2–3 letras**.
- Se acontecer, **move** a palavra curta para o **início do próximo bloco** (mantendo continuidade).
- **Exceção**: bloco de **uma** palavra pode exceder 11.
- **Pré-processo**: vírgulas viram **“!”** colada (não cria novas frases além do que você definiu por vírgulas).

> Resultado: linhas curtas, respiráveis, sem “pêndulos” gramaticais no fim e com excelente encaixe visual no CapCut.

---

## 5) Gating por dBFS no áudio (anti‑atropelo)

- **Primeiro encaixe**: mede **pico no início do primeiro chunk**; se “alto” (ex.: > −12 dBFS nos primeiros ~150 ms), **não** faz overlap → usa **gap** curto (≈18 ms).
- **Demais transições**: mede **pico no fim do anterior** e **início do atual**; se “alto” (ex.: > −8 dBFS), **reduz** overlap (≈30% do base) ou **zera** com pequeno gap, conforme severidade.
- **Caso normal**: usa sua `calculate_smart_timing(...)` (pausas naturais/respirações).

> Intenção: **preservar a consoante inicial** das frases e evitar “bombeamento” nas colagens quando a transição é quente.

---

## 6) Execução — passo a passo

### 6.1 Pré‑requisitos
- **FFmpeg** instalado (Homebrew: `brew install ffmpeg`).
- Python libs (exemplos):
```bash
pip install pydub numpy faster-whisper pyloudnorm webrtcvad
```

### 6.2 Rodar
```bash
python3 main.py --publico M --advance_ms 70 --preroll_ms 40 --postroll_ms 25 --min_dur_ms 190 --join_gap_ms 0
```
- Se quiser usar áudio externo (pasta `audios/`): `--usar_audio_externo`.
- Editor intermediário: remova `--sem_editor` para editar manualmente o `_formatado.txt`.

### 6.3 Saídas
- **Áudio** final: `output/podcast_final.wav`
- **SRT** final: `output/capcut_*.srt`
- **Roteiro formatado** (1 bloco/linha): `roteiro_formatado.txt`

---

## 7) Critérios de aceitação (QA)

- **SRT**: sem gaps/overlaps (após colar), cobertura **0..fim** do áudio, **1:1** com os blocos do `_formatado.txt`.
- **Split**: nenhum bloco termina com **tabu** nem **2–3 letras**; **“E/QUE”** sempre no começo de bloco; **≤ 11** sem espaços (salvo linha unitária).
- **Áudio**: primeiro encaixe sem atropelo se head alto; transições ruidosas com overlap reduzido/zerado; loudness final normalizado; true‑peak respeitado.
- **Calibração**: com `advance_ms=70`, blocos **cravam no CapCut** (ajuste fino permitido).

---

## 8) Testes rápidos (manuais)

1) **Roteiro com vírgulas** → ver se viram `!` e se os cortes batem em pontuação forte.
2) **Blocos que terminariam com “de”, “em”, “pra”, “pro”** → confirmar que migram para o início do próximo bloco.
3) **Áudio com ataque alto** no primeiro arquivo → verificar **sem overlap** inicial.
4) **Transição quente** (pico alto tail→head) → verificar **overlap reduzido/zero** e sem “engolir” consoantes.

---

## 9) Riscos & Mitigações

- **Falas muito longas** (sem pontuação): divisão pode concentrar cortes por limite de 11 — mitigar ajustando texto/respiração.
- **Transcrição pobre** (sem word timestamps confiáveis): cai no **fallback** proporcional por caracteres; manter qualidade do áudio fonte.
- **Ambiente FFmpeg** ausente: pydub falha — instale ffmpeg.

---

## 10) Próximos incrementos (opcionais)

- **CTA especial** (“clique/clica em saiba mais!” sempre bloco único com `!`).
- **Validador CLI** de blocos (report dos problemas antes do alinhamento).
- **Snap‑to‑quiet** opcional no início de cada bloco (suporte já previsto em `srt_utils.py`).

---

## 11) Mudanças resumidas (tipo changelog)

- `main.py`: `advance_ms=70`, log claro do split, fluxo mantido.
- `joiner.py`: +`first_join_timing`, +`calculate_smart_timing_dbaware`, loop de montagem revisado.
- `text_utils.py`: split reescrito (11 sem espaços, E/QUE head, anti‑corte 2–3 letras), pré‑processo convertendo vírgulas para `!`, correção de escopo/indentação.
- `config.py`: `MAX_BLOCK_LEN=11`.
- Outros: mantidos.

---

## 12) Como validar localmente (checklist)

- [ ] `python3 main.py` executa sem exceptions.
- [ ] Gera `roteiro_formatado.txt` coerente com as

Files changed (3) hide show
  1. index.html +7 -6
  2. script.js +14 -22
  3. style.css +18 -2
index.html CHANGED
@@ -78,17 +78,18 @@
78
  <div id="outputContainer" class="bg-gray-50 border border-gray-200 rounded-lg p-4 h-64 overflow-y-auto">
79
  <p class="text-gray-500 text-center py-20">Formatted text will appear here</p>
80
  </div>
81
-
82
  <div class="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
83
  <h3 class="font-bold text-blue-800 mb-2">Formatting Guidelines</h3>
84
  <ul class="text-sm text-blue-700 list-disc pl-5 space-y-1">
85
- <li>Maximum 14 characters per block (excluding spaces)</li>
86
  <li>Breaks at !, ?, . punctuation marks</li>
87
- <li>No weak endings (articles, prepositions, short words)</li>
88
- <li>Connectives start new blocks</li>
 
 
89
  </ul>
90
  </div>
91
- </div>
92
  </div>
93
 
94
  <!-- Features Section -->
@@ -121,7 +122,7 @@
121
  </div>
122
  </div>
123
  </div>
124
- </div>
125
  </main>
126
 
127
  <custom-footer></custom-footer>
 
78
  <div id="outputContainer" class="bg-gray-50 border border-gray-200 rounded-lg p-4 h-64 overflow-y-auto">
79
  <p class="text-gray-500 text-center py-20">Formatted text will appear here</p>
80
  </div>
 
81
  <div class="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
82
  <h3 class="font-bold text-blue-800 mb-2">Formatting Guidelines</h3>
83
  <ul class="text-sm text-blue-700 list-disc pl-5 space-y-1">
84
+ <li>Maximum 11 characters per block (excluding spaces)</li>
85
  <li>Breaks at !, ?, . punctuation marks</li>
86
+ <li>Commas converted to ! without space</li>
87
+ <li>No weak endings (articles, prepositions, 2-3 letter words)</li>
88
+ <li>Connectives (E/QUE) start new blocks</li>
89
+ <li>Single word blocks can exceed 11 characters</li>
90
  </ul>
91
  </div>
92
+ </div>
93
  </div>
94
 
95
  <!-- Features Section -->
 
122
  </div>
123
  </div>
124
  </div>
125
+ </div>
126
  </main>
127
 
128
  <custom-footer></custom-footer>
script.js CHANGED
@@ -8,15 +8,13 @@ document.addEventListener('DOMContentLoaded', function() {
8
  const clearBtn = document.getElementById('clearBtn');
9
  const languageSelect = document.getElementById('languageSelect');
10
  const uppercaseCheck = document.getElementById('uppercaseCheck');
11
-
12
  // Tabu words for each language
13
  const tabuWords = {
14
  PT: ['o', 'a', 'os', 'as', 'um', 'uma', 'me', 'te', 'se', 'lhe', 'nos', 'de', 'do', 'da', 'em', 'no', 'na', 'por', 'para', 'com', 'ao', 'e', 'ou', 'mas', 'que', 'como', 'porque', 'quando', 'então', 'só', 'já', 'também', 'tipo', 'nosso', 'nossa'],
15
  EN: ['a', 'an', 'the', 'of', 'in', 'on', 'at', 'to', 'for', 'with', 'from', 'by', 'and', 'or', 'but'],
16
  ES: ['el', 'la', 'los', 'las', 'un', 'una', 'de', 'en', 'a', 'por', 'para', 'con', 'y', 'o', 'pero', 'que']
17
  };
18
-
19
- // Format text function
20
  function formatText() {
21
  const text = inputText.value.trim();
22
  if (!text) {
@@ -34,16 +32,14 @@ document.addEventListener('DOMContentLoaded', function() {
34
  outputContainer.innerHTML = `<p class="text-red-500 text-center py-20">Error formatting text: ${error.message}</p>`;
35
  }
36
  }
37
-
38
  // Text processing function
39
  function processText(text, language, uppercaseMode) {
40
- // Normalize punctuation
41
- let normalized = text.replace(/!+/g, '!').replace(/\?+/g, '?');
42
 
43
  // Split by strong punctuation
44
  const sentences = normalized.split(/([.!?]+)/).filter(s => s.trim() !== '');
45
-
46
- let blocks = [];
47
 
48
  for (let i = 0; i < sentences.length; i += 2) {
49
  const sentence = sentences[i];
@@ -61,7 +57,6 @@ document.addEventListener('DOMContentLoaded', function() {
61
 
62
  return blocks.join('\n');
63
  }
64
-
65
  // Process individual sentence
66
  function processSentence(sentence, language) {
67
  const words = sentence.trim().split(/\s+/);
@@ -72,28 +67,28 @@ document.addEventListener('DOMContentLoaded', function() {
72
  const word = words[i];
73
 
74
  // Handle connectives that must start a new block
75
- if (isConnective(word, language) && currentBlock) {
76
  if (currentBlock) blocks.push(currentBlock.trim());
77
  currentBlock = word;
78
  continue;
79
  }
80
 
81
- // Check if adding this word would exceed limit
82
  const testBlock = currentBlock ? `${currentBlock} ${word}` : word;
83
  const charCount = testBlock.replace(/\s/g, '').length;
84
 
85
  // If single word exceeds limit, it's an exception
86
- if (!currentBlock && charCount > 14) {
87
  blocks.push(word);
88
  currentBlock = '';
89
  continue;
90
  }
91
 
92
  // If adding word exceeds limit, finalize current block
93
- if (currentBlock && charCount > 14) {
94
- // Check if last word is tabu or short
95
  const lastWord = currentBlock.split(' ').pop();
96
- if (isTabu(lastWord, language) || lastWord.length <= 4) {
97
  // Remove last word and add to next block
98
  const parts = currentBlock.split(' ');
99
  const newBlock = parts.slice(0, -1).join(' ');
@@ -110,9 +105,9 @@ document.addEventListener('DOMContentLoaded', function() {
110
 
111
  // Add remaining block
112
  if (currentBlock) {
113
- // Final check for tabu ending
114
  const lastWord = currentBlock.split(' ').pop();
115
- if (isTabu(lastWord, language) || lastWord.length <= 4) {
116
  const parts = currentBlock.split(' ');
117
  const newBlock = parts.slice(0, -1).join(' ');
118
  if (newBlock) blocks.push(newBlock);
@@ -124,8 +119,7 @@ document.addEventListener('DOMContentLoaded', function() {
124
 
125
  return blocks;
126
  }
127
-
128
- // Check if word is a connective
129
  function isConnective(word, language) {
130
  const connectives = {
131
  PT: ['E', 'É', 'QUE'],
@@ -135,14 +129,12 @@ document.addEventListener('DOMContentLoaded', function() {
135
 
136
  return connectives[language].includes(word.toUpperCase());
137
  }
138
-
139
  // Check if word is tabu
140
  function isTabu(word, language) {
141
  if (!word) return false;
142
  return tabuWords[language].includes(word.toLowerCase());
143
  }
144
-
145
- // Copy to clipboard
146
  function copyToClipboard() {
147
  const text = outputContainer.innerText;
148
  if (!text) return;
 
8
  const clearBtn = document.getElementById('clearBtn');
9
  const languageSelect = document.getElementById('languageSelect');
10
  const uppercaseCheck = document.getElementById('uppercaseCheck');
 
11
  // Tabu words for each language
12
  const tabuWords = {
13
  PT: ['o', 'a', 'os', 'as', 'um', 'uma', 'me', 'te', 'se', 'lhe', 'nos', 'de', 'do', 'da', 'em', 'no', 'na', 'por', 'para', 'com', 'ao', 'e', 'ou', 'mas', 'que', 'como', 'porque', 'quando', 'então', 'só', 'já', 'também', 'tipo', 'nosso', 'nossa'],
14
  EN: ['a', 'an', 'the', 'of', 'in', 'on', 'at', 'to', 'for', 'with', 'from', 'by', 'and', 'or', 'but'],
15
  ES: ['el', 'la', 'los', 'las', 'un', 'una', 'de', 'en', 'a', 'por', 'para', 'con', 'y', 'o', 'pero', 'que']
16
  };
17
+ // Format text function
 
18
  function formatText() {
19
  const text = inputText.value.trim();
20
  if (!text) {
 
32
  outputContainer.innerHTML = `<p class="text-red-500 text-center py-20">Error formatting text: ${error.message}</p>`;
33
  }
34
  }
 
35
  // Text processing function
36
  function processText(text, language, uppercaseMode) {
37
+ // Normalize punctuation - convert commas to exclamation marks without space
38
+ let normalized = text.replace(/,/g, '!').replace(/!+/g, '!').replace(/\?+/g, '?').replace(/\s*!\s*/g, '!');
39
 
40
  // Split by strong punctuation
41
  const sentences = normalized.split(/([.!?]+)/).filter(s => s.trim() !== '');
42
+ let blocks = [];
 
43
 
44
  for (let i = 0; i < sentences.length; i += 2) {
45
  const sentence = sentences[i];
 
57
 
58
  return blocks.join('\n');
59
  }
 
60
  // Process individual sentence
61
  function processSentence(sentence, language) {
62
  const words = sentence.trim().split(/\s+/);
 
67
  const word = words[i];
68
 
69
  // Handle connectives that must start a new block
70
+ if (isConnective(word, language)) {
71
  if (currentBlock) blocks.push(currentBlock.trim());
72
  currentBlock = word;
73
  continue;
74
  }
75
 
76
+ // Check if adding this word would exceed limit (11 chars without spaces)
77
  const testBlock = currentBlock ? `${currentBlock} ${word}` : word;
78
  const charCount = testBlock.replace(/\s/g, '').length;
79
 
80
  // If single word exceeds limit, it's an exception
81
+ if (!currentBlock && charCount > 11) {
82
  blocks.push(word);
83
  currentBlock = '';
84
  continue;
85
  }
86
 
87
  // If adding word exceeds limit, finalize current block
88
+ if (currentBlock && charCount > 11) {
89
+ // Check if last word is tabu or short (2-3 letters)
90
  const lastWord = currentBlock.split(' ').pop();
91
+ if (isTabu(lastWord, language) || (lastWord.length >= 2 && lastWord.length <= 3)) {
92
  // Remove last word and add to next block
93
  const parts = currentBlock.split(' ');
94
  const newBlock = parts.slice(0, -1).join(' ');
 
105
 
106
  // Add remaining block
107
  if (currentBlock) {
108
+ // Final check for tabu ending or short words (2-3 letters)
109
  const lastWord = currentBlock.split(' ').pop();
110
+ if (isTabu(lastWord, language) || (lastWord.length >= 2 && lastWord.length <= 3)) {
111
  const parts = currentBlock.split(' ');
112
  const newBlock = parts.slice(0, -1).join(' ');
113
  if (newBlock) blocks.push(newBlock);
 
119
 
120
  return blocks;
121
  }
122
+ // Check if word is a connective
 
123
  function isConnective(word, language) {
124
  const connectives = {
125
  PT: ['E', 'É', 'QUE'],
 
129
 
130
  return connectives[language].includes(word.toUpperCase());
131
  }
 
132
  // Check if word is tabu
133
  function isTabu(word, language) {
134
  if (!word) return false;
135
  return tabuWords[language].includes(word.toLowerCase());
136
  }
137
+ // Copy to clipboard
 
138
  function copyToClipboard() {
139
  const text = outputContainer.innerText;
140
  if (!text) return;
style.css CHANGED
@@ -143,7 +143,6 @@ body {
143
  from { opacity: 0; transform: translateY(20px); }
144
  to { opacity: 1; transform: translateY(0); }
145
  }
146
-
147
  /* Custom utility classes */
148
  .shadow-lg {
149
  box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
@@ -157,4 +156,21 @@ body {
157
  background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
158
  -webkit-background-clip: text;
159
  -webkit-text-fill-color: transparent;
160
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  from { opacity: 0; transform: translateY(20px); }
144
  to { opacity: 1; transform: translateY(0); }
145
  }
 
146
  /* Custom utility classes */
147
  .shadow-lg {
148
  box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
 
156
  background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
157
  -webkit-background-clip: text;
158
  -webkit-text-fill-color: transparent;
159
+ }
160
+
161
+ /* Update guidelines box */
162
+ .bg-blue-50 {
163
+ background-color: #eff6ff;
164
+ }
165
+
166
+ .border-blue-200 {
167
+ border-color: #bfdbfe;
168
+ }
169
+
170
+ .text-blue-800 {
171
+ color: #1e40af;
172
+ }
173
+
174
+ .text-blue-700 {
175
+ color: #2563eb;
176
+ }