eubottura commited on
Commit
d789176
·
verified ·
1 Parent(s): c3f25f6

Deve respeitar esses criterios, === PROJETO ESTRUTURADO PARA ANÁLISE ===

Browse files

Total de arquivos: 10
Data de geração: 03/02/2026, 16:38:28

--- CONTEÚDO DOS ARQUIVOS ---

Arquivo: modules/silence.py
"from pydub import AudioSegment
from modules.utils import ensure_directory_exists
from modules.conversion import load_audio

def detect_leading_silence_advanced(audio_segment, silence_threshold=-70.0, chunk_size=1):
"""
Detecta o silêncio no início do áudio.

:param audio_segment: AudioSegment a ser analisado
:param silence_threshold: Limiar em dBFS para considerar silêncio
:param chunk_size: Tamanho do chunk para análise em ms
:return: Duração do silêncio detectado em ms
"""
trim_ms = 0
while trim_ms < len(audio_segment):
chunk = audio_segment[trim_ms:trim_ms + chunk_size]
if chunk.dBFS > silence_threshold:
break
trim_ms += chunk_size
return trim_ms

def remove_silence(audio_segment, silence_thresh=-70, min_silence_len=120):
"""
Remove os silêncios do áudio recebido.

:param audio_segment: AudioSegment de entrada
:param silence_thresh: limiar de silêncio em dBFS
:param min_silence_len: duração mínima do silêncio em ms
:return: AudioSegment sem silêncios
"""
start_trim = detect_leading_silence_advanced(audio_segment, silence_thresh)
end_trim = detect_leading_silence_advanced(audio_segment.reverse(), silence_thresh)
trimmed_audio = audio_segment[start_trim:len(audio_segment) - end_trim]
print(f"[SILENCE] Início cortado: {start_trim} ms, Fim cortado: {end_trim} ms")
return trimmed_audio
"

Arquivo: modules/config.py
""""
config.py

Centraliza todas as constantes e parâmetros do sistema, garantindo:
- Imutabilidade das coleções críticas (frozenset).
- Configuração única de limites, tempos e modelo Whisper.
- Facilidade de ajustes sem tocar na lógica de negócio.
"""

from modules.utils import ensure_directory_exists
from modules.conversion import load_audio

# Preposições e conjunções que influenciam a divisão de blocos
PREPOSICOES_CONJUNCOES = frozenset({
"e", "ou", "mas", "por", "com", "para", "pra", "em", "de", "do", "da",
"dos", "das", "no", "na", "nos", "nas", "a", "ao", "aos", "às", "sob",
"sobre", "como"
})

# Alias legível para SHORT_WORDS
SHORT_WORDS = PREPOSICOES_CONJUNCOES

# Palavras que não podem terminar um bloco
SHORT_WORDS_NAO_FINAL = frozenset({"e", "o", "a"})

# Combinações válidas de short words que podem ficar juntas
COMBINACOES_SHORT_VALIDAS = frozenset({
("e", "que"), ("e", "para"), ("ou", "que"), ("que", "para")
})

# Verbos comuns que influenciam regras de corte de bloco
VERBOS_COMUNS = frozenset({
"vai", "faz", "fica", "chega", "compra", "leva", "olha", "vem", "assista",
"corre", "garanta", "pega", "toma", "prova", "muda", "entra", "mostra",
"sai", "pede", "segue", "testa", "curte", "usa", "ganha", "volta",
"deixa", "tenha", "recebe", "aproveita", "veja", "assine", "descubra"
})

# Parâmetros de divisão de tempo para SRT
TEMPO_MIN_BLOCO = 0.01 # segundos mínimos por bloco
ESPACO_MAXIMO = 0.2 # segundos máximos de lacuna entre blocos

# Tamanho máximo de bloco (contagem de caracteres sem pontuação)
MAX_BLOCK_LEN = 6

# Configurações do modelo Whisper
WHISPER_MODEL_NAME = "small" # modelo a ser carregado
WHISPER_DEVICE = "cpu" # dispositivo: "cpu" ou "cuda"
WHISPER_BEAM_SIZE = 5 # beam size para transcrição
"

Arquivo: modules/my_helpers.py
""""
my_helpers.py

Módulo de funções auxiliares:
- NAO_TERMINA: palavras que não podem ficar sozinhas no final de um bloco.
- smart_capitalize: capitaliza apenas a primeira letra de um texto.
- nome_aleatorio: gera nomes únicos com prefixo e extensão customizáveis.
- normaliza_pontuacao: unifica repetições de pontuações e espaços.
- open_editor: abre o editor padrão do sistema para revisão manual.
"""
from modules.utils import ensure_directory_exists
from modules.conversion import load_audio

import os
import re
import random
import string
import subprocess
import platform
import logging
from typing import Set

logger = logging.getLogger(__name__)

# Palavras que nunca ficam sozinhas no final de bloco
NAO_TERMINA = {
'não', 'só', 'sem', 'tá', 'e', 'a', 'têm', 'do', 'da', 'dos', 'das', 'de',
'no', 'na', 'nos', 'nas', 'um', 'uma', 'uns', 'umas', 'por', 'para', 'em',
'com', 'que', 'se', 'mas', 'ou', 'porque', 'como', 'quando', 'onde', 'ao', 'à', 'às'
}

def smart_capitalize(texto: str) -> str:
"""
Capitaliza só a primeira palavra do texto, respeitando nomes próprios já em maiúsculo.
"""
if not texto:
return texto
return texto[0].upper() + texto[1:] if len(texto) > 1 else texto.upper()

# Outras funções utilitárias podem ser colocadas aqui, se necessário.

# Exemplo de uso:
# texto = normaliza_pontuacao('oi!!! tudo bem?? sim.')
# print(texto) # oi! tudo bem? sim.

def nome_aleatorio(ext: str = "srt", prefix: str = "") -> str:
"""
Gera nome aleatório:
- ext: extensão de arquivo (ex: 'srt', 'txt').
- prefix: string prefixada ao início.
Raise:
TypeError: se ext ou prefix não forem str.
"""
if not isinstance(ext, str) or not isinstance(prefix, str):
raise TypeError("ext e prefix devem ser strings.")
letras = ''.join(random.choices(string.ascii_lowercase, k=8))
return f"{prefix}{letras}.{ext}"

def normaliza_pontuacao(texto: str) -> str:
"""
Normaliza múltiplas pontuações consecutivas e espaços:
- !?! vira !
- Espaços duplicados somem
- Remove espaços antes de pontuação
"""
texto = re.sub(r'!+', '!', texto)
texto = re.sub(r'\?+', '?', texto)
texto = re.sub(r'\.+', '.', texto)
texto = re.sub(r' +', ' ', texto)
texto = re.sub(r'\s+([!?.])', r'\1', texto)
return texto.strip()

def open_editor(path: str) -> None:
"""
Abre arquivo no editor padrão:
- macOS: 'open'
- Windows: os.startfile
- Linux: 'xdg-open'
Exceptions são logadas e instruem abertura manual.
"""
sistema = platform.system()
try:
if sistema == "Darwin":
subprocess.call(['open', path])
elif sistema == "Windows":
os.startfile(path) # type: ignore
else:
subprocess.call(['xdg-open', path])
except Exception as e:
logger.exception(f"Falha ao abrir editor para '{path}': {e}")
print(f"Abra manualmente: {path}")
"

Arquivo: modules/text_utils.py
"# -*- coding: utf-8 -*-
"""
text_utils.py — divisão de roteiro em blocos legíveis, sem inventar pontuação.

Objetivo: produzir linhas “respiráveis” para leitura/CapCut, mantendo a pontuação
do roteiro e evitando cortes feios (artigos/preps/fillers no fim de linha).

Regras fortes:
- NUNCA adicionar pontuação que não exista no texto original.
- Sempre quebrar em pontuação forte '!', '?', '.'.
- Um bloco NUNCA termina com palavra tabu nem com palavra curta (<=4).
- Teto de 14 caracteres (sem espaços) por bloco; se exceder, corta para o próximo.
Exceção: pode passar de 14 se a linha tiver apenas **1** palavra.

Heurísticas:
- Preço corrido: "POR <num>" + sequências "E <num>" (cada uma em linha), para se bater '!' no número.
- Materiais: "FEITA/FEITO/..." + "EM" (mesma linha), depois cada item em linha; "E <item>" inicia linha.
- Sujeito curto: "A/O/AS/OS <substantivo> [MARCA]".
- "E ..." inicia linha; "NÃO ..." em linha curta; "NUNCA" sozinho; "ISSO AQUI" junto.
- "QUE ..." inicia linha curta; "NA/NO/NAS/NOS/EM ..." inicia linha curta.
- "CLIQUE EM SAIBA MAIS" → "CLIQUE" / "EM SAIBA" / "MAIS".
"""

import os
import re
import logging
from typing import List, Set, Optional

# Você pode manter MAX_BLOCK_LEN, mas aqui vamos usar 14 como padrão forte:
try:
from modules.config import MAX_BLOCK_LEN
except Exception:
MAX_BLOCK_LEN = 14

from modules.my_helpers import normaliza_pontuacao, NAO_TERMINA, smart_capitalize

logger = logging.getLogger(__name__)

# --------------------
# Vocabulários
# --------------------

TABU_FINAL: Set[str] = set(NAO_TERMINA) | {
'de', 'é', 'do','da','dos','das','em','no','na','nos','nas',
'por','pelo','pela','pelos','pelas',
'para','pra','pro','pros','pras',
'com','sem','ao','aos','à','às',
'num','numa','nuns','numas','dum','duma','duns','dumas',
'sobre','até','e','ou','mas','que','se','como','porque','porquê',
'quando','enquanto','então','o','a','os','as','um','uma','uns','umas',
'me','te','se','não','só','já','também','tanto','aí','né','tá','tipo',
'qualquer', 'nossa', 'nosso', 'nossos', 'nossas'
}

MATERIAL_VERBS = {
'feita','feito','feitas','feitos',
'confeccionada','confeccionado','confeccionadas','confeccionados',
'produzida','produzido','produzidas','produzidos',
'fabricada','fabricado','fabricadas','fabricados',
}

NUMBER_WORDS = {
'zero','um','uma','dois','duas','três','tres','quatro','cinco','seis','sete','oito','nove',
'dez','onze','doze','treze','quatorze','catorze','quinze','dezesseis','dezessete','dezoito','dezenove',
'vinte','trinta','quarenta','cinquenta','sessenta','setenta','oitenta','noventa',
'cem','cento','duzentos','trezentos','quatrocentos','quinhentos','seiscentos',
'setecentos','oitocentos','novecentos','mil','milhão','milhoes','bilhão','bilhoes'
}

# Pontas GENÉRICAS que costumam sinalizar mudança de tópico após listas de materiais
TOPIC_SHIFT_HEADS = {
# artigos/determinantes
'a','o','as','os','um','uma','uns','umas',
# pronomes demonstrativos/comuns
'isso','isto','aquilo','esse','essa','esses','essas','este','esta','estes','estas',
'aquele','aquela','aqueles','aquelas',
# verbos muito comuns (ser/ter/ir)
'é','e','são','foi','será','tem','vai','vamos',
# conectivos gerais
'mas','porém','porem','entretanto','então','entao','agora',
# ganchos de call-to-action/comércio genéricos
'clique','aproveite','promoção','promocao','oferta','atenção','atencao'
}

# --------------------
# Helpers
# --------------------

def _strip_punct(token: str) -> str:
"""Remove pont

Files changed (8) hide show
  1. README.md +8 -5
  2. components/navbar.js +67 -0
  3. components/sidebar.js +83 -0
  4. components/stat-card.js +55 -0
  5. components/upload-zone.js +112 -0
  6. index.html +316 -19
  7. script.js +545 -0
  8. style.css +167 -19
README.md CHANGED
@@ -1,10 +1,13 @@
1
  ---
2
- title: Capcutsync Pro
3
- emoji: 🌍
4
- colorFrom: green
5
- colorTo: yellow
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
1
  ---
2
+ title: CapCutSync Pro 🎬
3
+ colorFrom: red
4
+ colorTo: gray
5
+ emoji: 🐳
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite-v3
10
  ---
11
 
12
+ # Welcome to your new DeepSite project!
13
+ This project was created with [DeepSite](https://huggingface.co/deepsite).
components/navbar.js ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class AppNavbar extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ }
5
+
6
+ connectedCallback() {
7
+ this.innerHTML = `
8
+ <nav class="fixed top-0 left-0 right-0 z-50 bg-slate-900/80 backdrop-blur-md border-b border-slate-800 h-16">
9
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-full">
10
+ <div class="flex items-center justify-between h-full">
11
+ <!-- Logo -->
12
+ <div class="flex items-center gap-3">
13
+ <div class="w-8 h-8 bg-gradient-to-br from-primary-500 to-secondary-500 rounded-lg flex items-center justify-center shadow-lg shadow-primary-500/20">
14
+ <i data-feather="mic" class="w-5 h-5 text-white"></i>
15
+ </div>
16
+ <div class="flex flex-col">
17
+ <span class="text-lg font-bold bg-gradient-to-r from-slate-100 to-slate-300 bg-clip-text text-transparent">
18
+ CapCutSync Pro
19
+ </span>
20
+ <span class="text-[10px] text-slate-500 uppercase tracking-wider">Pipeline de Áudio</span>
21
+ </div>
22
+ </div>
23
+
24
+ <!-- Desktop Menu -->
25
+ <div class="hidden md:flex items-center gap-6">
26
+ <a href="#" class="text-sm text-slate-300 hover:text-white transition-colors flex items-center gap-2">
27
+ <i data-feather="book-open" class="w-4 h-4"></i>
28
+ Documentação
29
+ </a>
30
+ <a href="#" class="text-sm text-slate-300 hover:text-white transition-colors flex items-center gap-2">
31
+ <i data-feather="github" class="w-4 h-4"></i>
32
+ GitHub
33
+ </a>
34
+ <div class="h-4 w-px bg-slate-700"></div>
35
+ <button class="flex items-center gap-2 px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 rounded-lg text-sm font-medium transition-all border border-slate-700 hover:border-slate-600">
36
+ <i data-feather="settings" class="w-4 h-4"></i>
37
+ Configurações
38
+ </button>
39
+ </div>
40
+
41
+ <!-- Mobile Menu Button -->
42
+ <button id="mobile-menu-btn" class="md:hidden p-2 text-slate-400 hover:text-white">
43
+ <i data-feather="menu" class="w-6 h-6"></i>
44
+ </button>
45
+ </div>
46
+ </div>
47
+
48
+ <!-- Mobile Menu -->
49
+ <div id="mobile-menu" class="hidden md:hidden bg-slate-900 border-t border-slate-800">
50
+ <div class="px-4 py-3 space-y-2">
51
+ <a href="#" class="block px-3 py-2 text-slate-300 hover:bg-slate-800 rounded-lg text-sm">Documentação</a>
52
+ <a href="#" class="block px-3 py-2 text-slate-300 hover:bg-slate-800 rounded-lg text-sm">GitHub</a>
53
+ <a href="#" class="block px-3 py-2 text-slate-300 hover:bg-slate-800 rounded-lg text-sm">Configurações</a>
54
+ </div>
55
+ </div>
56
+ </nav>
57
+ `;
58
+
59
+ // Mobile menu toggle
60
+ this.querySelector('#mobile-menu-btn').addEventListener('click', () => {
61
+ const menu = this.querySelector('#mobile-menu');
62
+ menu.classList.toggle('hidden');
63
+ });
64
+ }
65
+ }
66
+
67
+ customElements.define('app-navbar', AppNavbar);
components/sidebar.js ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class AppSidebar extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ }
5
+
6
+ connectedCallback() {
7
+ this.innerHTML = `
8
+ <aside class="w-64 bg-slate-900 border-r border-slate-800 h-full overflow-y-auto">
9
+ <div class="p-4 space-y-6">
10
+
11
+ <!-- Pipeline Steps -->
12
+ <div>
13
+ <h3 class="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3 px-2">Etapas do Pipeline</h3>
14
+ <nav class="space-y-1">
15
+ <a href="#" class="flex items-center gap-3 px-3 py-2.5 text-sm font-medium rounded-lg bg-primary-500/10 text-primary-400 border border-primary-500/20">
16
+ <i data-feather="upload-cloud" class="w-4 h-4"></i>
17
+ Upload
18
+ <span class="ml-auto w-2 h-2 rounded-full bg-primary-500"></span>
19
+ </a>
20
+ <a href="#" class="flex items-center gap-3 px-3 py-2.5 text-sm font-medium rounded-lg text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors">
21
+ <i data-feather="scissors" class="w-4 h-4"></i>
22
+ Remoção de Silêncio
23
+ </a>
24
+ <a href="#" class="flex items-center gap-3 px-3 py-2.5 text-sm font-medium rounded-lg text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors">
25
+ <i data-feather="mic" class="w-4 h-4"></i>
26
+ Transcrição
27
+ </a>
28
+ <a href="#" class="flex items-center gap-3 px-3 py-2.5 text-sm font-medium rounded-lg text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors">
29
+ <i data-feather="align-left" class="w-4 h-4"></i>
30
+ Divisão de Texto
31
+ </a>
32
+ <a href="#" class="flex items-center gap-3 px-3 py-2.5 text-sm font-medium rounded-lg text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors">
33
+ <i data-feather="anchor" class="w-4 h-4"></i>
34
+ Alinhamento
35
+ </a>
36
+ <a href="#" class="flex items-center gap-3 px-3 py-2.5 text-sm font-medium rounded-lg text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors">
37
+ <i data-feather="type" class="w-4 h-4"></i>
38
+ Exportar SRT
39
+ </a>
40
+ </nav>
41
+ </div>
42
+
43
+ <!-- Quick Stats -->
44
+ <div>
45
+ <h3 class="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3 px-2">Estatísticas Rápidas</h3>
46
+ <div class="space-y-3 px-2">
47
+ <div class="flex justify-between items-center text-sm">
48
+ <span class="text-slate-400">Threshold</span>
49
+ <span class="text-slate-200 font-mono">-70dB</span>
50
+ </div>
51
+ <div class="flex justify-between items-center text-sm">
52
+ <span class="text-slate-400">Modelo</span>
53
+ <span class="text-secondary-400 font-mono text-xs">small</span>
54
+ </div>
55
+ <div class="flex justify-between items-center text-sm">
56
+ <span class="text-slate-400">Max Block</span>
57
+ <span class="text-slate-200 font-mono">14</span>
58
+ </div>
59
+ </div>
60
+ </div>
61
+
62
+ <!-- Help Card -->
63
+ <div class="p-3 rounded-xl bg-gradient-to-br from-slate-800 to-slate-900 border border-slate-700">
64
+ <div class="flex items-start gap-3">
65
+ <div class="w-8 h-8 rounded-lg bg-secondary-500/20 flex items-center justify-center flex-shrink-0">
66
+ <i data-feather="help-circle" class="w-4 h-4 text-secondary-400"></i>
67
+ </div>
68
+ <div>
69
+ <h4 class="text-sm font-medium text-slate-200 mb-1">Dica CapCut</h4>
70
+ <p class="text-xs text-slate-400 leading-relaxed">
71
+ Use Advance de 70ms e Preroll de 40ms para sincronização perfeita com vídeos verticais.
72
+ </p>
73
+ </div>
74
+ </div>
75
+ </div>
76
+
77
+ </div>
78
+ </aside>
79
+ `;
80
+ }
81
+ }
82
+
83
+ customElements.define('app-sidebar', AppSidebar);
components/stat-card.js ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class StatCard extends HTMLElement {
2
+ static get observedAttributes() {
3
+ return ['icon', 'label', 'value', 'color'];
4
+ }
5
+
6
+ constructor() {
7
+ super();
8
+ }
9
+
10
+ connectedCallback() {
11
+ this.render();
12
+ }
13
+
14
+ attributeChangedCallback() {
15
+ this.render();
16
+ }
17
+
18
+ render() {
19
+ const icon = this.getAttribute('icon') || 'activity';
20
+ const label = this.getAttribute('label') || 'Label';
21
+ const value = this.getAttribute('value') || '0';
22
+ const color = this.getAttribute('color') || 'primary';
23
+
24
+ const colorClasses = {
25
+ primary: 'text-primary-400 bg-primary-500/10 border-primary-500/20',
26
+ secondary: 'text-secondary-400 bg-secondary-500/10 border-secondary-500/20',
27
+ green: 'text-emerald-400 bg-emerald-500/10 border-emerald-500/20',
28
+ red: 'text-red-400 bg-red-500/10 border-red-500/20',
29
+ slate: 'text-slate-400 bg-slate-500/10 border-slate-500/20'
30
+ };
31
+
32
+ const colorClass = colorClasses[color] || colorClasses.primary;
33
+
34
+ this.innerHTML = `
35
+ <div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 shadow-lg backdrop-blur-sm hover:border-slate-700 transition-colors">
36
+ <div class="flex items-start justify-between">
37
+ <div>
38
+ <p class="text-slate-500 text-xs font-medium uppercase tracking-wider mb-1">${label}</p>
39
+ <h4 class="text-2xl font-bold text-slate-100">${value}</h4>
40
+ </div>
41
+ <div class="w-10 h-10 rounded-xl ${colorClass} flex items-center justify-center border">
42
+ <i data-feather="${icon}" class="w-5 h-5"></i>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ `;
47
+
48
+ // Re-initialize feather icons for this component
49
+ if (window.feather) {
50
+ feather.replace();
51
+ }
52
+ }
53
+ }
54
+
55
+ customElements.define('stat-card', StatCard);
components/upload-zone.js ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class UploadZone extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ this.dragCounter = 0;
5
+ }
6
+
7
+ connectedCallback() {
8
+ this.innerHTML = `
9
+ <div id="drop-zone" class="relative group">
10
+ <div class="border-2 border-dashed border-slate-700 rounded-2xl p-8 text-center transition-all duration-300 bg-slate-900/50 hover:border-primary-500/50 hover:bg-primary-500/5">
11
+
12
+ <!-- Icon -->
13
+ <div class="w-16 h-16 mx-auto mb-4 rounded-full bg-slate-800 flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
14
+ <i data-feather="upload-cloud" class="w-8 h-8 text-slate-400 group-hover:text-primary-400 transition-colors"></i>
15
+ </div>
16
+
17
+ <h3 class="text-lg font-semibold text-slate-300 mb-2 group-hover:text-slate-200">
18
+ Arraste arquivos de áudio
19
+ </h3>
20
+ <p class="text-sm text-slate-500 mb-4">
21
+ ou <span class="text-primary-400 cursor-pointer hover:underline">clique para selecionar</span>
22
+ </p>
23
+
24
+ <div class="flex items-center justify-center gap-2 text-xs text-slate-600">
25
+ <span class="px-2 py-1 rounded bg-slate-800">MP3</span>
26
+ <span class="px-2 py-1 rounded bg-slate-800">WAV</span>
27
+ <span class="px-2 py-1 rounded bg-slate-800">M4A</span>
28
+ <span class="px-2 py-1 rounded bg-slate-800">FLAC</span>
29
+ </div>
30
+
31
+ <!-- Hidden Input -->
32
+ <input type="file" id="file-input" multiple accept="audio/*" class="hidden">
33
+ </div>
34
+
35
+ <!-- Processing Overlay -->
36
+ <div id="upload-overlay" class="absolute inset-0 bg-slate-900/90 backdrop-blur-sm rounded-2xl flex items-center justify-center hidden">
37
+ <div class="text-center">
38
+ <div class="w-12 h-12 border-4 border-primary-500/30 border-t-primary-500 rounded-full animate-spin mx-auto mb-3"></div>
39
+ <p class="text-sm text-slate-300">Analisando áudio...</p>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ `;
44
+
45
+ this.setupEventListeners();
46
+ }
47
+
48
+ setupEventListeners() {
49
+ const dropZone = this.querySelector('#drop-zone');
50
+ const fileInput = this.querySelector('#file-input');
51
+ const overlay = this.querySelector('#upload-overlay');
52
+
53
+ // Click to select
54
+ dropZone.addEventListener('click', (e) => {
55
+ if (e.target !== fileInput) {
56
+ fileInput.click();
57
+ }
58
+ });
59
+
60
+ fileInput.addEventListener('change', (e) => {
61
+ this.handleFiles(e.target.files);
62
+ });
63
+
64
+ // Drag and Drop
65
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
66
+ dropZone.addEventListener(eventName, (e) => {
67
+ e.preventDefault();
68
+ e.stopPropagation();
69
+ }, false);
70
+ });
71
+
72
+ ['dragenter', 'dragover'].forEach(eventName => {
73
+ dropZone.addEventListener(eventName, () => {
74
+ this.dragCounter++;
75
+ dropZone.classList.add('drag-over');
76
+ }, false);
77
+ });
78
+
79
+ ['dragleave', 'drop'].forEach(eventName => {
80
+ dropZone.addEventListener(eventName, () => {
81
+ this.dragCounter--;
82
+ if (this.dragCounter === 0) {
83
+ dropZone.classList.remove('drag-over');
84
+ }
85
+ }, false);
86
+ });
87
+
88
+ dropZone.addEventListener('drop', (e) => {
89
+ this.dragCounter = 0;
90
+ dropZone.classList.remove('drag-over');
91
+
92
+ const files = e.dataTransfer.files;
93
+ if (files.length > 0) {
94
+ // Show overlay briefly for effect
95
+ overlay.classList.remove('hidden');
96
+ setTimeout(() => {
97
+ overlay.classList.add('hidden');
98
+ this.handleFiles(files);
99
+ }, 500);
100
+ }
101
+ });
102
+ }
103
+
104
+ handleFiles(files) {
105
+ // Dispatch custom event to main app
106
+ document.dispatchEvent(new CustomEvent('files-uploaded', {
107
+ detail: { files: Array.from(files) }
108
+ }));
109
+ }
110
+ }
111
+
112
+ customElements.define('upload-zone', UploadZone);
index.html CHANGED
@@ -1,19 +1,316 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="pt-BR" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>CapCutSync Pro - Pipeline de Áudio Inteligente</title>
7
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236366f1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z'/%3E%3Cpath d='M19 10v2a7 7 0 0 1-14 0v-2'/%3E%3Cline x1='12' y1='19' x2='12' y2='22'/%3E%3C/svg%3E">
8
+ <link rel="stylesheet" href="style.css">
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <script src="https://unpkg.com/feather-icons"></script>
11
+ <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
12
+ <script>
13
+ tailwind.config = {
14
+ darkMode: 'class',
15
+ theme: {
16
+ extend: {
17
+ colors: {
18
+ primary: {
19
+ 50: '#eef2ff',
20
+ 100: '#e0e7ff',
21
+ 200: '#c7d2fe',
22
+ 300: '#a5b4fc',
23
+ 400: '#818cf8',
24
+ 500: '#6366f1',
25
+ 600: '#4f46e5',
26
+ 700: '#4338ca',
27
+ 800: '#3730a3',
28
+ 900: '#312e81',
29
+ 950: '#1e1b4b',
30
+ },
31
+ secondary: {
32
+ 50: '#fffbeb',
33
+ 100: '#fef3c7',
34
+ 200: '#fde68a',
35
+ 300: '#fcd34d',
36
+ 400: '#fbbf24',
37
+ 500: '#f59e0b',
38
+ 600: '#d97706',
39
+ 700: '#b45309',
40
+ 800: '#92400e',
41
+ 900: '#78350f',
42
+ 950: '#451a03',
43
+ }
44
+ },
45
+ animation: {
46
+ 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
47
+ 'wave': 'wave 1.5s ease-in-out infinite',
48
+ },
49
+ keyframes: {
50
+ wave: {
51
+ '0%, 100%': { transform: 'scaleY(1)' },
52
+ '50%': { transform: 'scaleY(1.5)' },
53
+ }
54
+ }
55
+ }
56
+ }
57
+ }
58
+ </script>
59
+ </head>
60
+ <body class="bg-slate-950 text-slate-100 font-sans antialiased overflow-x-hidden">
61
+
62
+ <app-navbar></app-navbar>
63
+
64
+ <div class="flex h-screen pt-16">
65
+ <app-sidebar id="sidebar" class="hidden md:block"></app-sidebar>
66
+
67
+ <main class="flex-1 overflow-y-auto bg-slate-950/50 backdrop-blur-sm p-4 md:p-6 lg:p-8">
68
+ <div class="max-w-7xl mx-auto space-y-6">
69
+
70
+ <!-- Header Stats -->
71
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
72
+ <stat-card icon="file-audio" label="Áudios Processados" value="0" id="stat-audios"></stat-card>
73
+ <stat-card icon="clock" label="Tempo Total" value="00:00" id="stat-time"></stat-card>
74
+ <stat-card icon="activity" label="Status" value="Ocioso" color="secondary" id="stat-status"></stat-card>
75
+ <stat-card icon="cpu" label="Pipeline" value="Pronto" id="stat-pipeline"></stat-card>
76
+ </div>
77
+
78
+ <!-- Main Content Grid -->
79
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
80
+
81
+ <!-- Left Column: Upload & Files -->
82
+ <div class="lg:col-span-1 space-y-6">
83
+ <upload-zone></upload-zone>
84
+
85
+ <div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 shadow-xl backdrop-blur-md">
86
+ <h3 class="text-lg font-semibold text-slate-200 mb-4 flex items-center gap-2">
87
+ <i data-feather="folder" class="w-5 h-5 text-primary-400"></i>
88
+ Arquivos na Fila
89
+ </h3>
90
+ <div id="file-queue" class="space-y-2 max-h-64 overflow-y-auto pr-2">
91
+ <p class="text-slate-500 text-sm italic text-center py-8">Nenhum arquivo na fila</p>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ <!-- Center Column: Configuration -->
97
+ <div class="lg:col-span-2 space-y-6">
98
+
99
+ <!-- Tabs -->
100
+ <div class="bg-slate-900/80 border border-slate-800 rounded-2xl overflow-hidden shadow-xl backdrop-blur-md">
101
+ <div class="flex border-b border-slate-800">
102
+ <button class="tab-btn flex-1 py-4 px-6 text-sm font-medium text-primary-400 border-b-2 border-primary-500 bg-primary-500/10" data-tab="silence">
103
+ Silêncio & Áudio
104
+ </button>
105
+ <button class="tab-btn flex-1 py-4 px-6 text-sm font-medium text-slate-400 hover:text-slate-200 transition-colors" data-tab="text">
106
+ Texto & Blocos
107
+ </button>
108
+ <button class="tab-btn flex-1 py-4 px-6 text-sm font-medium text-slate-400 hover:text-slate-200 transition-colors" data-tab="srt">
109
+ SRT & Alinhamento
110
+ </button>
111
+ </div>
112
+
113
+ <!-- Tab Content: Silence -->
114
+ <div id="tab-silence" class="tab-content p-6 space-y-6">
115
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
116
+ <div class="space-y-2">
117
+ <label class="text-sm font-medium text-slate-300">Threshold de Silêncio (dB)</label>
118
+ <div class="flex items-center gap-3">
119
+ <input type="range" id="silence-threshold" min="-80" max="-40" value="-70"
120
+ class="flex-1 h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-primary-500">
121
+ <span class="text-sm font-mono text-primary-400 w-12 text-right" id="val-threshold">-70</span>
122
+ </div>
123
+ <p class="text-xs text-slate-500">Valores mais altos = mais sensível</p>
124
+ </div>
125
+
126
+ <div class="space-y-2">
127
+ <label class="text-sm font-medium text-slate-300">Min. Silêncio (ms)</label>
128
+ <div class="flex items-center gap-3">
129
+ <input type="range" id="min-silence" min="50" max="500" value="150"
130
+ class="flex-1 h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-primary-500">
131
+ <span class="text-sm font-mono text-primary-400 w-12 text-right" id="val-silence">150</span>
132
+ </div>
133
+ </div>
134
+
135
+ <div class="space-y-2">
136
+ <label class="text-sm font-medium text-slate-300">Overlap Base (ms)</label>
137
+ <div class="flex items-center gap-3">
138
+ <input type="range" id="base-overlap" min="50" max="300" value="190"
139
+ class="flex-1 h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-primary-500">
140
+ <span class="text-sm font-mono text-primary-400 w-12 text-right" id="val-overlap">190</span>
141
+ </div>
142
+ </div>
143
+
144
+ <div class="space-y-2">
145
+ <label class="text-sm font-medium text-slate-300">Keep Silence (ms)</label>
146
+ <div class="flex items-center gap-3">
147
+ <input type="range" id="keep-silence" min="0" max="200" value="70"
148
+ class="flex-1 h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-primary-500">
149
+ <span class="text-sm font-mono text-primary-400 w-12 text-right" id="val-keep">70</span>
150
+ </div>
151
+ </div>
152
+ </div>
153
+
154
+ <div class="flex items-center gap-4 pt-4 border-t border-slate-800">
155
+ <label class="flex items-center gap-2 cursor-pointer">
156
+ <input type="checkbox" id="normalize-lufs" checked class="w-4 h-4 rounded border-slate-600 text-primary-500 focus:ring-primary-500 bg-slate-800">
157
+ <span class="text-sm text-slate-300">Normalizar LUFS</span>
158
+ </label>
159
+ <label class="flex items-center gap-2 cursor-pointer">
160
+ <input type="checkbox" id="use-vad" checked class="w-4 h-4 rounded border-slate-600 text-primary-500 focus:ring-primary-500 bg-slate-800">
161
+ <span class="text-sm text-slate-300">Detecção VAD</span>
162
+ </label>
163
+ </div>
164
+ </div>
165
+
166
+ <!-- Tab Content: Text -->
167
+ <div id="tab-text" class="tab-content hidden p-6 space-y-6">
168
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
169
+ <div class="space-y-2">
170
+ <label class="text-sm font-medium text-slate-300">Máx. Caracteres/Bloco</label>
171
+ <input type="number" id="max-block-len" value="14"
172
+ class="w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-2 text-slate-200 focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none">
173
+ <p class="text-xs text-slate-500">Limite duro (sem espaços)</p>
174
+ </div>
175
+
176
+ <div class="space-y-2">
177
+ <label class="text-sm font-medium text-slate-300">Público Alvo</label>
178
+ <select id="publico" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-2 text-slate-200 focus:ring-2 focus:ring-primary-500 outline-none">
179
+ <option value="M">Padrão (M)</option>
180
+ <option value="H">Homens (H - uppercase)</option>
181
+ </select>
182
+ </div>
183
+ </div>
184
+
185
+ <div class="space-y-2">
186
+ <label class="text-sm font-medium text-slate-300">Roteiro/Transcrição</label>
187
+ <textarea id="script-text" rows="6" placeholder="Cole aqui o roteiro para divisão em blocos..."
188
+ class="w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-3 text-slate-200 placeholder-slate-500 focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none resize-none font-mono text-sm"></textarea>
189
+ <div class="flex justify-between text-xs text-slate-500">
190
+ <span>Regras automáticas: E/QUE iniciam blocos</span>
191
+ <span id="char-count">0 caracteres</span>
192
+ </div>
193
+ </div>
194
+
195
+ <button id="preview-blocks" class="w-full py-2 px-4 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg transition-colors text-sm font-medium flex items-center justify-center gap-2">
196
+ <i data-feather="grid" class="w-4 h-4"></i>
197
+ Pré-visualizar Blocos
198
+ </button>
199
+
200
+ <div id="blocks-preview" class="hidden space-y-2 max-h-48 overflow-y-auto p-3 bg-slate-950 rounded-lg border border-slate-800">
201
+ <!-- Blocks will appear here -->
202
+ </div>
203
+ </div>
204
+
205
+ <!-- Tab Content: SRT -->
206
+ <div id="tab-srt" class="tab-content hidden p-6 space-y-6">
207
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
208
+ <div class="space-y-2">
209
+ <label class="text-sm font-medium text-slate-300">Modelo Whisper</label>
210
+ <select id="whisper-model" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-2 text-slate-200 outline-none">
211
+ <option value="small">Small (balanceado)</option>
212
+ <option value="medium">Medium (preciso)</option>
213
+ <option value="tiny">Tiny (rápido)</option>
214
+ </select>
215
+ </div>
216
+
217
+ <div class="space-y-2">
218
+ <label class="text-sm font-medium text-slate-300">Calibração CapCut (ms)</label>
219
+ <input type="number" id="capcut-advance" value="70"
220
+ class="w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-2 text-slate-200 outline-none">
221
+ </div>
222
+ </div>
223
+
224
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
225
+ <div class="space-y-2">
226
+ <label class="text-sm font-medium text-slate-300">Preroll (ms)</label>
227
+ <input type="number" id="preroll" value="40" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-2 text-slate-200 outline-none">
228
+ </div>
229
+ <div class="space-y-2">
230
+ <label class="text-sm font-medium text-slate-300">Postroll (ms)</label>
231
+ <input type="number" id="postroll" value="25" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-2 text-slate-200 outline-none">
232
+ </div>
233
+ </div>
234
+
235
+ <div class="bg-primary-900/20 border border-primary-700/30 rounded-lg p-4">
236
+ <h4 class="text-sm font-medium text-primary-300 mb-2 flex items-center gap-2">
237
+ <i data-feather="info" class="w-4 h-4"></i>
238
+ Alinhamento Inteligente
239
+ </h4>
240
+ <p class="text-xs text-primary-200/70 leading-relaxed">
241
+ Usa âncoras de primeira e última palavra forte para alinnar blocos do roteiro com timestamps do Whisper.
242
+ Similaridade fonética + sufixo para matching tolerante.
243
+ </p>
244
+ </div>
245
+ </div>
246
+ </div>
247
+
248
+ <!-- Visualizer -->
249
+ <div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-6 shadow-xl backdrop-blur-md">
250
+ <div class="flex items-center justify-between mb-4">
251
+ <h3 class="text-lg font-semibold text-slate-200 flex items-center gap-2">
252
+ <i data-feather="bar-chart-2" class="w-5 h-5 text-secondary-400"></i>
253
+ Visualizador de Forma de Onda
254
+ </h3>
255
+ <div class="flex gap-2">
256
+ <button id="play-preview" class="p-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 transition-colors">
257
+ <i data-feather="play" class="w-4 h-4"></i>
258
+ </button>
259
+ <button id="reset-view" class="p-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 transition-colors">
260
+ <i data-feather="maximize" class="w-4 h-4"></i>
261
+ </button>
262
+ </div>
263
+ </div>
264
+ <canvas id="waveform" class="w-full h-32 bg-slate-950 rounded-lg border border-slate-800"></canvas>
265
+ <div class="flex justify-between mt-2 text-xs text-slate-500 font-mono">
266
+ <span>00:00:00</span>
267
+ <span id="total-duration">00:00:00</span>
268
+ </div>
269
+ </div>
270
+ </div>
271
+ </div>
272
+
273
+ <!-- Action Bar -->
274
+ <div class="fixed bottom-6 left-1/2 transform -translate-x-1/2 z-40">
275
+ <div class="bg-slate-900/90 backdrop-blur-md border border-slate-700 rounded-full px-6 py-3 shadow-2xl flex items-center gap-4">
276
+ <button id="btn-clear" class="px-4 py-2 text-sm text-slate-400 hover:text-slate-200 transition-colors">
277
+ Limpar
278
+ </button>
279
+ <div class="w-px h-6 bg-slate-700"></div>
280
+ <button id="btn-process" class="px-6 py-2 bg-gradient-to-r from-primary-600 to-primary-500 hover:from-primary-500 hover:to-primary-400 text-white rounded-full text-sm font-semibold shadow-lg shadow-primary-500/25 transition-all transform hover:scale-105 flex items-center gap-2">
281
+ <i data-feather="zap" class="w-4 h-4"></i>
282
+ Processar Pipeline
283
+ </button>
284
+ </div>
285
+ </div>
286
+
287
+ <!-- Logs Panel -->
288
+ <div class="bg-black/40 border border-slate-800 rounded-2xl p-4 font-mono text-xs h-48 overflow-hidden flex flex-col">
289
+ <div class="flex items-center justify-between mb-2 pb-2 border-b border-slate-800">
290
+ <span class="text-slate-400 uppercase tracking-wider text-xs font-bold">Console de Logs</span>
291
+ <button id="clear-logs" class="text-slate-500 hover:text-slate-300">
292
+ <i data-feather="trash-2" class="w-3 h-3"></i>
293
+ </button>
294
+ </div>
295
+ <div id="log-container" class="flex-1 overflow-y-auto space-y-1 text-slate-300">
296
+ <div class="text-slate-500">[SYSTEM] CapCutSync Pro v2.0 inicializado...</div>
297
+ <div class="text-slate-500">[SYSTEM] Aguardando arquivos de áudio...</div>
298
+ </div>
299
+ </div>
300
+
301
+ </div>
302
+ </main>
303
+ </div>
304
+
305
+ <!-- Web Components -->
306
+ <script src="components/navbar.js"></script>
307
+ <script src="components/sidebar.js"></script>
308
+ <script src="components/upload-zone.js"></script>
309
+ <script src="components/stat-card.js"></script>
310
+
311
+ <!-- Main Logic -->
312
+ <script src="script.js"></script>
313
+ <script>feather.replace();</script>
314
+ <script src="https://huggingface.co/deepsite/deepsite-badge.js"></script>
315
+ </body>
316
+ </html>
script.js ADDED
@@ -0,0 +1,545 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // CapCutSync Pro - Main Application Logic
2
+
3
+ class AudioPipeline {
4
+ constructor() {
5
+ this.files = [];
6
+ this.config = {
7
+ silence: {
8
+ threshold: -70,
9
+ minLen: 150,
10
+ keepSilence: 70
11
+ },
12
+ overlap: {
13
+ base: 190
14
+ },
15
+ text: {
16
+ maxBlockLen: 14,
17
+ publico: 'M'
18
+ },
19
+ srt: {
20
+ model: 'small',
21
+ advance: 70,
22
+ preroll: 40,
23
+ postroll: 25
24
+ }
25
+ };
26
+ this.isProcessing = false;
27
+ this.waveformData = [];
28
+
29
+ this.init();
30
+ }
31
+
32
+ init() {
33
+ this.setupEventListeners();
34
+ this.setupRangeSliders();
35
+ this.setupTabs();
36
+ this.initWaveform();
37
+ this.startClock();
38
+ }
39
+
40
+ setupEventListeners() {
41
+ // File Upload via Drag & Drop
42
+ const uploadZone = document.querySelector('upload-zone');
43
+
44
+ // Process Button
45
+ document.getElementById('btn-process').addEventListener('click', () => this.startProcessing());
46
+
47
+ // Clear Button
48
+ document.getElementById('btn-clear').addEventListener('click', () => this.clearAll());
49
+
50
+ // Clear Logs
51
+ document.getElementById('clear-logs').addEventListener('click', () => {
52
+ document.getElementById('log-container').innerHTML = '';
53
+ });
54
+
55
+ // Preview Blocks
56
+ document.getElementById('preview-blocks').addEventListener('click', () => this.previewBlocks());
57
+
58
+ // Text input character count
59
+ document.getElementById('script-text').addEventListener('input', (e) => {
60
+ document.getElementById('char-count').textContent = `${e.target.value.length} caracteres`;
61
+ });
62
+
63
+ // Waveform controls
64
+ document.getElementById('play-preview').addEventListener('click', () => this.togglePlayPreview());
65
+ document.getElementById('reset-view').addEventListener('click', () => this.resetWaveform());
66
+
67
+ // Listen for custom events from web components
68
+ document.addEventListener('files-uploaded', (e) => this.handleFiles(e.detail.files));
69
+ document.addEventListener('file-removed', (e) => this.removeFile(e.detail.index));
70
+ }
71
+
72
+ setupRangeSliders() {
73
+ const sliders = [
74
+ { id: 'silence-threshold', display: 'val-threshold' },
75
+ { id: 'min-silence', display: 'val-silence' },
76
+ { id: 'base-overlap', display: 'val-overlap' },
77
+ { id: 'keep-silence', display: 'val-keep' }
78
+ ];
79
+
80
+ sliders.forEach(({ id, display }) => {
81
+ const slider = document.getElementById(id);
82
+ const displayEl = document.getElementById(display);
83
+
84
+ slider.addEventListener('input', (e) => {
85
+ displayEl.textContent = e.target.value;
86
+ this.updateConfig();
87
+ });
88
+ });
89
+ }
90
+
91
+ setupTabs() {
92
+ const tabs = document.querySelectorAll('.tab-btn');
93
+ const contents = document.querySelectorAll('.tab-content');
94
+
95
+ tabs.forEach(tab => {
96
+ tab.addEventListener('click', () => {
97
+ const target = tab.dataset.tab;
98
+
99
+ // Update active states
100
+ tabs.forEach(t => {
101
+ if (t.dataset.tab === target) {
102
+ t.classList.add('text-primary-400', 'border-b-2', 'border-primary-500', 'bg-primary-500/10');
103
+ t.classList.remove('text-slate-400');
104
+ } else {
105
+ t.classList.remove('text-primary-400', 'border-b-2', 'border-primary-500', 'bg-primary-500/10');
106
+ t.classList.add('text-slate-400');
107
+ }
108
+ });
109
+
110
+ contents.forEach(c => {
111
+ if (c.id === `tab-${target}`) {
112
+ c.classList.remove('hidden');
113
+ } else {
114
+ c.classList.add('hidden');
115
+ }
116
+ });
117
+ });
118
+ });
119
+ }
120
+
121
+ updateConfig() {
122
+ this.config.silence.threshold = parseInt(document.getElementById('silence-threshold').value);
123
+ this.config.silence.minLen = parseInt(document.getElementById('min-silence').value);
124
+ this.config.silence.keepSilence = parseInt(document.getElementById('keep-silence').value);
125
+ this.config.overlap.base = parseInt(document.getElementById('base-overlap').value);
126
+ this.config.text.maxBlockLen = parseInt(document.getElementById('max-block-len').value);
127
+ this.config.text.publico = document.getElementById('publico').value;
128
+ this.config.srt.model = document.getElementById('whisper-model').value;
129
+ this.config.srt.advance = parseInt(document.getElementById('capcut-advance').value);
130
+ }
131
+
132
+ handleFiles(newFiles) {
133
+ const validFiles = Array.from(newFiles).filter(file =>
134
+ file.type.startsWith('audio/') ||
135
+ ['.mp3', '.wav', '.m4a', '.aac', '.flac', '.ogg'].some(ext =>
136
+ file.name.toLowerCase().endsWith(ext)
137
+ )
138
+ );
139
+
140
+ if (validFiles.length !== newFiles.length) {
141
+ this.log('Apenas arquivos de áudio são permitidos', 'warning');
142
+ }
143
+
144
+ this.files = [...this.files, ...validFiles];
145
+ this.updateFileQueue();
146
+ this.log(`${validFiles.length} arquivo(s) adicionado(s) à fila`, 'success');
147
+
148
+ // Simulate waveform generation for first file
149
+ if (validFiles.length > 0) {
150
+ this.generateMockWaveform();
151
+ this.updateStats();
152
+ }
153
+ }
154
+
155
+ removeFile(index) {
156
+ this.files.splice(index, 1);
157
+ this.updateFileQueue();
158
+ this.log('Arquivo removido da fila', 'info');
159
+ this.updateStats();
160
+ }
161
+
162
+ updateFileQueue() {
163
+ const container = document.getElementById('file-queue');
164
+
165
+ if (this.files.length === 0) {
166
+ container.innerHTML = '<p class="text-slate-500 text-sm italic text-center py-8">Nenhum arquivo na fila</p>';
167
+ return;
168
+ }
169
+
170
+ container.innerHTML = this.files.map((file, index) => `
171
+ <div class="flex items-center justify-between p-3 bg-slate-800/50 rounded-lg border border-slate-700/50 group hover:border-primary-500/30 transition-colors">
172
+ <div class="flex items-center gap-3 overflow-hidden">
173
+ <div class="w-8 h-8 rounded bg-primary-500/20 flex items-center justify-center text-primary-400 flex-shrink-0">
174
+ <i data-feather="music" class="w-4 h-4"></i>
175
+ </div>
176
+ <div class="min-w-0">
177
+ <p class="text-sm text-slate-200 truncate font-medium">${file.name}</p>
178
+ <p class="text-xs text-slate-500">${this.formatFileSize(file.size)}</p>
179
+ </div>
180
+ </div>
181
+ <button onclick="document.dispatchEvent(new CustomEvent('file-removed', {detail: {index: ${index}}}))"
182
+ class="p-1.5 text-slate-500 hover:text-red-400 hover:bg-red-400/10 rounded transition-colors">
183
+ <i data-feather="x" class="w-4 h-4"></i>
184
+ </button>
185
+ </div>
186
+ `).join('');
187
+
188
+ feather.replace();
189
+ }
190
+
191
+ formatFileSize(bytes) {
192
+ if (bytes === 0) return '0 Bytes';
193
+ const k = 1024;
194
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
195
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
196
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
197
+ }
198
+
199
+ previewBlocks() {
200
+ const text = document.getElementById('script-text').value;
201
+ if (!text.trim()) {
202
+ this.log('Insira um roteiro primeiro', 'warning');
203
+ return;
204
+ }
205
+
206
+ // Simulate the Python text splitting logic
207
+ const blocks = this.simulateTextSplit(text);
208
+ const container = document.getElementById('blocks-preview');
209
+
210
+ container.classList.remove('hidden');
211
+ container.innerHTML = blocks.map((block, i) => `
212
+ <div class="block-item flex items-start gap-3 p-2 rounded bg-slate-900 border border-slate-800 text-sm">
213
+ <span class="text-primary-500 font-mono text-xs mt-0.5">${i + 1}</span>
214
+ <span class="text-slate-300">${block}</span>
215
+ <span class="text-xs text-slate-600 ml-auto whitespace-nowrap">${block.replace(/\s/g, '').length} chars</span>
216
+ </div>
217
+ `).join('');
218
+
219
+ this.log(`Roteiro dividido em ${blocks.length} blocos`, 'success');
220
+ }
221
+
222
+ simulateTextSplit(text) {
223
+ // Simplified version of the Python logic
224
+ const maxLen = this.config.text.maxBlockLen;
225
+ const words = text.split(/\s+/);
226
+ const blocks = [];
227
+ let current = [];
228
+ let currentLen = 0;
229
+
230
+ const weakWords = ['e', 'ou', 'mas', 'por', 'com', 'para', 'em', 'de', 'do', 'da', 'a', 'o', 'que', 'se'];
231
+
232
+ words.forEach(word => {
233
+ const cleanWord = word.replace(/[^\w\s]/gi, '');
234
+ const wordLen = cleanWord.length;
235
+
236
+ if (currentLen + wordLen > maxLen && current.length > 0) {
237
+ // Check if last word is weak (don't end block with weak word)
238
+ const lastWord = current[current.length - 1].toLowerCase();
239
+ if (weakWords.includes(lastWord) && words.length > 1) {
240
+ // Move weak word to next block
241
+ const weak = current.pop();
242
+ blocks.push(current.join(' '));
243
+ current = [weak, word];
244
+ currentLen = weak.length + wordLen;
245
+ } else {
246
+ blocks.push(current.join(' '));
247
+ current = [word];
248
+ currentLen = wordLen;
249
+ }
250
+ } else {
251
+ current.push(word);
252
+ currentLen += wordLen;
253
+ }
254
+ });
255
+
256
+ if (current.length > 0) {
257
+ blocks.push(current.join(' '));
258
+ }
259
+
260
+ return blocks;
261
+ }
262
+
263
+ async startProcessing() {
264
+ if (this.files.length === 0) {
265
+ this.log('Nenhum arquivo para processar', 'error');
266
+ return;
267
+ }
268
+
269
+ this.isProcessing = true;
270
+ this.updateStatus('Processando...', 'processing');
271
+
272
+ const btn = document.getElementById('btn-process');
273
+ btn.disabled = true;
274
+ btn.classList.add('processing-pulse');
275
+ btn.innerHTML = `<i data-feather="loader" class="w-4 h-4 animate-spin"></i> Processando...`;
276
+ feather.replace();
277
+
278
+ this.log('Iniciando pipeline de processamento...', 'info');
279
+
280
+ // Simulate pipeline steps
281
+ const steps = [
282
+ { msg: 'Carregando áudio e detectando silêncios...', time: 1500, type: 'silence' },
283
+ { msg: 'Aplicando VAD e removendo pausas longas...', time: 2000, type: 'vad' },
284
+ { msg: 'Normalizando loudness (LUFS)...', time: 1200, type: 'audio' },
285
+ { msg: 'Transcrevendo com Whisper...', time: 3000, type: 'whisper' },
286
+ { msg: 'Dividindo roteiro em blocos inteligentes...', time: 800, type: 'text' },
287
+ { msg: 'Alinhando âncoras de início/fim...', time: 1500, type: 'align' },
288
+ { msg: 'Calibrando timestamps para CapCut...', time: 1000, type: 'srt' },
289
+ { msg: 'Exportando SRT e áudio final...', time: 1200, type: 'export' }
290
+ ];
291
+
292
+ for (const step of steps) {
293
+ this.log(step.msg, 'info');
294
+ await this.delay(step.time);
295
+
296
+ // Simulate progress on waveform
297
+ this.updateWaveformProgress(steps.indexOf(step) / steps.length);
298
+ }
299
+
300
+ this.log('Pipeline concluído com sucesso!', 'success');
301
+ this.updateStatus('Concluído', 'success');
302
+
303
+ // Reset button
304
+ btn.disabled = false;
305
+ btn.classList.remove('processing-pulse');
306
+ btn.innerHTML = `<i data-feather="zap" class="w-4 h-4"></i> Processar Pipeline`;
307
+ feather.replace();
308
+
309
+ this.isProcessing = false;
310
+
311
+ // Show download simulation
312
+ this.showDownloadOptions();
313
+ }
314
+
315
+ delay(ms) {
316
+ return new Promise(resolve => setTimeout(resolve, ms));
317
+ }
318
+
319
+ updateStatus(text, type) {
320
+ const statusEl = document.getElementById('stat-status');
321
+ statusEl.setAttribute('value', text);
322
+
323
+ const colors = {
324
+ processing: 'secondary',
325
+ success: 'green',
326
+ idle: 'secondary'
327
+ };
328
+ statusEl.setAttribute('color', colors[type] || 'secondary');
329
+ }
330
+
331
+ log(message, type = 'info') {
332
+ const container = document.getElementById('log-container');
333
+ const entry = document.createElement('div');
334
+ const time = new Date().toLocaleTimeString('pt-BR');
335
+
336
+ entry.className = `log-entry ${type}`;
337
+ entry.innerHTML = `<span class="text-slate-600">[${time}]</span> <span class="${this.getLogColor(type)}">${message}</span>`;
338
+
339
+ container.appendChild(entry);
340
+ container.scrollTop = container.scrollHeight;
341
+ }
342
+
343
+ getLogColor(type) {
344
+ const colors = {
345
+ info: 'text-primary-400',
346
+ success: 'text-emerald-400',
347
+ warning: 'text-secondary-400',
348
+ error: 'text-red-400'
349
+ };
350
+ return colors[type] || 'text-slate-300';
351
+ }
352
+
353
+ clearAll() {
354
+ this.files = [];
355
+ this.updateFileQueue();
356
+ document.getElementById('script-text').value = '';
357
+ document.getElementById('blocks-preview').classList.add('hidden');
358
+ this.resetWaveform();
359
+ this.log('Fila limpa', 'info');
360
+ this.updateStatus('Ocioso', 'idle');
361
+ }
362
+
363
+ updateStats() {
364
+ document.getElementById('stat-audios').setAttribute('value', this.files.length.toString());
365
+
366
+ // Calculate total estimated time
367
+ const totalSeconds = this.files.length * 120; // Mock 2 min per file
368
+ const mins = Math.floor(totalSeconds / 60);
369
+ const secs = totalSeconds % 60;
370
+ document.getElementById('stat-time').setAttribute('value', `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`);
371
+ }
372
+
373
+ startClock() {
374
+ setInterval(() => {
375
+ if (!this.isProcessing) {
376
+ const now = new Date();
377
+ // Optional: update some clock element if needed
378
+ }
379
+ }, 1000);
380
+ }
381
+
382
+ // Waveform Visualization
383
+ initWaveform() {
384
+ const canvas = document.getElementById('waveform');
385
+ const ctx = canvas.getContext('2d');
386
+
387
+ // Set canvas size
388
+ const resize = () => {
389
+ canvas.width = canvas.offsetWidth;
390
+ canvas.height = canvas.offsetHeight;
391
+ if (this.waveformData.length === 0) {
392
+ this.drawEmptyWaveform();
393
+ } else {
394
+ this.drawWaveform();
395
+ }
396
+ };
397
+
398
+ window.addEventListener('resize', resize);
399
+ resize();
400
+ }
401
+
402
+ generateMockWaveform() {
403
+ const points = 200;
404
+ this.waveformData = [];
405
+
406
+ for (let i = 0; i < points; i++) {
407
+ // Generate somewhat realistic audio waveform pattern
408
+ const base = Math.sin(i * 0.1) * 0.3;
409
+ const noise = Math.random() * 0.4;
410
+ const envelope = Math.exp(-Math.pow((i - points/2) / (points/4), 2)) * 0.5;
411
+ this.waveformData.push(base + noise + envelope);
412
+ }
413
+
414
+ this.drawWaveform();
415
+
416
+ // Update total duration display
417
+ const mins = Math.floor(this.files.length * 2.5);
418
+ const secs = Math.floor((this.files.length * 2.5 % 1) * 60);
419
+ document.getElementById('total-duration').textContent =
420
+ `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
421
+ }
422
+
423
+ drawWaveform() {
424
+ const canvas = document.getElementById('waveform');
425
+ const ctx = canvas.getContext('2d');
426
+ const width = canvas.width;
427
+ const height = canvas.height;
428
+ const barWidth = width / this.waveformData.length;
429
+
430
+ ctx.clearRect(0, 0, width, height);
431
+
432
+ // Draw gradient bars
433
+ const gradient = ctx.createLinearGradient(0, 0, 0, height);
434
+ gradient.addColorStop(0, '#6366f1');
435
+ gradient.addColorStop(0.5, '#818cf8');
436
+ gradient.addColorStop(1, '#c7d2fe');
437
+
438
+ this.waveformData.forEach((value, i) => {
439
+ const barHeight = value * height * 0.8;
440
+ const x = i * barWidth;
441
+ const y = (height - barHeight) / 2;
442
+
443
+ ctx.fillStyle = gradient;
444
+ ctx.fillRect(x, y, barWidth - 1, barHeight);
445
+ });
446
+
447
+ // Draw center line
448
+ ctx.strokeStyle = 'rgba(99, 102, 241, 0.3)';
449
+ ctx.lineWidth = 1;
450
+ ctx.beginPath();
451
+ ctx.moveTo(0, height / 2);
452
+ ctx.lineTo(width, height / 2);
453
+ ctx.stroke();
454
+ }
455
+
456
+ drawEmptyWaveform() {
457
+ const canvas = document.getElementById('waveform');
458
+ const ctx = canvas.getContext('2d');
459
+ const width = canvas.width;
460
+ const height = canvas.height;
461
+
462
+ ctx.clearRect(0, 0, width, height);
463
+ ctx.strokeStyle = '#1e293b';
464
+ ctx.lineWidth = 2;
465
+ ctx.setLineDash([5, 5]);
466
+ ctx.beginPath();
467
+ ctx.moveTo(0, height / 2);
468
+ ctx.lineTo(width, height / 2);
469
+ ctx.stroke();
470
+ ctx.setLineDash([]);
471
+ }
472
+
473
+ updateWaveformProgress(percent) {
474
+ if (this.waveformData.length === 0) return;
475
+
476
+ const canvas = document.getElementById('waveform');
477
+ const ctx = canvas.getContext('2d');
478
+
479
+ // Redraw base
480
+ this.drawWaveform();
481
+
482
+ // Draw progress overlay
483
+ const width = canvas.width;
484
+ const height = canvas.height;
485
+ const progressWidth = width * percent;
486
+
487
+ ctx.fillStyle = 'rgba(245, 158, 11, 0.2)';
488
+ ctx.fillRect(0, 0, progressWidth, height);
489
+
490
+ // Progress line
491
+ ctx.strokeStyle = '#f59e0b';
492
+ ctx.lineWidth = 2;
493
+ ctx.beginPath();
494
+ ctx.moveTo(progressWidth, 0);
495
+ ctx.lineTo(progressWidth, height);
496
+ ctx.stroke();
497
+ }
498
+
499
+ resetWaveform() {
500
+ this.waveformData = [];
501
+ this.drawEmptyWaveform();
502
+ document.getElementById('total-duration').textContent = '00:00:00';
503
+ }
504
+
505
+ togglePlayPreview() {
506
+ this.log('Preview de áudio não disponível no modo de demonstração', 'warning');
507
+ }
508
+
509
+ showDownloadOptions() {
510
+ // Create a toast notification for download
511
+ const toast = document.createElement('div');
512
+ toast.className = 'fixed top-20 right-6 bg-slate-800 border border-primary-500/30 rounded-xl p-4 shadow-2xl z-50 animate-slide-in';
513
+ toast.innerHTML = `
514
+ <div class="flex items-start gap-3">
515
+ <div class="w-10 h-10 rounded-full bg-primary-500/20 flex items-center justify-center text-primary-400">
516
+ <i data-feather="download" class="w-5 h-5"></i>
517
+ </div>
518
+ <div>
519
+ <h4 class="text-sm font-semibold text-slate-200">Processamento Concluído!</h4>
520
+ <p class="text-xs text-slate-400 mt-1">Arquivos prontos para download</p>
521
+ <div class="flex gap-2 mt-3">
522
+ <button class="px-3 py-1.5 bg-primary-600 hover:bg-primary-500 text-white text-xs rounded-lg transition-colors">
523
+ Baixar SRT
524
+ </button>
525
+ <button class="px-3 py-1.5 bg-slate-700 hover:bg-slate-600 text-slate-200 text-xs rounded-lg transition-colors">
526
+ Áudio WAV
527
+ </button>
528
+ </div>
529
+ </div>
530
+ </div>
531
+ `;
532
+
533
+ document.body.appendChild(toast);
534
+ feather.replace();
535
+
536
+ setTimeout(() => {
537
+ toast.remove();
538
+ }, 5000);
539
+ }
540
+ }
541
+
542
+ // Initialize App when DOM is ready
543
+ document.addEventListener('DOMContentLoaded', () => {
544
+ window.app = new AudioPipeline();
545
+ });
style.css CHANGED
@@ -1,28 +1,176 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
 
4
  }
5
 
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
  }
10
 
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
  }
17
 
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
  }
25
 
26
- .card p:last-child {
27
- margin-bottom: 0;
 
 
 
 
28
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Custom Scrollbar */
2
+ ::-webkit-scrollbar {
3
+ width: 8px;
4
+ height: 8px;
5
  }
6
 
7
+ ::-webkit-scrollbar-track {
8
+ background: #0f172a;
 
9
  }
10
 
11
+ ::-webkit-scrollbar-thumb {
12
+ background: #334155;
13
+ border-radius: 4px;
 
 
14
  }
15
 
16
+ ::-webkit-scrollbar-thumb:hover {
17
+ background: #475569;
 
 
 
 
18
  }
19
 
20
+ /* Range Slider Customization */
21
+ input[type="range"] {
22
+ -webkit-appearance: none;
23
+ appearance: none;
24
+ background: transparent;
25
+ cursor: pointer;
26
  }
27
+
28
+ input[type="range"]::-webkit-slider-track {
29
+ background: #1e293b;
30
+ height: 6px;
31
+ border-radius: 3px;
32
+ }
33
+
34
+ input[type="range"]::-webkit-slider-thumb {
35
+ -webkit-appearance: none;
36
+ appearance: none;
37
+ background: #6366f1;
38
+ height: 18px;
39
+ width: 18px;
40
+ border-radius: 50%;
41
+ margin-top: -6px;
42
+ box-shadow: 0 0 10px rgba(99, 102, 241, 0.5);
43
+ transition: all 0.2s;
44
+ }
45
+
46
+ input[type="range"]::-webkit-slider-thumb:hover {
47
+ transform: scale(1.1);
48
+ box-shadow: 0 0 15px rgba(99, 102, 241, 0.8);
49
+ }
50
+
51
+ input[type="range"]::-moz-range-track {
52
+ background: #1e293b;
53
+ height: 6px;
54
+ border-radius: 3px;
55
+ }
56
+
57
+ input[type="range"]::-moz-range-thumb {
58
+ background: #6366f1;
59
+ height: 18px;
60
+ width: 18px;
61
+ border-radius: 50%;
62
+ border: none;
63
+ box-shadow: 0 0 10px rgba(99, 102, 241, 0.5);
64
+ }
65
+
66
+ /* Animations */
67
+ @keyframes gradient-shift {
68
+ 0%, 100% { background-position: 0% 50%; }
69
+ 50% { background-position: 100% 50%; }
70
+ }
71
+
72
+ .animate-gradient {
73
+ background-size: 200% 200%;
74
+ animation: gradient-shift 8s ease infinite;
75
+ }
76
+
77
+ /* Glassmorphism utilities */
78
+ .glass {
79
+ background: rgba(15, 23, 42, 0.7);
80
+ backdrop-filter: blur(12px);
81
+ -webkit-backdrop-filter: blur(12px);
82
+ border: 1px solid rgba(255, 255, 255, 0.05);
83
+ }
84
+
85
+ /* Waveform Canvas */
86
+ #waveform {
87
+ image-rendering: pixelated;
88
+ cursor: crosshair;
89
+ }
90
+
91
+ /* Tab Transitions */
92
+ .tab-content {
93
+ transition: opacity 0.3s ease-in-out;
94
+ }
95
+
96
+ .tab-content.hidden {
97
+ display: none;
98
+ opacity: 0;
99
+ }
100
+
101
+ .tab-content:not(.hidden) {
102
+ opacity: 1;
103
+ }
104
+
105
+ /* Block Preview Items */
106
+ .block-item {
107
+ animation: slideIn 0.3s ease-out;
108
+ }
109
+
110
+ @keyframes slideIn {
111
+ from {
112
+ opacity: 0;
113
+ transform: translateX(-10px);
114
+ }
115
+ to {
116
+ opacity: 1;
117
+ transform: translateX(0);
118
+ }
119
+ }
120
+
121
+ /* Log Entries */
122
+ .log-entry {
123
+ border-left: 2px solid transparent;
124
+ padding-left: 8px;
125
+ animation: fadeIn 0.2s ease-out;
126
+ }
127
+
128
+ .log-entry.info { border-left-color: #6366f1; }
129
+ .log-entry.success { border-left-color: #10b981; }
130
+ .log-entry.warning { border-left-color: #f59e0b; }
131
+ .log-entry.error { border-left-color: #ef4444; }
132
+
133
+ @keyframes fadeIn {
134
+ from { opacity: 0; }
135
+ to { opacity: 1; }
136
+ }
137
+
138
+ /* Responsive adjustments */
139
+ @media (max-width: 768px) {
140
+ .glass {
141
+ backdrop-filter: none;
142
+ -webkit-backdrop-filter: none;
143
+ }
144
+ }
145
+
146
+ /* Custom Checkbox */
147
+ input[type="checkbox"] {
148
+ accent-color: #6366f1;
149
+ }
150
+
151
+ /* Drag over state */
152
+ .drag-over {
153
+ border-color: #6366f1 !important;
154
+ background: rgba(99, 102, 241, 0.1) !important;
155
+ }
156
+
157
+ /* Processing pulse */
158
+ .processing-pulse {
159
+ position: relative;
160
+ }
161
+
162
+ .processing-pulse::after {
163
+ content: '';
164
+ position: absolute;
165
+ inset: -2px;
166
+ border-radius: inherit;
167
+ background: linear-gradient(45deg, #6366f1, #f59e0b);
168
+ opacity: 0;
169
+ z-index: -1;
170
+ animation: pulse-ring 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
171
+ }
172
+
173
+ @keyframes pulse-ring {
174
+ 0%, 100% { opacity: 0; transform: scale(1); }
175
+ 50% { opacity: 0.5; transform: scale(1.02); }
176
+ }