[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:
parent
4ec9b41e2c
commit
9bc81ea5a8
1 changed files with 187 additions and 35 deletions
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue