from flask import Flask, render_template, request, jsonify, send_file, flash, redirect, url_for import os import subprocess import json from werkzeug.utils import secure_filename import threading import time import moviepy.editor as mp from PIL import Image, ImageDraw, ImageFont import io import base64 import random app = Flask(__name__) app.secret_key = 'transcricao_audio_key_2024' # Configurações UPLOAD_FOLDER = 'uploads' OUTPUT_FOLDER = 'outputs' TEMP_FOLDER = 'temp' ALLOWED_EXTENSIONS = {'mp3', 'wav', 'mp4', 'm4a', 'flac', 'ogg'} ALLOWED_TRANSCRIPTION = {'json'} # Criar pastas se não existirem for folder in [UPLOAD_FOLDER, OUTPUT_FOLDER, TEMP_FOLDER]: os.makedirs(folder, exist_ok=True) app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER app.config['OUTPUT_FOLDER'] = OUTPUT_FOLDER app.config['TEMP_FOLDER'] = TEMP_FOLDER app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # 500MB max # Status dos processamentos processing_status = {} def allowed_file(filename, allowed_extensions): return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions def create_slide_image(text, width=1920, height=1080): """ Cria uma imagem com o texto do slide seguindo regras VSL: - Fonte Open Sans 42 - Máximo 2 linhas em 90% dos slides - Palavras importantes em negrito preto ou vermelho - Texto centralizado - Fundo branco """ # Criar imagem com fundo branco img = Image.new('RGB', (width, height), 'white') draw = ImageDraw.Draw(img) try: # Tentar carregar Open Sans do sistema font_regular = ImageFont.truetype("/usr/share/fonts/truetype/open-sans/OpenSans-Regular.ttf", 42) font_bold = ImageFont.truetype("/usr/share/fonts/truetype/open-sans/OpenSans-Bold.ttf", 42) except: # Fallback para fonte do sistema try: font_regular = ImageFont.truetype("/usr/share/fonts/liberation/LiberationSans-Regular.ttf", 42) font_bold = ImageFont.truetype("/usr/share/fonts/liberation/LiberationSans-Bold.ttf", 42) except: print("Aviso: Usando fonte padrão") font_regular = ImageFont.load_default() font_bold = font_regular # Processar texto para identificar palavras em negrito words = [] current_pos = 0 text_without_tags = "" # Identificar tags de negrito/vermelho while current_pos < len(text): if text[current_pos:].startswith(''): end_pos = text.find('', current_pos) if end_pos != -1: word = text[current_pos+23:end_pos] words.append(('red-bold', word)) text_without_tags += word + " " current_pos = end_pos + 7 continue elif text[current_pos:].startswith(''): end_pos = text.find('', current_pos) if end_pos != -1: word = text[current_pos+25:end_pos] words.append(('black-bold', word)) text_without_tags += word + " " current_pos = end_pos + 7 continue if text[current_pos] == '<': end_pos = text.find('>', current_pos) if end_pos != -1: current_pos = end_pos + 1 continue text_without_tags += text[current_pos] current_pos += 1 # Quebrar texto em linhas (máximo 2) words_clean = text_without_tags.split() lines = [] current_line = [] max_width = width * 0.8 # 80% da largura para margens for word in words_clean: test_line = ' '.join(current_line + [word]) bbox = draw.textbbox((0, 0), test_line, font=font_regular) line_width = bbox[2] - bbox[0] if line_width <= max_width: current_line.append(word) else: if len(lines) < 2: # Máximo 2 linhas lines.append(' '.join(current_line)) current_line = [word] else: break if current_line and len(lines) < 2: lines.append(' '.join(current_line)) # Calcular altura total do texto total_height = 0 line_spacing = 20 # Espaçamento entre linhas for line in lines: bbox = draw.textbbox((0, 0), line, font=font_regular) total_height += (bbox[3] - bbox[1]) + line_spacing # Posição inicial Y (centralizado verticalmente) current_y = (height - total_height) / 2 # Desenhar cada linha for line in lines: # Calcular largura da linha para centralizar bbox = draw.textbbox((0, 0), line, font=font_regular) line_width = bbox[2] - bbox[0] current_x = (width - line_width) / 2 # Desenhar palavras com formatação apropriada for style, word in words: if word in line: if style == 'red-bold': draw.text((current_x, current_y), word, font=font_bold, fill='#dc2626') elif style == 'black-bold': draw.text((current_x, current_y), word, font=font_bold, fill='black') else: draw.text((current_x, current_y), word, font=font_regular, fill='black') # Mover posição X bbox = draw.textbbox((0, 0), word + ' ', font=font_regular) current_x += bbox[2] - bbox[0] current_y += bbox[3] - bbox[1] + line_spacing # Adicionar "..." no final de 90% dos slides if random.random() < 0.9: # 90% de chance draw.text( (width - 60, height - 60), "...", font=font_regular, fill='black', anchor="rb" ) return img @app.route('/export_video', methods=['POST']) def export_video(): """Exporta os slides como vídeo com sincronização precisa""" try: data = request.json slides = data.get('slides', []) audio_data = data.get('audio', '') if not slides: return jsonify({'error': 'Nenhum slide fornecido'}), 400 # Criar diretório temporário único temp_dir = os.path.join(app.config['TEMP_FOLDER'], str(int(time.time()))) os.makedirs(temp_dir, exist_ok=True) try: # Gerar imagens dos slides slide_paths = [] for i, slide in enumerate(slides): img = create_slide_image(slide['text']) path = os.path.join(temp_dir, f'slide_{i:03d}.png') img.save(path) slide_paths.append(path) # Criar vídeo dos slides com timing preciso clips = [] for i, (path, slide) in enumerate(zip(slide_paths, slides)): # Calcular duração exata baseada nos timestamps do JSON if i < len(slides) - 1: duration = slides[i + 1]['start'] - slide['start'] else: # Para o último slide, usar o tempo final do áudio ou um valor fixo duration = 3.0 # valor padrão se não houver próximo slide # Criar clip com duração exata clip = mp.ImageClip(path, duration=duration) # Definir tempo de início exato clip = clip.set_start(slide['start']) clips.append(clip) # Concatenar clips mantendo os tempos exatos video = mp.CompositeVideoClip(clips) # Adicionar áudio mantendo sincronização if audio_data: # Decodificar áudio base64 audio_bytes = base64.b64decode(audio_data.split(',')[1]) audio_path = os.path.join(temp_dir, 'audio.mp3') with open(audio_path, 'wb') as f: f.write(audio_bytes) # Carregar áudio e sincronizar audio = mp.AudioFileClip(audio_path) # Garantir que o vídeo tenha a mesma duração do áudio video = video.set_duration(audio.duration) video = video.set_audio(audio) # Exportar vídeo mantendo qualidade output_path = os.path.join(temp_dir, 'output.mp4') video.write_videofile( output_path, fps=30, # Aumentar FPS para transições mais suaves codec='libx264', audio_codec='aac', audio_bitrate='192k', # Melhor qualidade de áudio bitrate='8000k', # Melhor qualidade de vídeo preset='slow' # Melhor compressão ) return send_file(output_path, as_attachment=True, download_name='vsl_video.mp4') finally: # Limpar arquivos temporários for path in slide_paths: try: os.remove(path) except: pass if 'audio_path' in locals(): try: os.remove(audio_path) except: pass try: os.remove(output_path) except: pass try: os.rmdir(temp_dir) except: pass except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/upload_transcription', methods=['POST']) def upload_transcription(): """Upload de arquivo de transcrição""" if 'file' not in request.files: return jsonify({'success': False, 'error': 'Nenhum arquivo enviado'}) file = request.files['file'] if file.filename == '': return jsonify({'success': False, 'error': 'Nenhum arquivo selecionado'}) if file and allowed_file(file.filename, ALLOWED_TRANSCRIPTION): filename = secure_filename(file.filename) if not filename.endswith('_transcricao.json'): filename = filename.rsplit('.', 1)[0] + '_transcricao.json' filepath = os.path.join(app.config['OUTPUT_FOLDER'], filename) file.save(filepath) return jsonify({'success': True}) return jsonify({'success': False, 'error': 'Formato de arquivo não suportado'}) @app.route('/delete_transcription/') def delete_transcription(filename): """Deleta uma transcrição""" try: filepath = os.path.join(app.config['OUTPUT_FOLDER'], filename) if os.path.exists(filepath): os.remove(filepath) return jsonify({'success': True}) return jsonify({'success': False, 'error': 'Arquivo não encontrado'}) except Exception as e: return jsonify({'success': False, 'error': str(e)}) def run_transcription(audio_path, job_id): """Executa a transcrição em background""" try: print(f"[{job_id}] Iniciando transcrição para {audio_path}") processing_status[job_id]['status'] = 'processing' processing_status[job_id]['message'] = 'Iniciando transcrição...' result = subprocess.run( ['python', 'transcriptor.py', audio_path], capture_output=True, text=True, cwd='.' ) print(f"[{job_id}] Transcrição finalizada com código {result.returncode}") if result.stdout: print(f"[{job_id}] STDOUT: {result.stdout}") if result.stderr: print(f"[{job_id}] STDERR: {result.stderr}") base_name = os.path.splitext(os.path.basename(audio_path))[0] json_file = f"{base_name}_transcricao.json" if result.returncode == 0: if os.path.exists(json_file): output_path = os.path.join(OUTPUT_FOLDER, json_file) os.rename(json_file, output_path) processing_status[job_id]['status'] = 'completed' processing_status[job_id]['message'] = 'Transcrição concluída com sucesso!' processing_status[job_id]['output_file'] = json_file print(f"[{job_id}] JSON salvo em: {output_path}") else: processing_status[job_id]['status'] = 'error' processing_status[job_id]['message'] = 'Arquivo JSON não foi gerado' print(f"[{job_id}] ERRO: JSON não encontrado: {json_file}") else: processing_status[job_id]['status'] = 'error' processing_status[job_id]['message'] = f'Erro na transcrição: {result.stderr}' print(f"[{job_id}] ERRO na execução do script") except Exception as e: processing_status[job_id]['status'] = 'error' processing_status[job_id]['message'] = f'Erro interno: {str(e)}' print(f"[{job_id}] EXCEPTION: {str(e)}") @app.route('/') def index(): return render_template('dashboard.html') @app.route('/transcricao') def transcricao(): return render_template('index.html') @app.route('/vsl') def vsl(): return render_template('vsl.html') @app.route('/upload', methods=['POST']) def upload_file(): if 'file' not in request.files: flash('Nenhum arquivo selecionado') return redirect(request.url) file = request.files['file'] if file.filename == '': flash('Nenhum arquivo selecionado') return redirect(request.url) if file and allowed_file(file.filename, ALLOWED_EXTENSIONS): filename = secure_filename(file.filename) filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) file.save(filepath) # Criar job ID único job_id = f"{int(time.time())}_{filename}" processing_status[job_id] = { 'status': 'queued', 'message': 'Aguardando processamento...', 'filename': filename } # Iniciar transcrição em background thread = threading.Thread(target=run_transcription, args=(filepath, job_id)) thread.daemon = True thread.start() return render_template('processing.html', job_id=job_id, filename=filename) else: flash('Formato de arquivo não suportado. Use: MP3, WAV, MP4, M4A, FLAC, OGG') return redirect(url_for('index')) @app.route('/status/') def check_status(job_id): if job_id in processing_status: return jsonify(processing_status[job_id]) else: return jsonify({'status': 'not_found', 'message': 'Job não encontrado'}) @app.route('/download/') def download_file(filename): try: file_path = os.path.join(OUTPUT_FOLDER, filename) if os.path.exists(file_path): return send_file(file_path, as_attachment=True) else: flash('Arquivo não encontrado') return redirect(url_for('index')) except Exception as e: flash(f'Erro ao baixar arquivo: {str(e)}') return redirect(url_for('index')) @app.route('/results') def results(): output_files = [] if os.path.exists(OUTPUT_FOLDER): for filename in os.listdir(OUTPUT_FOLDER): if filename.endswith('.json'): filepath = os.path.join(OUTPUT_FOLDER, filename) file_stats = os.stat(filepath) output_files.append({ 'name': filename, 'size': round(file_stats.st_size / 1024, 2), 'modified': time.ctime(file_stats.st_mtime) }) return render_template('results.html', files=output_files) @app.route('/list_transcriptions') def list_transcriptions(): """Lista todas as transcrições disponíveis no servidor""" files = [] if os.path.exists(OUTPUT_FOLDER): for filename in os.listdir(OUTPUT_FOLDER): if filename.endswith('.json'): file_path = os.path.join(OUTPUT_FOLDER, filename) stats = os.stat(file_path) files.append({ 'name': filename, 'path': file_path, 'modified': stats.st_mtime * 1000 # Converter para milissegundos para JavaScript }) return jsonify(files) @app.route('/get_transcription/') def get_transcription(filename): """Retorna o conteúdo de uma transcrição específica""" try: file_path = os.path.join(OUTPUT_FOLDER, filename) if os.path.exists(file_path): with open(file_path, 'r', encoding='utf-8') as f: return jsonify(json.load(f)) else: return jsonify({'error': 'Arquivo não encontrado'}), 404 except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/save_transcription', methods=['POST']) def save_transcription(): """Salva uma transcrição editada""" try: data = request.json if not data or 'metadata' not in data or 'words' not in data: return jsonify({'success': False, 'error': 'Formato de transcrição inválido'}) # Gerar nome do arquivo baseado no arquivo original base_name = os.path.splitext(os.path.basename(data['metadata']['arquivo']))[0] output_file = f"{base_name}_transcricao.json" output_path = os.path.join(OUTPUT_FOLDER, output_file) # Salvar arquivo with open(output_path, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2) return jsonify({'success': True}) except Exception as e: return jsonify({'success': False, 'error': str(e)}) if __name__ == '__main__': app.run(host='0.0.0.0', port=7860, debug=False)