feat: publish workphone SDK deployment and API docs
This commit is contained in:
414
sdk/agent/voice_agent.py
Normal file
414
sdk/agent/voice_agent.py
Normal file
@@ -0,0 +1,414 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
工作手机Agent - 语音控制版
|
||||
运行在手机上,支持语音命令和自主执行任务
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import argparse
|
||||
import base64
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
try:
|
||||
import websockets
|
||||
import uiautomator2 as u2
|
||||
except ImportError:
|
||||
print("请安装依赖: pip install websockets uiautomator2")
|
||||
exit(1)
|
||||
|
||||
# 尝试导入语音识别
|
||||
try:
|
||||
import speech_recognition as sr
|
||||
VOICE_ENABLED = True
|
||||
except ImportError:
|
||||
VOICE_ENABLED = False
|
||||
print("语音功能未启用,安装: pip install SpeechRecognition")
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VoiceAgent:
|
||||
"""
|
||||
语音控制Agent
|
||||
|
||||
功能:
|
||||
1. 手机本地运行,无需电脑
|
||||
2. 语音命令控制
|
||||
3. 自主执行任务队列
|
||||
4. 连接远程服务器接收命令
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_id: str,
|
||||
server_url: str = None,
|
||||
voice_enabled: bool = True
|
||||
):
|
||||
self.device_id = device_id
|
||||
self.server_url = server_url
|
||||
self.voice_enabled = voice_enabled and VOICE_ENABLED
|
||||
|
||||
# 初始化uiautomator2
|
||||
self.d = u2.connect()
|
||||
self.d.implicitly_wait(10.0)
|
||||
|
||||
# 任务队列
|
||||
self.task_queue: List[Dict] = []
|
||||
self.running = False
|
||||
|
||||
# 语音识别器
|
||||
if self.voice_enabled:
|
||||
self.recognizer = sr.Recognizer()
|
||||
|
||||
# 预设命令
|
||||
self.commands = {
|
||||
"打开微信": self._open_wechat,
|
||||
"打开豆包": self._open_doubao,
|
||||
"打开抖音": self._open_douyin,
|
||||
"打开设置": self._open_settings,
|
||||
"截图": self._screenshot,
|
||||
"返回": self._go_back,
|
||||
"回到桌面": self._go_home,
|
||||
"向上滑": self._swipe_up,
|
||||
"向下滑": self._swipe_down,
|
||||
}
|
||||
|
||||
logger.info(f"VoiceAgent初始化完成: {device_id}")
|
||||
logger.info(f"语音控制: {'启用' if self.voice_enabled else '禁用'}")
|
||||
|
||||
# ========== 基础操作 ==========
|
||||
|
||||
def _open_wechat(self):
|
||||
"""打开微信"""
|
||||
self.d.app_start("com.tencent.mm")
|
||||
return {"success": True, "message": "微信已打开"}
|
||||
|
||||
def _open_doubao(self):
|
||||
"""打开豆包"""
|
||||
self.d.app_start("com.larus.nova")
|
||||
return {"success": True, "message": "豆包已打开"}
|
||||
|
||||
def _open_douyin(self):
|
||||
"""打开抖音"""
|
||||
self.d.app_start("com.ss.android.ugc.aweme")
|
||||
return {"success": True, "message": "抖音已打开"}
|
||||
|
||||
def _open_settings(self):
|
||||
"""打开设置"""
|
||||
self.d.app_start("com.android.settings")
|
||||
return {"success": True, "message": "设置已打开"}
|
||||
|
||||
def _screenshot(self):
|
||||
"""截图"""
|
||||
img = self.d.screenshot(format='raw')
|
||||
# 保存到手机
|
||||
path = f"/sdcard/screenshot_{int(time.time())}.png"
|
||||
with open(path, 'wb') as f:
|
||||
f.write(img)
|
||||
return {"success": True, "message": f"截图已保存: {path}"}
|
||||
|
||||
def _go_back(self):
|
||||
"""返回"""
|
||||
self.d.press("back")
|
||||
return {"success": True, "message": "已返回"}
|
||||
|
||||
def _go_home(self):
|
||||
"""回到桌面"""
|
||||
self.d.press("home")
|
||||
return {"success": True, "message": "已回到桌面"}
|
||||
|
||||
def _swipe_up(self):
|
||||
"""向上滑动"""
|
||||
self.d.swipe_ext("up", scale=0.8)
|
||||
return {"success": True, "message": "已向上滑动"}
|
||||
|
||||
def _swipe_down(self):
|
||||
"""向下滑动"""
|
||||
self.d.swipe_ext("down", scale=0.8)
|
||||
return {"success": True, "message": "已向下滑动"}
|
||||
|
||||
# ========== 语音识别 ==========
|
||||
|
||||
def listen_voice(self, timeout: int = 5) -> Optional[str]:
|
||||
"""监听语音命令"""
|
||||
if not self.voice_enabled:
|
||||
return None
|
||||
|
||||
try:
|
||||
with sr.Microphone() as source:
|
||||
logger.info("请说话...")
|
||||
self.recognizer.adjust_for_ambient_noise(source, duration=0.5)
|
||||
audio = self.recognizer.listen(source, timeout=timeout)
|
||||
|
||||
# 使用Google语音识别(中文)
|
||||
text = self.recognizer.recognize_google(audio, language="zh-CN")
|
||||
logger.info(f"识别到: {text}")
|
||||
return text
|
||||
except sr.WaitTimeoutError:
|
||||
return None
|
||||
except sr.UnknownValueError:
|
||||
logger.warning("无法识别语音")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"语音识别错误: {e}")
|
||||
return None
|
||||
|
||||
def process_voice_command(self, text: str) -> Dict:
|
||||
"""处理语音命令"""
|
||||
text = text.strip()
|
||||
|
||||
# 精确匹配
|
||||
if text in self.commands:
|
||||
return self.commands[text]()
|
||||
|
||||
# 模糊匹配
|
||||
for cmd, func in self.commands.items():
|
||||
if cmd in text or text in cmd:
|
||||
return func()
|
||||
|
||||
# 发送消息命令: "给XXX发消息说XXX"
|
||||
if "发消息" in text or "发送" in text:
|
||||
return self._parse_send_message(text)
|
||||
|
||||
# 打开APP命令: "打开XXX"
|
||||
if text.startswith("打开"):
|
||||
app_name = text[2:].strip()
|
||||
return self._open_app_by_name(app_name)
|
||||
|
||||
return {"success": False, "message": f"未识别的命令: {text}"}
|
||||
|
||||
def _parse_send_message(self, text: str) -> Dict:
|
||||
"""解析发消息命令"""
|
||||
# 示例: "给张三发消息说你好"
|
||||
import re
|
||||
match = re.search(r'给(.+?)(发消息|发送)(.+)', text)
|
||||
if match:
|
||||
contact = match.group(1).strip()
|
||||
message = match.group(3).replace("说", "").strip()
|
||||
return self._send_wechat_message(contact, message)
|
||||
return {"success": False, "message": "无法解析发消息命令"}
|
||||
|
||||
def _send_wechat_message(self, contact: str, message: str) -> Dict:
|
||||
"""发送微信消息"""
|
||||
try:
|
||||
# 打开微信
|
||||
self.d.app_start("com.tencent.mm")
|
||||
time.sleep(2)
|
||||
|
||||
# 搜索联系人
|
||||
if self.d(text="搜索").exists(timeout=3):
|
||||
self.d(text="搜索").click()
|
||||
time.sleep(0.5)
|
||||
|
||||
self.d.send_keys(contact)
|
||||
time.sleep(1)
|
||||
|
||||
if self.d(text=contact).exists(timeout=3):
|
||||
self.d(text=contact).click()
|
||||
time.sleep(1)
|
||||
|
||||
# 发送消息
|
||||
self.d.send_keys(message)
|
||||
time.sleep(0.3)
|
||||
|
||||
if self.d(text="发送").exists(timeout=3):
|
||||
self.d(text="发送").click()
|
||||
|
||||
return {"success": True, "message": f"已发送给{contact}: {message}"}
|
||||
except Exception as e:
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _open_app_by_name(self, name: str) -> Dict:
|
||||
"""根据名称打开APP"""
|
||||
app_map = {
|
||||
"微信": "com.tencent.mm",
|
||||
"豆包": "com.larus.nova",
|
||||
"抖音": "com.ss.android.ugc.aweme",
|
||||
"小红书": "com.xingin.xhs",
|
||||
"淘宝": "com.taobao.taobao",
|
||||
"支付宝": "com.eg.android.AlipayGphone",
|
||||
"设置": "com.android.settings",
|
||||
"相机": "com.android.camera",
|
||||
"浏览器": "com.android.chrome",
|
||||
}
|
||||
|
||||
package = app_map.get(name)
|
||||
if package:
|
||||
self.d.app_start(package)
|
||||
return {"success": True, "message": f"{name}已打开"}
|
||||
|
||||
return {"success": False, "message": f"未知APP: {name}"}
|
||||
|
||||
# ========== 任务队列 ==========
|
||||
|
||||
def add_task(self, task: Dict):
|
||||
"""添加任务到队列"""
|
||||
task["id"] = f"task_{int(time.time() * 1000)}"
|
||||
task["status"] = "pending"
|
||||
task["created_at"] = datetime.now().isoformat()
|
||||
self.task_queue.append(task)
|
||||
logger.info(f"添加任务: {task}")
|
||||
return task["id"]
|
||||
|
||||
async def process_task_queue(self):
|
||||
"""处理任务队列"""
|
||||
while self.running:
|
||||
if self.task_queue:
|
||||
task = self.task_queue[0]
|
||||
if task["status"] == "pending":
|
||||
task["status"] = "running"
|
||||
logger.info(f"执行任务: {task['id']}")
|
||||
|
||||
try:
|
||||
result = await self._execute_task(task)
|
||||
task["status"] = "completed"
|
||||
task["result"] = result
|
||||
except Exception as e:
|
||||
task["status"] = "failed"
|
||||
task["error"] = str(e)
|
||||
|
||||
self.task_queue.pop(0)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def _execute_task(self, task: Dict) -> Dict:
|
||||
"""执行单个任务"""
|
||||
task_type = task.get("type")
|
||||
params = task.get("params", {})
|
||||
|
||||
if task_type == "voice_command":
|
||||
return self.process_voice_command(params.get("text", ""))
|
||||
|
||||
elif task_type == "click":
|
||||
self.d.click(params["x"], params["y"])
|
||||
return {"success": True}
|
||||
|
||||
elif task_type == "send_message":
|
||||
return self._send_wechat_message(
|
||||
params.get("contact"),
|
||||
params.get("message")
|
||||
)
|
||||
|
||||
elif task_type == "open_app":
|
||||
return self._open_app_by_name(params.get("name"))
|
||||
|
||||
return {"success": False, "message": f"未知任务类型: {task_type}"}
|
||||
|
||||
# ========== 远程连接 ==========
|
||||
|
||||
async def connect_server(self):
|
||||
"""连接远程服务器"""
|
||||
if not self.server_url:
|
||||
return
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
logger.info(f"连接服务器: {self.server_url}")
|
||||
|
||||
async with websockets.connect(self.server_url) as ws:
|
||||
# 注册
|
||||
await ws.send(json.dumps({
|
||||
"type": "register",
|
||||
"device_id": self.device_id,
|
||||
"capabilities": ["voice", "ui_control"]
|
||||
}))
|
||||
|
||||
async for message in ws:
|
||||
data = json.loads(message)
|
||||
await self._handle_server_message(data, ws)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"服务器连接错误: {e}")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def _handle_server_message(self, data: Dict, ws):
|
||||
"""处理服务器消息"""
|
||||
msg_type = data.get("type")
|
||||
|
||||
if msg_type == "execute":
|
||||
# 添加到任务队列
|
||||
task_id = self.add_task(data.get("task", {}))
|
||||
await ws.send(json.dumps({
|
||||
"type": "task_queued",
|
||||
"task_id": task_id
|
||||
}))
|
||||
|
||||
elif msg_type == "voice_command":
|
||||
result = self.process_voice_command(data.get("text", ""))
|
||||
await ws.send(json.dumps({
|
||||
"type": "result",
|
||||
"command_id": data.get("command_id"),
|
||||
"result": result
|
||||
}))
|
||||
|
||||
# ========== 主循环 ==========
|
||||
|
||||
async def voice_loop(self):
|
||||
"""语音监听循环"""
|
||||
if not self.voice_enabled:
|
||||
return
|
||||
|
||||
logger.info("开始语音监听...")
|
||||
while self.running:
|
||||
text = self.listen_voice(timeout=3)
|
||||
if text:
|
||||
# 唤醒词检测
|
||||
if "小助手" in text or "你好" in text:
|
||||
logger.info("唤醒成功,等待命令...")
|
||||
command = self.listen_voice(timeout=5)
|
||||
if command:
|
||||
result = self.process_voice_command(command)
|
||||
logger.info(f"执行结果: {result}")
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def start(self):
|
||||
"""启动Agent"""
|
||||
self.running = True
|
||||
logger.info("VoiceAgent启动")
|
||||
|
||||
tasks = [
|
||||
self.process_task_queue(),
|
||||
]
|
||||
|
||||
if self.server_url:
|
||||
tasks.append(self.connect_server())
|
||||
|
||||
if self.voice_enabled:
|
||||
tasks.append(self.voice_loop())
|
||||
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
def stop(self):
|
||||
"""停止Agent"""
|
||||
self.running = False
|
||||
logger.info("VoiceAgent停止")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='语音控制Agent')
|
||||
parser.add_argument('--device-id', default='voice-agent-001', help='设备ID')
|
||||
parser.add_argument('--server', help='远程服务器地址 (ws://xxx:8899/ws/device/xxx)')
|
||||
parser.add_argument('--no-voice', action='store_true', help='禁用语音控制')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
agent = VoiceAgent(
|
||||
device_id=args.device_id,
|
||||
server_url=args.server,
|
||||
voice_enabled=not args.no_voice
|
||||
)
|
||||
|
||||
try:
|
||||
asyncio.run(agent.start())
|
||||
except KeyboardInterrupt:
|
||||
agent.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user