[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> # [Author] : Cortana Rosero One <cortana@rosero.one>
# [Generated] : Created by Claude Code (claude-3-7-sonnet-20250219) # [Generated] : Created by Claude Code (claude-3-7-sonnet-20250219)
# [Created] : 2025/03/30 16:45:00 # [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 # [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 os
import sys import sys
import json
import subprocess import subprocess
import argparse import argparse
import time 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 import AudioSegment
from pydub.playback import play from pydub.playback import play
@ -66,36 +70,140 @@ def play_sound(sound_type):
# Si hay algún error reproduciendo el sonido, simplemente continuamos # Si hay algún error reproduciendo el sonido, simplemente continuamos
pass pass
def recognize_speech(language="es-ES"): def download_model(language="es"):
"""Captura audio del micrófono y lo convierte a texto""" """Descarga el modelo de Vosk si no existe"""
recognizer = sr.Recognizer() # 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: # 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)") print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.YELLOW}Escuchando...{Colors.END} (Presiona Ctrl+C para detener)")
play_sound("start") play_sound("start")
# Ajustar para ruido ambiental # Preparar reconocedor
recognizer.adjust_for_ambient_noise(source, duration=0.5) rec = KaldiRecognizer(model, samplerate)
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}")
# Intentar reconocer usando Google Speech Recognition try:
text = recognizer.recognize_google(audio, language=language) with sd.RawInputStream(samplerate=samplerate, blocksize=8000,
play_sound("stop") dtype='int16', channels=1, callback=callback):
return text
except sr.UnknownValueError: # Variables para controlar el reconocimiento
play_sound("error") start_time = time.time()
print(f"{Colors.RED}No se pudo entender el audio{Colors.END}") timeout = 10 # segundos
return None last_text_time = time.time()
except sr.RequestError as e: final_result = ""
play_sound("error") partial_results = []
print(f"{Colors.RED}Error en el servicio de reconocimiento: {e}{Colors.END}")
return None # 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: except Exception as e:
play_sound("error") play_sound("error")
print(f"{Colors.RED}Error: {e}{Colors.END}") print(f"{Colors.RED}Error en el reconocimiento de voz: {e}{Colors.END}")
return None 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): def send_to_claude(text, silent=False):
"""Envía el texto reconocido a Claude Code""" """Envía el texto reconocido a Claude Code"""
@ -158,19 +266,63 @@ def interactive_mode(language="es-ES", continuous=False):
print(f"{Colors.BLUE}[Claude Voice]{Colors.END} {Colors.GREEN}¡Hasta pronto!{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(): 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('-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('-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('-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('-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() 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: if args.text:
# Modo de texto directo
send_to_claude(args.text, args.silent) send_to_claude(args.text, args.silent)
else: return
# Modo interactivo con reconocimiento de voz # Modo interactivo con reconocimiento de voz
interactive_mode(args.language, args.continuous) interactive_mode(args.language, args.continuous)