mixvideo-v2/apps/desktop/src/pages/tools/OmniHumanDetectionTool.tsx

347 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useCallback } from 'react';
import {
User,
Upload,
CheckCircle,
XCircle,
Loader2,
Image as ImageIcon,
AlertCircle,
Info
} from 'lucide-react';
import { open } from '@tauri-apps/plugin-dialog';
import { useNotifications } from '../../components/NotificationSystem';
import videoGenerationService from '../../services/videoGenerationService';
import fileUploadService from '../../services/fileUploadService';
import {
RealmanAvatarPictureCreateRoleOmniResponse,
RealmanAvatarPictureCreateRoleOmniResultData,
RealmanAvatarPictureCreateRoleOmniSubmitData
} from '../../types/videoGeneration';
const OmniHumanDetectionTool: React.FC = () => {
const [selectedImage, setSelectedImage] = useState<string>('');
const [imagePreview, setImagePreview] = useState<string>('');
const [isProcessing, setIsProcessing] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [currentTaskId, setCurrentTaskId] = useState<string>('');
const [result, setResult] = useState<RealmanAvatarPictureCreateRoleOmniResponse | null>(null);
const [errorMessage, setErrorMessage] = useState<string>('');
const { addNotification, success, error } = useNotifications();
// 类型守卫:检查是否为查询结果数据
const isResultData = (data: any): data is RealmanAvatarPictureCreateRoleOmniResultData => {
return data && typeof data === 'object' && ('status' in data || 'image_urls' in data || 'resp_data' in data);
};
// 类型守卫:检查是否为提交任务数据
const isSubmitData = (data: any): data is RealmanAvatarPictureCreateRoleOmniSubmitData => {
return data && typeof data === 'object' && 'task_id' in data;
};
// 轮询查询结果
const pollForResult = useCallback(async (taskId: string): Promise<RealmanAvatarPictureCreateRoleOmniResponse> => {
const maxAttempts = 30; // 最多轮询30次
const pollInterval = 2000; // 每2秒轮询一次
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const result = await videoGenerationService.realmanAvatarPictureCreateRoleOmniGetResult(taskId);
// 检查任务状态
if (result.Result.data && isResultData(result.Result.data)) {
const resultData = result.Result.data;
if (resultData.status === 'done' || resultData.image_urls?.length) {
// 任务完成,返回结果
return result;
} else if (resultData.status === 'failed') {
// 任务失败
throw new Error('任务处理失败');
}
}
// 任务还在处理中,继续轮询
setUploadProgress(80 + (attempt / maxAttempts) * 15); // 80-95% 的进度用于轮询
await new Promise(resolve => setTimeout(resolve, pollInterval));
} catch (err) {
if (attempt === maxAttempts) {
throw err;
}
// 继续重试
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
}
throw new Error('任务处理超时,请稍后手动查询结果');
}, []);
// 选择图片文件
const handleSelectImage = useCallback(async () => {
try {
const selected = await open({
multiple: false,
filters: [
{
name: 'Images',
extensions: ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp']
}
]
});
if (selected) {
const imagePath = Array.isArray(selected) ? selected[0] : selected;
setSelectedImage(imagePath);
// 创建预览URL
const previewUrl = `file://${imagePath}`;
setImagePreview(previewUrl);
setResult(null);
setErrorMessage('');
}
} catch (err) {
console.error('选择图片失败:', err);
error('选择图片失败');
}
}, [addNotification]);
// 提交主体识别任务
const handleSubmitDetection = useCallback(async () => {
if (!selectedImage) {
addNotification({ type: 'warning', title: '请先选择图片' });
return;
}
setIsProcessing(true);
setErrorMessage('');
setResult(null);
setUploadProgress(0);
try {
// 1. 先上传图片到云端
setUploadProgress(10);
const uploadResult = await fileUploadService.uploadFileToCloud(
selectedImage,
undefined,
(progress) => {
setUploadProgress(10 + progress * 0.6); // 上传占 60% 进度
}
);
if (uploadResult.status !== 'success' || !uploadResult.url) {
throw new Error(uploadResult.error || '图片上传失败');
}
setUploadProgress(70);
// 2. 使用云端 URL 调用识别 API
const submitResponse = await videoGenerationService.realmanAvatarPictureCreateRoleOmniSubmitTask(uploadResult.url);
setUploadProgress(80);
if (submitResponse.Result.code === 10000) {
const data = submitResponse.Result.data;
const taskId = data && isSubmitData(data) ? data.task_id : undefined;
if (taskId) {
setCurrentTaskId(taskId);
success('主体识别任务提交成功,正在处理中...');
// 3. 轮询查询结果
const finalResult = await pollForResult(taskId);
setUploadProgress(100);
setResult(finalResult);
success('主体识别完成!');
} else {
throw new Error('未获取到任务ID');
}
} else {
setErrorMessage(`识别失败: ${submitResponse.Result.message}`);
error(`识别失败: ${submitResponse.Result.message}`);
}
} catch (err) {
const errMsg = err instanceof Error ? err.message : '未知错误';
setErrorMessage(errMsg);
error(`主体识别失败: ${errMsg}`);
} finally {
setIsProcessing(false);
}
}, [selectedImage, success, error]);
// 清除结果
const handleClear = useCallback(() => {
setSelectedImage('');
setImagePreview('');
setResult(null);
setErrorMessage('');
setCurrentTaskId('');
setUploadProgress(0);
}, []);
return (
<div className="p-6 max-w-4xl mx-auto">
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
{/* 标题 */}
<div className="border-b border-gray-200 p-6">
<div className="flex items-center gap-3">
<User className="w-6 h-6 text-blue-600" />
<div>
<h1 className="text-2xl font-bold text-gray-900">OmniHuman </h1>
<p className="text-gray-600 mt-1"></p>
</div>
</div>
</div>
{/* 功能说明 */}
<div className="p-6 bg-blue-50 border-b border-gray-200">
<div className="flex items-start gap-3">
<Info className="w-5 h-5 text-blue-600 mt-0.5" />
<div className="text-sm text-blue-800">
<p className="font-medium mb-2"></p>
<ul className="list-disc list-inside space-y-1">
<li></li>
<li></li>
<li>PNGJPGJPEGGIFBMPWebP</li>
</ul>
</div>
</div>
</div>
{/* 图片选择区域 */}
<div className="p-6 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900 mb-4"></h3>
<div className="flex gap-4">
<button
onClick={handleSelectImage}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Upload className="w-4 h-4" />
</button>
{selectedImage && (
<button
onClick={handleClear}
className="flex items-center gap-2 px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
>
</button>
)}
</div>
{/* 图片预览 */}
{imagePreview && (
<div className="mt-4">
<div className="flex items-center gap-2 mb-2">
<ImageIcon className="w-4 h-4 text-gray-600" />
<span className="text-sm text-gray-600"></span>
</div>
<div className="border border-gray-200 rounded-lg p-4 bg-gray-50">
<img
src={imagePreview}
alt="预览图片"
className="max-w-full max-h-64 object-contain mx-auto rounded"
/>
</div>
</div>
)}
</div>
{/* 操作按钮 */}
<div className="p-6 border-b border-gray-200">
<button
onClick={handleSubmitDetection}
disabled={!selectedImage || isProcessing}
className="flex items-center gap-2 px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
{isProcessing ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<User className="w-4 h-4" />
)}
{isProcessing ? `处理中... ${uploadProgress}%` : '开始识别'}
</button>
</div>
{/* 结果显示 */}
<div className="p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4"></h3>
{errorMessage && (
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-center gap-2">
<XCircle className="w-5 h-5 text-red-600" />
<span className="text-red-800 font-medium"></span>
</div>
<p className="text-red-700 mt-2">{errorMessage}</p>
</div>
)}
{result && (
<div className="space-y-4">
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<CheckCircle className="w-5 h-5 text-green-600" />
<span className="text-green-800 font-medium"></span>
</div>
<div className="text-sm text-green-700">
<p>ID: {currentTaskId || (result.Result.data && isSubmitData(result.Result.data) ? result.Result.data.task_id : '未知')}</p>
<p>ID: {result.Result.request_id}</p>
<p>: {result.Result.time_elapsed}</p>
</div>
</div>
{result.Result.data && isResultData(result.Result.data) && result.Result.data.image_urls && result.Result.data.image_urls.length > 0 && (
<div>
<h4 className="font-medium text-gray-900 mb-2"></h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{result.Result.data.image_urls.map((url: string, index: number) => (
<div key={index} className="border border-gray-200 rounded-lg p-4 bg-gray-50">
<img
src={url}
alt={`处理结果 ${index + 1}`}
className="max-w-full max-h-64 object-contain mx-auto rounded"
/>
<div className="mt-2 text-center">
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 text-sm"
>
</a>
</div>
</div>
))}
</div>
</div>
)}
{result.Result.data && isResultData(result.Result.data) && result.Result.data.resp_data && (
<div>
<h4 className="font-medium text-gray-900 mb-2"></h4>
<div className="p-3 bg-gray-50 border border-gray-200 rounded text-sm font-mono">
{result.Result.data.resp_data}
</div>
</div>
)}
</div>
)}
{!result && !errorMessage && !isProcessing && (
<div className="text-center py-8 text-gray-500">
<AlertCircle className="w-12 h-12 mx-auto mb-3 text-gray-400" />
<p></p>
</div>
)}
</div>
</div>
</div>
);
};
export default OmniHumanDetectionTool;