diff --git a/bin/claude_voice.py b/bin/claude_voice.py index 4cfbd00..dfa8984 100755 --- a/bin/claude_voice.py +++ b/bin/claude_voice.py @@ -5,16 +5,20 @@ # [Author] : Cortana Rosero One # [Generated] : Created by Claude Code (claude-3-7-sonnet-20250219) # [Created] : 2025/03/30 16:45:00 -# [Modified] : 2025/03/30 16:45:00 +# [Modified] : 2025/03/30 17:25:00 # [Version] : 1.3.0 -# [Use Notes] : Instalar dependencias: pip install speechrecognition pydub pyaudio +# [Use Notes] : Instalar dependencias: pip install vosk sounddevice pydub import os import sys +import json import subprocess import argparse import time -import speech_recognition as sr +import queue +import threading +import sounddevice as sd +from vosk import Model, KaldiRecognizer from pydub import AudioSegment from pydub.playback import play @@ -66,36 +70,140 @@ def play_sound(sound_type): # Si hay algún error reproduciendo el sonido, simplemente continuamos pass -def recognize_speech(language="es-ES"): - """Captura audio del micrófono y lo convierte a texto""" - recognizer = sr.Recognizer() +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" + } - with sr.Microphone() as source: - print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.YELLOW}Escuchando...{Colors.END} (Presiona Ctrl+C para detener)") - play_sound("start") + # 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 - # Ajustar para ruido ambiental - recognizer.adjust_for_ambient_noise(source, duration=0.5) - try: - audio = recognizer.listen(source, timeout=10, phrase_time_limit=15) - print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.GREEN}Procesando audio...{Colors.END}") + 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): - # Intentar reconocer usando Google Speech Recognition - text = recognizer.recognize_google(audio, language=language) - play_sound("stop") - return text - except sr.UnknownValueError: - play_sound("error") - print(f"{Colors.RED}No se pudo entender el audio{Colors.END}") - return None - except sr.RequestError as e: - play_sound("error") - print(f"{Colors.RED}Error en el servicio de reconocimiento: {e}{Colors.END}") - return None - except Exception as e: - play_sound("error") - print(f"{Colors.RED}Error: {e}{Colors.END}") - return None + # 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""" @@ -158,21 +266,65 @@ def interactive_mode(language="es-ES", continuous=False): 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') + 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: - # Modo de texto directo send_to_claude(args.text, args.silent) - else: - # Modo interactivo con reconocimiento de voz - interactive_mode(args.language, args.continuous) + return + + # Modo interactivo con reconocimiento de voz + interactive_mode(args.language, args.continuous) if __name__ == "__main__": main() \ No newline at end of file