Spaces:
Running
Deve respeitar esses criterios, === PROJETO ESTRUTURADO PARA ANÁLISE ===
Browse filesTotal 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
- README.md +8 -5
- components/navbar.js +67 -0
- components/sidebar.js +83 -0
- components/stat-card.js +55 -0
- components/upload-zone.js +112 -0
- index.html +316 -19
- script.js +545 -0
- style.css +167 -19
|
@@ -1,10 +1,13 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
| 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).
|
|
@@ -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);
|
|
@@ -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);
|
|
@@ -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);
|
|
@@ -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);
|
|
@@ -1,19 +1,316 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|
|
@@ -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 |
+
});
|
|
@@ -1,28 +1,176 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
| 4 |
}
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
margin-top: 0;
|
| 9 |
}
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
margin-bottom: 10px;
|
| 15 |
-
margin-top: 5px;
|
| 16 |
}
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
margin: 0 auto;
|
| 21 |
-
padding: 16px;
|
| 22 |
-
border: 1px solid lightgray;
|
| 23 |
-
border-radius: 16px;
|
| 24 |
}
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|