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

211 lines
6.8 KiB
Python

"""
语音命令路由
处理来自APP和PC端的语音指令
"""
from __future__ import annotations
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",
]
}