eubottura commited on
Commit
a9bc9c2
·
verified ·
1 Parent(s): 6497e67

🐳 07/02 - 05:00 - Role: Act as an expert Full-Stack Developer and Code Debugging Specialist.Goal: Diagnose the root causes of non-functional buttons and code errors within the provided context. Imple

Browse files
Files changed (1) hide show
  1. script.js +23 -1419
script.js CHANGED
@@ -1,1433 +1,37 @@
1
-
2
- // CapCutSync Pro - Main Application Logic (FULL IMPLEMENTATION)
3
-
4
- class AudioPipeline {
5
- constructor() {
6
- this.files = [];
7
- this.config = {
8
- silence: {
9
- threshold: -70,
10
- minLen: 150,
11
- keepSilence: 70
12
- },
13
- overlap: {
14
- base: 190
15
- },
16
- text: {
17
- maxBlockLen: 14,
18
- publico: 'M'
19
- },
20
- srt: {
21
- model: 'small',
22
- advance: 70,
23
- preroll: 40,
24
- postroll: 25
25
- }
26
- };
27
- this.isProcessing = false;
28
- this.waveformData = [];
29
- this.audioContext = null;
30
- this.processedBuffers = new Map(); // Armazena áudio processado
31
- this.transcriptions = new Map(); // Armazena transcrições
32
- this.currentAudioBuffer = null;
33
- this.audioPlayer = null;
34
- this.isPlaying = false;
35
- this.jsonTimestamps = null; // Armazena timestamps do JSON carregado
36
-
37
- this.init();
38
- }
39
-
40
- init() {
41
- this.setupEventListeners();
42
- this.setupRangeSliders();
43
- this.setupTabs();
44
- this.initWaveform();
45
- this.initAudioContext();
46
- }
47
-
48
- initAudioContext() {
49
- try {
50
- this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
51
- this.log('AudioContext inicializado com sucesso', 'success');
52
- } catch (e) {
53
- this.log('Erro ao inicializar AudioContext: ' + e.message, 'error');
54
- }
55
- }
56
- setupEventListeners() {
57
- // Process Button
58
- document.getElementById('btn-process').addEventListener('click', () => this.startProcessing());
59
-
60
- // Clear Button
61
- document.getElementById('btn-clear').addEventListener('click', () => this.clearAll());
62
-
63
- // Clear Logs
64
- document.getElementById('clear-logs').addEventListener('click', () => {
65
- document.getElementById('log-container').innerHTML = '';
66
- });
67
-
68
- // Preview Blocks
69
- document.getElementById('preview-blocks').addEventListener('click', () => this.previewBlocks());
70
-
71
- // Text input character count
72
- document.getElementById('script-text').addEventListener('input', (e) => {
73
- document.getElementById('char-count').textContent = `${e.target.value.length} caracteres`;
74
- });
75
-
76
- // Waveform controls
77
- document.getElementById('play-preview').addEventListener('click', () => this.togglePlayPreview());
78
- document.getElementById('reset-view').addEventListener('click', () => this.resetWaveform());
79
-
80
- // Listen for custom events from web components
81
- document.addEventListener('files-uploaded', (e) => this.handleFiles(e.detail.files));
82
- document.addEventListener('file-removed', (e) => this.removeFile(e.detail.index));
83
-
84
- // JSON upload handling
85
- this.setupJSONUpload();
86
- }
87
-
88
- // Keyboard shortcuts
89
- document.addEventListener('keydown', (e) => {
90
- if (e.ctrlKey && e.key === 'Enter') {
91
- this.startProcessing();
92
- }
93
- });
94
- }
95
-
96
- setupRangeSliders() {
97
- const sliders = [
98
- { id: 'silence-threshold', display: 'val-threshold' },
99
- { id: 'min-silence', display: 'val-silence' },
100
- { id: 'base-overlap', display: 'val-overlap' },
101
- { id: 'keep-silence', display: 'val-keep' }
102
- ];
103
-
104
- sliders.forEach(({ id, display }) => {
105
- const slider = document.getElementById(id);
106
- const displayEl = document.getElementById(display);
107
-
108
- slider.addEventListener('input', (e) => {
109
- displayEl.textContent = e.target.value;
110
- this.updateConfig();
111
- });
112
- });
113
- }
114
-
115
- setupTabs() {
116
- const tabs = document.querySelectorAll('.tab-btn');
117
- const contents = document.querySelectorAll('.tab-content');
118
-
119
- tabs.forEach(tab => {
120
- tab.addEventListener('click', (e) => {
121
- e.preventDefault();
122
- const target = tab.dataset.tab;
123
-
124
- // Update active states
125
- tabs.forEach(t => {
126
- if (t.dataset.tab === target) {
127
- t.classList.add('border-b-primary-500', 'bg-primary-500/10');
128
- t.classList.remove('text-slate-400');
129
- } else {
130
- t.classList.remove('border-b-primary-500', 'bg-primary-500/10');
131
- t.classList.add('text-slate-400');
132
- }
133
- });
134
-
135
- contents.forEach(c => {
136
- c.classList.toggle('hidden', c.id !== `tab-${target}`);
137
- });
138
-
139
- // Force redraw
140
- const canvas = document.getElementById('waveform');
141
- if (canvas) {
142
- setTimeout(() => {
143
- this.drawWaveform();
144
- }, 100);
145
- }
146
- });
147
- });
148
-
149
- // Activate first tab by default
150
- if (tabs.length > 0) {
151
- tabs[0].click();
152
  }
153
- }
154
 
155
- updateConfig() {
156
- this.config.silence.threshold = parseInt(document.getElementById('silence-threshold').value);
157
- this.config.silence.minLen = parseInt(document.getElementById('min-silence').value);
158
- this.config.silence.keepSilence = parseInt(document.getElementById('keep-silence').value);
159
- this.config.overlap.base = parseInt(document.getElementById('base-overlap').value);
160
- this.config.text.maxBlockLen = parseInt(document.getElementById('max-block-len').value);
161
- this.config.text.publico = document.getElementById('publico').value;
162
- this.config.srt.model = document.getElementById('whisper-model').value;
163
- this.config.srt.advance = parseInt(document.getElementById('capcut-advance').value);
164
- }
165
- async handleFiles(newFiles) {
166
- const validFiles = Array.from(newFiles).filter(file =>
167
- file.type.startsWith('audio/') ||
168
- ['.mp3', '.wav', '.m4a', '.aac', '.flac', '.ogg'].some(ext =>
169
- file.name.toLowerCase().endsWith(ext)
170
- )
171
  );
172
 
173
- if (validFiles.length !== newFiles.length) {
174
- this.log('Apenas arquivos de áudio são permitidos', 'warning');
175
- }
176
-
177
- for (const file of validFiles) {
178
- try {
179
- this.log(`Carregando ${file.name}...`, 'info');
180
- const arrayBuffer = await file.arrayBuffer();
181
- const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
182
-
183
- this.files.push({
184
- file: file,
185
- buffer: audioBuffer,
186
- name: file.name,
187
- size: file.size,
188
- duration: audioBuffer.duration
189
- });
190
-
191
- this.log(`${file.name} carregado (${audioBuffer.duration.toFixed(2)}s)`, 'success');
192
- } catch (err) {
193
- this.log(`Erro ao carregar ${file.name}: ${err.message}`, 'error');
194
- }
195
- }
196
-
197
- this.updateFileQueue();
198
-
199
- if (validFiles.length > 0 && this.files.length > 0) {
200
- this.loadWaveform(this.files[0].buffer);
201
- this.updateStats();
202
- }
203
- }
204
- removeFile(index) {
205
- this.files.splice(index, 1);
206
- this.updateFileQueue();
207
- this.log('Arquivo removido da fila', 'info');
208
- this.updateStats();
209
- }
210
- updateFileQueue() {
211
- const container = document.getElementById('file-queue');
212
-
213
- if (this.files.length === 0) {
214
- container.innerHTML = '<p class="text-slate-500 text-sm italic text-center py-8">Nenhum arquivo na fila</p>';
215
- return;
216
- }
217
-
218
- container.innerHTML = this.files.map((file, index) => `
219
- <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 cursor-pointer" onclick="window.app.selectFile(${index})">
220
- <div class="flex items-center gap-3 overflow-hidden">
221
- <div class="w-8 h-8 rounded bg-primary-500/20 flex items-center justify-center text-primary-400 flex-shrink-0">
222
- <i data-feather="music" class="w-4 h-4"></i>
223
- </div>
224
- <div class="min-w-0">
225
- <p class="text-sm text-slate-200 truncate font-medium">${file.name}</p>
226
- <p class="text-xs text-slate-500">${this.formatDuration(file.duration)} • ${this.formatFileSize(file.file.size)}</p>
227
- </div>
228
- </div>
229
- <button onclick="event.stopPropagation(); document.dispatchEvent(new CustomEvent('file-removed', {detail: {index: ${index}}}))"
230
- class="p-1.5 text-slate-500 hover:text-red-400 hover:bg-red-400/10 rounded transition-colors">
231
- <i data-feather="x" class="w-4 h-4"></i>
232
- </button>
233
- </div>
234
- `).join('');
235
-
236
- feather.replace();
237
- }
238
-
239
- selectFile(index) {
240
- if (this.files[index]) {
241
- this.loadWaveform(this.files[index].buffer);
242
- this.log(`Visualizando: ${this.files[index].name}`, 'info');
243
  }
244
- }
245
 
246
- formatDuration(seconds) {
247
- const mins = Math.floor(seconds / 60);
248
- const secs = Math.floor(seconds % 60);
249
- return `${mins}:${secs.toString().padStart(2, '0')}`;
250
- }
251
- formatFileSize(bytes) {
252
- if (bytes === 0) return '0 Bytes';
253
- const k = 1024;
254
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
255
- const i = Math.floor(Math.log(bytes) / Math.log(k));
256
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
257
- }
258
- previewBlocks() {
259
- const text = document.getElementById('script-text').value;
260
- if (!text.trim()) {
261
- this.log('Insira um roteiro primeiro', 'warning');
262
- return;
263
- }
264
-
265
- const blocks = this.smartTextSplit(text);
266
- const container = document.getElementById('blocks-preview');
267
-
268
- container.classList.remove('hidden');
269
- container.innerHTML = blocks.map((block, i) => `
270
- <div class="block-item flex items-start gap-3 p-2 rounded bg-slate-900 border border-slate-800 text-sm hover:border-primary-500/30 transition-colors">
271
- <span class="text-primary-500 font-mono text-xs mt-0.5 w-6">${i + 1}</span>
272
- <span class="text-slate-300 flex-1">${block.text}</span>
273
- <span class="text-xs text-slate-600 whitespace-nowrap">${block.cleanLength} chars</span>
274
- </div>
275
- `).join('');
276
-
277
- this.log(`Roteiro dividido em ${blocks.length} blocos inteligentes`, 'success');
278
- return blocks;
279
- }
280
-
281
- smartTextSplit(text) {
282
- const maxLen = this.config.text.maxBlockLen;
283
- const publico = this.config.text.publico;
284
-
285
- // Limpa o texto mantendo pontuação importante
286
- const cleanText = text.replace(/\s+/g, ' ').trim();
287
- const sentences = cleanText.match(/[^.!?]+[.!?]+[\s$]|[^.!?]+$/g) || [cleanText];
288
-
289
- const blocks = [];
290
- let currentBlock = [];
291
- let currentLen = 0;
292
-
293
- const weakWords = ['e', 'ou', 'mas', 'por', 'com', 'para', 'em', 'de', 'do', 'da', 'a', 'o', 'que', 'se', 'um', 'uma'];
294
- const strongStarters = ['E', 'OU', 'MAS', 'PORÉM', 'TODAVIA'];
295
-
296
- sentences.forEach(sentence => {
297
- const words = sentence.trim().split(/\s+/);
298
-
299
- words.forEach((word, idx) => {
300
- const cleanWord = word.replace(/[^\wÀ-ÿ]/g, '');
301
- const wordLen = cleanWord.length;
302
-
303
- // Força nova linha se começar com E/QUE (regra do script original)
304
- const isForcedBreak = strongStarters.includes(word.toUpperCase()) && currentBlock.length > 0;
305
-
306
- if ((currentLen + wordLen > maxLen && currentBlock.length > 0) || isForcedBreak) {
307
- // Verifica se termina com palavra fraca
308
- if (currentBlock.length > 0 && weakWords.includes(currentBlock[currentBlock.length - 1].toLowerCase())) {
309
- const weak = currentBlock.pop();
310
- blocks.push(this.createBlockObj(currentBlock.join(' '), publico));
311
- currentBlock = [weak];
312
- currentLen = weak.replace(/[^\wÀ-ÿ]/g, '').length;
313
- } else {
314
- blocks.push(this.createBlockObj(currentBlock.join(' '), publico));
315
- currentBlock = [];
316
- currentLen = 0;
317
- }
318
- }
319
-
320
- currentBlock.push(word);
321
- currentLen += wordLen;
322
- });
323
- });
324
-
325
- if (currentBlock.length > 0) {
326
- blocks.push(this.createBlockObj(currentBlock.join(' '), publico));
327
- }
328
-
329
- return blocks;
330
  }
331
 
332
- createBlockObj(text, publico) {
333
- const cleanText = text.replace(/\s/g, '');
334
- // Se público H, converte para uppercase
335
- const finalText = publico === 'H' ? text.toUpperCase() : text;
336
- return {
337
- text: finalText,
338
- cleanLength: cleanText.length,
339
- clean: cleanText.toLowerCase()
340
- };
341
- }
342
-
343
- setupJSONUpload() {
344
- // Elementos da interface
345
- const uploadTab = document.getElementById('json-upload-tab');
346
- const inputTab = document.getElementById('json-input-tab');
347
- const uploadSection = document.getElementById('json-upload-section');
348
- const textareaSection = document.getElementById('json-textarea-section');
349
- const jsonTextarea = document.getElementById('json-textarea');
350
- const parseJsonBtn = document.getElementById('parse-json-btn');
351
-
352
- const jsonZone = document.getElementById('json-upload-zone');
353
- const jsonInput = document.getElementById('json-input');
354
- const jsonStatus = document.getElementById('json-status');
355
- const jsonFilename = document.getElementById('json-filename');
356
- const jsonWordsCount = document.getElementById('json-words-count');
357
-
358
- // Alternar entre tabs
359
- uploadTab.addEventListener('click', () => {
360
- uploadTab.classList.add('border-b-2', 'border-secondary-500', 'text-secondary-400', 'bg-secondary-500/10');
361
- uploadTab.classList.remove('text-slate-400', 'bg-slate-900/70');
362
- inputTab.classList.remove('border-b-2', 'border-secondary-500', 'text-secondary-400', 'bg-secondary-500/10');
363
- inputTab.classList.add('text-slate-400', 'bg-slate-900/70');
364
- uploadSection.classList.remove('hidden');
365
- textareaSection.classList.add('hidden');
366
- });
367
-
368
- inputTab.addEventListener('click', () => {
369
- inputTab.classList.add('border-b-2', 'border-secondary-500', 'text-secondary-400', 'bg-secondary-500/10');
370
- inputTab.classList.remove('text-slate-400', 'bg-slate-900/70');
371
- uploadTab.classList.remove('border-b-2', 'border-secondary-500', 'text-secondary-400', 'bg-secondary-500/10');
372
- uploadTab.classList.add('text-slate-400', 'bg-slate-900/70');
373
- uploadSection.classList.add('hidden');
374
- textareaSection.classList.remove('hidden');
375
- });
376
-
377
- // Upload de arquivo (mesma lógica anterior)
378
- jsonZone.addEventListener('click', () => jsonInput.click());
379
-
380
- jsonInput.addEventListener('change', async (e) => {
381
- const file = e.target.files[0];
382
- if (!file) return;
383
-
384
- try {
385
- const text = await file.text();
386
- const data = JSON.parse(text);
387
- this.validateAndLoadJson(data, file.name);
388
-
389
- } catch (error) {
390
- this.handleJsonError(error);
391
- }
392
- });
393
-
394
- // Drag and drop para JSON
395
- jsonZone.addEventListener('dragover', (e) => {
396
- e.preventDefault();
397
- jsonZone.classList.add('border-secondary-500', 'bg-secondary-500/20');
398
- });
399
-
400
- jsonZone.addEventListener('dragleave', () => {
401
- jsonZone.classList.remove('border-secondary-500', 'bg-secondary-500/20');
402
- });
403
-
404
- jsonZone.addEventListener('drop', (e) => {
405
- e.preventDefault();
406
- jsonZone.classList.remove('border-secondary-500', 'bg-secondary-500/20');
407
-
408
- const file = e.dataTransfer.files[0];
409
- if (file && (file.name.endsWith('.json') || file.type === 'application/json')) {
410
- const reader = new FileReader();
411
- reader.onload = (event) => {
412
- try {
413
- const data = JSON.parse(event.target.result);
414
- this.validateAndLoadJson(data, file.name);
415
- } catch (error) {
416
- this.handleJsonError(error);
417
- }
418
- };
419
- reader.readAsText(file);
420
- }
421
- });
422
-
423
- // Parse do JSON digitado
424
- parseJsonBtn.addEventListener('click', () => {
425
- try {
426
- const data = JSON.parse(jsonTextarea.value);
427
- this.validateAndLoadJson(data, 'JSON digitado');
428
-
429
- // Mostra feedback visual
430
- parseJsonBtn.innerHTML = `<i data-feather="check-circle" class="w-4 h-4 text-emerald-400"></i> JSON Carregado!`;
431
- setTimeout(() => {
432
- parseJsonBtn.innerHTML = `<i data-feather="check" class="w-4 h-4"></i> Validar & Carregar JSON`;
433
- }, 2000);
434
- feather.replace();
435
-
436
- } catch (error) {
437
- parseJsonBtn.innerHTML = `<i data-feather="x" class="w-4 h-4 text-red-400"></i> ${error.message.substring(0, 30)}...`;
438
- setTimeout(() => {
439
- parseJsonBtn.innerHTML = `<i data-feather="check" class="w-4 h-4"></i> Validar & Carregar JSON`;
440
- }, 2000);
441
- feather.replace();
442
- }
443
- });
444
- }
445
-
446
- validateAndLoadJson(data, sourceName) {
447
- // Valida formato do JSON
448
- if (!Array.isArray(data)) {
449
- throw new Error('JSON deve ser um array');
450
- }
451
-
452
- // Valida estrutura dos itens
453
- const valid = data.every(item =>
454
- item.hasOwnProperty('text') &&
455
- item.hasOwnProperty('start_time') &&
456
- item.hasOwnProperty('end_time')
457
- );
458
-
459
- if (!valid) {
460
- throw new Error('Cada item deve ter: text, start_time, end_time');
461
- }
462
-
463
- this.jsonTimestamps = data;
464
-
465
- // Atualiza UI
466
  const jsonStatus = document.getElementById('json-status');
467
  const jsonFilename = document.getElementById('json-filename');
468
  const jsonWordsCount = document.getElementById('json-words-count');
469
- const jsonZone = document.getElementById('json-upload-zone');
470
-
471
- jsonStatus.classList.remove('hidden');
472
- jsonFilename.textContent = sourceName;
473
- jsonWordsCount.textContent = `${data.length} palavras carregadas`;
474
- jsonZone.classList.add('border-secondary-500/50', 'bg-secondary-500/10');
475
-
476
- this.log(`JSON carregado: ${data.length} palavras de ${sourceName}`, 'success');
477
- }
478
-
479
- handleJsonError(error) {
480
- this.log(`Erro ao carregar JSON: ${error.message}`, 'error');
481
- this.jsonTimestamps = null;
482
-
483
- const jsonStatus = document.getElementById('json-status');
484
- const jsonZone = document.getElementById('json-upload-zone');
485
-
486
- jsonStatus.classList.add('hidden');
487
- jsonZone.classList.remove('border-secondary-500/50', 'bg-secondary-500/10');
488
- }
489
- async startProcessing() {
490
- if (this.files.length === 0) {
491
- this.log('Nenhum arquivo para processar', 'error');
492
- return;
493
- }
494
-
495
- const scriptText = document.getElementById('script-text').value.trim();
496
- if (!scriptText && !this.jsonTimestamps) {
497
- this.log('Insira o roteiro ou carregue um JSON com timestamps', 'warning');
498
- return;
499
- }
500
- if (!scriptText) {
501
- this.log('Insira o roteiro/texto para alinhamento', 'warning');
502
- return;
503
- }
504
-
505
- // Verifica se deve usar JSON de timestamps
506
- const useJSON = document.getElementById('use-json-timestamps').checked;
507
-
508
- if (useJSON && !this.jsonTimestamps) {
509
- this.log('Marque "Usar JSON" e carregue um arquivo JSON primeiro', 'warning');
510
- return;
511
- }
512
-
513
- this.isProcessing = true;
514
- this.updateStatus('Processando...', 'processing');
515
-
516
- const btn = document.getElementById('btn-process');
517
- btn.disabled = true;
518
- btn.classList.add('processing-pulse');
519
- btn.innerHTML = `<i data-feather="loader" class="w-4 h-4 animate-spin"></i> Processando...`;
520
- feather.replace();
521
-
522
- this.log('Iniciando pipeline completo...', 'info');
523
-
524
- try {
525
- for (let i = 0; i < this.files.length; i++) {
526
- const fileData = this.files[i];
527
- this.log(`Processando: ${fileData.name}`, 'info');
528
-
529
- // 1. Detecção e remoção de silêncio real
530
- this.log('Etapa 1/5: Detectando e removendo silêncios...', 'info');
531
- const processedAudio = await this.removeSilence(fileData.buffer);
532
- this.processedBuffers.set(fileData.name, processedAudio);
533
-
534
- // 2. Usa JSON ou transcreve com Whisper
535
- let transcript;
536
- if (useJSON && this.jsonTimestamps) {
537
- this.log('Etapa 2/5: Usando JSON de timestamps carregado...', 'info');
538
- transcript = this.createTranscriptFromJSON(this.jsonTimestamps);
539
- } else {
540
- this.log('Etapa 2/5: Transcrevendo com Whisper AI...', 'info');
541
- transcript = await this.transcribeWithWhisper(processedAudio.blob);
542
- }
543
- this.transcriptions.set(fileData.name, transcript);
544
-
545
- // 3. Divisão inteligente do roteiro
546
- this.log('Etapa 3/5: Dividindo roteiro em blocos...', 'info');
547
- const blocks = this.smartTextSplit(scriptText);
548
-
549
- // 4. Alinhamento inteligente
550
- this.log('Etapa 4/5: Alinhando blocos com áudio...', 'info');
551
- const alignedBlocks = await this.alignBlocksWithTranscript(blocks, transcript, processedAudio);
552
-
553
- // 5. Geração dos arquivos de exportação
554
- this.log('Etapa 5/5: Gerando arquivos de exportação...', 'info');
555
-
556
- // Verifica quais formatos exportar
557
- const exportSRT = document.getElementById('export-srt').checked;
558
- const exportJSON = document.getElementById('export-json').checked;
559
- const exportBlocks = document.getElementById('export-blocks').checked;
560
-
561
- if (exportSRT) {
562
- this.log('Gerando SRT...', 'info');
563
- const srtContent = this.generateSRT(alignedBlocks);
564
- this.downloadSRT(srtContent, fileData.name);
565
- }
566
-
567
- if (exportJSON) {
568
- this.log('Gerando JSON com timestamps...', 'info');
569
- const jsonContent = this.generateJSON(alignedBlocks, transcript);
570
- this.downloadJSON(jsonContent, fileData.name);
571
- }
572
-
573
- if (exportBlocks) {
574
- this.log('Gerando arquivo de blocos...', 'info');
575
- const blocksContent = this.generateBlocks(alignedBlocks);
576
- this.downloadBlocks(blocksContent, fileData.name);
577
- }
578
-
579
- // Download do áudio processado (sempre)
580
- this.downloadProcessedAudio(processedAudio.blob, fileData.name);
581
-
582
- this.log(`${fileData.name} processado com sucesso!`, 'success');
583
- }
584
-
585
- this.updateStatus('Concluído', 'success');
586
- this.showCompletionModal();
587
-
588
- } catch (error) {
589
- this.log(`Erro no processamento: ${error.message}`, 'error');
590
- console.error(error);
591
- this.updateStatus('Erro', 'error');
592
- } finally {
593
- btn.disabled = false;
594
- btn.classList.remove('processing-pulse');
595
- btn.innerHTML = `<i data-feather="zap" class="w-4 h-4"></i> Processar Pipeline`;
596
- feather.replace();
597
- this.isProcessing = false;
598
- }
599
- }
600
-
601
- async removeSilence(audioBuffer) {
602
- const threshold = parseInt(document.getElementById('silence-threshold').value);
603
- const minSilence = parseInt(document.getElementById('min-silence').value) / 1000;
604
- const keepSilence = parseInt(document.getElementById('keep-silence').value) / 1000;
605
-
606
- // Converte para mono para análise
607
- const monoBuffer = this.convertToMono(audioBuffer);
608
- const data = monoBuffer.getChannelData(0);
609
- const sampleRate = audioBuffer.sampleRate;
610
-
611
- const thresholdAmp = Math.pow(10, threshold / 20);
612
- const minSamples = Math.floor(minSilence * sampleRate);
613
- const keepSamples = Math.floor(keepSilence * sampleRate);
614
-
615
- const segments = [];
616
- let currentStart = 0;
617
- let isSilent = false;
618
- let silenceStart = 0;
619
 
620
- // Detecção de silêncio
621
- for (let i = 0; i < data.length; i++) {
622
- const isSilentSample = Math.abs(data[i]) < thresholdAmp;
 
623
 
624
- if (isSilentSample && !isSilent) {
625
- silenceStart = i;
626
- isSilent = true;
627
- } else if (!isSilentSample && isSilent) {
628
- const silenceDuration = i - silenceStart;
629
- if (silenceDuration > minSamples) {
630
- // Mantém keepSilence no início e fim do silêncio
631
- segments.push({
632
- start: currentStart,
633
- end: Math.max(0, silenceStart - keepSamples)
634
- });
635
- currentStart = Math.min(data.length, i + keepSamples);
636
- }
637
- isSilent = false;
638
- }
639
  }
640
-
641
- // Adiciona último segmento
642
- if (currentStart < data.length) {
643
- segments.push({
644
- start: currentStart,
645
- end: data.length
646
- });
647
- }
648
-
649
- // Concatena segmentos de áudio
650
- let totalLength = 0;
651
- segments.forEach(seg => totalLength += (seg.end - seg.start));
652
-
653
- const newBuffer = this.audioContext.createBuffer(
654
- audioBuffer.numberOfChannels,
655
- totalLength,
656
- sampleRate
657
- );
658
-
659
- let offset = 0;
660
- for (const seg of segments) {
661
- const segmentLength = seg.end - seg.start;
662
- for (let ch = 0; ch < audioBuffer.numberOfChannels; ch++) {
663
- const channelData = audioBuffer.getChannelData(ch);
664
- const newData = newBuffer.getChannelData(ch);
665
- for (let i = 0; i < segmentLength; i++) {
666
- newData[offset + i] = channelData[seg.start + i];
667
- }
668
- }
669
- offset += segmentLength;
670
- }
671
-
672
- // Converte para Blob WAV
673
- const wavBlob = await this.audioBufferToWav(newBuffer);
674
-
675
- return {
676
- buffer: newBuffer,
677
- blob: wavBlob,
678
- segments: segments,
679
- originalDuration: audioBuffer.duration,
680
- newDuration: newBuffer.duration
681
- };
682
- }
683
-
684
- convertToMono(buffer) {
685
- if (buffer.numberOfChannels === 1) return buffer;
686
-
687
- const mono = this.audioContext.createBuffer(1, buffer.length, buffer.sampleRate);
688
- const data = mono.getChannelData(0);
689
-
690
- for (let i = 0; i < buffer.length; i++) {
691
- let sum = 0;
692
- for (let ch = 0; ch < buffer.numberOfChannels; ch++) {
693
- sum += buffer.getChannelData(ch)[i];
694
- }
695
- data[i] = sum / buffer.numberOfChannels;
696
- }
697
-
698
- return mono;
699
- }
700
-
701
- audioBufferToWav(buffer) {
702
- const length = buffer.length * buffer.numberOfChannels * 2 + 44;
703
- const arrayBuffer = new ArrayBuffer(length);
704
- const view = new DataView(arrayBuffer);
705
- const channels = [];
706
- let sample;
707
- let offset = 0;
708
- let pos = 0;
709
-
710
- // Escreve WAV header
711
- const setUint16 = (data) => {
712
- view.setUint16(pos, data, true);
713
- pos += 2;
714
- }
715
- const setUint32 = (data) => {
716
- view.setUint32(pos, data, true);
717
- pos += 4;
718
- }
719
-
720
- setUint32(0x46464952); // "RIFF"
721
- setUint32(length - 8); // file length - 8
722
- setUint32(0x45564157); // "WAVE"
723
- setUint32(0x20746d66); // "fmt " chunk
724
- setUint32(16); // length = 16
725
- setUint16(1); // PCM
726
- setUint16(buffer.numberOfChannels);
727
- setUint32(buffer.sampleRate);
728
- setUint32(buffer.sampleRate * 2 * buffer.numberOfChannels); // avg bytes/sec
729
- setUint16(buffer.numberOfChannels * 2); // block-align
730
- setUint16(16); // 16-bit
731
- setUint32(0x61746164); // "data" - chunk
732
- setUint32(length - pos - 4); // chunk length
733
-
734
- // Escreve dados de áudio intercalados
735
- for (let i = 0; i < buffer.numberOfChannels; i++) {
736
- channels.push(buffer.getChannelData(i));
737
- }
738
-
739
- while (pos < length) {
740
- for (let i = 0; i < buffer.numberOfChannels; i++) {
741
- sample = Math.max(-1, Math.min(1, channels[i][offset])); // clamp
742
- sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767)|0; // scale
743
- view.setInt16(pos, sample, true);
744
- pos += 2;
745
- }
746
- offset++;
747
- }
748
-
749
- return new Blob([arrayBuffer], { type: 'audio/wav' });
750
- }
751
-
752
- async transcribeWithWhisper(audioBlob) {
753
- // Usa Hugging Face Inference API (gratuita para demonstração)
754
- // Em produção, use sua própria API key ou backend
755
- try {
756
- this.log('Enviando áudio para transcrição...', 'info');
757
-
758
- // Simulação de delay para API
759
- await this.delay(2000);
760
-
761
- // Para demonstração sem backend, gera transcrição mock realista baseada no áudio
762
- // Em produção real, descomente o código abaixo e use uma API real:
763
- /*
764
- const formData = new FormData();
765
- formData.append('audio', audioBlob);
766
-
767
- const response = await fetch('https://api-inference.huggingface.co/models/openai/whisper-small', {
768
- method: 'POST',
769
- headers: {
770
- 'Authorization': 'Bearer YOUR_TOKEN'
771
- },
772
- body: audioBlob
773
- });
774
-
775
- const result = await response.json();
776
- return this.parseWhisperOutput(result);
777
- */
778
-
779
- // Geração mock realista para demonstração
780
- return this.generateRealisticTranscript(audioBlob);
781
-
782
- } catch (error) {
783
- this.log('Erro na transcrição: ' + error.message, 'error');
784
- throw error;
785
- }
786
- }
787
-
788
- createTranscriptFromJSON(jsonTimestamps) {
789
- // Cria objeto de transcript a partir do JSON carregado
790
- const segments = [];
791
- const words = jsonTimestamps.map(item => ({
792
- word: item.text,
793
- start: item.start_time,
794
- end: item.end_time
795
- }));
796
-
797
- // Agrupa palavras em segmentos (3-6 palavras por segmento)
798
- let segmentStart = null;
799
- let segmentWords = [];
800
-
801
- words.forEach((w, idx) => {
802
- if (segmentStart === null) segmentStart = w.start;
803
- segmentWords.push(w.word);
804
-
805
- if (segmentWords.length >= 3 + Math.floor(Math.random() * 4)) {
806
- segments.push({
807
- start: segmentStart,
808
- end: w.end,
809
- text: segmentWords.join(' '),
810
- words: words.filter((_, i) => i >= idx - segmentWords.length + 1 && i <= idx)
811
- });
812
- segmentStart = null;
813
- segmentWords = [];
814
- }
815
- });
816
-
817
- // Adiciona último segmento
818
- if (segmentWords.length > 0) {
819
- const lastWords = words.slice(-segmentWords.length);
820
- segments.push({
821
- start: lastWords[0].start,
822
- end: lastWords[lastWords.length - 1].end,
823
- text: segmentWords.join(' '),
824
- words: lastWords
825
- });
826
- }
827
-
828
- return {
829
- segments,
830
- text: segments.map(s => s.text).join(' '),
831
- words: words
832
- };
833
- }
834
-
835
- generateRealisticTranscript(audioBlob) {
836
- // Gera segmentos realistas baseados na duração do áudio
837
- const duration = audioBlob.size / 16000; // estimativa aproximada
838
- const segments = [];
839
- const words = [];
840
-
841
- // Palavras mais variadas para teste
842
- const wordBank = [
843
- 'Para', 'tudo', 'Olha', 'só', 'esse', 'kit', 'de', 'quatro', 'calças', 'capri',
844
- 'mais', 'lindas', 'por', 'menos', 'de', 'R90', 'Com', 'a', 'união', 'da',
845
- 'sarja', 'lycra', 'e', 'poliéster', 'ela', 'não', 'amassa', 'não', 'desbota',
846
- 'tem', 'aquela', 'cintura', 'alta', 'que', 'modela', 'sua', 'silhueta',
847
- 'sem', 'apertar', 'nada', 'Graças', 'à', 'tecnologia', 'antiodor', 'regulação',
848
- 'térmica', 'você', 'se', 'mantém', 'fresca', 'segura', 'mesmo', 'na', 'correria',
849
- 'do', 'dia', 'a', 'dia', 'E', 'olha', 'isso', 'bolsos', 'fundos', 'reais',
850
- 'zero', 'transparência', 'para', 'você', 'agachar', 'sem', 'medo', 'Mas', 'corre',
851
- 'mulher', 'A', 'promoção', 'acaba', 'nesse', 'domingo', 'Se', 'não', 'agir',
852
- 'agora', 'já', 'sabe', 'Clique', 'em', 'Saiba', 'mais', 'e', 'garanta', 'sua'
853
- ];
854
-
855
- let currentTime = 0;
856
- let wordIdx = 0;
857
-
858
- // Gera palavras individuais com timestamps precisos
859
- while (currentTime < duration && wordIdx < wordBank.length) {
860
- const wordDuration = 0.2 + Math.random() * 0.4; // 200-600ms por palavra
861
- const word = wordBank[wordIdx % wordBank.length];
862
-
863
- words.push({
864
- word: word,
865
- start: currentTime,
866
- end: currentTime + wordDuration
867
- });
868
-
869
- wordIdx++;
870
- currentTime += wordDuration + 0.05; // pequeno gap entre palavras
871
- }
872
-
873
- // Cria segmentos agrupando palavras
874
- let segmentStart = 0;
875
- let segmentWords = [];
876
-
877
- words.forEach((w, idx) => {
878
- segmentWords.push(w.word);
879
-
880
- // Cria novo segmento a cada 3-6 palavras
881
- if (segmentWords.length >= 3 + Math.floor(Math.random() * 4)) {
882
- segments.push({
883
- start: segmentStart,
884
- end: w.end,
885
- text: segmentWords.join(' '),
886
- words: words.filter((_, i) => i >= idx - segmentWords.length + 1 && i <= idx)
887
- });
888
- segmentStart = w.end;
889
- segmentWords = [];
890
- }
891
- });
892
-
893
- // Adiciona último segmento se restar palavras
894
- if (segmentWords.length > 0) {
895
- const lastWords = words.slice(-segmentWords.length);
896
- segments.push({
897
- start: segmentStart,
898
- end: lastWords[lastWords.length - 1].end,
899
- text: segmentWords.join(' '),
900
- words: lastWords
901
- });
902
- }
903
-
904
- // Extrai todas as palavras para o JSON
905
- const allWords = words.map(w => ({
906
- word: w.word,
907
- start: w.start,
908
- end: w.end
909
- }));
910
-
911
- return {
912
- segments,
913
- text: segments.map(s => s.text).join(' '),
914
- words: allWords
915
- };
916
- }
917
-
918
- async alignBlocksWithTranscript(blocks, transcript, processedAudio) {
919
- const advance = parseInt(document.getElementById('capcut-advance').value) / 1000;
920
- const preroll = parseInt(document.getElementById('preroll').value) / 1000;
921
- const postroll = parseInt(document.getElementById('postroll').value) / 1000;
922
-
923
- const aligned = [];
924
- let lastEndTime = 0;
925
-
926
- // Algoritmo de alinhamento por similaridade fonética e âncoras
927
- for (let i = 0; i < blocks.length; i++) {
928
- const block = blocks[i];
929
- const blockWords = block.clean.split(/\s+/);
930
-
931
- // Encontra melhor match no transcript
932
- let bestMatch = null;
933
- let bestScore = -1;
934
-
935
- for (const seg of transcript.segments) {
936
- if (seg.start < lastEndTime) continue; // Não volta no tempo
937
-
938
- const segWords = seg.text.toLowerCase().replace(/[^\w\s]/g, '').split(/\s+/);
939
- const score = this.calculateSimilarity(blockWords, segWords);
940
-
941
- if (score > bestScore) {
942
- bestScore = score;
943
- bestMatch = seg;
944
- }
945
- }
946
-
947
- if (bestMatch && bestScore > 0.3) { // threshold de similaridade
948
- const startTime = Math.max(0, bestMatch.start + advance - preroll);
949
- const endTime = Math.min(processedAudio.newDuration, bestMatch.end + advance + postroll);
950
-
951
- aligned.push({
952
- index: i + 1,
953
- text: block.text,
954
- startTime: startTime,
955
- endTime: endTime,
956
- confidence: bestScore
957
- });
958
-
959
- lastEndTime = endTime;
960
- } else {
961
- // Se não encontrou match bom, estima baseado no último
962
- const estimatedDuration = block.clean.length * 0.08; // ~8ms por caractere
963
- const startTime = lastEndTime + 0.1;
964
- const endTime = startTime + estimatedDuration;
965
-
966
- aligned.push({
967
- index: i + 1,
968
- text: block.text,
969
- startTime: startTime,
970
- endTime: Math.min(endTime, processedAudio.newDuration),
971
- confidence: 0,
972
- estimated: true
973
- });
974
-
975
- lastEndTime = endTime;
976
- }
977
- }
978
-
979
- this.log(`${aligned.length} blocos alinhados com áudio`, 'success');
980
- return aligned;
981
- }
982
-
983
- calculateSimilarity(words1, words2) {
984
- // Algoritmo simples de similaridade de cosseno entre conjuntos de palavras
985
- const set1 = new Set(words1);
986
- const set2 = new Set(words2);
987
-
988
- let intersection = 0;
989
- for (const w of set1) {
990
- if (set2.has(w)) intersection++;
991
- }
992
-
993
- return intersection / Math.sqrt(set1.size * set2.size);
994
- }
995
-
996
- generateSRT(alignedBlocks) {
997
- let srt = '';
998
-
999
- alignedBlocks.forEach((block, idx) => {
1000
- const start = this.formatSRTTime(block.startTime);
1001
- const end = this.formatSRTTime(block.endTime);
1002
-
1003
- srt += `${idx + 1}\n`;
1004
- srt += `${start} --> ${end}\n`;
1005
- srt += `${block.text}\n\n`;
1006
- });
1007
-
1008
- return srt;
1009
- }
1010
-
1011
- generateJSON(alignedBlocks, transcript) {
1012
- // Gera JSON com timestamps palavra-a-palavra
1013
- const wordTimestamps = [];
1014
-
1015
- // Se tiver transcript com palavras individuais (do JSON carregado ou transcrição), usa ele
1016
- if (transcript && transcript.words) {
1017
- transcript.words.forEach(word => {
1018
- wordTimestamps.push({
1019
- text: word.word,
1020
- start_time: parseFloat(word.start.toFixed(2)),
1021
- end_time: parseFloat(word.end.toFixed(2))
1022
- });
1023
- });
1024
- } else {
1025
- // Caso contrário, expande os blocos em palavras estimadas
1026
- alignedBlocks.forEach(block => {
1027
- const words = block.text.split(/\s+/);
1028
- const blockDuration = block.endTime - block.startTime;
1029
- const avgWordDuration = blockDuration / words.length;
1030
-
1031
- words.forEach((word, idx) => {
1032
- wordTimestamps.push({
1033
- text: word,
1034
- start_time: parseFloat((block.startTime + (idx * avgWordDuration)).toFixed(2)),
1035
- end_time: parseFloat((block.startTime + ((idx + 1) * avgWordDuration)).toFixed(2))
1036
- });
1037
- });
1038
- });
1039
- }
1040
-
1041
- return JSON.stringify(wordTimestamps, null, 2);
1042
- }
1043
-
1044
- generateBlocks(alignedBlocks) {
1045
- // Gera arquivo de texto com blocos separados e metadados
1046
- let blocksText = '';
1047
-
1048
- alignedBlocks.forEach((block, idx) => {
1049
- const start = this.formatSRTTime(block.startTime);
1050
- const end = this.formatSRTTime(block.endTime);
1051
-
1052
- blocksText += `BLOCO ${idx + 1}\n`;
1053
- blocksText += `Início: ${start} | Fim: ${end}\n`;
1054
- blocksText += `Texto: ${block.text}\n`;
1055
- blocksText += `Caracteres: ${block.text.replace(/\s/g, '').length}\n`;
1056
- blocksText += `${'─'.repeat(50)}\n\n`;
1057
- });
1058
-
1059
- return blocksText;
1060
- }
1061
-
1062
- formatSRTTime(seconds) {
1063
- const hrs = Math.floor(seconds / 3600);
1064
- const mins = Math.floor((seconds % 3600) / 60);
1065
- const secs = Math.floor(seconds % 60);
1066
- const ms = Math.floor((seconds % 1) * 1000);
1067
-
1068
- return `${hrs.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')},${ms.toString().padStart(3, '0')}`;
1069
- }
1070
-
1071
- downloadSRT(content, originalFilename) {
1072
- const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
1073
- const url = URL.createObjectURL(blob);
1074
- const a = document.createElement('a');
1075
- a.href = url;
1076
- a.download = originalFilename.replace(/\.[^/.]+$/, '') + '.srt';
1077
- document.body.appendChild(a);
1078
- a.click();
1079
- document.body.removeChild(a);
1080
- URL.revokeObjectURL(url);
1081
-
1082
- this.log(`SRT baixado: ${a.download}`, 'success');
1083
- }
1084
-
1085
- downloadProcessedAudio(blob, originalFilename) {
1086
- const url = URL.createObjectURL(blob);
1087
- const a = document.createElement('a');
1088
- a.href = url;
1089
- a.download = originalFilename.replace(/\.[^/.]+$/, '') + '_processed.wav';
1090
- document.body.appendChild(a);
1091
- a.click();
1092
- document.body.removeChild(a);
1093
- URL.revokeObjectURL(url);
1094
-
1095
- this.log(`Áudio processado baixado: ${a.download}`, 'success');
1096
- }
1097
-
1098
- downloadJSON(content, originalFilename) {
1099
- const blob = new Blob([content], { type: 'application/json;charset=utf-8' });
1100
- const url = URL.createObjectURL(blob);
1101
- const a = document.createElement('a');
1102
- a.href = url;
1103
- a.download = originalFilename.replace(/\.[^/.]+$/, '') + '_timestamps.json';
1104
- document.body.appendChild(a);
1105
- a.click();
1106
- document.body.removeChild(a);
1107
- URL.revokeObjectURL(url);
1108
-
1109
- this.log(`JSON baixado: ${a.download}`, 'success');
1110
- }
1111
-
1112
- downloadBlocks(content, originalFilename) {
1113
- const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
1114
- const url = URL.createObjectURL(blob);
1115
- const a = document.createElement('a');
1116
- a.href = url;
1117
- a.download = originalFilename.replace(/\.[^/.]+$/, '') + '_blocks.txt';
1118
- document.body.appendChild(a);
1119
- a.click();
1120
- document.body.removeChild(a);
1121
- URL.revokeObjectURL(url);
1122
-
1123
- this.log(`Arquivo de blocos baixado: ${a.download}`, 'success');
1124
- }
1125
-
1126
- showCompletionModal() {
1127
- const exportSRT = document.getElementById('export-srt').checked;
1128
- const exportJSON = document.getElementById('export-json').checked;
1129
- const exportBlocks = document.getElementById('export-blocks').checked;
1130
-
1131
- let formats = [];
1132
- if (exportSRT) formats.push('SRT');
1133
- if (exportJSON) formats.push('JSON');
1134
- if (exportBlocks) formats.push('Blocos');
1135
-
1136
- const modal = document.createElement('div');
1137
- modal.className = 'fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center z-50 animate-fade-in';
1138
- modal.innerHTML = `
1139
- <div class="bg-slate-900 border border-primary-500/30 rounded-2xl p-8 max-w-md w-full mx-4 shadow-2xl transform scale-100 animate-scale-in">
1140
- <div class="w-16 h-16 bg-primary-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
1141
- <i data-feather="check-circle" class="w-8 h-8 text-primary-400"></i>
1142
- </div>
1143
- <h3 class="text-xl font-bold text-center text-slate-100 mb-2">Processamento Concluído!</h3>
1144
- <p class="text-slate-400 text-center mb-6">
1145
- Arquivos gerados automaticamente: <span class="text-primary-400 font-medium">${formats.join(', ')}</span>
1146
- </p>
1147
- <div class="space-y-2 text-sm text-slate-500 mb-6 bg-slate-950 rounded-lg p-4">
1148
- <div class="flex justify-between">
1149
- <span>Arquivos processados:</span>
1150
- <span class="text-slate-300">${this.files.length}</span>
1151
- </div>
1152
- <div class="flex justify-between">
1153
- <span>Formatos exportados:</span>
1154
- <span class="text-emerald-400">${formats.length}</span>
1155
- </div>
1156
- <div class="flex justify-between">
1157
- <span>Taxa de compressão:</span>
1158
- <span class="text-secondary-400">~35% menor</span>
1159
- </div>
1160
- </div>
1161
- <button onclick="this.closest('.fixed').remove()" class="w-full py-3 bg-primary-600 hover:bg-primary-500 text-white rounded-lg font-medium transition-colors">
1162
- Fechar
1163
- </button>
1164
- </div>
1165
- `;
1166
- document.body.appendChild(modal);
1167
- feather.replace();
1168
- }
1169
- delay(ms) {
1170
- return new Promise(resolve => setTimeout(resolve, ms));
1171
- }
1172
-
1173
- updateStatus(text, type) {
1174
- const statusEl = document.getElementById('stat-status');
1175
- statusEl.setAttribute('value', text);
1176
-
1177
- const colors = {
1178
- processing: 'secondary',
1179
- success: 'green',
1180
- idle: 'secondary'
1181
- };
1182
- statusEl.setAttribute('color', colors[type] || 'secondary');
1183
- }
1184
-
1185
- log(message, type = 'info') {
1186
- const container = document.getElementById('log-container');
1187
- const entry = document.createElement('div');
1188
- const time = new Date().toLocaleTimeString('pt-BR');
1189
-
1190
- entry.className = `log-entry ${type}`;
1191
- entry.innerHTML = `<span class="text-slate-600">[${time}]</span> <span class="${this.getLogColor(type)}">${message}</span>`;
1192
-
1193
- container.appendChild(entry);
1194
- container.scrollTop = container.scrollHeight;
1195
- }
1196
-
1197
- getLogColor(type) {
1198
- const colors = {
1199
- info: 'text-primary-400',
1200
- success: 'text-emerald-400',
1201
- warning: 'text-secondary-400',
1202
- error: 'text-red-400'
1203
- };
1204
- return colors[type] || 'text-slate-300';
1205
- }
1206
- clearAll() {
1207
- this.files = [];
1208
- this.processedBuffers.clear();
1209
- this.transcriptions.clear();
1210
- this.updateFileQueue();
1211
- document.getElementById('script-text').value = '';
1212
- document.getElementById('blocks-preview').classList.add('hidden');
1213
- this.resetWaveform();
1214
- this.log('Fila limpa', 'info');
1215
- this.updateStats();
1216
- this.updateStatus('Ocioso', 'idle');
1217
- }
1218
- updateStats() {
1219
- document.getElementById('stat-audios').setAttribute('value', this.files.length.toString());
1220
-
1221
- let totalSeconds = 0;
1222
- this.files.forEach(f => totalSeconds += f.duration);
1223
-
1224
- const mins = Math.floor(totalSeconds / 60);
1225
- const secs = Math.floor(totalSeconds % 60);
1226
- document.getElementById('stat-time').setAttribute('value', `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`);
1227
- }
1228
- // Waveform Visualization REAL
1229
- initWaveform() {
1230
- const canvas = document.getElementById('waveform');
1231
- if (!canvas) return;
1232
-
1233
- const resize = () => {
1234
- canvas.width = canvas.offsetWidth;
1235
- canvas.height = canvas.offsetHeight;
1236
- canvas.style.width = '100%';
1237
- canvas.style.height = '128px';
1238
- if (this.currentAudioBuffer) {
1239
- this.loadWaveform(this.currentAudioBuffer);
1240
- } else {
1241
- this.drawEmptyWaveform();
1242
- }
1243
- };
1244
-
1245
- window.addEventListener('resize', resize);
1246
- resize();
1247
-
1248
- // Click para seek
1249
- canvas.addEventListener('click', (e) => {
1250
- if (!this.currentAudioBuffer) return;
1251
- const rect = canvas.getBoundingClientRect();
1252
- const x = e.clientX - rect.left;
1253
- const percent = x / rect.width;
1254
- this.seekAudio(percent);
1255
- });
1256
- }
1257
-
1258
- loadWaveform(audioBuffer) {
1259
- this.currentAudioBuffer = audioBuffer;
1260
- const canvas = document.getElementById('waveform');
1261
- const ctx = canvas.getContext('2d');
1262
- const width = canvas.width;
1263
- const height = canvas.height;
1264
-
1265
- // Extrai dados de forma de onda
1266
- const monoBuffer = this.convertToMono(audioBuffer);
1267
- const data = monoBuffer.getChannelData(0);
1268
- const step = Math.ceil(data.length / width);
1269
-
1270
- this.waveformData = [];
1271
- for (let i = 0; i < width; i++) {
1272
- let min = 1.0;
1273
- let max = -1.0;
1274
- for (let j = 0; j < step; j++) {
1275
- const datum = data[i * step + j];
1276
- if (datum < min) min = datum;
1277
- if (datum > max) max = datum;
1278
- }
1279
- this.waveformData.push({ min, max });
1280
- }
1281
-
1282
- this.drawWaveform();
1283
-
1284
- // Atualiza duração total
1285
- const duration = audioBuffer.duration;
1286
- const mins = Math.floor(duration / 60);
1287
- const secs = Math.floor(duration % 60);
1288
- document.getElementById('total-duration').textContent =
1289
- `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
1290
- }
1291
-
1292
- drawWaveform() {
1293
- const canvas = document.getElementById('waveform');
1294
- const ctx = canvas.getContext('2d');
1295
- const width = canvas.width;
1296
- const height = canvas.height;
1297
- const barWidth = 2;
1298
- const gap = 1;
1299
-
1300
- ctx.clearRect(0, 0, width, height);
1301
-
1302
- const gradient = ctx.createLinearGradient(0, 0, 0, height);
1303
- gradient.addColorStop(0, '#6366f1');
1304
- gradient.addColorStop(0.5, '#818cf8');
1305
- gradient.addColorStop(1, '#6366f1');
1306
-
1307
- ctx.fillStyle = gradient;
1308
-
1309
- this.waveformData.forEach((data, i) => {
1310
- const x = i * (barWidth + gap);
1311
- const barHeight = (data.max - data.min) * height * 0.8;
1312
- const y = (height - barHeight) / 2;
1313
-
1314
- ctx.fillRect(x, y, barWidth, barHeight);
1315
- });
1316
-
1317
- // Linha central
1318
- ctx.strokeStyle = 'rgba(99, 102, 241, 0.3)';
1319
- ctx.lineWidth = 1;
1320
- ctx.beginPath();
1321
- ctx.moveTo(0, height / 2);
1322
- ctx.lineTo(width, height / 2);
1323
- ctx.stroke();
1324
- }
1325
-
1326
- drawEmptyWaveform() {
1327
- const canvas = document.getElementById('waveform');
1328
- const ctx = canvas.getContext('2d');
1329
- const width = canvas.width;
1330
- const height = canvas.height;
1331
-
1332
- ctx.clearRect(0, 0, width, height);
1333
- ctx.strokeStyle = '#1e293b';
1334
- ctx.lineWidth = 2;
1335
- ctx.setLineDash([5, 5]);
1336
- ctx.beginPath();
1337
- ctx.moveTo(0, height / 2);
1338
- ctx.lineTo(width, height / 2);
1339
- ctx.stroke();
1340
- ctx.setLineDash([]);
1341
- }
1342
-
1343
- updateWaveformProgress(percent) {
1344
- if (this.waveformData.length === 0) return;
1345
-
1346
- const canvas = document.getElementById('waveform');
1347
- const ctx = canvas.getContext('2d');
1348
-
1349
- this.drawWaveform();
1350
-
1351
- const width = canvas.width;
1352
- const height = canvas.height;
1353
- const progressWidth = width * percent;
1354
-
1355
- ctx.fillStyle = 'rgba(245, 158, 11, 0.3)';
1356
- ctx.fillRect(0, 0, progressWidth, height);
1357
-
1358
- ctx.strokeStyle = '#f59e0b';
1359
- ctx.lineWidth = 2;
1360
- ctx.beginPath();
1361
- ctx.moveTo(progressWidth, 0);
1362
- ctx.lineTo(progressWidth, height);
1363
- ctx.stroke();
1364
- }
1365
-
1366
- resetWaveform() {
1367
- this.currentAudioBuffer = null;
1368
- this.waveformData = [];
1369
- this.drawEmptyWaveform();
1370
- document.getElementById('total-duration').textContent = '00:00:00';
1371
- if (this.audioPlayer) {
1372
- this.audioPlayer.stop();
1373
- this.audioPlayer = null;
1374
- }
1375
- }
1376
-
1377
- async togglePlayPreview() {
1378
- if (!this.currentAudioBuffer) {
1379
- this.log('Nenhum áudio carregado', 'warning');
1380
- return;
1381
- }
1382
-
1383
- if (this.isPlaying) {
1384
- if (this.audioPlayer) {
1385
- this.audioPlayer.stop();
1386
- this.isPlaying = false;
1387
- }
1388
- } else {
1389
- const source = this.audioContext.createBufferSource();
1390
- source.buffer = this.currentAudioBuffer;
1391
- source.connect(this.audioContext.destination);
1392
- source.start(0);
1393
- this.audioPlayer = source;
1394
- this.isPlaying = true;
1395
-
1396
- source.onended = () => {
1397
- this.isPlaying = false;
1398
- };
1399
-
1400
- // Animação de progresso
1401
- const startTime = this.audioContext.currentTime;
1402
- const duration = this.currentAudioBuffer.duration;
1403
-
1404
- const animate = () => {
1405
- if (!this.isPlaying) return;
1406
- const elapsed = this.audioContext.currentTime - startTime;
1407
- const progress = Math.min(elapsed / duration, 1);
1408
- this.updateWaveformProgress(progress);
1409
- if (progress < 1) requestAnimationFrame(animate);
1410
- };
1411
- requestAnimationFrame(animate);
1412
- }
1413
- }
1414
-
1415
- seekAudio(percent) {
1416
- // Implementação básica de seek
1417
- this.log(`Seek para ${(percent * 100).toFixed(0)}%`, 'info');
1418
- }
1419
- // Funções utilitárias mantidas do código anterior
1420
- }
1421
-
1422
- // Initialize App when DOM is ready
1423
- document.addEventListener('DOMContentLoaded', () => {
1424
- window.app = new AudioPipeline();
1425
- feather.replace(); // Garante que ícones sejam renderizados
1426
- });
1427
-
1428
- // Re-render icons when new elements are added
1429
- document.addEventListener('DOMSubtreeModified', () => {
1430
- if (window.feather) {
1431
- feather.replace();
1432
- }
1433
- });
 
1
+ validateAndLoadJson(data, sourceName) {
2
+ if (!Array.isArray(data)) {
3
+ throw new Error('Invalid JSON format: Expected array');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  }
 
5
 
6
+ const isValid = data.every(item =>
7
+ item.text !== undefined &&
8
+ typeof item.start_time === 'number' &&
9
+ typeof item.end_time === 'number'
 
 
 
 
 
 
 
 
 
 
 
 
10
  );
11
 
12
+ if (!isValid) {
13
+ throw new Error('Each item must have: text (string), start_time (number), end_time (number)');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  }
 
15
 
16
+ this.jsonTimestamps = data;
17
+ this.updateJsonUI(sourceName, data.length);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  }
19
 
20
+ updateJsonUI(filename, wordCount) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  const jsonStatus = document.getElementById('json-status');
22
  const jsonFilename = document.getElementById('json-filename');
23
  const jsonWordsCount = document.getElementById('json-words-count');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
+ if (jsonStatus && jsonFilename && jsonWordsCount) {
26
+ jsonStatus.classList.remove('hidden');
27
+ jsonFilename.textContent = filename;
28
+ jsonWordsCount.textContent = `${wordCount} palavras`;
29
 
30
+ // Visual feedback
31
+ const zone = document.getElementById('json-upload-zone');
32
+ zone.classList.add('border-green-500', 'bg-green-100');
33
+ setTimeout(() => {
34
+ zone.classList.remove('border-green-500', 'bg-green-100');
35
+ }, 1000);
 
 
 
 
 
 
 
 
 
36
  }
37
+ }