456 lines
14 KiB
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>
|