增加运维机器人单元
This commit is contained in:
1 parent
479d263b88
commit
1b60ab0a2e
42 files changed
+20469
No files matched your search
@@ -0,0 +1,674 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AI智能助手 - 语音命令行前端
|
||||
支持WebSocket直连、语音输入、语音输出、命令系统
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import threading
|
||||
import warnings
|
||||
from typing import Optional, Dict, Any
|
||||
import websockets
|
||||
from colorama import init, Fore, Style as ColoramaStyle
|
||||
|
||||
init(autoreset=True)
|
||||
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
try:
|
||||
import speech_recognition as sr
|
||||
except ImportError:
|
||||
print("❌ 缺少语音识别库,请安装: pip install SpeechRecognition pyaudio")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
import pyttsx3
|
||||
except ImportError:
|
||||
print("❌ 缺少文本转语音库,请安装: pip install pyttsx3")
|
||||
sys.exit(1)
|
||||
|
||||
class Colors:
|
||||
USER = Fore.CYAN
|
||||
AI = Fore.GREEN
|
||||
TASK = Fore.YELLOW
|
||||
SYSTEM = Fore.BLUE
|
||||
ERROR = Fore.RED
|
||||
INFO = Fore.MAGENTA
|
||||
WARNING = Fore.LIGHTYELLOW_EX
|
||||
RESET = ColoramaStyle.RESET_ALL
|
||||
BOLD = ColoramaStyle.BRIGHT
|
||||
|
||||
class TextToSpeech:
|
||||
def __init__(self):
|
||||
self.engine = None
|
||||
self.available = False
|
||||
old_stderr = None
|
||||
try:
|
||||
old_stderr = os.dup(2)
|
||||
null_fd = os.open('/dev/null', os.O_WRONLY)
|
||||
os.dup2(null_fd, 2)
|
||||
os.close(null_fd)
|
||||
|
||||
self.engine = pyttsx3.init()
|
||||
self.engine.setProperty('rate', 150)
|
||||
voices = self.engine.getProperty('voices')
|
||||
|
||||
chinese_voice = None
|
||||
for voice in voices:
|
||||
try:
|
||||
voice_name = str(voice.name).lower() if hasattr(voice, 'name') else ''
|
||||
lang_attr = ''
|
||||
if hasattr(voice, 'language'):
|
||||
lang_attr = str(voice.language).lower()
|
||||
elif hasattr(voice, 'languages'):
|
||||
lang_attr = str(voice.languages[0]).lower() if voice.languages else ''
|
||||
|
||||
if 'chinese' in voice_name or 'zh' in lang_attr or 'mandarin' in voice_name:
|
||||
if 'mandarin' in voice_name and 'pinyin' in voice_name:
|
||||
chinese_voice = voice
|
||||
break
|
||||
elif chinese_voice is None:
|
||||
chinese_voice = voice
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if chinese_voice:
|
||||
self.engine.setProperty('voice', chinese_voice.id)
|
||||
print(f"{Colors.INFO}🔊 已选择语音: {chinese_voice.name}{Colors.RESET}")
|
||||
|
||||
try:
|
||||
self.engine.say("测试")
|
||||
self.engine.runAndWait()
|
||||
self.available = True
|
||||
except Exception:
|
||||
self.available = False
|
||||
|
||||
except Exception:
|
||||
self.available = False
|
||||
finally:
|
||||
if old_stderr is not None:
|
||||
os.dup2(old_stderr, 2)
|
||||
os.close(old_stderr)
|
||||
|
||||
def speak(self, text: str):
|
||||
if not self.available or not self.engine:
|
||||
return
|
||||
old_stderr = None
|
||||
try:
|
||||
old_stderr = os.dup(2)
|
||||
null_fd = os.open('/dev/null', os.O_WRONLY)
|
||||
os.dup2(null_fd, 2)
|
||||
os.close(null_fd)
|
||||
self.engine.say(text)
|
||||
self.engine.runAndWait()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
if old_stderr is not None:
|
||||
os.dup2(old_stderr, 2)
|
||||
os.close(old_stderr)
|
||||
|
||||
class StreamingMessage:
|
||||
def __init__(self):
|
||||
self.content = ""
|
||||
self.is_active = False
|
||||
|
||||
def add_chunk(self, chunk: str):
|
||||
self.content += chunk
|
||||
self.is_active = True
|
||||
|
||||
def get_content(self) -> str:
|
||||
return self.content
|
||||
|
||||
def clear(self):
|
||||
self.content = ""
|
||||
self.is_active = False
|
||||
|
||||
class WebSocketClient:
|
||||
def __init__(self, uri: str):
|
||||
self.uri = uri
|
||||
self.websocket = None
|
||||
self.connected = False
|
||||
self.streaming_messages: Dict[str, StreamingMessage] = {
|
||||
'chat': StreamingMessage(),
|
||||
'tasks': {}
|
||||
}
|
||||
self.running = True
|
||||
|
||||
async def connect(self):
|
||||
try:
|
||||
self.websocket = await websockets.connect(self.uri)
|
||||
self.connected = True
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"{Colors.ERROR}❌ 连接失败: {e}{Colors.RESET}")
|
||||
return False
|
||||
|
||||
async def disconnect(self):
|
||||
if self.websocket:
|
||||
await self.websocket.close()
|
||||
self.connected = False
|
||||
|
||||
async def send_message(self, message: str):
|
||||
if not self.websocket or not self.connected:
|
||||
print(f"{Colors.ERROR}❌ 未连接到服务器{Colors.RESET}")
|
||||
return False
|
||||
|
||||
try:
|
||||
if message.startswith('/'):
|
||||
data = {'type': 'command', 'command': message}
|
||||
else:
|
||||
data = {'type': 'message', 'content': message}
|
||||
|
||||
await self.websocket.send(json.dumps(data))
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"{Colors.ERROR}❌ 发送失败: {e}{Colors.RESET}")
|
||||
return False
|
||||
|
||||
async def receive_messages(self, tts: TextToSpeech):
|
||||
try:
|
||||
async for message in self.websocket:
|
||||
try:
|
||||
data = json.loads(message)
|
||||
await self.handle_message(data, tts)
|
||||
except json.JSONDecodeError:
|
||||
await self.handle_stream_chunk(message, 'chat', tts)
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
self.connected = False
|
||||
print(f"\n{Colors.ERROR}❌ 连接已断开{Colors.RESET}")
|
||||
except Exception as e:
|
||||
print(f"\n{Colors.ERROR}❌ 接收消息错误: {e}{Colors.RESET}")
|
||||
|
||||
async def handle_message(self, data: dict, tts: TextToSpeech):
|
||||
msg_type = data.get('type', '')
|
||||
content = data.get('content', '')
|
||||
|
||||
if msg_type == 'chunk':
|
||||
await self.handle_stream_chunk(content, 'chat', tts)
|
||||
|
||||
elif msg_type == 'complete':
|
||||
await self.handle_stream_complete('chat', tts)
|
||||
|
||||
elif msg_type == 'dialogue_ended':
|
||||
await self.handle_dialogue_ended(tts)
|
||||
|
||||
elif msg_type == 'task_process':
|
||||
msg_subtype = data.get('msg_type', '')
|
||||
task_content = data.get('content', '')
|
||||
task_id = data.get('task_id', 'unknown')
|
||||
execution_id = data.get('execution_id', 'unknown')
|
||||
task_title = data.get('task_title', '未知任务')
|
||||
|
||||
if msg_subtype == 'chunk':
|
||||
await self.handle_stream_chunk(
|
||||
task_content, 'task', tts,
|
||||
{'task_id': task_id, 'execution_id': execution_id, 'title': task_title}
|
||||
)
|
||||
elif msg_subtype == 'complete':
|
||||
await self.handle_stream_complete('task', tts, execution_id)
|
||||
|
||||
elif msg_type == 'task_status':
|
||||
task_id = data.get('task_id', 'unknown')
|
||||
title = data.get('title', '未知')
|
||||
status = data.get('status', 'unknown')
|
||||
message = data.get('message', content)
|
||||
|
||||
status_icon = {
|
||||
'executing': '⚙️', 'complete': '✅', 'failed': '❌',
|
||||
'added': '➕', 'batch_start': '🚀', 'batch_complete': '🏁'
|
||||
}.get(status, '📋')
|
||||
|
||||
print(f"{Colors.TASK}{status_icon} [任务 {task_id}] {title}: {message}{Colors.RESET}")
|
||||
|
||||
elif msg_type == 'task_error':
|
||||
task_id = data.get('task_id', 'unknown')
|
||||
execution_id = data.get('execution_id', 'unknown')
|
||||
task_title = data.get('task_title', '未知')
|
||||
error = data.get('error', '未知错误')
|
||||
|
||||
print(f"{Colors.ERROR}❌ [任务 {task_id}] {task_title}: {error}{Colors.RESET}")
|
||||
if tts:
|
||||
tts.speak(f"任务 {task_id} 出错了,错误是 {error}")
|
||||
|
||||
if execution_id:
|
||||
await self.handle_stream_complete('task', tts, execution_id)
|
||||
|
||||
elif msg_type == 'error':
|
||||
print(f"{Colors.ERROR}❌ {content}{Colors.RESET}")
|
||||
if tts:
|
||||
tts.speak(f"错误,{content}")
|
||||
|
||||
elif msg_type == 'info':
|
||||
print(f"{Colors.INFO}ℹ️ {content}{Colors.RESET}")
|
||||
if tts:
|
||||
tts.speak(content)
|
||||
|
||||
elif msg_type == 'command_result':
|
||||
print(f"{Colors.SYSTEM}📋 {content}{Colors.RESET}")
|
||||
|
||||
elif msg_type == 'connection_ack':
|
||||
print(f"{Colors.AI}✅ {content}{Colors.RESET}")
|
||||
|
||||
elif msg_type == 'notification':
|
||||
print(f"{Colors.INFO}🔔 {content}{Colors.RESET}")
|
||||
if tts:
|
||||
tts.speak(content)
|
||||
|
||||
else:
|
||||
if content:
|
||||
await self.handle_stream_chunk(content, 'chat', tts)
|
||||
|
||||
async def handle_stream_chunk(self, content: str, msg_type: str = 'chat',
|
||||
tts: TextToSpeech = None, task_info: dict = None):
|
||||
if msg_type == 'chat':
|
||||
stream = self.streaming_messages['chat']
|
||||
stream.add_chunk(content)
|
||||
|
||||
if not stream.is_active:
|
||||
print(f"\n{Colors.AI}🤖 AI助手: ", end='', flush=True)
|
||||
stream.is_active = True
|
||||
|
||||
print(content, end='', flush=True)
|
||||
|
||||
elif msg_type == 'task' and task_info:
|
||||
execution_id = task_info.get('execution_id', 'unknown')
|
||||
task_id = task_info.get('task_id', 'unknown')
|
||||
title = task_info.get('title', '未知任务')
|
||||
|
||||
if execution_id not in self.streaming_messages['tasks']:
|
||||
self.streaming_messages['tasks'][execution_id] = StreamingMessage()
|
||||
print(f"\n{Colors.TASK}⚡ 定时任务 #{task_id} - {title}{Colors.RESET}")
|
||||
print(f"{Colors.TASK}📋 ", end='', flush=True)
|
||||
|
||||
stream = self.streaming_messages['tasks'][execution_id]
|
||||
stream.add_chunk(content)
|
||||
print(content, end='', flush=True)
|
||||
|
||||
async def handle_stream_complete(self, msg_type: str = 'chat',
|
||||
tts: TextToSpeech = None, execution_id: str = None):
|
||||
if msg_type == 'chat':
|
||||
stream = self.streaming_messages['chat']
|
||||
print()
|
||||
if tts and getattr(tts, 'available', True) and stream.content:
|
||||
tts.speak(stream.content)
|
||||
stream.clear()
|
||||
|
||||
elif msg_type == 'task' and execution_id:
|
||||
if execution_id in self.streaming_messages['tasks']:
|
||||
print()
|
||||
del self.streaming_messages['tasks'][execution_id]
|
||||
|
||||
async def handle_dialogue_ended(self, tts: TextToSpeech):
|
||||
print(f"\n{Colors.SYSTEM}✅ 对话已结束,可以开始新对话{Colors.RESET}")
|
||||
if tts:
|
||||
tts.speak("对话已结束")
|
||||
self.streaming_messages['chat'].clear()
|
||||
self.streaming_messages['tasks'].clear()
|
||||
|
||||
class VoiceInterface:
|
||||
def __init__(self):
|
||||
self.client: Optional[WebSocketClient] = None
|
||||
self.tts = None
|
||||
self.recognizer = None
|
||||
self.microphone = None
|
||||
self.whisper_model = None
|
||||
self.has_audio = False
|
||||
self.has_tts = False
|
||||
self.has_microphone = False
|
||||
|
||||
try:
|
||||
self.tts = TextToSpeech()
|
||||
self.has_tts = self.tts.available
|
||||
if self.has_tts:
|
||||
print(f"{Colors.INFO}🔊 语音输出已就绪{Colors.RESET}")
|
||||
else:
|
||||
print(f"{Colors.WARNING}⚠️ 语音输出不可用{Colors.RESET}")
|
||||
except Exception as e:
|
||||
self.tts = None
|
||||
self.has_tts = False
|
||||
print(f"{Colors.WARNING}⚠️ 语音输出不可用: {e}{Colors.RESET}")
|
||||
|
||||
try:
|
||||
self.recognizer = sr.Recognizer()
|
||||
|
||||
print(f"{Colors.INFO}🎤 尝试检测音频设备...{Colors.RESET}")
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f:
|
||||
test_file = f.name
|
||||
|
||||
result = subprocess.run(
|
||||
['arecord', '-f', 'cd', '-d', '1', test_file],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
|
||||
os.unlink(test_file)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f"{Colors.INFO}🎤 arecord 检测到设备,将使用 arecord 录音{Colors.RESET}")
|
||||
self.has_microphone = True
|
||||
self.use_arecord = True
|
||||
|
||||
print(f"{Colors.INFO}📥 加载 Whisper 模型(离线语音识别)...{Colors.RESET}")
|
||||
try:
|
||||
import whisper
|
||||
self.whisper_model = whisper.load_model("medium", device="cpu")
|
||||
print(f"{Colors.INFO}✅ Whisper 模型加载成功 (medium 模型){Colors.RESET}")
|
||||
except Exception as e:
|
||||
print(f"{Colors.WARNING}⚠️ Whisper 模型加载失败: {e}{Colors.RESET}")
|
||||
self.whisper_model = None
|
||||
|
||||
print(f"{Colors.INFO}🎤 麦克风已就绪{Colors.RESET}")
|
||||
else:
|
||||
devices = sr.Microphone.list_microphone_names()
|
||||
if devices:
|
||||
print(f"{Colors.INFO}🎤 检测到 {len(devices)} 个音频设备{Colors.RESET}")
|
||||
self.microphone = sr.Microphone()
|
||||
with self.microphone as source:
|
||||
self.recognizer.adjust_for_ambient_noise(source, duration=0.1)
|
||||
self.has_microphone = True
|
||||
self.use_arecord = False
|
||||
print(f"{Colors.INFO}🎤 麦克风已就绪 (使用 speech_recognition){Colors.RESET}")
|
||||
else:
|
||||
raise RuntimeError("没有检测到音频设备")
|
||||
except Exception as e:
|
||||
print(f"{Colors.WARNING}⚠️ 麦克风不可用: {e}{Colors.RESET}")
|
||||
self.has_microphone = False
|
||||
self.use_arecord = False
|
||||
|
||||
self.has_audio = self.has_tts or self.has_microphone
|
||||
|
||||
if not self.has_audio:
|
||||
print(f"{Colors.INFO}ℹ️ 将以纯文本模式运行{Colors.RESET}")
|
||||
|
||||
self.commands = {
|
||||
'/help': '显示帮助信息',
|
||||
'/connect': '连接到服务器',
|
||||
'/disconnect': '断开连接',
|
||||
'/quit': '退出程序',
|
||||
'/clear': '清屏',
|
||||
'/status': '显示连接状态',
|
||||
'/list': '列出可用命令',
|
||||
'/memories': '查看记忆',
|
||||
'/memos': '查看备忘录',
|
||||
'/check': '检查提醒',
|
||||
'/config': '查看配置',
|
||||
'/new': '新建对话',
|
||||
'/tasks': '查看任务列表',
|
||||
}
|
||||
|
||||
def print_banner(self):
|
||||
banner = f"""
|
||||
{Colors.BOLD}{Colors.AI}
|
||||
╔══════════════════════════════════════════╗
|
||||
║ AI智能助手 - 语音前端 ║
|
||||
║ WebSocket 直连版本 ║
|
||||
╚══════════════════════════════════════════╝
|
||||
{Colors.RESET}
|
||||
"""
|
||||
print(banner)
|
||||
print(f"{Colors.INFO}输入 /help 查看可用命令")
|
||||
print(f"输入语音开始对话,或输入文本命令{Colors.RESET}")
|
||||
print()
|
||||
|
||||
def listen_voice(self) -> Optional[str]:
|
||||
"""监听语音输入"""
|
||||
if not self.has_microphone:
|
||||
print(f"{Colors.WARNING}⚠️ 麦克风不可用,请使用文本输入{Colors.RESET}")
|
||||
return None
|
||||
|
||||
if not self.client or not self.client.connected:
|
||||
print(f"{Colors.ERROR}❌ 未连接到服务器,请先连接{Colors.RESET}")
|
||||
return None
|
||||
|
||||
print(f"{Colors.INFO}🎤 正在听... (按 Ctrl+C 取消){Colors.RESET}")
|
||||
|
||||
try:
|
||||
if self.use_arecord:
|
||||
import subprocess
|
||||
import tempfile
|
||||
import wave
|
||||
import numpy as np
|
||||
import whisper
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f:
|
||||
temp_file = f.name
|
||||
|
||||
result = subprocess.run(
|
||||
['arecord', '-f', 'cd', '-d', '5', temp_file],
|
||||
capture_output=True, text=True,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"{Colors.ERROR}❌ 录音失败: {result.stderr}{Colors.RESET}")
|
||||
if os.path.exists(temp_file):
|
||||
os.unlink(temp_file)
|
||||
return None
|
||||
|
||||
if not os.path.exists(temp_file) or os.path.getsize(temp_file) == 0:
|
||||
print(f"{Colors.WARNING}⚠️ 录音文件为空{Colors.RESET}")
|
||||
if os.path.exists(temp_file):
|
||||
os.unlink(temp_file)
|
||||
return None
|
||||
|
||||
if self.whisper_model:
|
||||
with wave.open(temp_file, 'rb') as wav_file:
|
||||
frames = wav_file.readframes(wav_file.getnframes())
|
||||
audio_data = np.frombuffer(frames, dtype=np.int16).astype(np.float32) / 32768.0
|
||||
|
||||
os.unlink(temp_file)
|
||||
|
||||
audio_padded = whisper.pad_or_trim(audio_data)
|
||||
mel = whisper.log_mel_spectrogram(audio_padded, n_mels=80)
|
||||
|
||||
options = whisper.DecodingOptions(language='zh', without_timestamps=True)
|
||||
result = whisper.decode(self.whisper_model, mel, options)
|
||||
|
||||
text = result.text.strip()
|
||||
if text:
|
||||
print(f"{Colors.USER}👤 语音输入: {text}{Colors.RESET}")
|
||||
return text
|
||||
else:
|
||||
print(f"{Colors.INFO}⚠️ 未识别到语音内容{Colors.RESET}")
|
||||
return None
|
||||
else:
|
||||
with sr.AudioFile(temp_file) as source:
|
||||
audio = self.recognizer.record(source)
|
||||
os.unlink(temp_file)
|
||||
text = self.recognizer.recognize_google(audio, language='zh-CN')
|
||||
print(f"{Colors.USER}👤 语音输入: {text}{Colors.RESET}")
|
||||
return text
|
||||
|
||||
else:
|
||||
with self.microphone as source:
|
||||
self.recognizer.adjust_for_ambient_noise(source)
|
||||
audio = self.recognizer.listen(source, timeout=15)
|
||||
|
||||
text = self.recognizer.recognize_google(audio, language='zh-CN')
|
||||
print(f"{Colors.USER}👤 语音输入: {text}{Colors.RESET}")
|
||||
return text
|
||||
|
||||
except sr.WaitTimeoutError:
|
||||
print(f"{Colors.INFO}⏱️ 等待超时,请重试{Colors.RESET}")
|
||||
return None
|
||||
except sr.UnknownValueError:
|
||||
print(f"{Colors.INFO}😕 无法识别语音,请重试{Colors.RESET}")
|
||||
return None
|
||||
except sr.RequestError as e:
|
||||
print(f"{Colors.ERROR}❌ 语音识别服务错误: {e}{Colors.RESET}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"{Colors.ERROR}❌ 语音输入错误: {e}{Colors.RESET}")
|
||||
return None
|
||||
|
||||
async def handle_command(self, command: str):
|
||||
parts = command.split()
|
||||
cmd = parts[0].lower()
|
||||
|
||||
if cmd == '/help':
|
||||
self.show_help()
|
||||
|
||||
elif cmd == '/connect':
|
||||
if len(parts) < 2:
|
||||
print(f"{Colors.ERROR}用法: /connect ws://host:port{Colors.RESET}")
|
||||
return
|
||||
|
||||
uri = parts[1]
|
||||
if self.client and self.client.connected:
|
||||
await self.client.disconnect()
|
||||
|
||||
self.client = WebSocketClient(uri)
|
||||
if await self.client.connect():
|
||||
print(f"{Colors.AI}✅ 已连接到 {uri}{Colors.RESET}")
|
||||
if self.tts and self.has_tts:
|
||||
self.tts.speak(f"已连接到 {uri}")
|
||||
asyncio.create_task(self.client.receive_messages(self.tts))
|
||||
else:
|
||||
self.client = None
|
||||
|
||||
elif cmd == '/disconnect':
|
||||
if self.client:
|
||||
await self.client.disconnect()
|
||||
self.client = None
|
||||
print(f"{Colors.SYSTEM}已断开连接{Colors.RESET}")
|
||||
if self.tts and self.has_tts:
|
||||
self.tts.speak("已断开连接")
|
||||
else:
|
||||
print(f"{Colors.ERROR}未连接到服务器{Colors.RESET}")
|
||||
|
||||
elif cmd == '/status':
|
||||
if self.client and self.client.connected:
|
||||
print(f"{Colors.AI}✅ 已连接: {self.client.uri}{Colors.RESET}")
|
||||
if self.tts and self.has_tts:
|
||||
self.tts.speak(f"已连接到 {self.client.uri}")
|
||||
else:
|
||||
print(f"{Colors.SYSTEM}未连接{Colors.RESET}")
|
||||
if self.tts and self.has_tts:
|
||||
self.tts.speak("未连接")
|
||||
|
||||
elif cmd == '/clear':
|
||||
os.system('clear' if os.name == 'posix' else 'cls')
|
||||
self.print_banner()
|
||||
|
||||
elif cmd == '/quit':
|
||||
print(f"{Colors.SYSTEM}再见!{Colors.RESET}")
|
||||
if self.tts:
|
||||
self.tts.speak("再见")
|
||||
if self.client:
|
||||
await self.client.disconnect()
|
||||
sys.exit(0)
|
||||
|
||||
elif cmd in self.commands:
|
||||
if self.client and self.client.connected:
|
||||
await self.client.send_message(command)
|
||||
else:
|
||||
print(f"{Colors.ERROR}未连接到服务器,请先使用 /connect 连接{Colors.RESET}")
|
||||
|
||||
else:
|
||||
print(f"{Colors.ERROR}未知命令: {cmd}{Colors.RESET}")
|
||||
|
||||
def show_help(self):
|
||||
help_text = f"""
|
||||
{Colors.BOLD}可用命令:{Colors.RESET}
|
||||
|
||||
{Colors.USER}本地命令:{Colors.RESET}
|
||||
/connect <url> 连接到WebSocket服务器
|
||||
/disconnect 断开连接
|
||||
/status 显示连接状态
|
||||
/clear 清屏
|
||||
/quit 退出程序
|
||||
/help 显示此帮助
|
||||
|
||||
{Colors.AI}服务器命令 (需要先连接):{Colors.RESET}
|
||||
"""
|
||||
for cmd, desc in self.commands.items():
|
||||
if cmd not in ['/help', '/connect', '/disconnect', '/quit', '/clear', '/status']:
|
||||
help_text += f" {cmd:<15} {desc}\n"
|
||||
|
||||
print(help_text)
|
||||
|
||||
async def send_user_message(self, message: str):
|
||||
if not self.client or not self.client.connected:
|
||||
print(f"{Colors.ERROR}❌ 未连接到服务器{Colors.RESET}")
|
||||
return
|
||||
|
||||
print(f"\n{Colors.USER}👤 用户: {message}{Colors.RESET}")
|
||||
await self.client.send_message(message)
|
||||
|
||||
async def run(self):
|
||||
self.print_banner()
|
||||
|
||||
prompt = f"{Colors.USER}>>> {Colors.RESET}"
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = await asyncio.to_thread(
|
||||
lambda: input(prompt)
|
||||
)
|
||||
user_input = user_input.strip()
|
||||
|
||||
if not user_input:
|
||||
if self.has_microphone:
|
||||
print(f"{Colors.INFO}🎤 开始语音输入...{Colors.RESET}")
|
||||
voice_text = self.listen_voice()
|
||||
if voice_text:
|
||||
await self.send_user_message(voice_text)
|
||||
else:
|
||||
print(f"{Colors.WARNING}⚠️ 麦克风不可用,请输入文本{Colors.RESET}")
|
||||
continue
|
||||
|
||||
if user_input.startswith('/'):
|
||||
await self.handle_command(user_input)
|
||||
else:
|
||||
await self.send_user_message(user_input)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n{Colors.INFO}⏹️ 已取消{Colors.RESET}")
|
||||
continue
|
||||
except EOFError:
|
||||
print(f"\n{Colors.SYSTEM}再见!{Colors.RESET}")
|
||||
if self.tts:
|
||||
self.tts.speak("再见")
|
||||
if self.client:
|
||||
await self.client.disconnect()
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"{Colors.ERROR}错误: {e}{Colors.RESET}")
|
||||
|
||||
async def main():
|
||||
cli = VoiceInterface()
|
||||
|
||||
default_uri = "ws://localhost:8765"
|
||||
uri = sys.argv[1] if len(sys.argv) > 1 else default_uri
|
||||
|
||||
cli.client = WebSocketClient(uri)
|
||||
if await cli.client.connect():
|
||||
print(f"{Colors.AI}✅ 已连接到 {uri}{Colors.RESET}")
|
||||
if cli.tts and cli.has_tts:
|
||||
cli.tts.speak(f"已连接到 {uri}")
|
||||
asyncio.create_task(cli.client.receive_messages(cli.tts))
|
||||
else:
|
||||
print(f"{Colors.WARNING}⚠️ 无法连接到 {uri}{Colors.RESET}")
|
||||
print(f"{Colors.INFO}ℹ️ 你可以通过命令行参数指定地址: python voice_client.py ws://your-server:8765{Colors.RESET}")
|
||||
print(f"{Colors.INFO}ℹ️ 或者使用 /connect 命令手动连接{Colors.RESET}")
|
||||
|
||||
try:
|
||||
await cli.run()
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
if cli.client:
|
||||
await cli.client.disconnect()
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n{Colors.SYSTEM}程序已退出{Colors.RESET}")
|
||||
Reference in new issue
Block a user