Compare commits
	
		
			14 commits
		
	
	
		
			31834d4bed
			...
			3eb5cfc7c5
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 3eb5cfc7c5 | |||
| 36b01e493f | |||
| 50becc3c61 | |||
| b54ea64da5 | |||
| c35f705537 | |||
| ccdfa68813 | |||
| 5fec8b3f31 | |||
| 431473e0f1 | |||
| 4b025f904e | |||
| cf68c85869 | |||
| 9bc81ea5a8 | |||
| 4ec9b41e2c | |||
| b74c5e6826 | |||
| 32b049fe65 | 
					 3 changed files with 620 additions and 17 deletions
				
			
		
							
								
								
									
										72
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										72
									
								
								README.md
									
									
									
									
									
								
							|  | @ -39,14 +39,25 @@ bin/bootstrap.sh | ||||||
| bin/update.sh | bin/update.sh | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
|  | ### Estructura de Directorios | ||||||
|  | 
 | ||||||
|  | Durante la instalación y uso, MRDevs Tools crea varios directorios importantes: | ||||||
|  | 
 | ||||||
|  | - `~/devs/bin/`: Scripts y herramientas ejecutables | ||||||
|  | - `~/devs/ollama/`: Configuración y datos para Ollama AI | ||||||
|  | - `~/devs/sounds/`: Archivos de sonido para notificaciones | ||||||
|  | - `~/.vosk/`: Modelos para reconocimiento de voz local | ||||||
|  | - `~/.cortana/`: Configuración y tokens para Claude Code | ||||||
|  | - `~/.developer/`: Tokens y configuraciones para herramientas de desarrollo | ||||||
|  | 
 | ||||||
| ## 🔧 Funcionalidades Principales | ## 🔧 Funcionalidades Principales | ||||||
| 
 | 
 | ||||||
| ### Gestión del Entorno de Desarrollo | ### Gestión del Entorno de Desarrollo | ||||||
| 
 | 
 | ||||||
| | Comando | Descripción | | | Comando | Descripción | | ||||||
| |---------|-------------| | |---------|-------------| | ||||||
| | `bin/bootstrap.sh` | Instala herramientas básicas (incluyendo oathtool y zbar) y configura la gestión de contenedores | | | `bin/bootstrap.sh` | Instala herramientas básicas (incluyendo oathtool, zbar y redis-cli) y configura la gestión de contenedores | | ||||||
| | `bin/update.sh` | Actualiza el entorno de desarrollo y herramientas (incluyendo oathtool y zbar) | | | `bin/update.sh` | Actualiza el entorno de desarrollo y herramientas (incluyendo oathtool, zbar y redis-cli) | | ||||||
| | `bin/npm_install.sh` | Instala NodeJS y npm de forma interactiva | | | `bin/npm_install.sh` | Instala NodeJS y npm de forma interactiva | | ||||||
| | `bin/project_new.sh` | Crea un nuevo proyecto con estructura estandarizada según el tipo seleccionado | | | `bin/project_new.sh` | Crea un nuevo proyecto con estructura estandarizada según el tipo seleccionado | | ||||||
| 
 | 
 | ||||||
|  | @ -67,7 +78,7 @@ bin/update.sh | ||||||
| | `bin/sora_enable.sh` | Activa el alias 'sora' para Aider permanentemente | | | `bin/sora_enable.sh` | Activa el alias 'sora' para Aider permanentemente | | ||||||
| | `bin/sora_disable.sh` | Desactiva el alias 'sora' para Aider | | | `bin/sora_disable.sh` | Desactiva el alias 'sora' para Aider | | ||||||
| | `bin/ai_token.sh` | Gestiona tokens de múltiples proveedores de IA vía SOPS | | | `bin/ai_token.sh` | Gestiona tokens de múltiples proveedores de IA vía SOPS | | ||||||
| | `bin/ollama_up.sh` | Inicia el servicio Ollama (IA local) con podman-compose | | | `bin/ollama_up.sh` | Inicia el servicio Ollama (IA local) con podman-compose. Crea y configura `~/devs/ollama/` | | ||||||
| | `bin/ollama_down.sh` | Detiene el servicio Ollama (IA local) | | | `bin/ollama_down.sh` | Detiene el servicio Ollama (IA local) | | ||||||
| | `bin/ollama.sh` | Cliente para interactuar con Ollama (auto-inicia el servicio si es necesario) | | | `bin/ollama.sh` | Cliente para interactuar con Ollama (auto-inicia el servicio si es necesario) | | ||||||
| | `bin/nodered.sh` | Administra Node-RED con soporte para ejecución, monitoreo y gestión completa del servicio | | | `bin/nodered.sh` | Administra Node-RED con soporte para ejecución, monitoreo y gestión completa del servicio | | ||||||
|  | @ -150,6 +161,48 @@ bin/cortana_unalias.sh | ||||||
| 
 | 
 | ||||||
| El token se encripta usando SOPS y se almacena de forma segura en `$HOME/.cortana/cortana.sops.yaml`. | El token se encripta usando SOPS y se almacena de forma segura en `$HOME/.cortana/cortana.sops.yaml`. | ||||||
| 
 | 
 | ||||||
|  | ##### Control por voz (Experimental) | ||||||
|  | 
 | ||||||
|  | MRDevs Tools incluye un script experimental para interactuar con Claude Code mediante comandos de voz: | ||||||
|  | 
 | ||||||
|  | ```bash | ||||||
|  | # Ver instrucciones para instalar dependencias | ||||||
|  | bin/claude_voice.py --install-deps | ||||||
|  | 
 | ||||||
|  | # Para sistemas Ubuntu/Debian | ||||||
|  | sudo apt install python3-pyaudio python3-pip | ||||||
|  | pip install --user vosk pydub sounddevice wget | ||||||
|  | 
 | ||||||
|  | # Modo interactivo básico (español por defecto) | ||||||
|  | bin/claude_voice.py | ||||||
|  | 
 | ||||||
|  | # Ver idiomas soportados | ||||||
|  | bin/claude_voice.py --list-languages | ||||||
|  | 
 | ||||||
|  | # Usar idioma específico (inglés) | ||||||
|  | bin/claude_voice.py --language en-us | ||||||
|  | 
 | ||||||
|  | # Modo continuo (escucha hasta que digas "salir") | ||||||
|  | bin/claude_voice.py --continuous | ||||||
|  | 
 | ||||||
|  | # Listar dispositivos de audio disponibles | ||||||
|  | bin/claude_voice.py --list-devices | ||||||
|  | 
 | ||||||
|  | # Especificar dispositivo de audio por ID | ||||||
|  | bin/claude_voice.py --device 1 | ||||||
|  | 
 | ||||||
|  | # Ver la versión de Claude Code instalada | ||||||
|  | bin/claude_voice.py --version | ||||||
|  | 
 | ||||||
|  | # Modificar tiempo máximo de espera para respuestas (en segundos) | ||||||
|  | bin/claude_voice.py --timeout 30 | ||||||
|  | 
 | ||||||
|  | # Enviar texto directamente (sin voz) | ||||||
|  | bin/claude_voice.py --text "Cómo puedo crear un archivo en Python" | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | > **NOTA**: Esta funcionalidad es experimental y requiere un micrófono configurado correctamente. El reconocimiento de voz utiliza Vosk, una solución local que no requiere conexión a Internet. La primera vez que ejecutes el script con un nuevo idioma, descargará automáticamente el modelo de reconocimiento correspondiente. Los archivos de sonido para notificaciones se almacenan en `~/devs/sounds/`. | ||||||
|  | 
 | ||||||
| #### Aider CLI | #### Aider CLI | ||||||
| 
 | 
 | ||||||
| ```bash | ```bash | ||||||
|  | @ -392,12 +445,15 @@ El script analizará el código fuente, contará las líneas efectivas, y calcul | ||||||
| ### Estructura de Directorios | ### Estructura de Directorios | ||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
| bin/ | devs/ | ||||||
| ├── lib/           # Bibliotecas compartidas | ├── bin/           # Scripts ejecutables y herramientas | ||||||
| ├── msg/           # Archivos de mensajes multilingües | │   ├── lib/       # Bibliotecas compartidas | ||||||
| ├── config/        # Configuraciones y parámetros | │   ├── msg/       # Archivos de mensajes multilingües | ||||||
|  | │   └── config/    # Configuraciones y parámetros | ||||||
| │       └── *.gitignore # Plantillas de .gitignore específicas por tipo de proyecto | │       └── *.gitignore # Plantillas de .gitignore específicas por tipo de proyecto | ||||||
| └── ansible/       # Recursos para automatización | ├── ollama/        # Configuración y datos de Ollama (IA local) | ||||||
|  | │   └── data/      # Almacenamiento persistente para modelos de Ollama | ||||||
|  | └── sounds/        # Archivos de audio para notificaciones | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### Componentes Principales | ### Componentes Principales | ||||||
|  |  | ||||||
							
								
								
									
										521
									
								
								bin/claude_voice.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										521
									
								
								bin/claude_voice.py
									
									
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,521 @@ | ||||||
|  | #!/usr/bin/env python3 | ||||||
|  | # [Script]     : claude_voice.py | ||||||
|  | # [Apps]       : MRDEVS TOOLS | ||||||
|  | # [Description]: Convierte instrucciones de voz a texto para Claude Code | ||||||
|  | # [Author]     : Cortana Rosero One <cortana@rosero.one> | ||||||
|  | # [Generated]  : Created by Claude Code (claude-3-7-sonnet-20250219) | ||||||
|  | # [Created]    : 2025/03/30 16:45:00 | ||||||
|  | # [Modified]   : 2025/03/30 17:45:00 | ||||||
|  | # [Version]    : 1.3.0 | ||||||
|  | # [Use Notes]  : Instalar dependencias en Ubuntu/Debian: sudo apt install python3-pyaudio python3-pip && pip install --user vosk pydub sounddevice wget | ||||||
|  | 
 | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | import json | ||||||
|  | import subprocess | ||||||
|  | import argparse | ||||||
|  | import time | ||||||
|  | import queue | ||||||
|  | import threading | ||||||
|  | 
 | ||||||
|  | # Intentar importar las dependencias | ||||||
|  | try: | ||||||
|  |     import sounddevice as sd | ||||||
|  |     from vosk import Model, KaldiRecognizer | ||||||
|  |     from pydub import AudioSegment | ||||||
|  |     from pydub.playback import play | ||||||
|  |     DEPS_LOADED = True | ||||||
|  | except ImportError: | ||||||
|  |     DEPS_LOADED = False | ||||||
|  | 
 | ||||||
|  | # Colores para la salida | ||||||
|  | class Colors: | ||||||
|  |     PURPLE = '\033[95m' | ||||||
|  |     BLUE = '\033[94m' | ||||||
|  |     CYAN = '\033[96m' | ||||||
|  |     GREEN = '\033[92m' | ||||||
|  |     YELLOW = '\033[93m' | ||||||
|  |     RED = '\033[91m' | ||||||
|  |     BOLD = '\033[1m' | ||||||
|  |     UNDERLINE = '\033[4m' | ||||||
|  |     END = '\033[0m' | ||||||
|  | 
 | ||||||
|  | def play_sound(sound_type): | ||||||
|  |     """Reproduce un sonido para indicar estados""" | ||||||
|  |     # Obtener el directorio del script y del proyecto | ||||||
|  |     script_dir = os.path.dirname(os.path.abspath(__file__)) | ||||||
|  |     bin_dir = os.path.dirname(script_dir) if script_dir.endswith("/bin") else script_dir | ||||||
|  |     project_dir = os.path.dirname(bin_dir) if bin_dir.endswith("/bin") else os.path.dirname(os.path.abspath(__file__)) | ||||||
|  |      | ||||||
|  |     # Usar sounds en el directorio del proyecto, no en /bin | ||||||
|  |     sounds_dir = os.path.join(project_dir, "sounds") | ||||||
|  |     if not os.path.exists(sounds_dir): | ||||||
|  |         os.makedirs(sounds_dir, exist_ok=True) | ||||||
|  |      | ||||||
|  |     # Usar sonidos predeterminados si existen, o crearlos | ||||||
|  |     sound_files = { | ||||||
|  |         "start": os.path.join(sounds_dir, "start.mp3"), | ||||||
|  |         "stop": os.path.join(sounds_dir, "stop.mp3"), | ||||||
|  |         "error": os.path.join(sounds_dir, "error.mp3") | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     # Si no hay archivo de sonido, usar un beep básico | ||||||
|  |     try: | ||||||
|  |         if os.path.exists(sound_files[sound_type]): | ||||||
|  |             sound = AudioSegment.from_file(sound_files[sound_type]) | ||||||
|  |             play(sound) | ||||||
|  |         else: | ||||||
|  |             # Frecuencias para diferentes tipos de sonidos | ||||||
|  |             if sound_type == "start": | ||||||
|  |                 print("\a")  # Beep básico del sistema | ||||||
|  |             elif sound_type == "stop": | ||||||
|  |                 print("\a") | ||||||
|  |                 time.sleep(0.1) | ||||||
|  |                 print("\a") | ||||||
|  |             elif sound_type == "error": | ||||||
|  |                 print("\a") | ||||||
|  |                 time.sleep(0.1) | ||||||
|  |                 print("\a") | ||||||
|  |                 time.sleep(0.1) | ||||||
|  |                 print("\a") | ||||||
|  |     except Exception: | ||||||
|  |         # Si hay algún error reproduciendo el sonido, simplemente continuamos | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  | def download_model(language="es"): | ||||||
|  |     """Descarga el modelo de Vosk si no existe""" | ||||||
|  |     # Mapeo de códigos de idioma estándar a formato de Vosk | ||||||
|  |     language_map = { | ||||||
|  |         "es-ES": "es", | ||||||
|  |         "es-AR": "es", | ||||||
|  |         "es-MX": "es", | ||||||
|  |         "es-CO": "es", | ||||||
|  |         "es": "es", | ||||||
|  |         "en-US": "en-us", | ||||||
|  |         "en-GB": "en-us", | ||||||
|  |         "en": "en-us", | ||||||
|  |         "fr-FR": "fr", | ||||||
|  |         "fr": "fr", | ||||||
|  |         "de-DE": "de", | ||||||
|  |         "de": "de", | ||||||
|  |         "it-IT": "it", | ||||||
|  |         "it": "it", | ||||||
|  |         "pt-PT": "pt", | ||||||
|  |         "pt-BR": "pt", | ||||||
|  |         "pt": "pt", | ||||||
|  |         "ru-RU": "ru", | ||||||
|  |         "ru": "ru" | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     # Obtener código de idioma para Vosk | ||||||
|  |     lang_code = language_map.get(language, language) | ||||||
|  |     if "-" in lang_code and lang_code not in language_map.values(): | ||||||
|  |         lang_code = lang_code.split("-")[0] | ||||||
|  |      | ||||||
|  |     # Establecer URLs de modelos verificados | ||||||
|  |     model_urls = { | ||||||
|  |         "es": "https://alphacephei.com/vosk/models/vosk-model-small-es-0.42.zip", | ||||||
|  |         "en-us": "https://alphacephei.com/vosk/models/vosk-model-small-en-us-0.15.zip", | ||||||
|  |         "fr": "https://alphacephei.com/vosk/models/vosk-model-small-fr-0.22.zip", | ||||||
|  |         "de": "https://alphacephei.com/vosk/models/vosk-model-small-de-0.15.zip", | ||||||
|  |         "it": "https://alphacephei.com/vosk/models/vosk-model-small-it-0.22.zip", | ||||||
|  |         "pt": "https://alphacephei.com/vosk/models/vosk-model-small-pt-0.3.zip", | ||||||
|  |         "ru": "https://alphacephei.com/vosk/models/vosk-model-small-ru-0.22.zip" | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     # Si no hay URL para este idioma, usar el inglés como fallback | ||||||
|  |     if lang_code not in model_urls: | ||||||
|  |         print(f"{Colors.YELLOW}No se encontró un modelo para el idioma '{lang_code}', usando el inglés como alternativa.{Colors.END}") | ||||||
|  |         lang_code = "en-us" | ||||||
|  |      | ||||||
|  |     # Usar el directorio de modelos con versión específica | ||||||
|  |     model_name = os.path.basename(model_urls[lang_code]).replace(".zip", "") | ||||||
|  |     model_path = os.path.expanduser(f"~/.vosk/models/{model_name}") | ||||||
|  |     if os.path.exists(model_path): | ||||||
|  |         return model_path | ||||||
|  |          | ||||||
|  |     print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.YELLOW}Descargando modelo de voz para {lang_code}...{Colors.END}") | ||||||
|  |      | ||||||
|  |     # Crear directorio para modelos si no existe | ||||||
|  |     os.makedirs(os.path.expanduser("~/.vosk/models"), exist_ok=True) | ||||||
|  |      | ||||||
|  |     # Descargar e instalar el modelo | ||||||
|  |     try: | ||||||
|  |         # Importar wget solo cuando se necesita | ||||||
|  |         import wget | ||||||
|  |         url = model_urls[lang_code] | ||||||
|  |         zip_path = os.path.expanduser(f"~/.vosk/models/{os.path.basename(url)}") | ||||||
|  |          | ||||||
|  |         print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.YELLOW}Descargando desde: {url}{Colors.END}") | ||||||
|  |          | ||||||
|  |         # Descargar el modelo | ||||||
|  |         wget.download(url, zip_path) | ||||||
|  |         print()  # Nueva línea después de la barra de progreso | ||||||
|  |          | ||||||
|  |         # Extraer el modelo | ||||||
|  |         import zipfile | ||||||
|  |         with zipfile.ZipFile(zip_path, 'r') as zip_ref: | ||||||
|  |             zip_ref.extractall(os.path.expanduser("~/.vosk/models/")) | ||||||
|  |          | ||||||
|  |         # Eliminar el zip | ||||||
|  |         os.remove(zip_path) | ||||||
|  |          | ||||||
|  |         print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.GREEN}Modelo descargado y extraído correctamente en {model_path}{Colors.END}") | ||||||
|  |         return model_path | ||||||
|  |     except Exception as e: | ||||||
|  |         print(f"{Colors.RED}Error al descargar el modelo: {e}{Colors.END}") | ||||||
|  |         print(f"{Colors.YELLOW}Por favor, descargue manualmente el modelo desde: {Colors.UNDERLINE}https://alphacephei.com/vosk/models{Colors.END}") | ||||||
|  |         print(f"{Colors.YELLOW}Y colóquelo en: {model_path}{Colors.END}") | ||||||
|  |         sys.exit(1) | ||||||
|  | 
 | ||||||
|  | def recognize_speech(language="es-ES"): | ||||||
|  |     """Captura audio del micrófono y lo convierte a texto usando Vosk (local)""" | ||||||
|  |     # Descargar o verificar modelo | ||||||
|  |     model_path = download_model(language) | ||||||
|  |      | ||||||
|  |     # Configurar el modelo | ||||||
|  |     model = Model(model_path) | ||||||
|  |     samplerate = 16000 | ||||||
|  |      | ||||||
|  |     # Configurar cola para recibir audio | ||||||
|  |     q = queue.Queue() | ||||||
|  |      | ||||||
|  |     # Función para callback de audio | ||||||
|  |     def callback(indata, frames, time, status): | ||||||
|  |         if status: | ||||||
|  |             print(f"{Colors.YELLOW}[Claude Voice] Status: {status}{Colors.END}") | ||||||
|  |         q.put(bytes(indata)) | ||||||
|  |      | ||||||
|  |     # Iniciar captura de audio | ||||||
|  |     print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.YELLOW}Escuchando...{Colors.END} (Presiona Ctrl+C para detener)") | ||||||
|  |     play_sound("start") | ||||||
|  |      | ||||||
|  |     # Preparar reconocedor | ||||||
|  |     rec = KaldiRecognizer(model, samplerate) | ||||||
|  |      | ||||||
|  |     try: | ||||||
|  |         with sd.RawInputStream(samplerate=samplerate, blocksize=8000,  | ||||||
|  |                               dtype='int16', channels=1, callback=callback): | ||||||
|  |              | ||||||
|  |             # Variables para controlar el reconocimiento | ||||||
|  |             start_time = time.time() | ||||||
|  |             timeout = 10  # segundos | ||||||
|  |             last_text_time = time.time() | ||||||
|  |             final_result = "" | ||||||
|  |             partial_results = [] | ||||||
|  |              | ||||||
|  |             # Procesar audio | ||||||
|  |             while True: | ||||||
|  |                 # Comprobar timeout | ||||||
|  |                 if (time.time() - start_time) > timeout: | ||||||
|  |                     break | ||||||
|  |                      | ||||||
|  |                 # Comprobar si hay silencio prolongado después de hablar | ||||||
|  |                 if final_result and (time.time() - last_text_time) > 1.5: | ||||||
|  |                     break | ||||||
|  |                  | ||||||
|  |                 # Obtener datos de audio | ||||||
|  |                 data = q.get() | ||||||
|  |                  | ||||||
|  |                 # Reconocer voz | ||||||
|  |                 if rec.AcceptWaveform(data): | ||||||
|  |                     result = json.loads(rec.Result()) | ||||||
|  |                     if result.get("text", ""): | ||||||
|  |                         text = result["text"] | ||||||
|  |                         final_result = text | ||||||
|  |                         last_text_time = time.time() | ||||||
|  |                         print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.GREEN}Procesando audio...{Colors.END}") | ||||||
|  |                 else: | ||||||
|  |                     # Resultados parciales | ||||||
|  |                     partial = json.loads(rec.PartialResult()) | ||||||
|  |                     if partial.get("partial", ""): | ||||||
|  |                         partial_text = partial["partial"] | ||||||
|  |                         if partial_text: | ||||||
|  |                             partial_results.append(partial_text) | ||||||
|  |                             last_text_time = time.time() | ||||||
|  |      | ||||||
|  |     except KeyboardInterrupt: | ||||||
|  |         print(f"\n{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.YELLOW}Reconocimiento interrumpido{Colors.END}") | ||||||
|  |     except Exception as e: | ||||||
|  |         play_sound("error") | ||||||
|  |         print(f"{Colors.RED}Error en el reconocimiento de voz: {e}{Colors.END}") | ||||||
|  |         return None | ||||||
|  |     finally: | ||||||
|  |         play_sound("stop") | ||||||
|  |      | ||||||
|  |     # Si no hay resultado final pero hay parciales, usar el último parcial | ||||||
|  |     if not final_result and partial_results: | ||||||
|  |         final_result = partial_results[-1] | ||||||
|  |      | ||||||
|  |     return final_result | ||||||
|  | 
 | ||||||
|  | def send_to_claude(text, silent=False, timeout=60): | ||||||
|  |     """Envía el texto reconocido a Claude Code""" | ||||||
|  |     if not text: | ||||||
|  |         return False | ||||||
|  |      | ||||||
|  |     if not silent: | ||||||
|  |         print(f"{Colors.BLUE}[Claude Voice]{Colors.END} Enviando a Claude Code: {Colors.BOLD}{text}{Colors.END}") | ||||||
|  |      | ||||||
|  |     try: | ||||||
|  |         # Usar la ruta de instalación de Claude Code | ||||||
|  |         claude_cmd = "claude" if os.system("which claude > /dev/null 2>&1") == 0 else "/usr/local/bin/claude" | ||||||
|  |          | ||||||
|  |         # Mostrar que estamos esperando respuesta | ||||||
|  |         print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.YELLOW}Esperando respuesta de Claude Code...{Colors.END}") | ||||||
|  |          | ||||||
|  |         # Usar un timer para mostrar actividad | ||||||
|  |         start_time = time.time() | ||||||
|  |         progress_chars = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'] | ||||||
|  |         progress_thread_active = True | ||||||
|  |          | ||||||
|  |         def show_progress(): | ||||||
|  |             i = 0 | ||||||
|  |             while progress_thread_active: | ||||||
|  |                 sys.stdout.write(f"\r{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.YELLOW}Esperando {progress_chars[i]} {int(time.time() - start_time)}s{Colors.END}") | ||||||
|  |                 sys.stdout.flush() | ||||||
|  |                 i = (i + 1) % len(progress_chars) | ||||||
|  |                 time.sleep(0.1) | ||||||
|  |             # Limpiar la línea cuando terminamos | ||||||
|  |             sys.stdout.write("\r" + " " * 60 + "\r") | ||||||
|  |             sys.stdout.flush() | ||||||
|  |          | ||||||
|  |         # Iniciar hilo de progreso | ||||||
|  |         progress_thread = threading.Thread(target=show_progress) | ||||||
|  |         progress_thread.daemon = True | ||||||
|  |         progress_thread.start() | ||||||
|  |          | ||||||
|  |         try: | ||||||
|  |             # Enviar el texto como entrada a Claude | ||||||
|  |             result = subprocess.run([claude_cmd, text],  | ||||||
|  |                                     stdout=subprocess.PIPE,  | ||||||
|  |                                     stderr=subprocess.PIPE, | ||||||
|  |                                     text=True, | ||||||
|  |                                     timeout=timeout, | ||||||
|  |                                     check=False) | ||||||
|  |              | ||||||
|  |             # Detener el hilo de progreso | ||||||
|  |             progress_thread_active = False | ||||||
|  |             progress_thread.join(1.0)  # Esperar a que termine, pero con timeout | ||||||
|  |              | ||||||
|  |             # Verificar resultado | ||||||
|  |             if result.returncode != 0: | ||||||
|  |                 print(f"\n{Colors.RED}Error al ejecutar Claude Code: {result.stderr}{Colors.END}") | ||||||
|  |                 return False | ||||||
|  |                  | ||||||
|  |             # Mostrar información sobre la respuesta | ||||||
|  |             print(f"\n{Colors.GREEN}Claude Code ha respondido exitosamente.{Colors.END}") | ||||||
|  |             return True | ||||||
|  |              | ||||||
|  |         except subprocess.TimeoutExpired: | ||||||
|  |             # Detener el hilo de progreso | ||||||
|  |             progress_thread_active = False | ||||||
|  |             progress_thread.join(1.0) | ||||||
|  |              | ||||||
|  |             print(f"\n{Colors.RED}Tiempo de espera agotado. Claude Code está tardando demasiado en responder.{Colors.END}") | ||||||
|  |             print(f"{Colors.YELLOW}La consulta fue enviada, pero puedes verificar la terminal de Claude Code para ver la respuesta.{Colors.END}") | ||||||
|  |             return False | ||||||
|  |              | ||||||
|  |     except Exception as e: | ||||||
|  |         print(f"\n{Colors.RED}Error al ejecutar Claude Code: {e}{Colors.END}") | ||||||
|  |          | ||||||
|  |         # Sugerir soluciones comunes | ||||||
|  |         if "No such file or directory" in str(e): | ||||||
|  |             print(f"{Colors.YELLOW}No se encontró el comando 'claude'. Asegúrate de tener Claude Code instalado correctamente.{Colors.END}") | ||||||
|  |             print(f"{Colors.YELLOW}Prueba instalarlo con: bin/claude_install.sh{Colors.END}") | ||||||
|  |          | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  | def interactive_mode(language="es-ES", continuous=False, timeout=60, device=None): | ||||||
|  |     """Modo interactivo que escucha continuamente comandos de voz""" | ||||||
|  |     print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.GREEN}Modo interactivo iniciado. Di tus instrucciones para Claude Code.{Colors.END}") | ||||||
|  |     print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.YELLOW}Di 'salir' o 'terminar' para finalizar{Colors.END}") | ||||||
|  |     print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.YELLOW}Di 'versión' para conocer la versión de Claude Code{Colors.END}") | ||||||
|  |      | ||||||
|  |     try: | ||||||
|  |         while True: | ||||||
|  |             text = recognize_speech(language) | ||||||
|  |              | ||||||
|  |             if text: | ||||||
|  |                 text = text.strip() | ||||||
|  |                 print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.GREEN}Reconocido: {Colors.BOLD}{text}{Colors.END}") | ||||||
|  |                  | ||||||
|  |                 # Verificar comandos de salida | ||||||
|  |                 if text.lower() in ["salir", "terminar", "exit", "quit", "goodbye", "bye"]: | ||||||
|  |                     print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.YELLOW}Saliendo del modo de voz...{Colors.END}") | ||||||
|  |                     break | ||||||
|  |                  | ||||||
|  |                 # Verificar comandos especiales | ||||||
|  |                 if text.lower() in ["versión", "version"]: | ||||||
|  |                     try: | ||||||
|  |                         # Usar la ruta de instalación de Claude Code | ||||||
|  |                         claude_cmd = "claude" if os.system("which claude > /dev/null 2>&1") == 0 else "/usr/local/bin/claude" | ||||||
|  |                          | ||||||
|  |                         # Obtener versión | ||||||
|  |                         result = subprocess.run([claude_cmd, "--version"],  | ||||||
|  |                                            stdout=subprocess.PIPE,  | ||||||
|  |                                            stderr=subprocess.PIPE, | ||||||
|  |                                            text=True, | ||||||
|  |                                            check=False) | ||||||
|  |                          | ||||||
|  |                         if result.returncode == 0: | ||||||
|  |                             version = result.stdout.strip() or "Desconocida" | ||||||
|  |                             print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.GREEN}Versión de Claude Code: {Colors.BOLD}{version}{Colors.END}") | ||||||
|  |                         else: | ||||||
|  |                             print(f"{Colors.RED}Error al obtener versión: {result.stderr}{Colors.END}") | ||||||
|  |                     except Exception as e: | ||||||
|  |                         print(f"{Colors.RED}Error al ejecutar Claude Code: {e}{Colors.END}") | ||||||
|  |                     continue | ||||||
|  |                  | ||||||
|  |                 # Enviar a Claude Code | ||||||
|  |                 success = send_to_claude(text, timeout=timeout) | ||||||
|  |                  | ||||||
|  |                 # Si no es modo continuo, salir después del primer comando exitoso | ||||||
|  |                 if not continuous and success: | ||||||
|  |                     break | ||||||
|  |              | ||||||
|  |             # Pausa breve entre reconocimientos | ||||||
|  |             if continuous: | ||||||
|  |                 time.sleep(1) | ||||||
|  |                  | ||||||
|  |     except KeyboardInterrupt: | ||||||
|  |         print(f"\n{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.YELLOW}Modo de voz interrumpido por el usuario{Colors.END}") | ||||||
|  |      | ||||||
|  |     print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.GREEN}¡Hasta pronto!{Colors.END}") | ||||||
|  | 
 | ||||||
|  | def list_audio_devices(): | ||||||
|  |     """Lista los dispositivos de audio disponibles""" | ||||||
|  |     print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.GREEN}Dispositivos de audio disponibles:{Colors.END}") | ||||||
|  |     devices = sd.query_devices() | ||||||
|  |      | ||||||
|  |     print(f"{Colors.CYAN}{'ID':<4} {'Nombre':<30} {'Canales (E/S)':<15} {'Predeterminado':<12}{Colors.END}") | ||||||
|  |     print("-" * 65) | ||||||
|  |      | ||||||
|  |     for i, device in enumerate(devices): | ||||||
|  |         default_mark = "" | ||||||
|  |         try: | ||||||
|  |             if device.get('name') == sd.query_devices(kind='input')['name']: | ||||||
|  |                 default_mark = "⭐ (entrada)" | ||||||
|  |             elif device.get('name') == sd.query_devices(kind='output')['name']: | ||||||
|  |                 default_mark = "⭐ (salida)" | ||||||
|  |         except: | ||||||
|  |             pass | ||||||
|  |          | ||||||
|  |         channels = f"{device.get('max_input_channels', 0)}/{device.get('max_output_channels', 0)}" | ||||||
|  |         print(f"{i:<4} {device.get('name', 'Desconocido'):<30} {channels:<15} {default_mark}") | ||||||
|  |      | ||||||
|  |     return True | ||||||
|  | 
 | ||||||
|  | def get_supported_languages(): | ||||||
|  |     """Devuelve una lista de idiomas soportados""" | ||||||
|  |     return { | ||||||
|  |         "es": "Español", | ||||||
|  |         "en-us": "Inglés (EEUU)", | ||||||
|  |         "fr": "Francés",  | ||||||
|  |         "de": "Alemán", | ||||||
|  |         "it": "Italiano", | ||||||
|  |         "pt": "Portugués", | ||||||
|  |         "ru": "Ruso" | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | def list_languages(): | ||||||
|  |     """Muestra una lista de idiomas soportados""" | ||||||
|  |     languages = get_supported_languages() | ||||||
|  |      | ||||||
|  |     print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.GREEN}Idiomas soportados:{Colors.END}") | ||||||
|  |     print(f"{Colors.CYAN}{'Código':<8} {'Idioma':<20}{Colors.END}") | ||||||
|  |     print("-" * 30) | ||||||
|  |      | ||||||
|  |     for code, name in languages.items(): | ||||||
|  |         print(f"{code:<8} {name:<20}") | ||||||
|  |      | ||||||
|  |     # Mostrar información sobre los alias | ||||||
|  |     print(f"\n{Colors.YELLOW}Nota: Todos los códigos regionales (es-ES, es-AR, etc.) se mapean al idioma base (es).{Colors.END}") | ||||||
|  |     return True | ||||||
|  | 
 | ||||||
|  | def main(): | ||||||
|  |     parser = argparse.ArgumentParser(description='Claude Code Voice - Convierte voz a texto para Claude Code usando reconocimiento local') | ||||||
|  |     parser.add_argument('-l', '--language', default='es', help='Idioma para reconocimiento (ej. es, en-us)') | ||||||
|  |     parser.add_argument('-c', '--continuous', action='store_true', help='Modo continuo - escucha constantemente hasta que digas "salir"') | ||||||
|  |     parser.add_argument('-t', '--text', help='Texto a enviar directamente (sin reconocimiento de voz)') | ||||||
|  |     parser.add_argument('-s', '--silent', action='store_true', help='Modo silencioso - no muestra mensajes extra') | ||||||
|  |     parser.add_argument('-d', '--device', type=int, help='ID del dispositivo de audio a utilizar') | ||||||
|  |     parser.add_argument('--timeout', type=int, default=60, help='Tiempo máximo de espera para respuesta de Claude Code (segundos)') | ||||||
|  |     parser.add_argument('--list-devices', action='store_true', help='Listar dispositivos de audio disponibles') | ||||||
|  |     parser.add_argument('--list-languages', action='store_true', help='Listar idiomas soportados') | ||||||
|  |     parser.add_argument('--version', action='store_true', help='Mostrar versión de Claude Code') | ||||||
|  |     parser.add_argument('--install-deps', action='store_true', help='Instalar dependencias') | ||||||
|  |      | ||||||
|  |     args = parser.parse_args() | ||||||
|  |      | ||||||
|  |     # Instalar dependencias si se solicita | ||||||
|  |     if args.install_deps: | ||||||
|  |         try: | ||||||
|  |             print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.YELLOW}Instrucciones para instalar dependencias...{Colors.END}") | ||||||
|  |             print("\nPara sistemas Ubuntu/Debian, ejecuta los siguientes comandos:") | ||||||
|  |             print(f"{Colors.GREEN}sudo apt install python3-pyaudio python3-pip{Colors.END}") | ||||||
|  |             print(f"{Colors.GREEN}pip install --user vosk pydub sounddevice wget{Colors.END}") | ||||||
|  |              | ||||||
|  |             print("\nPara otros sistemas, consulta la documentación de Vosk:") | ||||||
|  |             print(f"{Colors.GREEN}https://alphacephei.com/vosk/install{Colors.END}") | ||||||
|  |              | ||||||
|  |             print("\nSi prefieres usar un entorno virtual (recomendado):") | ||||||
|  |             print(f"{Colors.GREEN}sudo apt install python3-venv python3-pyaudio{Colors.END}") | ||||||
|  |             print(f"{Colors.GREEN}python3 -m venv ~/venv-claude-voice{Colors.END}") | ||||||
|  |             print(f"{Colors.GREEN}source ~/venv-claude-voice/bin/activate{Colors.END}") | ||||||
|  |             print(f"{Colors.GREEN}pip install vosk pydub sounddevice wget{Colors.END}") | ||||||
|  |             print(f"{Colors.GREEN}# Luego ejecuta: ~/venv-claude-voice/bin/python3 /home/mrosero/devs/bin/claude_voice.py{Colors.END}") | ||||||
|  |              | ||||||
|  |             return | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"{Colors.RED}Error al mostrar instrucciones: {e}{Colors.END}") | ||||||
|  |             sys.exit(1) | ||||||
|  |              | ||||||
|  |     # Verificar si las dependencias están instaladas | ||||||
|  |     if not DEPS_LOADED: | ||||||
|  |         print(f"{Colors.RED}Error: Faltan dependencias requeridas para el reconocimiento de voz.{Colors.END}") | ||||||
|  |         print(f"{Colors.YELLOW}Ejecuta '{sys.argv[0]} --install-deps' para ver instrucciones de instalación.{Colors.END}") | ||||||
|  |         sys.exit(1) | ||||||
|  |      | ||||||
|  |     # Listar dispositivos si se solicita | ||||||
|  |     if args.list_devices: | ||||||
|  |         list_audio_devices() | ||||||
|  |         return | ||||||
|  |          | ||||||
|  |     # Listar idiomas si se solicita | ||||||
|  |     if args.list_languages: | ||||||
|  |         list_languages() | ||||||
|  |         return | ||||||
|  |          | ||||||
|  |     # Mostrar versión de Claude Code si se solicita | ||||||
|  |     if args.version: | ||||||
|  |         try: | ||||||
|  |             # Usar la ruta de instalación de Claude Code | ||||||
|  |             claude_cmd = "claude" if os.system("which claude > /dev/null 2>&1") == 0 else "/usr/local/bin/claude" | ||||||
|  |              | ||||||
|  |             # Obtener versión | ||||||
|  |             result = subprocess.run([claude_cmd, "--version"],  | ||||||
|  |                                stdout=subprocess.PIPE,  | ||||||
|  |                                stderr=subprocess.PIPE, | ||||||
|  |                                text=True, | ||||||
|  |                                check=False) | ||||||
|  |              | ||||||
|  |             if result.returncode == 0: | ||||||
|  |                 version = result.stdout.strip() or "Desconocida" | ||||||
|  |                 print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.GREEN}Versión de Claude Code: {Colors.BOLD}{version}{Colors.END}") | ||||||
|  |             else: | ||||||
|  |                 print(f"{Colors.RED}Error al obtener versión: {result.stderr}{Colors.END}") | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"{Colors.RED}Error al ejecutar Claude Code: {e}{Colors.END}") | ||||||
|  |         return | ||||||
|  |      | ||||||
|  |     # Enviar texto directo si se proporciona | ||||||
|  |     if args.text: | ||||||
|  |         send_to_claude(args.text, args.silent, args.timeout) | ||||||
|  |         return | ||||||
|  |          | ||||||
|  |     # Modo interactivo con reconocimiento de voz | ||||||
|  |     interactive_mode(args.language, args.continuous, args.timeout, args.device) | ||||||
|  | 
 | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
|  | @ -55,8 +55,8 @@ source "${BIN_HOME}/${BIN_BASE}/${BIN_LIBS}/base.lib" | ||||||
| load_messages "${BIN_HOME}/${BIN_BASE}" "${BIN_MESG}" "${BIN_LANG}" "head" | load_messages "${BIN_HOME}/${BIN_BASE}" "${BIN_MESG}" "${BIN_LANG}" "head" | ||||||
| title="${head_000} ${head_002}" | title="${head_000} ${head_002}" | ||||||
| 
 | 
 | ||||||
| # Change to the directory containing the compose file | # Create the directory for Ollama in the development directory | ||||||
| cd "${BIN_HOME}/${BIN_BASE}/ollama" | mkdir -p "${BIN_HOME}/ollama" | ||||||
| 
 | 
 | ||||||
| # Check if we should use podman or docker (prefer podman) | # Check if we should use podman or docker (prefer podman) | ||||||
| if command -v podman >/dev/null 2>&1; then | if command -v podman >/dev/null 2>&1; then | ||||||
|  | @ -82,15 +82,41 @@ fi | ||||||
| 
 | 
 | ||||||
| echo "Using ${COMPOSE_CMD} to start Ollama service..." | echo "Using ${COMPOSE_CMD} to start Ollama service..." | ||||||
| 
 | 
 | ||||||
| # Fix relative path in volume mount if needed | # Copy compose file if it doesn't exist in the new location | ||||||
| if grep -q "../../data:/root/.ollama" "${BIN_HOME}/${BIN_BASE}/ollama/podman-compose.yml"; then | if [ ! -f "${BIN_HOME}/ollama/podman-compose.yml" ] && [ -f "${BIN_HOME}/${BIN_BASE}/ollama/podman-compose.yml" ]; then | ||||||
|   # Create data directory if it doesn't exist |   cp "${BIN_HOME}/${BIN_BASE}/ollama/podman-compose.yml" "${BIN_HOME}/ollama/" | ||||||
|   mkdir -p "${BIN_HOME}/data" | fi | ||||||
|  | 
 | ||||||
|  | # Change to the directory containing the compose file | ||||||
|  | cd "${BIN_HOME}/ollama" | ||||||
|  | 
 | ||||||
|  | # Create a subdirectory for Ollama data | ||||||
|  | mkdir -p "${BIN_HOME}/ollama/data" | ||||||
|  | 
 | ||||||
|  | # Update the compose file to use ollama/data directory | ||||||
|  | if [ -f "${BIN_HOME}/ollama/podman-compose.yml" ]; then | ||||||
|  |   # Update the volume mount to use ollama/data instead of data | ||||||
|  |   sed -i 's|../../data:/root/.ollama|../ollama/data:/root/.ollama|g' "${BIN_HOME}/ollama/podman-compose.yml" | ||||||
|  |   sed -i 's|${BIN_HOME}/data:/root/.ollama|${BIN_HOME}/ollama/data:/root/.ollama|g' "${BIN_HOME}/ollama/podman-compose.yml" | ||||||
|    |    | ||||||
|   # Start Ollama service with compose |   # Start Ollama service with compose | ||||||
|   ${COMPOSE_CMD} -f podman-compose.yml up -d |   ${COMPOSE_CMD} -f podman-compose.yml up -d | ||||||
| else | else | ||||||
|   ${COMPOSE_CMD} up -d |   # Create a basic compose file if none exists | ||||||
|  |   cat > "${BIN_HOME}/ollama/podman-compose.yml" <<EOF | ||||||
|  | version: '3' | ||||||
|  | services: | ||||||
|  |   ollama: | ||||||
|  |     image: ollama/ollama:latest | ||||||
|  |     container_name: ollama | ||||||
|  |     ports: | ||||||
|  |       - "11434:11434" | ||||||
|  |     volumes: | ||||||
|  |       - ./data:/root/.ollama | ||||||
|  |     restart: unless-stopped | ||||||
|  |     privileged: true | ||||||
|  | EOF | ||||||
|  |   ${COMPOSE_CMD} -f podman-compose.yml up -d | ||||||
| fi | fi | ||||||
| 
 | 
 | ||||||
| # Verify container is running | # Verify container is running | ||||||
|  | @ -106,7 +132,7 @@ if ! ${CONTAINER_CMD} container exists ollama 2>/dev/null || ! ${CONTAINER_CMD} | ||||||
|     ${CONTAINER_CMD} run -d --name ollama \ |     ${CONTAINER_CMD} run -d --name ollama \ | ||||||
|       --privileged \ |       --privileged \ | ||||||
|       -p 11434:11434 \ |       -p 11434:11434 \ | ||||||
|       -v "${BIN_HOME}/data:/root/.ollama" \ |       -v "${BIN_HOME}/ollama/data:/root/.ollama" \ | ||||||
|       --restart unless-stopped \ |       --restart unless-stopped \ | ||||||
|       docker.io/ollama/ollama:latest |       docker.io/ollama/ollama:latest | ||||||
|   fi |   fi | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue