devs/bin/claude_voice.py
Mauro Rosero P. 9bc81ea5a8
[IMPROVED] Actualizar claude_voice.py para utilizar reconocimiento de voz local
Cambiado el sistema de reconocimiento de voz de Google (online) a Vosk (offline):
- Uso de reconocimiento de voz local sin dependencia de servicios en la nube
- Añadido descargador automático de modelos de idioma
- Soporte para listar dispositivos de audio
- Nueva opción para instalar dependencias automáticamente
- Mejor gestión de errores y tiempo de espera

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-30 15:59:39 -05:00

330 lines
No EOL
13 KiB
Python
Executable file

#!/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:25:00
# [Version] : 1.3.0
# [Use Notes] : Instalar dependencias: pip install vosk sounddevice pydub
import os
import sys
import json
import subprocess
import argparse
import time
import queue
import threading
import sounddevice as sd
from vosk import Model, KaldiRecognizer
from pydub import AudioSegment
from pydub.playback import play
# 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"""
sounds_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "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",
"en-US": "en-us",
"fr-FR": "fr",
"de-DE": "de",
"it-IT": "it",
"pt-PT": "pt",
"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]
model_path = os.path.expanduser(f"~/.vosk/models/vosk-model-small-{lang_code}")
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 = f"https://alphacephei.com/vosk/models/vosk-model-small-{lang_code}.zip"
zip_path = os.path.expanduser(f"~/.vosk/models/vosk-model-small-{lang_code}.zip")
# 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{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):
"""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"
# Enviar el texto como entrada a Claude
result = subprocess.run([claude_cmd, text],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False)
if result.returncode != 0:
print(f"{Colors.RED}Error al ejecutar Claude Code: {result.stderr}{Colors.END}")
return False
return True
except Exception as e:
print(f"{Colors.RED}Error al ejecutar Claude Code: {e}{Colors.END}")
return False
def interactive_mode(language="es-ES", continuous=False):
"""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}")
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
# Enviar a Claude Code
success = send_to_claude(text)
# 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 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-ES', help='Idioma para reconocimiento (ej. es-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('--list-devices', action='store_true', help='Listar dispositivos de audio disponibles')
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}Instalando dependencias...{Colors.END}")
import pip
pip.main(['install', 'vosk', 'sounddevice', 'pydub', 'wget'])
print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.GREEN}Dependencias instaladas correctamente{Colors.END}")
return
except Exception as e:
print(f"{Colors.RED}Error al instalar dependencias: {e}{Colors.END}")
sys.exit(1)
# Listar dispositivos si se solicita
if args.list_devices:
list_audio_devices()
return
# Enviar texto directo si se proporciona
if args.text:
send_to_claude(args.text, args.silent)
return
# Modo interactivo con reconocimiento de voz
interactive_mode(args.language, args.continuous)
if __name__ == "__main__":
main()