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
- index.html +7 -6
- script.js +14 -22
- style.css +18 -2
|
@@ -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
|
| 86 |
<li>Breaks at !, ?, . punctuation marks</li>
|
| 87 |
-
<li>
|
| 88 |
-
<li>
|
|
|
|
|
|
|
| 89 |
</ul>
|
| 90 |
</div>
|
| 91 |
-
|
| 92 |
</div>
|
| 93 |
|
| 94 |
<!-- Features Section -->
|
|
@@ -121,7 +122,7 @@
|
|
| 121 |
</div>
|
| 122 |
</div>
|
| 123 |
</div>
|
| 124 |
-
|
| 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>
|
|
@@ -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)
|
| 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 >
|
| 87 |
blocks.push(word);
|
| 88 |
currentBlock = '';
|
| 89 |
continue;
|
| 90 |
}
|
| 91 |
|
| 92 |
// If adding word exceeds limit, finalize current block
|
| 93 |
-
if (currentBlock && charCount >
|
| 94 |
-
// Check if last word is tabu or short
|
| 95 |
const lastWord = currentBlock.split(' ').pop();
|
| 96 |
-
if (isTabu(lastWord, language) || lastWord.length <=
|
| 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 <=
|
| 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;
|
|
@@ -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 |
+
}
|