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
74
README.md
74
README.md
|
@ -39,14 +39,25 @@ bin/bootstrap.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
|
||||
|
||||
### Gestión del Entorno de Desarrollo
|
||||
|
||||
| Comando | Descripción |
|
||||
|---------|-------------|
|
||||
| `bin/bootstrap.sh` | Instala herramientas básicas (incluyendo oathtool y zbar) y configura la gestión de contenedores |
|
||||
| `bin/update.sh` | Actualiza el entorno de desarrollo y herramientas (incluyendo oathtool y zbar) |
|
||||
| `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, zbar y redis-cli) |
|
||||
| `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 |
|
||||
|
||||
|
@ -67,7 +78,7 @@ bin/update.sh
|
|||
| `bin/sora_enable.sh` | Activa el alias 'sora' para Aider permanentemente |
|
||||
| `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/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.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 |
|
||||
|
@ -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`.
|
||||
|
||||
##### 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
|
||||
|
||||
```bash
|
||||
|
@ -392,12 +445,15 @@ El script analizará el código fuente, contará las líneas efectivas, y calcul
|
|||
### Estructura de Directorios
|
||||
|
||||
```
|
||||
bin/
|
||||
├── lib/ # Bibliotecas compartidas
|
||||
├── msg/ # Archivos de mensajes multilingües
|
||||
├── config/ # Configuraciones y parámetros
|
||||
│ └── *.gitignore # Plantillas de .gitignore específicas por tipo de proyecto
|
||||
└── ansible/ # Recursos para automatización
|
||||
devs/
|
||||
├── bin/ # Scripts ejecutables y herramientas
|
||||
│ ├── lib/ # Bibliotecas compartidas
|
||||
│ ├── msg/ # Archivos de mensajes multilingües
|
||||
│ └── config/ # Configuraciones y parámetros
|
||||
│ └── *.gitignore # Plantillas de .gitignore específicas por tipo de proyecto
|
||||
├── ollama/ # Configuración y datos de Ollama (IA local)
|
||||
│ └── data/ # Almacenamiento persistente para modelos de Ollama
|
||||
└── sounds/ # Archivos de audio para notificaciones
|
||||
```
|
||||
|
||||
### 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"
|
||||
title="${head_000} ${head_002}"
|
||||
|
||||
# Change to the directory containing the compose file
|
||||
cd "${BIN_HOME}/${BIN_BASE}/ollama"
|
||||
# Create the directory for Ollama in the development directory
|
||||
mkdir -p "${BIN_HOME}/ollama"
|
||||
|
||||
# Check if we should use podman or docker (prefer podman)
|
||||
if command -v podman >/dev/null 2>&1; then
|
||||
|
@ -82,15 +82,41 @@ fi
|
|||
|
||||
echo "Using ${COMPOSE_CMD} to start Ollama service..."
|
||||
|
||||
# Fix relative path in volume mount if needed
|
||||
if grep -q "../../data:/root/.ollama" "${BIN_HOME}/${BIN_BASE}/ollama/podman-compose.yml"; then
|
||||
# Create data directory if it doesn't exist
|
||||
mkdir -p "${BIN_HOME}/data"
|
||||
# Copy compose file if it doesn't exist in the new location
|
||||
if [ ! -f "${BIN_HOME}/ollama/podman-compose.yml" ] && [ -f "${BIN_HOME}/${BIN_BASE}/ollama/podman-compose.yml" ]; then
|
||||
cp "${BIN_HOME}/${BIN_BASE}/ollama/podman-compose.yml" "${BIN_HOME}/ollama/"
|
||||
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
|
||||
${COMPOSE_CMD} -f podman-compose.yml up -d
|
||||
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
|
||||
|
||||
# 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 \
|
||||
--privileged \
|
||||
-p 11434:11434 \
|
||||
-v "${BIN_HOME}/data:/root/.ollama" \
|
||||
-v "${BIN_HOME}/ollama/data:/root/.ollama" \
|
||||
--restart unless-stopped \
|
||||
docker.io/ollama/ollama:latest
|
||||
fi
|
||||
|
|
Loading…
Reference in a new issue