Files
workphone-sdk/sdk/app/routers/voice.py

210 lines
6.7 KiB
Python

"""
语音命令路由
处理来自APP和PC端的语音指令
"""
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, HTTPException
from pydantic import BaseModel
from typing import Optional, List, Dict, Any
import json
import logging
import asyncio
from services.ai_agent import process_voice_command, ai_agent
from services.ws_hub import ws_hub
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/voice", tags=["语音控制"])
def _map_action_to_device(action: dict) -> dict:
"""将 AI 解析的 action 转为设备端 execute.data 格式"""
act = action.get("action", "")
params = action.get("params") or {}
if act == "open_app":
return {"action": "app_start", "params": {"package": params.get("package", "")}}
if act == "input_text":
return {"action": "input", "params": {"text": params.get("text", ""), "clear": False}}
if act == "key_event":
keycode = (params.get("keycode") or "KEYCODE_BACK").lower()
key = "back" if "back" in keycode else ("home" if "home" in keycode else "back")
return {"action": "press_key", "params": {"key": key}}
if act == "back":
return {"action": "press_key", "params": {"key": "back"}}
if act == "home":
return {"action": "press_key", "params": {"key": "home"}}
if act == "screenshot":
return {"action": "screenshot", "params": params}
return {"action": act, "params": params}
class VoiceCommandRequest(BaseModel):
"""语音命令请求"""
text: str
device_id: Optional[str] = None
project_id: Optional[str] = None
execute: bool = True # 是否立即执行
class VoiceCommandResponse(BaseModel):
"""语音命令响应"""
success: bool
message: str
text: str
actions: List[Dict[str, Any]]
executed: bool = False
@router.post("/command", response_model=VoiceCommandResponse)
async def voice_command(request: VoiceCommandRequest):
"""
处理语音命令
接收文本命令,解析意图,可选择立即执行
示例:
POST /api/v3/voice/command
{
"text": "打开微信",
"device_id": "device_001",
"execute": true
}
"""
# 解析命令
result = await process_voice_command(request.text, request.device_id)
executed = False
if request.execute and result["success"] and request.device_id:
try:
for action in result["actions"]:
payload = _map_action_to_device(action)
command = {
"type": "execute",
"command_id": f"voice_{id(action)}",
"data": payload
}
await ws_hub.send_to_device(request.device_id, command)
if action.get("action") == "wait":
wait_time = action.get("params", {}).get("seconds", 1)
await asyncio.sleep(min(max(wait_time, 0.5), 10))
executed = True
except Exception as e:
logger.error(f"执行命令失败: {e}")
result["message"] += f" (执行失败: {str(e)})"
return VoiceCommandResponse(
success=result["success"],
message=result["message"],
text=result["text"],
actions=result["actions"],
executed=executed
)
@router.post("/parse")
async def parse_command(request: VoiceCommandRequest):
"""
仅解析命令,不执行
用于预览AI解析结果
"""
result = await process_voice_command(request.text, request.device_id)
return result
@router.websocket("/ws")
async def voice_websocket(websocket: WebSocket):
"""
语音命令WebSocket
用于PC端实时语音控制
消息格式:
发送: {"text": "打开微信", "device_id": "xxx"}
接收: {"success": true, "actions": [...], "executed": true}
"""
await websocket.accept()
logger.info("PC语音控制连接已建立")
try:
while True:
# 接收消息
data = await websocket.receive_text()
message = json.loads(data)
text = message.get("text", "")
device_id = message.get("device_id")
execute = message.get("execute", True)
if not text:
await websocket.send_json({
"success": False,
"message": "请提供语音文本"
})
continue
# 处理命令
result = await process_voice_command(text, device_id)
executed = False
if execute and result["success"] and device_id:
try:
for action in result["actions"]:
payload = _map_action_to_device(action)
command = {
"type": "execute",
"command_id": f"voice_ws_{id(action)}",
"data": payload
}
await ws_hub.send_to_device(device_id, command)
if action.get("action") == "wait":
wait_time = action.get("params", {}).get("seconds", 1)
await asyncio.sleep(min(max(wait_time, 0.5), 10))
executed = True
except Exception as e:
logger.error(f"执行失败: {e}")
result["message"] += f" (执行失败: {str(e)})"
# 返回结果
result["executed"] = executed
await websocket.send_json(result)
except WebSocketDisconnect:
logger.info("PC语音控制连接断开")
except Exception as e:
logger.error(f"WebSocket错误: {e}")
@router.get("/apps")
async def get_common_apps():
"""
获取常用应用列表
用于语音识别提示
"""
return {
"apps": [
{"name": "微信", "package": "com.tencent.mm"},
{"name": "抖音", "package": "com.ss.android.ugc.aweme"},
{"name": "支付宝", "package": "com.eg.android.AlipayGphone"},
{"name": "淘宝", "package": "com.taobao.taobao"},
{"name": "微博", "package": "com.sina.weibo"},
{"name": "QQ", "package": "com.tencent.mobileqq"},
{"name": "设置", "package": "com.android.settings"},
{"name": "相机", "package": "com.android.camera"},
{"name": "浏览器", "package": "com.android.chrome"},
],
"commands": [
"打开微信",
"返回",
"回到桌面",
"向上滑动",
"向下滑动",
"截图",
"发送消息给xxx",
"搜索xxx",
]
}