feat: 完成 OmniHuman 主体识别完整功能

新增功能:
-  添加 RealmanAvatarPictureCreateRoleOmniGetResult API 查询任务结果
-  实现完整的提交任务 + 轮询查询结果流程
-  智能轮询机制:最多30次,每2秒一次,自动检测任务完成状态
-  完善的进度反馈:上传(10-70%) + 识别(70-80%) + 轮询(80-95%) + 完成(100%)
-  任务ID跟踪和显示,便于用户了解处理状态

技术实现:
- 后端: 新增查询结果方法和 Tauri 命令
- 前端: 实现轮询逻辑和状态管理
- 错误处理: 区分上传失败、识别失败、查询超时等不同场景
- 用户体验: 实时进度显示和详细状态反馈

API 集成:
- 提交任务: RealmanAvatarPictureCreateRoleOmniSubmitTask
- 查询结果: RealmanAvatarPictureCreateRoleOmniGetResult
- 完整流程: 图片上传  任务提交  轮询查询  结果展示

现在用户可以完整体验从图片上传到最终结果的全流程!
This commit is contained in:
imeepos 2025-08-05 18:45:24 +08:00
parent d58499b564
commit 1a97d54450
5 changed files with 176 additions and 8 deletions

View File

@ -92,6 +92,15 @@ pub struct RealmanAvatarPictureCreateRoleOmniRequest {
pub image_url: String,
}
/// OmniHuman 主体识别查询结果请求参数
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RealmanAvatarPictureCreateRoleOmniGetResultRequest {
/// 服务标识,固定值
pub req_key: String,
/// 任务ID
pub task_id: String,
}
/// 火山云 API 响应元数据
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VolcanoResponseMetadata {
@ -601,6 +610,62 @@ impl VolcanoVideoService {
Ok(api_response)
}
/// OmniHuman 主体识别 - 查询任务结果
/// 用于查询已提交的主体识别任务的处理结果
pub async fn realman_avatar_picture_create_role_omni_get_result(
&self,
task_id: String
) -> Result<RealmanAvatarPictureCreateRoleOmniResponse> {
info!("开始查询 OmniHuman 主体识别任务结果: {}", task_id);
// 构建请求参数
let request_body = RealmanAvatarPictureCreateRoleOmniGetResultRequest {
req_key: "realman_avatar_picture_create_role_omni".to_string(),
task_id,
};
// 火山云API调用 - OmniHuman 主体识别查询结果
let api_url = "https://visual.volcengineapi.com?Action=RealmanAvatarPictureCreateRoleOmniGetResult&Version=2024-06-06";
// 构建认证头
let now = Utc::now();
let timestamp = now.format("%Y%m%dT%H%M%SZ").to_string();
let date = now.format("%Y%m%d").to_string();
let auth_header = self.build_auth_header("POST", api_url, &timestamp, &date, &request_body)?;
info!("调用 OmniHuman 主体识别查询结果 API: {:?}", request_body);
let response = self.http_client
.post(api_url)
.header("Authorization", auth_header)
.header("Content-Type", "application/json")
.header("X-Date", timestamp)
.json(&request_body)
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(anyhow!("OmniHuman 主体识别查询结果 API 调用失败: {} - {}", status, error_text));
}
// 先获取响应文本进行调试
let response_text = response.text().await?;
info!("OmniHuman 查询结果 API 原始响应: {}", response_text);
// 尝试解析响应
let api_response: RealmanAvatarPictureCreateRoleOmniResponse = serde_json::from_str(&response_text)
.map_err(|e| anyhow!("解析查询结果响应失败: {} - 响应内容: {}", e, response_text))?;
if api_response.result.code != 10000 {
return Err(anyhow!("OmniHuman 主体识别查询结果 API 返回错误: {} - {}", api_response.result.code, api_response.result.message));
}
info!("OmniHuman 主体识别任务结果查询成功: {:?}", api_response);
Ok(api_response)
}
/// 批量删除视频生成记录
pub async fn batch_delete_video_generations(&self, ids: Vec<String>) -> Result<()> {
for id in ids {

View File

@ -503,6 +503,7 @@ pub fn run() {
commands::volcano_video_commands::batch_download_volcano_videos,
commands::volcano_video_commands::get_video_stream_base64,
commands::volcano_video_commands::realman_avatar_picture_create_role_omni_submit_task,
commands::volcano_video_commands::realman_avatar_picture_create_role_omni_get_result,
// 图像编辑命令
commands::image_editing_commands::set_image_editing_config,
commands::image_editing_commands::set_image_editing_api_key,

View File

@ -480,3 +480,40 @@ pub async fn realman_avatar_picture_create_role_omni_submit_task(
}
}
}
/// OmniHuman 主体识别 - 查询任务结果
/// 用于查询已提交的主体识别任务的处理结果
#[tauri::command]
pub async fn realman_avatar_picture_create_role_omni_get_result(
state: State<'_, AppState>,
task_id: String,
) -> Result<RealmanAvatarPictureCreateRoleOmniResponse, String> {
info!("开始查询 OmniHuman 主体识别任务结果: {}", task_id);
// 获取数据库连接
let database = {
let database_guard = state
.database
.lock()
.map_err(|e| format!("获取数据库失败: {}", e))?;
database_guard
.as_ref()
.ok_or("数据库未初始化")?
.clone()
};
// 创建服务实例
let service = VolcanoVideoService::new(database);
// 调用 OmniHuman 主体识别查询结果
match service.realman_avatar_picture_create_role_omni_get_result(task_id).await {
Ok(response) => {
info!("OmniHuman 主体识别任务结果查询成功: {:?}", response);
Ok(response)
}
Err(e) => {
error!("OmniHuman 主体识别任务结果查询失败: {}", e);
Err(e.to_string())
}
}
}

View File

@ -20,11 +20,46 @@ const OmniHumanDetectionTool: React.FC = () => {
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 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?.status === 'completed' || result.Result.data?.image_urls?.length) {
// 任务完成,返回结果
return result;
} else if (result.Result.data?.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 {
@ -85,16 +120,27 @@ const OmniHumanDetectionTool: React.FC = () => {
setUploadProgress(70);
// 2. 使用云端 URL 调用识别 API
const response = await videoGenerationService.realmanAvatarPictureCreateRoleOmniSubmitTask(uploadResult.url);
const submitResponse = await videoGenerationService.realmanAvatarPictureCreateRoleOmniSubmitTask(uploadResult.url);
setUploadProgress(100);
setResult(response);
setUploadProgress(80);
if (response.Result.code === 10000) {
success('主体识别任务提交成功');
if (submitResponse.Result.code === 10000) {
const taskId = submitResponse.Result.data?.task_id;
if (taskId) {
setCurrentTaskId(taskId);
success('主体识别任务提交成功,正在处理中...');
// 3. 轮询查询结果
const finalResult = await pollForResult(taskId);
setUploadProgress(100);
setResult(finalResult);
success('主体识别完成!');
} else {
throw new Error('未获取到任务ID');
}
} else {
setErrorMessage(`识别失败: ${response.Result.message}`);
error(`识别失败: ${response.Result.message}`);
setErrorMessage(`识别失败: ${submitResponse.Result.message}`);
error(`识别失败: ${submitResponse.Result.message}`);
}
} catch (err) {
const errMsg = err instanceof Error ? err.message : '未知错误';
@ -111,6 +157,8 @@ const OmniHumanDetectionTool: React.FC = () => {
setImagePreview('');
setResult(null);
setErrorMessage('');
setCurrentTaskId('');
setUploadProgress(0);
}, []);
return (
@ -221,7 +269,7 @@ const OmniHumanDetectionTool: React.FC = () => {
<span className="text-green-800 font-medium"></span>
</div>
<div className="text-sm text-green-700">
<p>ID: {result.Result.data?.task_id}</p>
<p>ID: {result.Result.data?.task_id || currentTaskId}</p>
<p>ID: {result.Result.request_id}</p>
<p>: {result.Result.time_elapsed}</p>
</div>

View File

@ -282,6 +282,23 @@ class VideoGenerationService implements VideoGenerationAPI {
throw new Error(`OmniHuman 主体识别任务失败: ${error}`);
}
}
/**
* OmniHuman -
*
*/
async realmanAvatarPictureCreateRoleOmniGetResult(taskId: string): Promise<RealmanAvatarPictureCreateRoleOmniResponse> {
try {
const response = await invoke<RealmanAvatarPictureCreateRoleOmniResponse>(
'realman_avatar_picture_create_role_omni_get_result',
{ taskId }
);
return response;
} catch (error) {
console.error('OmniHuman 主体识别结果查询失败:', error);
throw new Error(`OmniHuman 主体识别结果查询失败: ${error}`);
}
}
}
// 导出单例实例