mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-26 00:53:54 +08:00
add:测试页面增加拍照识物功能
This commit is contained in:
@@ -6,12 +6,25 @@ import { initMcpTools } from './core/mcp/tools.js?v=0127';
|
||||
import { uiController } from './ui/controller.js?v=0127';
|
||||
import { log } from './utils/logger.js?v=0127';
|
||||
|
||||
// 辅助函数:将Base64数据转换为Blob
|
||||
function dataURItoBlob(dataURI) {
|
||||
const byteString = atob(dataURI.split(',')[1]);
|
||||
const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
|
||||
const ab = new ArrayBuffer(byteString.length);
|
||||
const ia = new Uint8Array(ab);
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
ia[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
return new Blob([ab], { type: mimeString });
|
||||
}
|
||||
|
||||
// 应用类
|
||||
class App {
|
||||
constructor() {
|
||||
this.uiController = null;
|
||||
this.audioPlayer = null;
|
||||
this.live2dManager = null;
|
||||
this.cameraStream = null;
|
||||
}
|
||||
|
||||
// 初始化应用
|
||||
@@ -33,6 +46,8 @@ class App {
|
||||
await this.checkMicrophoneAvailability();
|
||||
// 初始化Live2D
|
||||
await this.initLive2D();
|
||||
// 初始化摄像头
|
||||
this.initCamera();
|
||||
// 关闭加载loading
|
||||
this.setModelLoadingStatus(false);
|
||||
log('应用初始化完成', 'success');
|
||||
@@ -99,6 +114,180 @@ class App {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化摄像头
|
||||
async initCamera() {
|
||||
const cameraContainer = document.getElementById('cameraContainer');
|
||||
const cameraVideo = document.getElementById('cameraVideo');
|
||||
|
||||
if (!cameraContainer || !cameraVideo) {
|
||||
log('摄像头元素未找到,跳过初始化', 'warning');
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
let isDragging = false;
|
||||
let currentX, currentY, initialX, initialY;
|
||||
let xOffset = 0, yOffset = 0;
|
||||
|
||||
cameraContainer.addEventListener('mousedown', dragStart);
|
||||
document.addEventListener('mousemove', drag);
|
||||
document.addEventListener('mouseup', dragEnd);
|
||||
cameraContainer.addEventListener('touchstart', dragStart, { passive: false });
|
||||
document.addEventListener('touchmove', drag, { passive: false });
|
||||
document.addEventListener('touchend', dragEnd);
|
||||
|
||||
function dragStart(e) {
|
||||
if (e.type === 'touchstart') {
|
||||
initialX = e.touches[0].clientX - xOffset;
|
||||
initialY = e.touches[0].clientY - yOffset;
|
||||
} else {
|
||||
initialX = e.clientX - xOffset;
|
||||
initialY = e.clientY - yOffset;
|
||||
}
|
||||
isDragging = true;
|
||||
cameraContainer.classList.add('dragging');
|
||||
}
|
||||
|
||||
function drag(e) {
|
||||
if (isDragging) {
|
||||
e.preventDefault();
|
||||
if (e.type === 'touchmove') {
|
||||
currentX = e.touches[0].clientX - initialX;
|
||||
currentY = e.touches[0].clientY - initialY;
|
||||
} else {
|
||||
currentX = e.clientX - initialX;
|
||||
currentY = e.clientY - initialY;
|
||||
}
|
||||
xOffset = currentX;
|
||||
yOffset = currentY;
|
||||
cameraContainer.style.transform = `translate3d(${currentX}px, ${currentY}px, 0)`;
|
||||
}
|
||||
}
|
||||
|
||||
function dragEnd() {
|
||||
initialX = currentX;
|
||||
initialY = currentY;
|
||||
isDragging = false;
|
||||
cameraContainer.classList.remove('dragging');
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
window.startCamera = async () => {
|
||||
try {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
log('浏览器不支持摄像头API', 'warning');
|
||||
return false;
|
||||
}
|
||||
log('正在请求摄像头权限...', 'info');
|
||||
this.cameraStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { width: 320, height: 240, facingMode: 'user' },
|
||||
audio: false
|
||||
});
|
||||
cameraVideo.srcObject = this.cameraStream;
|
||||
cameraContainer.classList.add('active');
|
||||
log('摄像头已启动', 'success');
|
||||
return true;
|
||||
} catch (error) {
|
||||
log(`启动摄像头失败: ${error.name} - ${error.message}`, 'error');
|
||||
if (error.name === 'NotAllowedError') {
|
||||
log('摄像头权限被拒绝,请检查浏览器设置', 'warning');
|
||||
} else if (error.name === 'NotFoundError') {
|
||||
log('未找到摄像头设备', 'warning');
|
||||
} else if (error.name === 'NotReadableError') {
|
||||
log('摄像头已被其他程序占用', 'warning');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
window.stopCamera = () => {
|
||||
if (this.cameraStream) {
|
||||
this.cameraStream.getTracks().forEach(track => track.stop());
|
||||
this.cameraStream = null;
|
||||
cameraVideo.srcObject = null;
|
||||
log('摄像头已关闭', 'info');
|
||||
}
|
||||
};
|
||||
|
||||
window.takePhoto = (question = '描述一下看到的物品') => {
|
||||
return new Promise(async (resolve) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const video = cameraVideo;
|
||||
|
||||
if (!video || video.readyState !== video.HAVE_ENOUGH_DATA) {
|
||||
log('无法拍照:摄像头未就绪', 'warning');
|
||||
resolve({
|
||||
success: false,
|
||||
error: '摄像头未就绪,请确保已连接且摄像头已启动'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.width = video.videoWidth || 320;
|
||||
canvas.height = video.videoHeight || 240;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
const photoData = canvas.toDataURL('image/jpeg', 0.8);
|
||||
log(`拍照成功,图像数据长度: ${photoData.length}`, 'success');
|
||||
|
||||
try {
|
||||
const visionApiUrl = 'http://localhost:8003/mcp/vision/explain';
|
||||
log(`正在发送图片到视觉分析接口: ${visionApiUrl}`, 'info');
|
||||
|
||||
const deviceId = document.getElementById('deviceMac')?.value || '';
|
||||
const clientId = document.getElementById('clientId')?.value || 'web_test_client';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('question', question);
|
||||
formData.append('image', dataURItoBlob(photoData), 'photo.jpg');
|
||||
|
||||
const response = await fetch(visionApiUrl, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'Device-Id': deviceId,
|
||||
'Client-Id': clientId
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const analysisResult = await response.json();
|
||||
log(`视觉分析完成: ${JSON.stringify(analysisResult).substring(0, 200)}...`, 'success');
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
message: question,
|
||||
photo_data: photoData,
|
||||
photo_width: canvas.width,
|
||||
photo_height: canvas.height,
|
||||
vision_analysis: analysisResult
|
||||
});
|
||||
} catch (error) {
|
||||
log(`视觉分析失败: ${error.message}`, 'error');
|
||||
resolve({
|
||||
success: true,
|
||||
message: question,
|
||||
photo_data: photoData,
|
||||
photo_width: canvas.width,
|
||||
photo_height: canvas.height,
|
||||
vision_analysis: {
|
||||
success: false,
|
||||
error: error.message,
|
||||
fallback: '无法连接到视觉分析服务'
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
log('摄像头初始化完成', 'success');
|
||||
resolve(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 创建并启动应用
|
||||
|
||||
Reference in New Issue
Block a user