RaiSantos commited on
Commit
8691692
·
1 Parent(s): 7c8ccca

teste claude 1

Browse files
Files changed (10) hide show
  1. .gitattributes +0 -35
  2. Dockerfile +25 -5
  3. app.py +128 -127
  4. readme_md.md +165 -0
  5. requirements.txt +10 -5
  6. static/main.js +0 -106
  7. templates/index.html +300 -661
  8. templates/processing.html +238 -0
  9. templates/results.html +275 -0
  10. transcriptor.py +107 -0
.gitattributes DELETED
@@ -1,35 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile CHANGED
@@ -1,15 +1,35 @@
1
- FROM python:3.9-slim
2
 
 
 
 
 
 
 
 
 
 
3
  WORKDIR /app
4
 
 
5
  COPY requirements.txt .
 
 
6
  RUN pip install --no-cache-dir -r requirements.txt
7
 
 
8
  COPY . .
9
 
10
- # Criar diretórios com permissão total de escrita
11
- RUN mkdir -p /data/uploads && chmod -R 777 /data/uploads
12
- RUN mkdir -p /data/transcriptions && chmod -R 777 /data/transcriptions
13
 
 
 
 
 
 
 
14
  EXPOSE 7860
15
- CMD ["python", "app.py"]
 
 
 
1
+ FROM python:3.10-slim
2
 
3
+ # Instalar dependências do sistema
4
+ RUN apt-get update && apt-get install -y \
5
+ ffmpeg \
6
+ git \
7
+ wget \
8
+ build-essential \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Definir diretório de trabalho
12
  WORKDIR /app
13
 
14
+ # Copiar arquivos de dependências
15
  COPY requirements.txt .
16
+
17
+ # Instalar dependências Python
18
  RUN pip install --no-cache-dir -r requirements.txt
19
 
20
+ # Copiar código da aplicação
21
  COPY . .
22
 
23
+ # Criar diretórios necessários
24
+ RUN mkdir -p uploads outputs templates
 
25
 
26
+ # Definir variáveis de ambiente
27
+ ENV FLASK_APP=app.py
28
+ ENV FLASK_ENV=production
29
+ ENV PYTHONUNBUFFERED=1
30
+
31
+ # Porta padrão do Hugging Face Spaces
32
  EXPOSE 7860
33
+
34
+ # Comando para executar a aplicação
35
+ CMD ["python", "app.py"]
app.py CHANGED
@@ -1,145 +1,146 @@
1
- from flask import Flask, render_template, request, jsonify, send_file
2
  import os
3
- import torch
4
- import whisperx
5
  import json
6
- from datetime import datetime
 
 
7
 
8
  app = Flask(__name__)
9
- app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 # 100MB máx
10
 
11
- # Novos caminhos válidos no Hugging Face
12
- UPLOAD_FOLDER = '/data/uploads'
13
- TRANSCRIPTIONS_FOLDER = '/data/transcriptions'
14
- SCORE_MINIMO = 0.5
15
 
16
- # Criar pastas com permissão
17
- for folder in [UPLOAD_FOLDER, TRANSCRIPTIONS_FOLDER]:
18
- os.makedirs(folder, exist_ok=True)
19
 
20
- @app.route('/')
21
- def index():
22
- return render_template('index.html')
23
-
24
- @app.route('/api/transcribe', methods=['POST'])
25
- def transcribe():
26
- try:
27
- if 'audio' not in request.files:
28
- return jsonify({'error': 'Nenhum arquivo de áudio enviado'}), 400
29
-
30
- audio_file = request.files['audio']
31
- filename = request.form.get('filename', f'transcricao_{datetime.now().strftime("%Y%m%d_%H%M%S")}')
32
- filename = "".join(c for c in filename if c.isalnum() or c in (' ', '-', '_')).rstrip()
33
-
34
- temp_path = os.path.join(UPLOAD_FOLDER, f"temp_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{audio_file.filename}")
35
- audio_file.save(temp_path)
36
-
37
- device = "cuda" if torch.cuda.is_available() else "cpu"
38
- compute_type = "float16" if device == "cuda" else "int8"
39
 
40
- model = whisperx.load_model("medium", device, compute_type=compute_type, language="pt")
41
- audio = whisperx.load_audio(temp_path)
42
- result = model.transcribe(audio)
43
 
44
- align_model, metadata = whisperx.load_align_model(language_code="pt", device=device)
45
- aligned = whisperx.align(result["segments"], align_model, metadata, audio, device)
46
 
47
- words = []
48
- for word in aligned["word_segments"]:
49
- if word["score"] < SCORE_MINIMO or not word["word"].strip():
50
- continue
51
- words.append({
52
- "word": word["word"].capitalize(),
53
- "start": round(word["start"], 3),
54
- "end": round(word["end"], 3),
55
- "score": round(word["score"], 3)
56
- })
57
-
58
- output = {
59
- "metadata": {
60
- "total_words": len(words),
61
- "arquivo": filename,
62
- "modelo": "WhisperX medium",
63
- "created_at": datetime.now().isoformat()
64
- },
65
- "words": words
66
- }
67
-
68
- json_filename = f"{filename}.json"
69
- json_path = os.path.join(TRANSCRIPTIONS_FOLDER, json_filename)
70
- with open(json_path, 'w', encoding='utf-8') as f:
71
- json.dump(output, f, ensure_ascii=False, indent=2)
72
-
73
- if os.path.exists(temp_path):
74
- os.remove(temp_path)
75
-
76
- return jsonify({
77
- 'success': True,
78
- 'filename': json_filename,
79
- 'word_count': len(words),
80
- 'duration': max(w['end'] for w in words) if words else 0
81
- })
82
-
83
- except Exception as e:
84
- return jsonify({'error': f'Erro na transcrição: {str(e)}'}), 500
85
-
86
- @app.route('/api/transcriptions', methods=['GET'])
87
- def list_transcriptions():
88
  try:
89
- files = []
90
- for filename in os.listdir(TRANSCRIPTIONS_FOLDER):
91
- if filename.endswith('.json'):
92
- filepath = os.path.join(TRANSCRIPTIONS_FOLDER, filename)
93
- stat = os.stat(filepath)
94
- try:
95
- with open(filepath, 'r', encoding='utf-8') as f:
96
- data = json.load(f)
97
- metadata = data.get('metadata', {})
98
- except:
99
- metadata = {}
100
- files.append({
101
- 'filename': filename,
102
- 'name': filename.replace('.json', ''),
103
- 'size': stat.st_size,
104
- 'created': datetime.fromtimestamp(stat.st_mtime).isoformat(),
105
- 'word_count': metadata.get('total_words', 0)
106
- })
107
- files.sort(key=lambda x: x['created'], reverse=True)
108
- return jsonify(files)
 
 
 
 
 
 
 
 
 
 
 
109
  except Exception as e:
110
- return jsonify({'error': str(e)}), 500
 
111
 
112
- @app.route('/api/transcription/<filename>')
113
- def get_transcription(filename):
114
- try:
115
- filepath = os.path.join(TRANSCRIPTIONS_FOLDER, filename)
116
- if not os.path.exists(filepath):
117
- return jsonify({'error': 'Arquivo não encontrado'}), 404
118
- with open(filepath, 'r', encoding='utf-8') as f:
119
- return jsonify(json.load(f))
120
- except Exception as e:
121
- return jsonify({'error': str(e)}), 500
122
-
123
- @app.route('/api/download/<filename>')
124
- def download_transcription(filename):
125
- try:
126
- filepath = os.path.join(TRANSCRIPTIONS_FOLDER, filename)
127
- if not os.path.exists(filepath):
128
- return jsonify({'error': 'Arquivo não encontrado'}), 404
129
- return send_file(filepath, as_attachment=True)
130
- except Exception as e:
131
- return jsonify({'error': str(e)}), 500
132
 
133
- @app.route('/api/delete/<filename>', methods=['DELETE'])
134
- def delete_transcription(filename):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  try:
136
- filepath = os.path.join(TRANSCRIPTIONS_FOLDER, filename)
137
- if not os.path.exists(filepath):
138
- return jsonify({'error': 'Arquivo não encontrado'}), 404
139
- os.remove(filepath)
140
- return jsonify({'success': True})
 
141
  except Exception as e:
142
- return jsonify({'error': str(e)}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
  if __name__ == '__main__':
145
- app.run(host='0.0.0.0', port=7860)
 
1
+ from flask import Flask, render_template, request, jsonify, send_file, flash, redirect, url_for
2
  import os
3
+ import subprocess
 
4
  import json
5
+ from werkzeug.utils import secure_filename
6
+ import threading
7
+ import time
8
 
9
  app = Flask(__name__)
10
+ app.secret_key = 'transcricao_audio_key_2024'
11
 
12
+ # Configurações
13
+ UPLOAD_FOLDER = 'uploads'
14
+ OUTPUT_FOLDER = 'outputs'
15
+ ALLOWED_EXTENSIONS = {'mp3', 'wav', 'mp4', 'm4a', 'flac', 'ogg'}
16
 
17
+ # Criar pastas se não existirem
18
+ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
19
+ os.makedirs(OUTPUT_FOLDER, exist_ok=True)
20
 
21
+ app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
22
+ app.config['OUTPUT_FOLDER'] = OUTPUT_FOLDER
23
+ app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # 500MB max
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
+ # Status dos processamentos
26
+ processing_status = {}
 
27
 
28
+ def allowed_file(filename):
29
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
30
 
31
+ def run_transcription(audio_path, job_id):
32
+ """Executa a transcrição em background"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  try:
34
+ processing_status[job_id]['status'] = 'processing'
35
+ processing_status[job_id]['message'] = 'Iniciando transcrição...'
36
+
37
+ # Executar o script de transcrição
38
+ result = subprocess.run(
39
+ ['python', 'transcriptor.py', audio_path],
40
+ capture_output=True,
41
+ text=True,
42
+ cwd='.'
43
+ )
44
+
45
+ if result.returncode == 0:
46
+ # Encontrar o arquivo JSON gerado
47
+ base_name = os.path.splitext(os.path.basename(audio_path))[0]
48
+ json_file = f"{base_name}_transcricao.json"
49
+
50
+ if os.path.exists(json_file):
51
+ # Mover para pasta de outputs
52
+ output_path = os.path.join(OUTPUT_FOLDER, json_file)
53
+ os.rename(json_file, output_path)
54
+
55
+ processing_status[job_id]['status'] = 'completed'
56
+ processing_status[job_id]['message'] = 'Transcrição concluída com sucesso!'
57
+ processing_status[job_id]['output_file'] = json_file
58
+ else:
59
+ processing_status[job_id]['status'] = 'error'
60
+ processing_status[job_id]['message'] = 'Arquivo JSON não foi gerado'
61
+ else:
62
+ processing_status[job_id]['status'] = 'error'
63
+ processing_status[job_id]['message'] = f'Erro na transcrição: {result.stderr}'
64
+
65
  except Exception as e:
66
+ processing_status[job_id]['status'] = 'error'
67
+ processing_status[job_id]['message'] = f'Erro interno: {str(e)}'
68
 
69
+ @app.route('/')
70
+ def index():
71
+ return render_template('index.html')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
+ @app.route('/upload', methods=['POST'])
74
+ def upload_file():
75
+ if 'file' not in request.files:
76
+ flash('Nenhum arquivo selecionado')
77
+ return redirect(request.url)
78
+
79
+ file = request.files['file']
80
+ if file.filename == '':
81
+ flash('Nenhum arquivo selecionado')
82
+ return redirect(request.url)
83
+
84
+ if file and allowed_file(file.filename):
85
+ filename = secure_filename(file.filename)
86
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
87
+ file.save(filepath)
88
+
89
+ # Criar job ID único
90
+ job_id = f"{int(time.time())}_{filename}"
91
+ processing_status[job_id] = {
92
+ 'status': 'queued',
93
+ 'message': 'Aguardando processamento...',
94
+ 'filename': filename
95
+ }
96
+
97
+ # Iniciar transcrição em background
98
+ thread = threading.Thread(target=run_transcription, args=(filepath, job_id))
99
+ thread.daemon = True
100
+ thread.start()
101
+
102
+ return render_template('processing.html', job_id=job_id, filename=filename)
103
+
104
+ else:
105
+ flash('Formato de arquivo não suportado. Use: MP3, WAV, MP4, M4A, FLAC, OGG')
106
+ return redirect(url_for('index'))
107
+
108
+ @app.route('/status/<job_id>')
109
+ def check_status(job_id):
110
+ if job_id in processing_status:
111
+ return jsonify(processing_status[job_id])
112
+ else:
113
+ return jsonify({'status': 'not_found', 'message': 'Job não encontrado'})
114
+
115
+ @app.route('/download/<filename>')
116
+ def download_file(filename):
117
  try:
118
+ file_path = os.path.join(OUTPUT_FOLDER, filename)
119
+ if os.path.exists(file_path):
120
+ return send_file(file_path, as_attachment=True)
121
+ else:
122
+ flash('Arquivo não encontrado')
123
+ return redirect(url_for('index'))
124
  except Exception as e:
125
+ flash(f'Erro ao baixar arquivo: {str(e)}')
126
+ return redirect(url_for('index'))
127
+
128
+ @app.route('/results')
129
+ def results():
130
+ """Página para listar todos os resultados disponíveis"""
131
+ output_files = []
132
+ if os.path.exists(OUTPUT_FOLDER):
133
+ for filename in os.listdir(OUTPUT_FOLDER):
134
+ if filename.endswith('.json'):
135
+ filepath = os.path.join(OUTPUT_FOLDER, filename)
136
+ file_stats = os.stat(filepath)
137
+ output_files.append({
138
+ 'name': filename,
139
+ 'size': round(file_stats.st_size / 1024, 2), # KB
140
+ 'modified': time.ctime(file_stats.st_mtime)
141
+ })
142
+
143
+ return render_template('results.html', files=output_files)
144
 
145
  if __name__ == '__main__':
146
+ app.run(host='0.0.0.0', port=7860, debug=False)
readme_md.md ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🎙️ Transcritor de Áudio com WhisperX
2
+
3
+ Sistema completo de transcrição de áudio usando WhisperX com interface web, otimizado para Hugging Face Spaces.
4
+
5
+ ## 🚀 Funcionalidades
6
+
7
+ - **Transcrição de alta qualidade** com WhisperX modelo "medium"
8
+ - **Alinhamento temporal preciso** de palavras
9
+ - **Correção gramatical** automática com PTT5
10
+ - **Interface web intuitiva** com drag & drop
11
+ - **Suporte a múltiplos formatos** de áudio (MP3, WAV, MP4, M4A, FLAC, OGG)
12
+ - **Processamento em background** com status em tempo real
13
+ - **Download de resultados** em formato JSON
14
+
15
+ ## 📁 Estrutura do Projeto
16
+
17
+ ```
18
+ /
19
+ ├── app.py # Aplicação Flask principal
20
+ ├── transcriptor.py # Script de transcrição adaptado
21
+ ├── requirements.txt # Dependências Python
22
+ ├── Dockerfile # Container Docker
23
+ ├── README.md # Este arquivo
24
+ └── templates/ # Templates HTML
25
+ ├── index.html # Página inicial
26
+ ├── processing.html # Status de processamento
27
+ └── results.html # Lista de resultados
28
+ ```
29
+
30
+ ## 🐳 Deploy no Hugging Face Spaces
31
+
32
+ ### 1. Criar Novo Space
33
+
34
+ 1. Acesse [Hugging Face Spaces](https://huggingface.co/spaces)
35
+ 2. Clique em "Create new Space"
36
+ 3. Configure:
37
+ - **Space name**: `transcricao-audio` (ou nome de sua preferência)
38
+ - **SDK**: Docker
39
+ - **Hardware**: GPU T4 small (recomendado) ou CPU basic
40
+
41
+ ### 2. Fazer Upload dos Arquivos
42
+
43
+ Faça upload de todos os arquivos na seguinte estrutura:
44
+
45
+ ```
46
+ seu-space/
47
+ ├── app.py
48
+ ├── transcriptor.py
49
+ ├── requirements.txt
50
+ ├── Dockerfile
51
+ ├── README.md
52
+ └── templates/
53
+ ├── index.html
54
+ ├── processing.html
55
+ └── results.html
56
+ ```
57
+
58
+ ### 3. Configurar o Space
59
+
60
+ No arquivo `README.md` do seu space (diferente deste), adicione o header:
61
+
62
+ ```yaml
63
+ ---
64
+ title: Transcritor de Áudio
65
+ emoji: 🎙️
66
+ colorFrom: blue
67
+ colorTo: purple
68
+ sdk: docker
69
+ pinned: false
70
+ ---
71
+ ```
72
+
73
+ ## 💻 Execução Local
74
+
75
+ ### Com Docker:
76
+
77
+ ```bash
78
+ # Build da imagem
79
+ docker build -t transcriptor .
80
+
81
+ # Executar container
82
+ docker run -p 7860:7860 transcriptor
83
+ ```
84
+
85
+ ### Sem Docker:
86
+
87
+ ```bash
88
+ # Instalar dependências
89
+ pip install -r requirements.txt
90
+
91
+ # Executar aplicação
92
+ python app.py
93
+ ```
94
+
95
+ Acesse: `http://localhost:7860`
96
+
97
+ ## 📊 Como Usar
98
+
99
+ 1. **Upload**: Arraste ou selecione um arquivo de áudio
100
+ 2. **Processamento**: Acompanhe o status em tempo real
101
+ 3. **Download**: Baixe o arquivo JSON com a transcrição
102
+
103
+ ## 📋 Formato de Saída
104
+
105
+ O arquivo JSON gerado contém:
106
+
107
+ ```json
108
+ {
109
+ "metadata": {
110
+ "total_words": 150,
111
+ "arquivo": "audio.mp3",
112
+ "modelo": "WhisperX medium",
113
+ "correcao_gramatical": true
114
+ },
115
+ "words": [
116
+ {
117
+ "word": "Palavra",
118
+ "start": 1.234,
119
+ "end": 1.567,
120
+ "score": 0.95
121
+ }
122
+ ]
123
+ }
124
+ ```
125
+
126
+ ## ⚙️ Configurações
127
+
128
+ ### Parâmetros Ajustáveis (transcriptor.py):
129
+ - `LANGUAGE`: Idioma da transcrição (padrão: "pt")
130
+ - `SCORE_MINIMO`: Score mínimo para aceitar palavras (padrão: 0.5)
131
+ - `TERMO_FIXO`: Lista de termos que não são corrigidos
132
+ - `MODEL_NAME`: Modelo de correção gramatical
133
+
134
+ ### Limites da Aplicação:
135
+ - Tamanho máximo de arquivo: 500MB
136
+ - Formatos suportados: MP3, WAV, MP4, M4A, FLAC, OGG
137
+
138
+ ## 🔧 Solução de Problemas
139
+
140
+ ### Erro de Memória:
141
+ - Reduza o tamanho do arquivo de áudio
142
+ - Use GPU no Hugging Face Spaces
143
+
144
+ ### Transcrição com Erros:
145
+ - Ajuste `SCORE_MINIMO` para filtrar palavras de baixa confiança
146
+ - Verifique qualidade do áudio original
147
+
148
+ ### Modelo não Carrega:
149
+ - Verifique conexão com internet
150
+ - Aguarde download dos modelos na primeira execução
151
+
152
+ ## 📝 Notas Importantes
153
+
154
+ - **Primeira execução**: Pode demorar devido ao download dos modelos
155
+ - **Processamento**: Tempo varia conforme duração do áudio e hardware
156
+ - **Armazenamento**: Arquivos são mantidos temporariamente no container
157
+ - **Privacidade**: Áudios são processados localmente no container
158
+
159
+ ## 🤝 Contribuições
160
+
161
+ Sugestões e melhorias são bem-vindas! Este projeto foi desenvolvido para facilitar a transcrição de áudios com alta qualidade.
162
+
163
+ ## 📄 Licença
164
+
165
+ Este projeto está disponível sob a licença MIT.
requirements.txt CHANGED
@@ -1,5 +1,10 @@
1
- flask
2
- torch
3
- whisperx
4
- transformers
5
- huggingface-hub
 
 
 
 
 
 
1
+ torch>=2.0.0
2
+ torchaudio>=2.0.0
3
+ whisperx>=3.1.0
4
+ transformers>=4.30.0
5
+ flask>=2.3.0
6
+ werkzeug>=2.3.0
7
+ numpy>=1.24.0
8
+ librosa>=0.10.0
9
+ soundfile>=0.12.0
10
+ ffmpeg-python>=0.2.0
static/main.js DELETED
@@ -1,106 +0,0 @@
1
- // Trocar seções do menu lateral
2
- document.querySelectorAll(".nav-item").forEach(item => {
3
- item.addEventListener("click", () => {
4
- document.querySelectorAll(".nav-item").forEach(i => i.classList.remove("active"));
5
- document.querySelectorAll(".content-section").forEach(section => section.classList.remove("active"));
6
- item.classList.add("active");
7
- document.getElementById(item.dataset.section + "-section").classList.add("active");
8
- });
9
- });
10
-
11
- // Gerenciar seleção de áudio e botão de transcrição
12
- const fileInput = document.getElementById("audio-file");
13
- const transcribeBtn = document.getElementById("transcribe-btn");
14
- let selectedAudio = null;
15
-
16
- fileInput.addEventListener("change", () => {
17
- if (fileInput.files.length > 0) {
18
- selectedAudio = fileInput.files[0];
19
- transcribeBtn.disabled = false;
20
- }
21
- });
22
-
23
- transcribeBtn.addEventListener("click", () => {
24
- if (!selectedAudio) return;
25
-
26
- const nameInput = document.getElementById("transcription-name").value || "transcricao";
27
- const progressContainer = document.getElementById("progress-container");
28
- const progressFill = document.getElementById("progress-fill");
29
- const progressText = document.getElementById("progress-text");
30
-
31
- const formData = new FormData();
32
- formData.append("audio", selectedAudio);
33
- formData.append("filename", nameInput);
34
-
35
- transcribeBtn.disabled = true;
36
- progressContainer.style.display = "block";
37
- progressFill.style.width = "30%";
38
- progressText.textContent = "Enviando áudio...";
39
-
40
- fetch("/api/transcribe", {
41
- method: "POST",
42
- body: formData
43
- })
44
- .then(res => res.json())
45
- .then(data => {
46
- if (data.success) {
47
- progressFill.style.width = "100%";
48
- progressText.textContent = "Transcrição concluída!";
49
- loadTranscriptions();
50
- } else {
51
- progressText.textContent = "Erro: " + (data.error || "falha ao transcrever");
52
- }
53
- })
54
- .catch(err => {
55
- progressText.textContent = "Erro: " + err.message;
56
- })
57
- .finally(() => {
58
- transcribeBtn.disabled = false;
59
- setTimeout(() => {
60
- progressContainer.style.display = "none";
61
- progressFill.style.width = "0%";
62
- }, 4000);
63
- });
64
- });
65
-
66
- // Carregar transcrições
67
- function loadTranscriptions() {
68
- fetch("/api/transcriptions")
69
- .then(res => res.json())
70
- .then(data => {
71
- const filesList = document.getElementById("files-list");
72
- filesList.innerHTML = "";
73
- data.forEach(file => {
74
- const item = document.createElement("div");
75
- item.className = "file-item";
76
- item.innerHTML = `
77
- <div class="file-info">
78
- <div class="file-icon"><i class="fas fa-file-alt"></i></div>
79
- <div class="file-details">
80
- <h4>${file.name}</h4>
81
- <p class="file-meta">${file.word_count} palavras • ${new Date(file.created).toLocaleString()}</p>
82
- </div>
83
- </div>
84
- <div class="file-actions">
85
- <button class="action-btn download" onclick="downloadFile('${file.filename}')"><i class="fas fa-download"></i></button>
86
- <button class="action-btn delete" onclick="deleteFile('${file.filename}')"><i class="fas fa-trash"></i></button>
87
- </div>
88
- `;
89
- filesList.appendChild(item);
90
- });
91
- });
92
- }
93
-
94
- function deleteFile(filename) {
95
- fetch(`/api/delete/${filename}`, { method: "DELETE" })
96
- .then(res => res.json())
97
- .then(() => loadTranscriptions());
98
- }
99
-
100
- function downloadFile(filename) {
101
- window.open(`/api/download/${filename}`, "_blank");
102
- }
103
-
104
- document.getElementById("refresh-files-btn").addEventListener("click", loadTranscriptions);
105
-
106
- // TODO: Carregar VSL com sincronização hipnótica (próxima etapa!)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
templates/index.html CHANGED
@@ -1,675 +1,314 @@
1
  <!DOCTYPE html>
2
  <html lang="pt-BR">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>VSL Ultra Pro - Transcrição e Geração</title>
7
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Open+Sans:wght@400;600;700&display=swap" rel="stylesheet">
8
- <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
9
  <style>
10
- * {
11
- margin: 0;
12
- padding: 0;
13
- box-sizing: border-box;
14
- }
15
-
16
- body {
17
- font-family: 'Inter', sans-serif;
18
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
19
- min-height: 100vh;
20
- color: #333;
21
- }
22
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  .container {
24
- display: flex;
25
- height: 100vh;
26
- }
27
-
28
- /* Sidebar */
29
- .sidebar {
30
- width: 300px;
31
- background: rgba(255, 255, 255, 0.95);
32
- backdrop-filter: blur(10px);
33
- border-right: 1px solid rgba(255, 255, 255, 0.2);
34
- padding: 30px 25px;
35
- overflow-y: auto;
36
- }
37
-
38
- .logo {
39
- text-align: center;
40
- margin-bottom: 40px;
41
- }
42
-
43
- .logo h1 {
44
- font-size: 24px;
45
- font-weight: 700;
46
- background: linear-gradient(135deg, #667eea, #764ba2);
47
- -webkit-background-clip: text;
48
- -webkit-text-fill-color: transparent;
49
- background-clip: text;
50
- margin-bottom: 5px;
51
- }
52
-
53
- .logo p {
54
- font-size: 12px;
55
- color: #666;
56
- font-weight: 500;
57
- }
58
-
59
- .nav-section {
60
- margin-bottom: 30px;
61
- }
62
-
63
- .nav-title {
64
- font-size: 14px;
65
- font-weight: 600;
66
- color: #666;
67
- margin-bottom: 15px;
68
- text-transform: uppercase;
69
- letter-spacing: 0.5px;
70
- }
71
-
72
- .nav-item {
73
- display: flex;
74
- align-items: center;
75
- padding: 12px 15px;
76
- margin-bottom: 8px;
77
- border-radius: 10px;
78
- cursor: pointer;
79
- transition: all 0.3s ease;
80
- font-weight: 500;
81
- }
82
-
83
- .nav-item:hover {
84
- background: rgba(102, 126, 234, 0.1);
85
- transform: translateX(5px);
86
- }
87
-
88
- .nav-item.active {
89
- background: linear-gradient(135deg, #667eea, #764ba2);
90
- color: white;
91
- box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
92
- }
93
-
94
- .nav-item i {
95
- margin-right: 12px;
96
- width: 20px;
97
- font-size: 16px;
98
- }
99
-
100
- /* Main Content */
101
- .main-content {
102
- flex: 1;
103
- padding: 30px;
104
- overflow-y: auto;
105
- }
106
-
107
- .content-section {
108
- display: none;
109
- background: rgba(255, 255, 255, 0.95);
110
- backdrop-filter: blur(10px);
111
- border-radius: 20px;
112
- padding: 30px;
113
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
114
- height: calc(100vh - 60px);
115
- }
116
-
117
- .content-section.active {
118
- display: block;
119
- }
120
-
121
- .section-title {
122
- font-size: 28px;
123
- font-weight: 700;
124
- margin-bottom: 10px;
125
- background: linear-gradient(135deg, #667eea, #764ba2);
126
- -webkit-background-clip: text;
127
- -webkit-text-fill-color: transparent;
128
- background-clip: text;
129
- }
130
-
131
- .section-description {
132
- color: #666;
133
- margin-bottom: 30px;
134
- font-size: 16px;
135
- }
136
-
137
- /* Upload Area */
138
- .upload-area {
139
- border: 2px dashed #ddd;
140
- border-radius: 15px;
141
- padding: 40px;
142
- text-align: center;
143
- margin-bottom: 30px;
144
- transition: all 0.3s ease;
145
- background: rgba(102, 126, 234, 0.05);
146
- }
147
-
148
- .upload-area:hover {
149
- border-color: #667eea;
150
- background: rgba(102, 126, 234, 0.1);
151
- }
152
-
153
- .upload-area.dragover {
154
- border-color: #667eea;
155
- background: rgba(102, 126, 234, 0.15);
156
- transform: scale(1.02);
157
- }
158
-
159
- .upload-icon {
160
- font-size: 48px;
161
- color: #667eea;
162
- margin-bottom: 20px;
163
- }
164
-
165
- .upload-text {
166
- font-size: 18px;
167
- font-weight: 600;
168
- margin-bottom: 10px;
169
- }
170
-
171
- .upload-subtext {
172
- color: #666;
173
- margin-bottom: 20px;
174
- }
175
-
176
- .file-input {
177
- display: none;
178
- }
179
-
180
- .upload-btn {
181
- background: linear-gradient(135deg, #667eea, #764ba2);
182
- color: white;
183
- padding: 12px 30px;
184
- border: none;
185
- border-radius: 25px;
186
- cursor: pointer;
187
- font-weight: 600;
188
- transition: all 0.3s ease;
189
- }
190
-
191
- .upload-btn:hover {
192
- transform: translateY(-2px);
193
- box-shadow: 0 5px 20px rgba(102, 126, 234, 0.3);
194
- }
195
-
196
- /* Form Controls */
197
- .form-group {
198
- margin-bottom: 20px;
199
- }
200
-
201
- .form-label {
202
- display: block;
203
- font-weight: 600;
204
- margin-bottom: 8px;
205
- color: #333;
206
- }
207
-
208
- .form-input {
209
- width: 100%;
210
- padding: 12px 15px;
211
- border: 2px solid #eee;
212
- border-radius: 10px;
213
- font-size: 16px;
214
- transition: all 0.3s ease;
215
- }
216
-
217
- .form-input:focus {
218
- outline: none;
219
- border-color: #667eea;
220
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
221
- }
222
-
223
- /* Buttons */
224
- .btn {
225
- padding: 12px 24px;
226
- border: none;
227
- border-radius: 10px;
228
- cursor: pointer;
229
- font-weight: 600;
230
- transition: all 0.3s ease;
231
- display: inline-flex;
232
- align-items: center;
233
- gap: 8px;
234
- }
235
-
236
- .btn-primary {
237
- background: linear-gradient(135deg, #667eea, #764ba2);
238
- color: white;
239
- }
240
-
241
- .btn-secondary {
242
- background: #f8f9fa;
243
- color: #666;
244
- border: 2px solid #eee;
245
- }
246
-
247
- .btn:hover {
248
- transform: translateY(-2px);
249
- box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
250
- }
251
-
252
- /* Progress Bar */
253
- .progress-container {
254
- margin: 20px 0;
255
- display: none;
256
- }
257
-
258
- .progress-bar {
259
- width: 100%;
260
- height: 6px;
261
- background: #eee;
262
- border-radius: 3px;
263
- overflow: hidden;
264
  }
265
 
266
- .progress-fill {
267
- height: 100%;
268
- background: linear-gradient(135deg, #667eea, #764ba2);
269
- width: 0%;
270
- transition: width 0.3s ease;
271
- }
272
-
273
- .progress-text {
274
- text-align: center;
275
- margin-top: 10px;
276
- font-weight: 600;
277
- color: #667eea;
278
- }
279
-
280
- /* Files List */
281
- .files-list {
282
- margin-top: 30px;
283
- }
284
-
285
- .file-item {
286
- display: flex;
287
- align-items: center;
288
- justify-content: space-between;
289
- padding: 15px;
290
- background: rgba(102, 126, 234, 0.05);
291
- border-radius: 10px;
292
- margin-bottom: 10px;
293
- transition: all 0.3s ease;
294
- }
295
-
296
- .file-item:hover {
297
- background: rgba(102, 126, 234, 0.1);
298
- transform: translateX(5px);
299
- }
300
-
301
- .file-info {
302
- display: flex;
303
- align-items: center;
304
- gap: 15px;
305
- }
306
-
307
- .file-icon {
308
- width: 40px;
309
- height: 40px;
310
- background: linear-gradient(135deg, #667eea, #764ba2);
311
- border-radius: 10px;
312
- display: flex;
313
- align-items: center;
314
- justify-content: center;
315
- color: white;
316
- font-size: 16px;
317
- }
318
-
319
- .file-details h4 {
320
- font-size: 16px;
321
- font-weight: 600;
322
- margin-bottom: 4px;
323
- }
324
-
325
- .file-meta {
326
- font-size: 12px;
327
- color: #666;
328
- }
329
-
330
- .file-actions {
331
- display: flex;
332
- gap: 10px;
333
- }
334
-
335
- .action-btn {
336
- width: 35px;
337
- height: 35px;
338
- border: none;
339
- border-radius: 8px;
340
- cursor: pointer;
341
- display: flex;
342
- align-items: center;
343
- justify-content: center;
344
- transition: all 0.3s ease;
345
- }
346
-
347
- .action-btn.download {
348
- background: #28a745;
349
- color: white;
350
- }
351
-
352
- .action-btn.delete {
353
- background: #dc3545;
354
- color: white;
355
- }
356
-
357
- .action-btn.use {
358
- background: #667eea;
359
- color: white;
360
- }
361
-
362
- /* VSL Generator */
363
- .vsl-controls {
364
- display: grid;
365
- grid-template-columns: 1fr 1fr;
366
- gap: 20px;
367
- margin-bottom: 30px;
368
- }
369
-
370
- .control-group {
371
- background: rgba(102, 126, 234, 0.05);
372
- padding: 20px;
373
- border-radius: 15px;
374
- }
375
-
376
- .audio-player {
377
- width: 100%;
378
- margin: 10px 0;
379
- }
380
-
381
- /* VSL Preview */
382
- .vsl-preview {
383
- background: white;
384
- border-radius: 15px;
385
- padding: 40px;
386
- min-height: 500px;
387
- display: flex;
388
- align-items: center;
389
- justify-content: center;
390
- font-family: 'Open Sans', sans-serif;
391
- font-size: 42px;
392
- line-height: 1.4;
393
- text-align: center;
394
- white-space: pre-line;
395
- color: #000;
396
- position: relative;
397
- overflow: hidden;
398
- }
399
-
400
- .vsl-preview.fullscreen {
401
- position: fixed;
402
- top: 0;
403
- left: 0;
404
- width: 100vw;
405
- height: 100vh;
406
- margin: 0;
407
- padding: 60px;
408
- border-radius: 0;
409
- background: white;
410
- z-index: 9999;
411
- font-size: 64px;
412
- }
413
-
414
- .vsl-preview.fullscreen.hd {
415
- width: 1920px;
416
- height: 1080px;
417
- transform-origin: top left;
418
- transform: scale(0.9);
419
- }
420
-
421
- .bold-black {
422
- font-weight: 700;
423
- color: #000;
424
- }
425
-
426
- .bold-red {
427
- font-weight: 700;
428
- color: #e74c3c;
429
- }
430
-
431
- /* Controls Row */
432
- .controls-row {
433
- display: flex;
434
- gap: 15px;
435
- margin-bottom: 20px;
436
- flex-wrap: wrap;
437
- }
438
-
439
- /* Responsive */
440
- @media (max-width: 768px) {
441
- .container {
442
- flex-direction: column;
443
- }
444
-
445
- .sidebar {
446
- width: 100%;
447
- height: auto;
448
- padding: 20px;
449
- }
450
-
451
- .main-content {
452
- padding: 20px;
453
- }
454
-
455
- .vsl-controls {
456
- grid-template-columns: 1fr;
457
- }
458
-
459
- .controls-row {
460
- flex-direction: column;
461
- }
462
-
463
- .vsl-preview {
464
- font-size: 24px;
465
- min-height: 300px;
466
- padding: 20px;
467
- }
468
- }
469
-
470
- /* Loading Animation */
471
- .loading {
472
- display: inline-block;
473
- width: 20px;
474
- height: 20px;
475
- border: 3px solid rgba(255, 255, 255, 0.3);
476
- border-radius: 50%;
477
- border-top-color: white;
478
- animation: spin 1s ease-in-out infinite;
479
- }
480
-
481
- @keyframes spin {
482
- to { transform: rotate(360deg); }
483
- }
484
-
485
- /* Notifications */
486
- .notification {
487
- position: fixed;
488
- top: 20px;
489
- right: 20px;
490
- padding: 15px 20px;
491
- border-radius: 10px;
492
- color: white;
493
- font-weight: 600;
494
- z-index: 10000;
495
- transform: translateX(400px);
496
- transition: transform 0.3s ease;
497
- }
498
-
499
- .notification.show {
500
- transform: translateX(0);
501
- }
502
-
503
- .notification.success {
504
- background: #28a745;
505
- }
506
-
507
- .notification.error {
508
- background: #dc3545;
509
- }
510
-
511
- .notification.info {
512
- background: #17a2b8;
513
  }
 
514
  </style>
515
- </head>
516
- <body>
517
  <div class="container">
518
- <!-- Sidebar -->
519
- <div class="sidebar">
520
- <div class="logo">
521
- <h1>VSL Ultra Pro</h1>
522
- <p>Transcrição e Geração Profissional</p>
523
- </div>
524
-
525
- <div class="nav-section">
526
- <div class="nav-title">Ferramentas</div>
527
- <div class="nav-item active" data-section="transcribe">
528
- <i class="fas fa-microphone"></i>
529
- <span>Transcrever Áudio</span>
530
- </div>
531
- <div class="nav-item" data-section="generate">
532
- <i class="fas fa-video"></i>
533
- <span>Gerar VSL</span>
534
- </div>
535
- </div>
536
-
537
- <div class="nav-section">
538
- <div class="nav-title">Arquivos</div>
539
- <div class="nav-item" data-section="files">
540
- <i class="fas fa-folder"></i>
541
- <span>Transcrições</span>
542
- </div>
543
- </div>
544
  </div>
545
-
546
- <!-- Main Content -->
547
- <div class="main-content">
548
- <!-- Transcribe Section -->
549
- <div class="content-section active" id="transcribe-section">
550
- <h1 class="section-title">Transcrever Áudio</h1>
551
- <p class="section-description">Faça upload do seu áudio e obtenha uma transcrição precisa com timestamps</p>
552
-
553
- <div class="upload-area" id="upload-area">
554
- <div class="upload-icon">
555
- <i class="fas fa-cloud-upload-alt"></i>
556
- </div>
557
- <div class="upload-text">Arraste seu arquivo de áudio aqui</div>
558
- <div class="upload-subtext">ou clique para selecionar (MP3, WAV, M4A)</div>
559
- <input type="file" id="audio-file" class="file-input" accept="audio/*">
560
- <button class="upload-btn" onclick="document.getElementById('audio-file').click()">
561
- Selecionar Arquivo
562
- </button>
563
- </div>
564
-
565
- <div class="form-group">
566
- <label class="form-label">Nome da Transcrição</label>
567
- <input type="text" id="transcription-name" class="form-input" placeholder="Digite um nome para sua transcrição">
568
- </div>
569
-
570
- <div class="progress-container" id="progress-container">
571
- <div class="progress-bar">
572
- <div class="progress-fill" id="progress-fill"></div>
573
- </div>
574
- <div class="progress-text" id="progress-text">Processando...</div>
575
- </div>
576
-
577
- <div class="controls-row">
578
- <button class="btn btn-primary" id="transcribe-btn" disabled>
579
- <i class="fas fa-microphone"></i>
580
- <span>Iniciar Transcrição</span>
581
- </button>
582
- </div>
583
- </div>
584
-
585
- <!-- Generate VSL Section -->
586
- <div class="content-section" id="generate-section">
587
- <h1 class="section-title">Gerar VSL</h1>
588
- <p class="section-description">Crie sua Video Sales Letter com sincronização perfeita</p>
589
-
590
- <div class="vsl-controls">
591
- <div class="control-group">
592
- <label class="form-label">Transcrição (JSON)</label>
593
- <input type="file" id="json-file" class="form-input" accept=".json">
594
- </div>
595
- <div class="control-group">
596
- <label class="form-label">Áudio de Narração</label>
597
- <input type="file" id="vsl-audio" class="form-input" accept="audio/*">
598
- <audio controls id="audio-player" class="audio-player"></audio>
599
- </div>
600
- </div>
601
-
602
- <div class="controls-row">
603
- <button class="btn btn-primary" id="start-sync-btn" disabled>
604
- <i class="fas fa-play"></i>
605
- Iniciar Apresentação
606
- </button>
607
- <button class="btn btn-secondary" id="fullscreen-btn">
608
- <i class="fas fa-expand"></i>
609
- Tela Cheia
610
- </button>
611
- <button class="btn btn-secondary" id="hd-btn">
612
- <i class="fas fa-tv"></i>
613
- Modo 1080p
614
- </button>
615
- </div>
616
-
617
- <div class="vsl-preview" id="vsl-preview">
618
- <p>Carregue uma transcrição e áudio para começar</p>
619
- </div>
620
- </div>
621
-
622
- <!-- Files Section -->
623
- <div class="content-section" id="files-section">
624
- <h1 class="section-title">Transcrições Salvas</h1>
625
- <p class="section-description">Gerencie suas transcrições e utilize-as em seus projetos</p>
626
-
627
- <div class="controls-row">
628
- <button class="btn btn-primary" id="refresh-files-btn">
629
- <i class="fas fa-sync"></i>
630
- Atualizar Lista
631
- </button>
632
- </div>
633
-
634
- <div class="files-list" id="files-list">
635
- <!-- Files will be loaded here -->
636
- </div>
637
- </div>
638
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
  </div>
640
 
641
  <script>
642
- // Global variables
643
- let currentSlides = [];
644
- let currentAudio = null;
645
- let isPlaying = false;
646
- let animationId = null;
647
-
648
- // Important words for highlighting
649
- const importantWords = new Set([
650
- 'DOR', 'PESO', 'VERGONHA', 'TRISTEZA', 'SOFRIMENTO', 'CULPA', 'FRACASSO',
651
- 'LIMITE', 'BALANÇA', 'FOME', 'FRACA', 'TRANCAR', 'ROUBEI', 'FUGIR',
652
- 'TRISTE', 'COMPULSÃO', 'AUTOESTIMA', 'REALIDADE', 'ESCONDER', 'MENTIRA',
653
- 'DIETAS', 'EFEITO SANFONA', 'EXCESSO', 'RESULTADO', 'ESFORÇO', 'ENERGIA',
654
- 'MUDANÇA', 'ESCOLHA', 'RENASCIMENTO', 'ESPERANÇA', 'CONQUISTA',
655
- 'TRANSFORMAÇÃO', 'CORAGEM', 'SOLUÇÃO', 'FUNCIONA', 'EMAGRECER', 'ALIMENTAR',
656
- 'VIDA', 'SAÚDE', 'LIBERDADE', 'VERDADE', 'MILAGRE', 'EMOÇÃO', 'SUPORTE',
657
- 'JEJUM', 'PERSONAL', 'METABOLISMO', 'SAUDÁVEL', 'LEVE', 'SE OLHAR', 'VER',
658
- 'SORRIR', 'ORGULHO', 'MERECE', 'BRASILEIRA', 'REAL', 'CAMINHO', 'PASSO',
659
- 'PLANO', 'REFEIÇÕES', 'PRAIA', 'ESCONDERIJO', 'CONSCIÊNCIA', 'RESPOSTA',
660
- 'DESAFIO', 'COBRIR', 'RECOMEÇO', 'REJEIÇÃO', 'RECOMPENSA', 'DECISÃO',
661
- 'ACEITAÇÃO', 'VITÓRIA', 'SUPERAÇÃO', 'MOTIVAÇÃO', 'PROGRESSO'
662
- ]);
663
-
664
- const veryImportantWords = new Set([
665
- 'NUNCA MAIS', 'DEFINITIVAMENTE', 'LIBERTAR', 'INSUPORTÁVEL',
666
- 'CICLO SEM FIM', 'TRANSFORMAÇÃO', 'MUDAR TUDO', 'A VERDADE', 'É AGORA',
667
- 'O MOMENTO', 'DECISÃO', 'A RESPOSTA', 'ME LIBERTAR', 'O COMEÇO',
668
- 'FOI ALI QUE TUDO MUDOU', 'NADA MUDA', 'A CULPA NÃO É SUA', 'VOCÊ MERECE',
669
- 'SIMPLES E ACESSÍVEL', HOJE', 'ÚLTIMA CHANCE', 'AGIR AGORA',
670
- 'VIDA NOVA', 'LIBERDADE', 'CAMINHO CERTO', 'NUNCA MAIS VOU VIVER ASSIM',
671
- 'EU ROUBEI', 'GORDA', 'TRAVEI', 'GORDINHA', 'PAZ', 'SÓ QUEM SENTE SABE',
672
- 'NÃO AGUENTO MAIS', 'CHEGA', 'ACORDEI', 'EU DECIDI', 'FOI NAQUELE MOMENTO',
673
- 'REALIZEI MEU SONHO', 'FUI ALÉM', 'É POSSÍVEL', 'É REAL',
674
- 'ME VI NO ESPELHO', 'ME EMOCIONEI', 'TUDO MUDOU', 'O DIA QUE VIREI A CHAVE',
675
- 'NUNCA', 'MAIS', 'PERDER', 'TUDO', 'ATERRORIZAVA', 'FRACASSA',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <!DOCTYPE html>
2
  <html lang="pt-BR">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Processando - Transcritor de Áudio</title>
 
 
7
  <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ padding: 20px;
22
+ }
23
+
24
+ .container {
25
+ background: white;
26
+ padding: 40px;
27
+ border-radius: 20px;
28
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
29
+ max-width: 500px;
30
+ width: 100%;
31
+ text-align: center;
32
+ }
33
+
34
+ h1 {
35
+ color: #333;
36
+ margin-bottom: 30px;
37
+ font-size: 2rem;
38
+ }
39
+
40
+ .file-info {
41
+ background: #f8f9fa;
42
+ padding: 20px;
43
+ border-radius: 10px;
44
+ margin-bottom: 30px;
45
+ }
46
+
47
+ .file-name {
48
+ font-weight: bold;
49
+ color: #333;
50
+ font-size: 1.1rem;
51
+ }
52
+
53
+ .status-container {
54
+ margin: 30px 0;
55
+ }
56
+
57
+ .spinner {
58
+ width: 60px;
59
+ height: 60px;
60
+ border: 6px solid #f3f3f3;
61
+ border-top: 6px solid #667eea;
62
+ border-radius: 50%;
63
+ animation: spin 1s linear infinite;
64
+ margin: 0 auto 20px;
65
+ }
66
+
67
+ @keyframes spin {
68
+ 0% {
69
+ transform: rotate(0deg);
70
+ }
71
+ 100% {
72
+ transform: rotate(360deg);
73
+ }
74
+ }
75
+
76
+ .status-text {
77
+ font-size: 1.1rem;
78
+ color: #666;
79
+ margin-bottom: 10px;
80
+ }
81
+
82
+ .status-message {
83
+ color: #333;
84
+ font-weight: 500;
85
+ }
86
+
87
+ .progress-bar {
88
+ width: 100%;
89
+ height: 8px;
90
+ background: #eee;
91
+ border-radius: 4px;
92
+ margin: 20px 0;
93
+ overflow: hidden;
94
+ }
95
+
96
+ .progress-fill {
97
+ height: 100%;
98
+ background: linear-gradient(90deg, #667eea, #764ba2);
99
+ width: 0%;
100
+ transition: width 0.5s ease;
101
+ animation: pulse 2s infinite;
102
+ }
103
+
104
+ @keyframes pulse {
105
+ 0% {
106
+ opacity: 0.6;
107
+ }
108
+ 50% {
109
+ opacity: 1;
110
+ }
111
+ 100% {
112
+ opacity: 0.6;
113
+ }
114
+ }
115
+
116
+ .btn {
117
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
118
+ color: white;
119
+ padding: 12px 30px;
120
+ border: none;
121
+ border-radius: 25px;
122
+ font-size: 1rem;
123
+ cursor: pointer;
124
+ transition: all 0.3s ease;
125
+ text-decoration: none;
126
+ display: inline-block;
127
+ margin: 10px;
128
+ }
129
+
130
+ .btn:hover {
131
+ transform: translateY(-2px);
132
+ box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2);
133
+ }
134
+
135
+ .btn-success {
136
+ background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
137
+ }
138
+
139
+ .btn-secondary {
140
+ background: #6c757d;
141
+ }
142
+
143
+ .success-icon {
144
+ font-size: 4rem;
145
+ color: #28a745;
146
+ margin-bottom: 20px;
147
+ }
148
+
149
+ .error-icon {
150
+ font-size: 4rem;
151
+ color: #dc3545;
152
+ margin-bottom: 20px;
153
+ }
154
+
155
+ .result-info {
156
+ background: #d4edda;
157
+ border: 1px solid #c3e6cb;
158
+ padding: 15px;
159
+ border-radius: 10px;
160
+ margin: 20px 0;
161
+ }
162
+
163
+ .error-info {
164
+ background: #f8d7da;
165
+ border: 1px solid #f5c6cb;
166
+ padding: 15px;
167
+ border-radius: 10px;
168
+ margin: 20px 0;
169
+ color: #721c24;
170
+ }
171
+
172
+ .hidden {
173
+ display: none !important;
174
+ }
175
+
176
+ @media (max-width: 600px) {
177
  .container {
178
+ padding: 30px 20px;
179
+ margin: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  }
181
 
182
+ h1 {
183
+ font-size: 1.6rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  }
185
+ }
186
  </style>
187
+ </head>
188
+ <body>
189
  <div class="container">
190
+ <h1 id="page-title">🎙️ Processando Transcrição</h1>
191
+
192
+ <div class="file-info">
193
+ <div class="file-name">📄 {{ filename }}</div>
194
+ </div>
195
+
196
+ <!-- Estado: Processando -->
197
+ <div id="processing-state" class="status-container">
198
+ <div class="spinner"></div>
199
+ <div class="status-text">Status:</div>
200
+ <div class="status-message" id="status-message">
201
+ Aguardando processamento...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  </div>
203
+ <div class="progress-bar">
204
+ <div
205
+ class="progress-fill"
206
+ id="progress-fill"
207
+ style="width: 20%"
208
+ ></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  </div>
210
+ </div>
211
+
212
+ <!-- Estado: Concluído -->
213
+ <div id="success-state" class="status-container hidden">
214
+ <div class="success-icon">✅</div>
215
+ <div class="status-text">Transcrição concluída!</div>
216
+ <div class="result-info">
217
+ <div>Arquivo gerado: <span id="output-filename"></span></div>
218
+ </div>
219
+ <a href="#" class="btn btn-success" id="download-btn">
220
+ 📥 Baixar Transcrição
221
+ </a>
222
+ </div>
223
+
224
+ <!-- Estado: Erro -->
225
+ <div id="error-state" class="status-container hidden">
226
+ <div class="error-icon">❌</div>
227
+ <div class="status-text">Erro no processamento</div>
228
+ <div class="error-info">
229
+ <div id="error-message">Ocorreu um erro durante a transcrição.</div>
230
+ </div>
231
+ </div>
232
+
233
+ <!-- Botões de navegação -->
234
+ <div style="margin-top: 30px">
235
+ <a href="/" class="btn btn-secondary">🔙 Nova Transcrição</a>
236
+ <a href="/results" class="btn btn-secondary">📂 Ver Resultados</a>
237
+ </div>
238
  </div>
239
 
240
  <script>
241
+ const jobId = "{{ job_id }}";
242
+ let progressWidth = 20;
243
+
244
+ const processingState = document.getElementById("processing-state");
245
+ const successState = document.getElementById("success-state");
246
+ const errorState = document.getElementById("error-state");
247
+ const statusMessage = document.getElementById("status-message");
248
+ const progressFill = document.getElementById("progress-fill");
249
+ const pageTitle = document.getElementById("page-title");
250
+ const downloadBtn = document.getElementById("download-btn");
251
+ const outputFilename = document.getElementById("output-filename");
252
+ const errorMessage = document.getElementById("error-message");
253
+
254
+ // Atualizar progresso visual
255
+ function updateProgress() {
256
+ progressWidth += Math.random() * 5;
257
+ if (progressWidth > 95) progressWidth = 95;
258
+ progressFill.style.width = progressWidth + "%";
259
+ }
260
+
261
+ // Verificar status periodicamente
262
+ function checkStatus() {
263
+ fetch(`/status/${jobId}`)
264
+ .then((response) => response.json())
265
+ .then((data) => {
266
+ statusMessage.textContent = data.message || "Processando...";
267
+
268
+ if (data.status === "completed") {
269
+ // Sucesso
270
+ processingState.classList.add("hidden");
271
+ successState.classList.remove("hidden");
272
+ pageTitle.textContent = "✅ Transcrição Concluída!";
273
+
274
+ if (data.output_file) {
275
+ outputFilename.textContent = data.output_file;
276
+ downloadBtn.href = `/download/${data.output_file}`;
277
+ }
278
+
279
+ progressFill.style.width = "100%";
280
+ } else if (data.status === "error") {
281
+ // Erro
282
+ processingState.classList.add("hidden");
283
+ errorState.classList.remove("hidden");
284
+ pageTitle.textContent = "❌ Erro na Transcrição";
285
+ errorMessage.textContent = data.message || "Erro desconhecido";
286
+ } else if (data.status === "processing") {
287
+ // Ainda processando
288
+ updateProgress();
289
+ setTimeout(checkStatus, 3000);
290
+ } else {
291
+ // Status desconhecido, continuar verificando
292
+ setTimeout(checkStatus, 2000);
293
+ }
294
+ })
295
+ .catch((error) => {
296
+ console.error("Erro ao verificar status:", error);
297
+ setTimeout(checkStatus, 5000);
298
+ });
299
+ }
300
+
301
+ // Iniciar verificação
302
+ setTimeout(checkStatus, 1000);
303
+
304
+ // Atualizar progresso visual periodicamente
305
+ const progressInterval = setInterval(() => {
306
+ if (!processingState.classList.contains("hidden")) {
307
+ updateProgress();
308
+ } else {
309
+ clearInterval(progressInterval);
310
+ }
311
+ }, 2000);
312
+ </script>
313
+ </body>
314
+ </html>
templates/processing.html ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="pt-BR">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Processando - Transcritor de Áudio</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ padding: 20px;
22
+ }
23
+
24
+ .container {
25
+ background: white;
26
+ padding: 40px;
27
+ border-radius: 20px;
28
+ box-shadow: 0 20px 40px rgba(0,0,0,0.1);
29
+ max-width: 500px;
30
+ width: 100%;
31
+ text-align: center;
32
+ }
33
+
34
+ h1 {
35
+ color: #333;
36
+ margin-bottom: 30px;
37
+ font-size: 2rem;
38
+ }
39
+
40
+ .file-info {
41
+ background: #f8f9fa;
42
+ padding: 20px;
43
+ border-radius: 10px;
44
+ margin-bottom: 30px;
45
+ }
46
+
47
+ .file-name {
48
+ font-weight: bold;
49
+ color: #333;
50
+ font-size: 1.1rem;
51
+ }
52
+
53
+ .status-container {
54
+ margin: 30px 0;
55
+ }
56
+
57
+ .spinner {
58
+ width: 60px;
59
+ height: 60px;
60
+ border: 6px solid #f3f3f3;
61
+ border-top: 6px solid #667eea;
62
+ border-radius: 50%;
63
+ animation: spin 1s linear infinite;
64
+ margin: 0 auto 20px;
65
+ }
66
+
67
+ @keyframes spin {
68
+ 0% { transform: rotate(0deg); }
69
+ 100% { transform: rotate(360deg); }
70
+ }
71
+
72
+ .status-text {
73
+ font-size: 1.1rem;
74
+ color: #666;
75
+ margin-bottom: 10px;
76
+ }
77
+
78
+ .status-message {
79
+ color: #333;
80
+ font-weight: 500;
81
+ }
82
+
83
+ .progress-bar {
84
+ width: 100%;
85
+ height: 8px;
86
+ background: #eee;
87
+ border-radius: 4px;
88
+ margin: 20px 0;
89
+ overflow: hidden;
90
+ }
91
+
92
+ .progress-fill {
93
+ height: 100%;
94
+ background: linear-gradient(90deg, #667eea, #764ba2);
95
+ width: 0%;
96
+ transition: width 0.5s ease;
97
+ animation: pulse 2s infinite;
98
+ }
99
+
100
+ @keyframes pulse {
101
+ 0% { opacity: 0.6; }
102
+ 50% { opacity: 1; }
103
+ 100% { opacity: 0.6; }
104
+ }
105
+
106
+ .btn {
107
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
108
+ color: white;
109
+ padding: 12px 30px;
110
+ border: none;
111
+ border-radius: 25px;
112
+ font-size: 1rem;
113
+ cursor: pointer;
114
+ transition: all 0.3s ease;
115
+ text-decoration: none;
116
+ display: inline-block;
117
+ margin: 10px;
118
+ }
119
+
120
+ .btn:hover {
121
+ transform: translateY(-2px);
122
+ box-shadow: 0 8px 15px rgba(0,0,0,0.2);
123
+ }
124
+
125
+ .btn-success {
126
+ background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
127
+ }
128
+
129
+ .btn-secondary {
130
+ background: #6c757d;
131
+ }
132
+
133
+ .success-icon {
134
+ font-size: 4rem;
135
+ color: #28a745;
136
+ margin-bottom: 20px;
137
+ }
138
+
139
+ .error-icon {
140
+ font-size: 4rem;
141
+ color: #dc3545;
142
+ margin-bottom: 20px;
143
+ }
144
+
145
+ .result-info {
146
+ background: #d4edda;
147
+ border: 1px solid #c3e6cb;
148
+ padding: 15px;
149
+ border-radius: 10px;
150
+ margin: 20px 0;
151
+ }
152
+
153
+ .error-info {
154
+ background: #f8d7da;
155
+ border: 1px solid #f5c6cb;
156
+ padding: 15px;
157
+ border-radius: 10px;
158
+ margin: 20px 0;
159
+ color: #721c24;
160
+ }
161
+
162
+ .hidden {
163
+ display: none !important;
164
+ }
165
+
166
+ @media (max-width: 600px) {
167
+ .container {
168
+ padding: 30px 20px;
169
+ margin: 10px;
170
+ }
171
+
172
+ h1 {
173
+ font-size: 1.6rem;
174
+ }
175
+ }
176
+ </style>
177
+ </head>
178
+ <body>
179
+ <div class="container">
180
+ <h1 id="page-title">🎙️ Processando Transcrição</h1>
181
+
182
+ <div class="file-info">
183
+ <div class="file-name">📄 {{ filename }}</div>
184
+ </div>
185
+
186
+ <!-- Estado: Processando -->
187
+ <div id="processing-state" class="status-container">
188
+ <div class="spinner"></div>
189
+ <div class="status-text">Status:</div>
190
+ <div class="status-message" id="status-message">Aguardando processamento...</div>
191
+ <div class="progress-bar">
192
+ <div class="progress-fill" id="progress-fill" style="width: 20%;"></div>
193
+ </div>
194
+ </div>
195
+
196
+ <!-- Estado: Concluído -->
197
+ <div id="success-state" class="status-container hidden">
198
+ <div class="success-icon">✅</div>
199
+ <div class="status-text">Transcrição concluída!</div>
200
+ <div class="result-info">
201
+ <div>Arquivo gerado: <span id="output-filename"></span></div>
202
+ </div>
203
+ <a href="#" class="btn btn-success" id="download-btn">
204
+ 📥 Baixar Transcrição
205
+ </a>
206
+ </div>
207
+
208
+ <!-- Estado: Erro -->
209
+ <div id="error-state" class="status-container hidden">
210
+ <div class="error-icon">❌</div>
211
+ <div class="status-text">Erro no processamento</div>
212
+ <div class="error-info">
213
+ <div id="error-message">Ocorreu um erro durante a transcrição.</div>
214
+ </div>
215
+ </div>
216
+
217
+ <!-- Botões de navegação -->
218
+ <div style="margin-top: 30px;">
219
+ <a href="/" class="btn btn-secondary">🔙 Nova Transcrição</a>
220
+ <a href="/results" class="btn btn-secondary">📂 Ver Resultados</a>
221
+ </div>
222
+ </div>
223
+
224
+ <script>
225
+ const jobId = '{{ job_id }}';
226
+ let progressWidth = 20;
227
+
228
+ const processingState = document.getElementById('processing-state');
229
+ const successState = document.getElementById('success-state');
230
+ const errorState = document.getElementById('error-state');
231
+ const statusMessage = document.getElementById('status-message');
232
+ const progressFill = document.getElementById('progress-fill');
233
+ const pageTitle = document.getElementById('page-title');
234
+ const downloadBtn = document.getElementById('download-btn');
235
+ const outputFilename = document.getElementById('output-filename');
236
+ const errorMessage = document.getElementById('error-message');
237
+
238
+ // Atualizar
templates/results.html ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="pt-BR">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Resultados - Transcritor de Áudio</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 800px;
23
+ margin: 0 auto;
24
+ background: white;
25
+ border-radius: 20px;
26
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
27
+ overflow: hidden;
28
+ }
29
+
30
+ .header {
31
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
32
+ color: white;
33
+ padding: 30px;
34
+ text-align: center;
35
+ }
36
+
37
+ h1 {
38
+ font-size: 2rem;
39
+ margin-bottom: 10px;
40
+ }
41
+
42
+ .subtitle {
43
+ opacity: 0.9;
44
+ font-size: 1.1rem;
45
+ }
46
+
47
+ .content {
48
+ padding: 30px;
49
+ }
50
+
51
+ .no-results {
52
+ text-align: center;
53
+ padding: 60px 20px;
54
+ color: #666;
55
+ }
56
+
57
+ .no-results-icon {
58
+ font-size: 4rem;
59
+ margin-bottom: 20px;
60
+ opacity: 0.5;
61
+ }
62
+
63
+ .files-grid {
64
+ display: grid;
65
+ gap: 20px;
66
+ margin-top: 20px;
67
+ }
68
+
69
+ .file-card {
70
+ background: #f8f9fa;
71
+ border: 2px solid #e9ecef;
72
+ border-radius: 15px;
73
+ padding: 20px;
74
+ transition: all 0.3s ease;
75
+ position: relative;
76
+ }
77
+
78
+ .file-card:hover {
79
+ border-color: #667eea;
80
+ transform: translateY(-2px);
81
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
82
+ }
83
+
84
+ .file-info {
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: space-between;
88
+ flex-wrap: wrap;
89
+ gap: 15px;
90
+ }
91
+
92
+ .file-details {
93
+ flex: 1;
94
+ min-width: 200px;
95
+ }
96
+
97
+ .file-name {
98
+ font-size: 1.1rem;
99
+ font-weight: 600;
100
+ color: #333;
101
+ margin-bottom: 8px;
102
+ word-break: break-all;
103
+ }
104
+
105
+ .file-meta {
106
+ display: flex;
107
+ gap: 20px;
108
+ color: #666;
109
+ font-size: 0.9rem;
110
+ flex-wrap: wrap;
111
+ }
112
+
113
+ .meta-item {
114
+ display: flex;
115
+ align-items: center;
116
+ gap: 5px;
117
+ }
118
+
119
+ .file-actions {
120
+ display: flex;
121
+ gap: 10px;
122
+ }
123
+
124
+ .btn {
125
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
126
+ color: white;
127
+ padding: 10px 20px;
128
+ border: none;
129
+ border-radius: 20px;
130
+ font-size: 0.9rem;
131
+ cursor: pointer;
132
+ transition: all 0.3s ease;
133
+ text-decoration: none;
134
+ display: inline-flex;
135
+ align-items: center;
136
+ gap: 8px;
137
+ }
138
+
139
+ .btn:hover {
140
+ transform: translateY(-1px);
141
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
142
+ }
143
+
144
+ .btn-secondary {
145
+ background: #6c757d;
146
+ }
147
+
148
+ .btn-small {
149
+ padding: 8px 15px;
150
+ font-size: 0.85rem;
151
+ }
152
+
153
+ .navigation {
154
+ text-align: center;
155
+ margin-top: 30px;
156
+ padding-top: 30px;
157
+ border-top: 1px solid #eee;
158
+ }
159
+
160
+ .stats {
161
+ background: #e8f4fd;
162
+ border-radius: 10px;
163
+ padding: 20px;
164
+ margin-bottom: 30px;
165
+ text-align: center;
166
+ }
167
+
168
+ .stats-number {
169
+ font-size: 2rem;
170
+ font-weight: bold;
171
+ color: #667eea;
172
+ }
173
+
174
+ .stats-label {
175
+ color: #666;
176
+ margin-top: 5px;
177
+ }
178
+
179
+ @media (max-width: 600px) {
180
+ .container {
181
+ margin: 10px;
182
+ border-radius: 15px;
183
+ }
184
+
185
+ .header {
186
+ padding: 20px;
187
+ }
188
+
189
+ h1 {
190
+ font-size: 1.6rem;
191
+ }
192
+
193
+ .content {
194
+ padding: 20px;
195
+ }
196
+
197
+ .file-info {
198
+ flex-direction: column;
199
+ align-items: flex-start;
200
+ }
201
+
202
+ .file-actions {
203
+ width: 100%;
204
+ justify-content: flex-end;
205
+ }
206
+
207
+ .file-meta {
208
+ flex-direction: column;
209
+ gap: 8px;
210
+ }
211
+ }
212
+ </style>
213
+ </head>
214
+ <body>
215
+ <div class="container">
216
+ <div class="header">
217
+ <h1>📂 Resultados das Transcrições</h1>
218
+ <p class="subtitle">Arquivos JSON gerados pelo sistema</p>
219
+ </div>
220
+
221
+ <div class="content">
222
+ {% if files %}
223
+ <div class="stats">
224
+ <div class="stats-number">{{ files|length }}</div>
225
+ <div class="stats-label">
226
+ {% if files|length == 1 %} arquivo disponível {% else %} arquivos
227
+ disponíveis {% endif %}
228
+ </div>
229
+ </div>
230
+
231
+ <div class="files-grid">
232
+ {% for file in files %}
233
+ <div class="file-card">
234
+ <div class="file-info">
235
+ <div class="file-details">
236
+ <div class="file-name">📄 {{ file.name }}</div>
237
+ <div class="file-meta">
238
+ <div class="meta-item">
239
+ <span>📏</span>
240
+ <span>{{ file.size }} KB</span>
241
+ </div>
242
+ <div class="meta-item">
243
+ <span>📅</span>
244
+ <span>{{ file.modified }}</span>
245
+ </div>
246
+ </div>
247
+ </div>
248
+
249
+ <div class="file-actions">
250
+ <a href="/download/{{ file.name }}" class="btn btn-small">
251
+ 📥 Baixar
252
+ </a>
253
+ </div>
254
+ </div>
255
+ </div>
256
+ {% endfor %}
257
+ </div>
258
+ {% else %}
259
+ <div class="no-results">
260
+ <div class="no-results-icon">📭</div>
261
+ <h3>Nenhuma transcrição encontrada</h3>
262
+ <p>Você ainda não transcreveu nenhum arquivo de áudio.</p>
263
+ </div>
264
+ {% endif %}
265
+
266
+ <div class="navigation">
267
+ <a href="/" class="btn"> 🎙️ Nova Transcrição </a>
268
+ <a href="javascript:location.reload()" class="btn btn-secondary">
269
+ 🔄 Atualizar Lista
270
+ </a>
271
+ </div>
272
+ </div>
273
+ </div>
274
+ </body>
275
+ </html>
transcriptor.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import whisperx
3
+ import json
4
+ import sys
5
+ import os
6
+ from transformers import AutoTokenizer, AutoModelForSeq2SeqLM, pipeline
7
+ import warnings
8
+ warnings.filterwarnings("ignore")
9
+
10
+ def main():
11
+ # === VERIFICAR ARGUMENTOS ===
12
+ if len(sys.argv) != 2:
13
+ print("Uso: python transcriptor.py <arquivo_audio>")
14
+ sys.exit(1)
15
+
16
+ AUDIO_PATH = sys.argv[1]
17
+
18
+ # Verificar se arquivo existe
19
+ if not os.path.exists(AUDIO_PATH):
20
+ print(f"[ERRO] Arquivo não encontrado: {AUDIO_PATH}")
21
+ sys.exit(1)
22
+
23
+ # Gerar nome do JSON baseado no áudio
24
+ base_name = os.path.splitext(os.path.basename(AUDIO_PATH))[0]
25
+ OUTPUT_JSON = f"{base_name}_transcricao.json"
26
+
27
+ # === CONFIGURAÇÕES ===
28
+ LANGUAGE = "pt"
29
+ TERMO_FIXO = ["CETOX", "CETOX31", "WhisperX", "VSL"]
30
+ MODEL_NAME = "unicamp-dl/ptt5-base-portuguese-vocab"
31
+ SCORE_MINIMO = 0.5
32
+
33
+ # === SETUP DISPOSITIVO ===
34
+ device = "cuda" if torch.cuda.is_available() else "cpu"
35
+ compute_type = "float16" if device == "cuda" else "int8"
36
+ print(f"[INFO] Usando dispositivo: {device.upper()}")
37
+
38
+ # === TRANSCRIÇÃO COM WHISPERX ===
39
+ print("[INFO] Carregando modelo WhisperX...")
40
+ model = whisperx.load_model("medium", device, compute_type=compute_type, language=LANGUAGE)
41
+
42
+ print("[INFO] Carregando áudio e transcrevendo...")
43
+ audio = whisperx.load_audio(AUDIO_PATH)
44
+ result = model.transcribe(audio)
45
+
46
+ print("[INFO] Carregando modelo de alinhamento...")
47
+ align_model, metadata = whisperx.load_align_model(language_code=LANGUAGE, device=device)
48
+
49
+ print("[INFO] Alinhando palavras com precisão...")
50
+ aligned = whisperx.align(result["segments"], align_model, metadata, audio, device)
51
+
52
+ # === CORREÇÃO GRAMATICAL COM PTT5 ===
53
+ print("[INFO] Carregando corretor gramatical...")
54
+ try:
55
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
56
+ model_corr = AutoModelForSeq2SeqLM.from_pretrained(MODEL_NAME)
57
+ corretor = pipeline("text2text-generation", model=model_corr, tokenizer=tokenizer, device=0 if device == "cuda" else -1)
58
+ corretor_disponivel = True
59
+ except Exception as e:
60
+ print(f"[AVISO] Correção desativada: {e}")
61
+ corretor_disponivel = False
62
+
63
+ def corrigir(palavra):
64
+ if not palavra.strip() or palavra.upper() in [t.upper() for t in TERMO_FIXO] or palavra.isnumeric():
65
+ return palavra
66
+ if not corretor_disponivel:
67
+ return palavra.capitalize()
68
+ try:
69
+ entrada = f"corrigir gramática: {palavra}"
70
+ saida = corretor(entrada, max_length=40, do_sample=False, num_beams=1)[0]["generated_text"]
71
+ return saida.strip().capitalize() or palavra.capitalize()
72
+ except:
73
+ return palavra.capitalize()
74
+
75
+ # === PROCESSAR RESULTADO FINAL ===
76
+ print("[INFO] Processando palavras...")
77
+ resultado = []
78
+ for word in aligned["word_segments"]:
79
+ if word["score"] < SCORE_MINIMO or not word["word"].strip():
80
+ continue
81
+ corrigida = corrigir(word["word"])
82
+ resultado.append({
83
+ "word": corrigida,
84
+ "start": round(word["start"], 3),
85
+ "end": round(word["end"], 3),
86
+ "score": round(word["score"], 3)
87
+ })
88
+
89
+ # === SALVAR EM JSON ===
90
+ output = {
91
+ "metadata": {
92
+ "total_words": len(resultado),
93
+ "arquivo": AUDIO_PATH,
94
+ "modelo": "WhisperX medium",
95
+ "correcao_gramatical": corretor_disponivel
96
+ },
97
+ "words": resultado
98
+ }
99
+
100
+ with open(OUTPUT_JSON, "w", encoding="utf-8") as f:
101
+ json.dump(output, f, ensure_ascii=False, indent=2)
102
+
103
+ print(f"[SUCESSO] Transcrição salva em {OUTPUT_JSON} com {len(resultado)} palavras!")
104
+ return OUTPUT_JSON
105
+
106
+ if __name__ == "__main__":
107
+ main()