teste claude 1
Browse files- .gitattributes +0 -35
- Dockerfile +25 -5
- app.py +128 -127
- readme_md.md +165 -0
- requirements.txt +10 -5
- static/main.js +0 -106
- templates/index.html +300 -661
- templates/processing.html +238 -0
- templates/results.html +275 -0
- 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.
|
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
|
11 |
-
RUN mkdir -p
|
12 |
-
RUN mkdir -p /data/transcriptions && chmod -R 777 /data/transcriptions
|
13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
EXPOSE 7860
|
15 |
-
|
|
|
|
|
|
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
|
4 |
-
import whisperx
|
5 |
import json
|
6 |
-
from
|
|
|
|
|
7 |
|
8 |
app = Flask(__name__)
|
9 |
-
app.
|
10 |
|
11 |
-
#
|
12 |
-
UPLOAD_FOLDER = '
|
13 |
-
|
14 |
-
|
15 |
|
16 |
-
# Criar pastas
|
17 |
-
|
18 |
-
|
19 |
|
20 |
-
|
21 |
-
|
22 |
-
|
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 |
-
|
41 |
-
|
42 |
-
result = model.transcribe(audio)
|
43 |
|
44 |
-
|
45 |
-
|
46 |
|
47 |
-
|
48 |
-
|
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 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
except Exception as e:
|
110 |
-
|
|
|
111 |
|
112 |
-
@app.route('/
|
113 |
-
def
|
114 |
-
|
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('/
|
134 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
135 |
try:
|
136 |
-
|
137 |
-
if
|
138 |
-
return
|
139 |
-
|
140 |
-
|
|
|
141 |
except Exception as e:
|
142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
2 |
-
|
3 |
-
whisperx
|
4 |
-
transformers
|
5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
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>
|
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 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
.container {
|
24 |
-
|
25 |
-
|
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 |
-
|
267 |
-
|
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 |
-
|
519 |
-
|
520 |
-
|
521 |
-
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
|
527 |
-
|
528 |
-
|
529 |
-
|
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 |
-
|
547 |
-
|
548 |
-
|
549 |
-
|
550 |
-
|
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 |
-
|
643 |
-
|
644 |
-
|
645 |
-
|
646 |
-
|
647 |
-
|
648 |
-
|
649 |
-
|
650 |
-
|
651 |
-
|
652 |
-
|
653 |
-
|
654 |
-
|
655 |
-
|
656 |
-
|
657 |
-
|
658 |
-
|
659 |
-
|
660 |
-
|
661 |
-
|
662 |
-
|
663 |
-
|
664 |
-
|
665 |
-
|
666 |
-
|
667 |
-
|
668 |
-
|
669 |
-
|
670 |
-
|
671 |
-
|
672 |
-
|
673 |
-
|
674 |
-
|
675 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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()
|