[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>
This commit is contained in:
Mauro Rosero P. 2025-03-30 15:59:39 -05:00
parent 4ec9b41e2c
commit 9bc81ea5a8
Signed by: mrosero
GPG key ID: 83BD2A5F674B7E26

View file

@ -5,16 +5,20 @@
# [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 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()