Files
workphone-sdk/sdk/app/static/voice_control.html

456 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>工作手机 - AI语音控制</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
min-height: 100vh;
color: #fff;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
}
h1 {
text-align: center;
margin-bottom: 10px;
font-size: 2rem;
}
.subtitle {
text-align: center;
color: #888;
margin-bottom: 30px;
}
.card {
background: rgba(255,255,255,0.1);
border-radius: 16px;
padding: 24px;
margin-bottom: 20px;
backdrop-filter: blur(10px);
}
.device-selector {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.device-selector input {
flex: 1;
padding: 12px 16px;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 8px;
background: rgba(0,0,0,0.3);
color: #fff;
font-size: 16px;
}
.device-selector button {
padding: 12px 24px;
background: #4CAF50;
border: none;
border-radius: 8px;
color: #fff;
cursor: pointer;
font-size: 16px;
}
.status {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #f44336;
}
.status-dot.connected {
background: #4CAF50;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.voice-area {
text-align: center;
padding: 40px;
}
.mic-button {
width: 120px;
height: 120px;
border-radius: 50%;
background: linear-gradient(135deg, #FF5722, #FF9800);
border: none;
font-size: 48px;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 10px 40px rgba(255,87,34,0.4);
}
.mic-button:hover {
transform: scale(1.1);
}
.mic-button.listening {
background: linear-gradient(135deg, #f44336, #e91e63);
animation: listening 0.5s ease-in-out infinite alternate;
}
@keyframes listening {
from { transform: scale(1); box-shadow: 0 10px 40px rgba(244,67,54,0.4); }
to { transform: scale(1.05); box-shadow: 0 15px 60px rgba(244,67,54,0.6); }
}
.voice-text {
margin-top: 20px;
font-size: 18px;
color: #B0BEC5;
}
.result {
margin-top: 20px;
padding: 16px;
background: rgba(0,0,0,0.3);
border-radius: 8px;
text-align: left;
}
.result h3 {
margin-bottom: 10px;
color: #4CAF50;
}
.action-list {
list-style: none;
}
.action-list li {
padding: 8px 0;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.action-list li:last-child {
border-bottom: none;
}
.log {
max-height: 300px;
overflow-y: auto;
font-family: monospace;
font-size: 14px;
background: rgba(0,0,0,0.5);
padding: 16px;
border-radius: 8px;
}
.log-entry {
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.log-time {
color: #888;
font-size: 12px;
}
.log-text {
color: #fff;
}
.log-success {
color: #4CAF50;
}
.log-error {
color: #f44336;
}
.text-input-area {
display: flex;
gap: 10px;
margin-top: 20px;
}
.text-input-area input {
flex: 1;
padding: 16px;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 8px;
background: rgba(0,0,0,0.3);
color: #fff;
font-size: 18px;
}
.text-input-area button {
padding: 16px 32px;
background: #2196F3;
border: none;
border-radius: 8px;
color: #fff;
cursor: pointer;
font-size: 16px;
}
.hint {
margin-top: 20px;
color: #888;
font-size: 14px;
}
.hint code {
background: rgba(255,255,255,0.1);
padding: 2px 6px;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="container">
<h1>🎙️ AI语音控制</h1>
<p class="subtitle">用语音控制你的工作手机</p>
<div class="card">
<div class="device-selector">
<input type="text" id="deviceId" placeholder="输入设备ID (如: device_001)">
<button onclick="connectDevice()">连接设备</button>
</div>
<div class="status">
<div class="status-dot" id="statusDot"></div>
<span id="statusText">未连接</span>
</div>
</div>
<div class="card">
<div class="voice-area">
<button class="mic-button" id="micButton" onclick="toggleVoice()">🎤</button>
<p class="voice-text" id="voiceText">点击麦克风开始语音输入</p>
</div>
<div class="text-input-area">
<input type="text" id="textInput" placeholder="或者直接输入命令..." onkeypress="handleKeyPress(event)">
<button onclick="sendTextCommand()">发送</button>
</div>
<div class="hint">
<p>试试说:<code>打开微信</code> <code>返回</code> <code>向上滑动</code> <code>截图</code></p>
</div>
</div>
<div class="card">
<h3 style="margin-bottom: 16px;">📋 执行日志</h3>
<div class="log" id="log">
<div class="log-entry">
<span class="log-time">等待连接...</span>
</div>
</div>
</div>
</div>
<script>
let ws = null;
let deviceId = '';
let recognition = null;
let isListening = false;
// 初始化语音识别
if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
recognition = new SpeechRecognition();
recognition.continuous = false;
recognition.interimResults = true;
recognition.lang = 'zh-CN';
recognition.onstart = function() {
isListening = true;
document.getElementById('micButton').classList.add('listening');
document.getElementById('voiceText').textContent = '正在听取...';
};
recognition.onresult = function(event) {
let finalText = '';
let interimText = '';
for (let i = 0; i < event.results.length; i++) {
if (event.results[i].isFinal) {
finalText += event.results[i][0].transcript;
} else {
interimText += event.results[i][0].transcript;
}
}
if (interimText) {
document.getElementById('voiceText').textContent = '识别中: ' + interimText;
}
if (finalText) {
document.getElementById('voiceText').textContent = '"' + finalText + '"';
document.getElementById('textInput').value = finalText;
sendCommand(finalText);
}
};
recognition.onerror = function(event) {
console.error('语音识别错误:', event.error);
addLog('语音识别错误: ' + event.error, 'error');
stopListening();
};
recognition.onend = function() {
stopListening();
};
}
function toggleVoice() {
if (!recognition) {
addLog('浏览器不支持语音识别', 'error');
return;
}
if (isListening) {
recognition.stop();
} else {
recognition.start();
}
}
function stopListening() {
isListening = false;
document.getElementById('micButton').classList.remove('listening');
document.getElementById('voiceText').textContent = '点击麦克风开始语音输入';
}
function connectDevice() {
deviceId = document.getElementById('deviceId').value.trim();
if (!deviceId) {
alert('请输入设备ID');
return;
}
// 连接WebSocket
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host;
ws = new WebSocket(`${protocol}//${host}/api/v3/voice/ws`);
ws.onopen = function() {
document.getElementById('statusDot').classList.add('connected');
document.getElementById('statusText').textContent = '已连接 - ' + deviceId;
addLog('已连接到服务器', 'success');
};
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
handleResponse(data);
};
ws.onclose = function() {
document.getElementById('statusDot').classList.remove('connected');
document.getElementById('statusText').textContent = '已断开';
addLog('连接已断开', 'error');
};
ws.onerror = function(error) {
console.error('WebSocket错误:', error);
addLog('连接错误', 'error');
};
}
function sendCommand(text) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
// 使用HTTP API
fetch('/api/v3/voice/command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: text,
device_id: deviceId,
execute: true
})
})
.then(res => res.json())
.then(data => handleResponse(data))
.catch(err => addLog('发送失败: ' + err.message, 'error'));
return;
}
ws.send(JSON.stringify({
text: text,
device_id: deviceId,
execute: true
}));
addLog('发送命令: ' + text);
}
function sendTextCommand() {
const text = document.getElementById('textInput').value.trim();
if (!text) return;
sendCommand(text);
document.getElementById('textInput').value = '';
}
function handleKeyPress(event) {
if (event.key === 'Enter') {
sendTextCommand();
}
}
function handleResponse(data) {
if (data.success) {
let msg = data.message;
if (data.actions && data.actions.length > 0) {
msg += '\n操作: ' + data.actions.map(a => a.description || a.action).join(' → ');
}
if (data.executed) {
msg += ' [已执行]';
}
addLog(msg, 'success');
} else {
addLog(data.message, 'error');
}
}
function addLog(message, type = '') {
const log = document.getElementById('log');
const time = new Date().toLocaleTimeString();
const className = type === 'success' ? 'log-success' : (type === 'error' ? 'log-error' : 'log-text');
const entry = document.createElement('div');
entry.className = 'log-entry';
entry.innerHTML = `
<span class="log-time">${time}</span>
<span class="${className}">${message}</span>
`;
log.insertBefore(entry, log.firstChild);
// 限制日志数量
while (log.children.length > 50) {
log.removeChild(log.lastChild);
}
}
</script>
</body>
</html>