# 🎯 TODO: Dashboard de Monitoreo y Reprocesamiento ## πŸ“‹ Objetivo Principal Crear un dashboard funcional en localhost que permita: 1. **Monitorear** archivos procesados y pendientes 2. **Visualizar** transcripciones y resΓΊmenes generados 3. **Regenerar resΓΊmenes** cuando no son satisfactorios (usando la transcripciΓ³n existente) 4. **Reprocesar** archivos completos si es necesario --- ## πŸ“Š Estado Actual del Proyecto ### βœ… Lo que YA existe: | Componente | UbicaciΓ³n | Estado | |------------|-----------|--------| | UI Dashboard | `templates/index.html` | βœ… Completo (1586 lΓ­neas) | | API Routes | `api/routes.py` | βœ… Parcial (281 lΓ­neas) | | Backend Flask | `api/__init__.py` | βœ… BΓ‘sico | | Generador Docs | `document/generators.py` | βœ… Completo | | Audio Processor | `processors/audio_processor.py` | βœ… Completo | | PDF Processor | `processors/pdf_processor.py` | βœ… Completo | | AI Providers | `services/ai/` | βœ… Completo | ### ❌ Lo que FALTA implementar: | Funcionalidad | Prioridad | Complejidad | |---------------|-----------|-------------| | Endpoint regenerar resumen | πŸ”΄ Alta | Media | | Vista previa de transcripciΓ³n | πŸ”΄ Alta | Baja | | Vista previa de resumen | πŸ”΄ Alta | Baja | | Editor de prompts | 🟑 Media | Media | | Historial de versiones | 🟒 Baja | Alta | --- ## πŸ—οΈ ARQUITECTURA DEL DASHBOARD ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ FRONTEND (index.html) β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Stats β”‚ β”‚ File List β”‚ β”‚ Preview β”‚ β”‚ Actions β”‚ β”‚ β”‚ β”‚ Cards β”‚ β”‚ + Filters β”‚ β”‚ Panel β”‚ β”‚ Panel β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ API ROUTES (routes.py) β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ GET /api/files β”‚ β”‚ GET /api/preview β”‚ β”‚ POST /api/regen β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ BACKEND SERVICES β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Document β”‚ β”‚ AI Provider β”‚ β”‚ WebDAV β”‚ β”‚ β”‚ β”‚ Generator β”‚ β”‚ (Gemini) β”‚ β”‚ Service β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` --- ## πŸ“ ARCHIVOS A MODIFICAR/CREAR ### 1. `api/routes.py` - Nuevos Endpoints #### 1.1 Endpoint: Obtener contenido de transcripciΓ³n ``` GET /api/transcription/ ``` **PropΓ³sito**: Devolver el contenido de la transcripciΓ³n (.txt) de un archivo de audio. **Request**: ``` GET /api/transcription/clase_historia_01 ``` **Response**: ```json { "success": true, "filename": "clase_historia_01", "transcription": "Contenido completo de la transcripciΓ³n...", "file_path": "/home/ren/proyectos/cbc/downloads/clase_historia_01.txt", "word_count": 3456, "char_count": 18234 } ``` **ImplementaciΓ³n**: ```python @app.route('/api/transcription/') def get_transcription(filename): """Obtener contenido de transcripciΓ³n""" try: # Buscar archivo .txt en downloads/ txt_path = settings.LOCAL_DOWNLOADS_PATH / f"{filename}.txt" if not txt_path.exists(): return jsonify({ 'success': False, 'message': f"TranscripciΓ³n no encontrada: {filename}.txt" }), 404 with open(txt_path, 'r', encoding='utf-8') as f: content = f.read() return jsonify({ 'success': True, 'filename': filename, 'transcription': content, 'file_path': str(txt_path), 'word_count': len(content.split()), 'char_count': len(content) }) except Exception as e: app.logger.error(f"Error getting transcription: {e}") return jsonify({ 'success': False, 'message': f"Error: {str(e)}" }), 500 ``` --- #### 1.2 Endpoint: Obtener contenido de resumen ``` GET /api/summary/ ``` **PropΓ³sito**: Devolver el contenido del resumen (.md) de un archivo procesado. **Request**: ``` GET /api/summary/clase_historia_01_unificado ``` **Response**: ```json { "success": true, "filename": "clase_historia_01_unificado", "summary": "# Resumen\n\n## Contenido...", "file_path": "/home/ren/proyectos/cbc/downloads/clase_historia_01_unificado.md", "formats_available": { "md": true, "docx": true, "pdf": true } } ``` **ImplementaciΓ³n**: ```python @app.route('/api/summary/') def get_summary(filename): """Obtener contenido de resumen""" try: # Normalizar nombre (agregar _unificado si no lo tiene) base_name = filename.replace('_unificado', '') unified_name = f"{base_name}_unificado" # Buscar archivo .md en downloads/ md_path = settings.LOCAL_DOWNLOADS_PATH / f"{unified_name}.md" if not md_path.exists(): return jsonify({ 'success': False, 'message': f"Resumen no encontrado: {unified_name}.md" }), 404 with open(md_path, 'r', encoding='utf-8') as f: content = f.read() # Verificar formatos disponibles formats = { 'md': md_path.exists(), 'docx': (settings.LOCAL_DOCX / f"{unified_name}.docx").exists(), 'pdf': (settings.LOCAL_DOWNLOADS_PATH / f"{unified_name}.pdf").exists() } return jsonify({ 'success': True, 'filename': unified_name, 'summary': content, 'file_path': str(md_path), 'formats_available': formats }) except Exception as e: app.logger.error(f"Error getting summary: {e}") return jsonify({ 'success': False, 'message': f"Error: {str(e)}" }), 500 ``` --- #### 1.3 Endpoint: Regenerar resumen (CRÍTICO) ``` POST /api/regenerate-summary ``` **PropΓ³sito**: Regenerar el resumen usando la transcripciΓ³n existente, sin reprocesar el audio. **Request**: ```json { "filename": "clase_historia_01", "custom_prompt": "Opcional: prompt personalizado para el resumen", "ai_provider": "gemini" } ``` **Response**: ```json { "success": true, "message": "Resumen regenerado exitosamente", "new_summary": "# Nuevo Resumen...", "files_updated": { "md": "/path/to/clase_historia_01_unificado.md", "docx": "/path/to/clase_historia_01_unificado.docx", "pdf": "/path/to/clase_historia_01_unificado.pdf" }, "processing_time": 4.5 } ``` **ImplementaciΓ³n detallada**: ```python @app.route('/api/regenerate-summary', methods=['POST']) def regenerate_summary(): """Regenerar resumen desde transcripciΓ³n existente""" try: data = request.get_json() filename = data.get('filename') custom_prompt = data.get('custom_prompt', None) if not filename: return jsonify({ 'success': False, 'message': "Nombre de archivo requerido" }), 400 # Paso 1: Obtener transcripciΓ³n base_name = filename.replace('_unificado', '').replace('.txt', '').replace('.md', '') txt_path = settings.LOCAL_DOWNLOADS_PATH / f"{base_name}.txt" if not txt_path.exists(): return jsonify({ 'success': False, 'message': f"TranscripciΓ³n no encontrada: {base_name}.txt" }), 404 with open(txt_path, 'r', encoding='utf-8') as f: transcription_text = f.read() if not transcription_text.strip(): return jsonify({ 'success': False, 'message': "La transcripciΓ³n estΓ‘ vacΓ­a" }), 400 # Paso 2: Regenerar resumen con IA import time start_time = time.time() from document.generators import DocumentGenerator doc_generator = DocumentGenerator() success, summary, output_files = doc_generator.generate_summary( transcription_text, base_name ) processing_time = time.time() - start_time if not success: return jsonify({ 'success': False, 'message': "Error al generar el resumen con IA" }), 500 # Paso 3: Subir a Nextcloud si estΓ‘ configurado if settings.has_webdav_config: from services.webdav_service import webdav_service for folder in [settings.RESUMENES_FOLDER, settings.DOCX_FOLDER]: try: webdav_service.makedirs(folder) except Exception: pass # Subir MD md_path = Path(output_files.get('markdown_path', '')) if md_path.exists(): remote_md = f"{settings.RESUMENES_FOLDER}/{md_path.name}" webdav_service.upload(md_path, remote_md) # Subir DOCX docx_path = Path(output_files.get('docx_path', '')) if docx_path.exists(): remote_docx = f"{settings.DOCX_FOLDER}/{docx_path.name}" webdav_service.upload(docx_path, remote_docx) # Subir PDF pdf_path = Path(output_files.get('pdf_path', '')) if pdf_path.exists(): remote_pdf = f"{settings.DOCX_FOLDER}/{pdf_path.name}" webdav_service.upload(pdf_path, remote_pdf) return jsonify({ 'success': True, 'message': "Resumen regenerado exitosamente", 'new_summary': summary[:500] + "..." if len(summary) > 500 else summary, 'files_updated': output_files, 'processing_time': round(processing_time, 2) }) except Exception as e: app.logger.error(f"Error regenerating summary: {e}") return jsonify({ 'success': False, 'message': f"Error: {str(e)}" }), 500 ``` --- #### 1.4 Endpoint: Listar todos los archivos procesados con detalles ``` GET /api/files-detailed ``` **PropΓ³sito**: Obtener lista de archivos con informaciΓ³n sobre transcripciones y resΓΊmenes disponibles. **Response**: ```json { "success": true, "files": [ { "filename": "clase_historia_01.mp3", "base_name": "clase_historia_01", "audio_path": "/path/to/audio.mp3", "has_transcription": true, "transcription_path": "/path/to/clase_historia_01.txt", "transcription_words": 3456, "has_summary": true, "summary_path": "/path/to/clase_historia_01_unificado.md", "formats": { "txt": true, "md": true, "docx": true, "pdf": true }, "processed": true, "last_modified": "2024-01-10 15:30:00" } ] } ``` **ImplementaciΓ³n**: ```python @app.route('/api/files-detailed') def get_files_detailed(): """Obtener lista detallada de archivos con transcripciones y resΓΊmenes""" try: files = [] downloads_path = settings.LOCAL_DOWNLOADS_PATH docx_path = settings.LOCAL_DOCX # Buscar archivos de audio for ext in settings.AUDIO_EXTENSIONS: for audio_file in downloads_path.glob(f"*{ext}"): base_name = audio_file.stem # Verificar transcripciΓ³n txt_path = downloads_path / f"{base_name}.txt" has_transcription = txt_path.exists() transcription_words = 0 if has_transcription: with open(txt_path, 'r', encoding='utf-8') as f: transcription_words = len(f.read().split()) # Verificar resumen md_path = downloads_path / f"{base_name}_unificado.md" has_summary = md_path.exists() # Verificar formatos formats = { 'txt': has_transcription, 'md': has_summary, 'docx': (docx_path / f"{base_name}_unificado.docx").exists(), 'pdf': (downloads_path / f"{base_name}_unificado.pdf").exists() } files.append({ 'filename': audio_file.name, 'base_name': base_name, 'audio_path': str(audio_file), 'has_transcription': has_transcription, 'transcription_path': str(txt_path) if has_transcription else None, 'transcription_words': transcription_words, 'has_summary': has_summary, 'summary_path': str(md_path) if has_summary else None, 'formats': formats, 'processed': has_transcription and has_summary, 'last_modified': datetime.fromtimestamp( audio_file.stat().st_mtime ).strftime('%Y-%m-%d %H:%M:%S') }) return jsonify({ 'success': True, 'files': sorted(files, key=lambda x: x['last_modified'], reverse=True), 'total': len(files), 'with_transcription': sum(1 for f in files if f['has_transcription']), 'with_summary': sum(1 for f in files if f['has_summary']) }) except Exception as e: app.logger.error(f"Error getting detailed files: {e}") return jsonify({ 'success': False, 'message': f"Error: {str(e)}" }), 500 ``` --- ### 2. `templates/index.html` - Nuevos Componentes UI #### 2.1 Panel de Preview (Sidebar Derecho) **UbicaciΓ³n**: DespuΓ©s del `files-container` existente **HTML a agregar** (despuΓ©s de lΓ­nea 1093): ```html

Selecciona un archivo

0 palabras 0 caracteres
Haz clic en un archivo para ver su transcripciΓ³n
Haz clic en un archivo para ver su resumen

Regenerando resumen con IA...

Esto puede tomar 10-30 segundos
``` --- #### 2.2 Estilos CSS para el Panel de Preview **Agregar al bloque `