feat: 添加AI模型面部头发修复工具

- 新增AI模型面部头发修复工具,支持单张图片和批量处理
- 基于ComfyUI的AI_MODEL_FACE_HAIR_FIX_TEMPLATE模板
- 支持自定义面部提示词和去噪强度参数
- 实现实时进度监听和结果展示
- 添加文件选择和路径管理功能
- 修复多个TypeScript编译错误
- 优化UI组件的类型定义和错误处理

新增功能:
- ai_model_face_hair_fix_single_image: 单张图片处理命令
- ai_model_face_hair_fix_batch_images: 批量图片处理命令
- AiModelFaceHairFixTool: 完整的前端UI组件

修复问题:
- ExecutionMonitor组件的showCompleted状态管理
- WorkflowManager的类型注解问题
- WorkflowV2Creator的变量名和状态引用
- Input组件的size属性类型冲突
- comfyuiV2Service缺失的updateTemplate方法
This commit is contained in:
imeepos
2025-08-11 00:52:21 +08:00
parent a5d425c6f2
commit b1e7191c10
15 changed files with 1112 additions and 588 deletions

View File

@@ -302,6 +302,8 @@ pub fn run() {
commands::debug_commands::validate_template_structure,
// 便捷工具命令
commands::tools_commands::clean_jsonl_data,
commands::tools_commands::ai_model_face_hair_fix_single_image,
commands::tools_commands::ai_model_face_hair_fix_batch_images,
// 服装搭配搜索命令
commands::outfit_search_commands::analyze_outfit_image,
commands::outfit_search_commands::search_similar_outfits,

View File

@@ -1,9 +1,18 @@
use tauri::{command, Emitter};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::collections::{HashSet, HashMap};
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::fs;
use anyhow::Result;
use comfyui_sdk::{
ComfyUIClient, ComfyUIClientConfig, ExecutionOptions,
AI_MODEL_FACE_HAIR_FIX_TEMPLATE
};
use comfyui_sdk::utils::SimpleCallbacks;
use serde_json::json;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataCleaningProgress {
@@ -220,3 +229,438 @@ fn count_lines(file_path: &str) -> Result<usize, String> {
let count = reader.lines().count();
Ok(count)
}
// ============================================================================
// 图片增强相关结构体和命令
// ============================================================================
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageEnhancementProgress {
pub current: usize,
pub total: usize,
pub percentage: f64,
pub status: String,
pub current_file: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageEnhancementResult {
pub success: bool,
pub message: String,
pub input_path: String,
pub output_path: Option<String>,
pub execution_time: f64,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchImageEnhancementResult {
pub success: bool,
pub message: String,
pub total_processed: usize,
pub successful_count: usize,
pub failed_count: usize,
pub results: Vec<ImageEnhancementResult>,
pub total_execution_time: f64,
}
/// 单张图片AI模型面部头发修复增强处理
#[command]
pub async fn ai_model_face_hair_fix_single_image(
input_image: String,
server_url: String,
face_prompt: String,
face_denoise: String,
output_image: String,
window: tauri::Window,
) -> Result<ImageEnhancementResult, String> {
let start_time = std::time::Instant::now();
// 发送开始处理的进度
let _ = window.emit("image-enhancement-progress", ImageEnhancementProgress {
current: 0,
total: 1,
percentage: 0.0,
status: "开始图片增强处理...".to_string(),
current_file: Some(input_image.clone()),
});
match ai_model_face_hair_fix_internal(&input_image, &server_url, &face_prompt, &face_denoise, &output_image, &window).await {
Ok(_) => {
let execution_time = start_time.elapsed().as_secs_f64();
let _ = window.emit("image-enhancement-progress", ImageEnhancementProgress {
current: 1,
total: 1,
percentage: 100.0,
status: "图片增强完成".to_string(),
current_file: Some(input_image.clone()),
});
Ok(ImageEnhancementResult {
success: true,
message: "图片增强成功".to_string(),
input_path: input_image,
output_path: Some(output_image),
execution_time,
error: None,
})
}
Err(e) => {
let execution_time = start_time.elapsed().as_secs_f64();
let _ = window.emit("image-enhancement-progress", ImageEnhancementProgress {
current: 1,
total: 1,
percentage: 100.0,
status: format!("图片增强失败: {}", e),
current_file: Some(input_image.clone()),
});
Ok(ImageEnhancementResult {
success: false,
message: format!("图片增强失败: {}", e),
input_path: input_image,
output_path: None,
execution_time,
error: Some(e),
})
}
}
}
/// 批量图片AI模型面部头发修复增强处理
#[command]
pub async fn ai_model_face_hair_fix_batch_images(
input_dir: String,
server_url: String,
face_prompt: String,
face_denoise: String,
output_dir: String,
window: tauri::Window,
) -> Result<BatchImageEnhancementResult, String> {
let start_time = std::time::Instant::now();
// 发送开始处理的进度
let _ = window.emit("image-enhancement-progress", ImageEnhancementProgress {
current: 0,
total: 0,
percentage: 0.0,
status: "扫描图片文件...".to_string(),
current_file: None,
});
// 扫描输入目录中的所有图片文件
let image_files = match scan_image_files(&input_dir).await {
Ok(files) => files,
Err(e) => return Err(format!("扫描图片文件失败: {}", e)),
};
if image_files.is_empty() {
return Ok(BatchImageEnhancementResult {
success: true,
message: "未找到图片文件".to_string(),
total_processed: 0,
successful_count: 0,
failed_count: 0,
results: vec![],
total_execution_time: start_time.elapsed().as_secs_f64(),
});
}
let total_files = image_files.len();
let mut results = Vec::new();
let mut successful_count = 0;
let mut failed_count = 0;
// 确保输出目录存在
if let Err(e) = fs::create_dir_all(&output_dir).await {
return Err(format!("创建输出目录失败: {}", e));
}
// 处理每个图片文件
for (index, (input_path, relative_path)) in image_files.iter().enumerate() {
let current = index + 1;
let percentage = (current as f64 / total_files as f64) * 100.0;
let _ = window.emit("image-enhancement-progress", ImageEnhancementProgress {
current,
total: total_files,
percentage,
status: format!("处理图片 {}/{}", current, total_files),
current_file: Some(input_path.clone()),
});
// 构建输出路径,保持目录结构
let output_path = Path::new(&output_dir).join(relative_path);
// 确保输出文件的目录存在
if let Some(parent) = output_path.parent() {
if let Err(e) = fs::create_dir_all(parent).await {
let error_msg = format!("创建输出目录失败: {}", e);
results.push(ImageEnhancementResult {
success: false,
message: error_msg.clone(),
input_path: input_path.clone(),
output_path: None,
execution_time: 0.0,
error: Some(error_msg),
});
failed_count += 1;
continue;
}
}
let output_path_str = output_path.to_string_lossy().to_string();
let file_start_time = std::time::Instant::now();
match ai_model_face_hair_fix_internal(input_path, &server_url, &face_prompt, &face_denoise, &output_path_str, &window).await {
Ok(_) => {
let execution_time = file_start_time.elapsed().as_secs_f64();
results.push(ImageEnhancementResult {
success: true,
message: "图片增强成功".to_string(),
input_path: input_path.clone(),
output_path: Some(output_path_str),
execution_time,
error: None,
});
successful_count += 1;
}
Err(e) => {
let execution_time = file_start_time.elapsed().as_secs_f64();
results.push(ImageEnhancementResult {
success: false,
message: format!("图片增强失败: {}", e),
input_path: input_path.clone(),
output_path: None,
execution_time,
error: Some(e),
});
failed_count += 1;
}
}
}
let total_execution_time = start_time.elapsed().as_secs_f64();
let _ = window.emit("image-enhancement-progress", ImageEnhancementProgress {
current: total_files,
total: total_files,
percentage: 100.0,
status: format!("批量处理完成: 成功 {}, 失败 {}", successful_count, failed_count),
current_file: None,
});
Ok(BatchImageEnhancementResult {
success: failed_count == 0,
message: format!("批量处理完成: 总计 {}, 成功 {}, 失败 {}", total_files, successful_count, failed_count),
total_processed: total_files,
successful_count,
failed_count,
results,
total_execution_time,
})
}
/// AI模型面部头发修复内部处理函数
async fn ai_model_face_hair_fix_internal(
input_image: &str,
server_url: &str,
face_prompt: &str,
face_denoise: &str,
output_image: &str,
window: &tauri::Window,
) -> Result<(), String> {
// 验证输入图片文件
if !Path::new(input_image).exists() {
return Err(format!("输入图片文件不存在: {}", input_image));
}
// 验证图片格式
validate_image_file(input_image)?;
// 初始化 ComfyUI 客户端
let mut client = ComfyUIClient::new(ComfyUIClientConfig {
base_url: server_url.to_string(),
..Default::default()
}).map_err(|e| format!("创建ComfyUI客户端失败: {}", e))?;
// 连接到服务器
client.connect().await
.map_err(|e| format!("连接ComfyUI服务器失败: {}", e))?;
// 注册模板
client.templates().register_from_data(AI_MODEL_FACE_HAIR_FIX_TEMPLATE.clone())
.map_err(|e| format!("注册模板失败: {}", e))?;
let template = client.templates_ref()
.get_by_id("ai-model-face-hair-fix")
.ok_or("模板未找到")?;
// 上传图片
let upload_response = client.upload_image(input_image, false).await
.map_err(|e| format!("上传图片失败: {}", e))?;
// 设置进度回调
let callbacks = Arc::new(
SimpleCallbacks::new()
.with_progress(|progress| {
let percentage = (progress.progress as f64 / progress.max as f64 * 100.0) as u32;
// 这里可以发送更详细的进度信息
})
.with_executing(|_node_id| {
// 节点执行回调
})
.with_error(|error| {
eprintln!("ComfyUI执行错误: {}", error.message);
})
);
// 设置参数
let mut parameters = HashMap::new();
parameters.insert("input_image".to_string(), json!(upload_response.name));
parameters.insert("face_prompt".to_string(), json!(face_prompt));
parameters.insert("face_denoise".to_string(), json!(face_denoise));
// 执行增强
let result = client.execute_template_with_callbacks(
template,
parameters,
ExecutionOptions {
timeout: Some(std::time::Duration::from_secs(180)),
priority: None,
},
callbacks,
).await.map_err(|e| format!("执行图片增强失败: {}", e))?;
if !result.success {
let error_msg = result.error
.map(|e| e.message)
.unwrap_or_else(|| "未知错误".to_string());
return Err(format!("图片增强失败: {}", error_msg));
}
// 获取输出图片URL并下载
if let Some(outputs) = &result.outputs {
let image_urls = client.outputs_to_urls(outputs);
if let Some(first_url) = image_urls.first() {
// 下载增强后的图片
download_image_from_url(first_url, output_image).await
.map_err(|e| format!("下载增强图片失败: {}", e))?;
} else {
return Err("未找到输出图片URL".to_string());
}
} else {
return Err("未找到输出结果".to_string());
}
// 断开连接
client.disconnect().await
.map_err(|e| format!("断开连接失败: {}", e))?;
Ok(())
}
/// 扫描目录中的所有图片文件
async fn scan_image_files(dir: &str) -> Result<Vec<(String, String)>, String> {
let mut image_files = Vec::new();
let base_path = Path::new(dir);
if !base_path.exists() {
return Err(format!("目录不存在: {}", dir));
}
scan_directory_recursive(base_path, base_path, &mut image_files).await?;
Ok(image_files)
}
/// 递归扫描目录
fn scan_directory_recursive<'a>(
current_dir: &'a Path,
base_dir: &'a Path,
image_files: &'a mut Vec<(String, String)>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), String>> + Send + 'a>> {
Box::pin(async move {
let mut entries = fs::read_dir(current_dir).await
.map_err(|e| format!("读取目录失败: {}", e))?;
while let Some(entry) = entries.next_entry().await
.map_err(|e| format!("读取目录项失败: {}", e))? {
let path = entry.path();
if path.is_dir() {
// 递归处理子目录
scan_directory_recursive(&path, base_dir, image_files).await?;
} else if path.is_file() {
// 检查是否为图片文件
if is_image_file(&path) {
let absolute_path = path.to_string_lossy().to_string();
let relative_path = path.strip_prefix(base_dir)
.map_err(|e| format!("计算相对路径失败: {}", e))?
.to_string_lossy()
.to_string();
image_files.push((absolute_path, relative_path));
}
}
}
Ok(())
})
}
/// 检查文件是否为图片文件
fn is_image_file(path: &Path) -> bool {
if let Some(extension) = path.extension() {
let ext = extension.to_string_lossy().to_lowercase();
matches!(ext.as_str(), "png" | "jpg" | "jpeg" | "bmp" | "tiff" | "webp")
} else {
false
}
}
/// 验证图片文件
fn validate_image_file(path: &str) -> Result<(), String> {
let path = Path::new(path);
if !path.exists() {
return Err(format!("文件不存在: {}", path.display()));
}
if !path.is_file() {
return Err(format!("路径不是文件: {}", path.display()));
}
// 检查文件扩展名
if let Some(extension) = path.extension() {
let ext = extension.to_string_lossy().to_lowercase();
match ext.as_str() {
"png" | "jpg" | "jpeg" | "bmp" | "tiff" | "webp" => Ok(()),
_ => Err(format!("不支持的图片格式: {}", ext)),
}
} else {
Err("文件没有扩展名".to_string())
}
}
/// 从URL下载图片到本地文件
async fn download_image_from_url(url: &str, output_path: &str) -> Result<(), String> {
let response = reqwest::get(url).await
.map_err(|e| format!("请求图片失败: {}", e))?;
if !response.status().is_success() {
return Err(format!("下载图片失败,状态码: {}", response.status()));
}
let bytes = response.bytes().await
.map_err(|e| format!("读取图片数据失败: {}", e))?;
fs::write(output_path, bytes).await
.map_err(|e| format!("保存图片失败: {}", e))?;
Ok(())
}

View File

@@ -33,6 +33,7 @@ import SimpleHedraLipSyncTool from './pages/tools/SimpleHedraLipSyncTool';
import HedraLipSyncRecords from './pages/tools/HedraLipSyncRecords';
import OmniHumanDetectionTool from './pages/tools/OmniHumanDetectionTool';
import { EnrichedAnalysisDemo } from './pages/tools/EnrichedAnalysisDemo';
import AiModelFaceHairFixTool from './pages/tools/AiModelFaceHairFixTool';
import MaterialCenter from './pages/MaterialCenter';
import VideoGeneration from './pages/VideoGeneration';
import { OutfitPhotoGenerationPage } from './pages/OutfitPhotoGeneration';
@@ -176,6 +177,7 @@ function App() {
<Route path="/tools/omni-human-detection" element={<OmniHumanDetectionTool />} />
<Route path="/tools/advanced-filter-demo" element={<AdvancedFilterTool />} />
<Route path="/tools/enriched-analysis-demo" element={<EnrichedAnalysisDemo />} />
<Route path="/tools/ai-model-face-hair-fix" element={<AiModelFaceHairFixTool />} />
</Routes>
</div>
</main>

View File

@@ -7,11 +7,6 @@ import {
DocumentDuplicateIcon,
WrenchScrewdriverIcon,
SparklesIcon,
Cog6ToothIcon,
RectangleStackIcon,
ServerIcon,
PlayIcon,
ChartBarIcon,
ChevronDownIcon,
} from '@heroicons/react/24/outline';
@@ -74,43 +69,6 @@ const Navigation: React.FC = () => {
icon: SparklesIcon,
description: 'AI穿搭方案推荐与素材检索'
},
{
name: 'ComfyUI',
icon: RectangleStackIcon,
description: 'AI工作流管理平台',
children: [
{
name: 'V2 仪表板',
href: '/comfyui-v2-dashboard',
icon: ChartBarIcon,
description: '现代化AI工作流管理仪表板'
},
{
name: '集群管理',
href: '/comfyui-management',
icon: ServerIcon,
description: '分布式ComfyUI集群管理'
},
{
name: '工作流测试',
href: '/comfyui-workflow-test',
icon: PlayIcon,
description: '工作流测试和调试'
},
{
name: '模板创建器测试',
href: '/workflow-template-creator-test',
icon: DocumentDuplicateIcon,
description: '工作流模板创建器功能测试'
}
]
},
{
name: 'AI工作流',
href: '/workflows',
icon: Cog6ToothIcon,
description: '管理和执行各种AI生成任务工作流'
},
{
name: '工具',
href: '/tools',

View File

@@ -26,7 +26,7 @@ interface ExecutionMonitorProps {
export const ExecutionMonitor: React.FC<ExecutionMonitorProps> = ({
className = '',
showCompleted = true,
showCompleted: initialShowCompleted = true,
maxItems = 10,
}) => {
const {
@@ -43,6 +43,7 @@ export const ExecutionMonitor: React.FC<ExecutionMonitorProps> = ({
const filteredExecutions = useFilteredExecutions();
const [autoRefresh, setAutoRefresh] = useState(true);
const [showCompleted, setShowCompleted] = useState(initialShowCompleted);
const [refreshInterval, setRefreshInterval] = useState<NodeJS.Timeout>();
// 自动刷新执行列表

View File

@@ -171,7 +171,7 @@ export const WorkflowManager: React.FC<WorkflowManagerProps> = ({
// 读取文件内容
const fileContent = await invoke('import_workflow_package');
const result = await invoke('comfyui_v2_import_workflows', {
const result = await invoke<{imported_count: number, skipped_count: number}>('comfyui_v2_import_workflows', {
request: {
workflow_data: fileContent,
overwrite_existing: false

View File

@@ -275,7 +275,7 @@ export const WorkflowV2Creator: React.FC<WorkflowV2CreatorProps> = ({
<div className="flex items-center space-x-3">
<Settings className="w-6 h-6 text-blue-600" />
<h2 className="text-lg font-semibold text-gray-900">
{editingWorkflow ? '编辑工作流' : '创建工作流'}
{editingTemplate ? '编辑工作流' : '创建工作流'}
</h2>
</div>
@@ -332,8 +332,11 @@ export const WorkflowV2Creator: React.FC<WorkflowV2CreatorProps> = ({
</label>
<input
type="text"
value={formData.name}
onChange={(e) => updateField('name', e.target.value)}
value={templateData.metadata.name}
onChange={(e) => setTemplateData(prev => ({
...prev,
metadata: { ...prev.metadata, name: e.target.value }
}))}
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors.name ? 'border-red-300' : 'border-gray-300'
}`}
@@ -352,8 +355,11 @@ export const WorkflowV2Creator: React.FC<WorkflowV2CreatorProps> = ({
</label>
<textarea
value={formData.description || ''}
onChange={(e) => updateField('description', e.target.value)}
value={templateData.metadata.description || ''}
onChange={(e) => setTemplateData(prev => ({
...prev,
metadata: { ...prev.metadata, description: e.target.value }
}))}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="描述这个工作流的功能和用途..."
@@ -366,7 +372,7 @@ export const WorkflowV2Creator: React.FC<WorkflowV2CreatorProps> = ({
</label>
<input
type="text"
value={formData.tags?.join(', ') || ''}
value={templateData.metadata.tags?.join(', ') || ''}
onChange={(e) => handleTagsChange(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="用逗号分隔例如AI, 图像, 生成"
@@ -413,8 +419,8 @@ export const WorkflowV2Creator: React.FC<WorkflowV2CreatorProps> = ({
</div>
)}
{/* 高级设置标签页 */}
{activeTab === 'advanced' && (
{/* 高级设置标签页 - 暂时隐藏 */}
{false && (
<div className="space-y-6">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-center space-x-2">
@@ -453,7 +459,7 @@ export const WorkflowV2Creator: React.FC<WorkflowV2CreatorProps> = ({
) : (
<Save className="w-4 h-4" />
)}
<span>{isSaving ? '保存中...' : (editingWorkflow ? '更新' : '创建')}</span>
<span>{isSaving ? '保存中...' : (editingTemplate ? '更新' : '创建')}</span>
</button>
</div>
</div>

View File

@@ -31,7 +31,7 @@ const inputVariants = cva(
);
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement>,
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'>,
VariantProps<typeof inputVariants> {
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
@@ -83,7 +83,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
<input
id={inputId}
className={cn(
inputVariants({ variant: finalVariant, size }),
inputVariants({ variant: finalVariant, size: size as "default" | "sm" | "lg" | null | undefined }),
leftIcon && 'pl-10',
rightIcon && 'pr-10',
className

View File

@@ -5,6 +5,7 @@
import React from 'react';
import { cn } from '../../utils/cn';
import { Card } from './Card';
export interface LoadingProps {
size?: 'sm' | 'md' | 'lg' | 'xl';

View File

@@ -3,16 +3,12 @@ import {
Wrench,
Database,
FileSearch,
Search,
Sparkles,
Heart,
ArrowLeftRight,
Image,
Mic,
Video,
Wand2,
FileText,
User
ImagePlus,
} from 'lucide-react';
import { Tool, ToolCategory, ToolStatus } from '../types/tool';
@@ -36,6 +32,21 @@ export const TOOLS_DATA: Tool[] = [
version: '1.0.0',
lastUpdated: '2024-01-29'
},
{
id: 'ai-model-face-hair-fix',
name: 'AI模型面部头发修复工具',
description: '专业的AI模型面部和头发细节修复工具支持单张图片和批量处理',
longDescription: '基于ComfyUI的专业AI模型面部头发修复工具使用分割和局部修复技术增强AI模型照片的面部和头发细节。支持单张图片处理和批量目录处理保持原有目录结构。提供可调节的面部提示词和去噪强度适用于AI模型照片后期处理、人像美化、细节增强等场景。',
icon: ImagePlus,
route: '/tools/ai-model-face-hair-fix',
category: ToolCategory.AI_TOOLS,
status: ToolStatus.STABLE,
tags: ['面部修复', '头发增强', 'AI模型', '细节修复', '批量处理', 'ComfyUI'],
isNew: true,
isPopular: true,
version: '1.0.0',
lastUpdated: '2024-01-29'
},
{
id: 'voice-clone',
name: '声音克隆与TTS工具',
@@ -51,96 +62,6 @@ export const TOOLS_DATA: Tool[] = [
version: '1.0.0',
lastUpdated: '2024-01-29'
},
{
id: 'similarity-search',
name: '相似度检索工具',
description: '基于AI的智能相似度搜索工具支持多种相关性阈值和快速搜索功能',
longDescription: '强大的AI驱动相似度检索工具基于先进的机器学习算法提供精准的内容匹配。支持可调节的相关性阈值、智能搜索建议、实时结果展示和批量处理功能。适用于图像、文本和多媒体内容的相似性分析。',
icon: Search,
route: '/tools/similarity-search',
category: ToolCategory.AI_TOOLS,
status: ToolStatus.STABLE,
tags: ['AI搜索', '相似度检索', '智能匹配', '机器学习', '内容分析'],
isNew: true,
isPopular: true,
version: '1.0.0',
lastUpdated: '2024-01-25'
},
{
id: 'outfit-recommendation',
name: 'AI穿搭方案推荐',
description: '基于TikTok视觉趋势的智能穿搭建议工具提供个性化的时尚搭配方案',
longDescription: '专业的AI穿搭顾问工具基于TikTok视觉趋势和时尚潮流为用户生成个性化的穿搭方案。支持多种风格选择、场合匹配、色彩搭配建议并提供TikTok优化建议和拍摄技巧助力内容创作和时尚搭配。',
icon: Sparkles,
route: '/tools/outfit-recommendation',
category: ToolCategory.AI_TOOLS,
status: ToolStatus.STABLE,
tags: ['AI穿搭', '时尚搭配', 'TikTok', '个性化推荐', '视觉趋势'],
isNew: true,
isPopular: true,
version: '1.0.0',
lastUpdated: '2024-01-25'
},
{
id: 'outfit-search',
name: '智能服装搜索',
description: '基于AI的智能服装搜索工具支持图像解析、相似度搜索和LLM问答',
longDescription: '专业的服装搜索工具基于先进的AI技术提供智能服装匹配和搜索功能。支持图像上传解析、多维度过滤搜索、相似度匹配、LLM智能问答等功能。提供直观的双列布局界面左侧展示搜索结果右侧提供搜索控制面板。',
icon: Search,
route: '/tools/outfit-search',
category: ToolCategory.AI_TOOLS,
status: ToolStatus.BETA,
tags: ['AI搜索', '服装匹配', '图像解析', 'LLM问答', '相似度搜索'],
isNew: true,
isPopular: true,
version: '1.0.0',
lastUpdated: '2024-01-26'
},
{
id: 'outfit-favorites',
name: '穿搭方案收藏管理',
description: '管理收藏的穿搭方案,支持基于收藏方案的智能素材检索',
longDescription: '专业的穿搭方案收藏管理工具,提供完整的收藏管理功能。支持收藏方案的添加、删除、搜索和筛选,以及基于收藏方案的智能素材检索功能。帮助用户建立个人穿搭方案库,快速找到适合的素材。',
icon: Heart,
route: '/tools/outfit-favorites',
category: ToolCategory.AI_TOOLS,
status: ToolStatus.STABLE,
tags: ['收藏管理', '穿搭方案', '素材检索', '个人库', '智能搜索'],
isNew: true,
isPopular: true,
version: '1.0.0',
lastUpdated: '2024-01-27'
},
{
id: 'outfit-comparison',
name: '穿搭方案对比分析',
description: '分屏对比两个收藏方案的素材检索结果,分析方案差异',
longDescription: '强大的穿搭方案对比分析工具,支持同时选择两个收藏方案进行分屏对比。并行执行素材检索,直观展示两个方案的检索结果差异,帮助用户更好地理解不同方案的特点和适用场景。',
icon: ArrowLeftRight,
route: '/tools/outfit-comparison',
category: ToolCategory.AI_TOOLS,
status: ToolStatus.STABLE,
tags: ['方案对比', '分屏展示', '差异分析', '素材检索', '对比分析'],
isNew: true,
isPopular: true,
version: '1.0.0',
lastUpdated: '2024-01-27'
},
{
id: 'material-search',
name: '智能素材检索',
description: '基于收藏穿搭方案的智能素材检索工具,快速找到匹配的素材',
longDescription: '专业的智能素材检索工具,基于收藏的穿搭方案进行精准的素材匹配。支持多维度检索条件生成、相关度排序、分页浏览等功能。提供直观的检索界面和丰富的筛选选项,帮助用户快速找到最适合的素材。',
icon: Search,
route: '/tools/material-search',
category: ToolCategory.AI_TOOLS,
status: ToolStatus.STABLE,
tags: ['素材检索', '智能匹配', '方案关联', '相关度排序', '精准搜索'],
isNew: true,
isPopular: true,
version: '1.0.0',
lastUpdated: '2024-01-27'
},
{
id: 'volcano-video-generation',
name: '图片模特模仿视频动作',
@@ -156,21 +77,6 @@ export const TOOLS_DATA: Tool[] = [
version: '1.0.0',
lastUpdated: '2024-01-31'
},
{
id: 'omni-human-detection',
name: 'OmniHuman 主体识别',
description: '基于火山云API的智能主体识别工具识别图片中是否包含人、类人、拟人等主体',
longDescription: '专业的OmniHuman主体识别工具基于火山云先进的计算机视觉API。能够准确识别图片中的人物、类人、拟人等主体返回识别结果和处理后的图片。支持多种图片格式提供详细的识别报告和算法返回数据。适用于内容审核、人物检测、智能分析等多种场景。',
icon: User,
route: '/tools/omni-human-detection',
category: ToolCategory.AI_TOOLS,
status: ToolStatus.STABLE,
tags: ['主体识别', '人物检测', '计算机视觉', '火山云API', '图像分析'],
isNew: true,
isPopular: false,
version: '1.0.0',
lastUpdated: '2024-08-04'
},
{
id: 'hedra-records',
name: '图片说话/唱歌',

View File

@@ -0,0 +1,609 @@
import React, { useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { open } from '@tauri-apps/plugin-dialog';
import { listen } from '@tauri-apps/api/event';
import {
ImagePlus,
Upload,
Download,
Settings,
Play,
CheckCircle,
XCircle,
AlertCircle,
Folder,
FolderOpen,
Image as ImageIcon,
Loader2
} from 'lucide-react';
interface ImageEnhancementProgress {
current: number;
total: number;
percentage: number;
status: string;
current_file?: string;
}
interface ImageEnhancementResult {
success: boolean;
message: string;
input_path: string;
output_path?: string;
execution_time: number;
error?: string;
}
interface BatchImageEnhancementResult {
success: boolean;
message: string;
total_processed: number;
successful_count: number;
failed_count: number;
results: ImageEnhancementResult[];
total_execution_time: number;
}
const AiModelFaceHairFixTool: React.FC = () => {
// 单张图片处理状态
const [singleImagePath, setSingleImagePath] = useState<string>('');
const [singleOutputPath, setSingleOutputPath] = useState<string>('');
// 批量处理状态
const [batchInputDir, setBatchInputDir] = useState<string>('');
const [batchOutputDir, setBatchOutputDir] = useState<string>('');
// 通用设置
const [serverUrl, setServerUrl] = useState<string>('http://192.168.0.193:8188');
const [facePrompt, setFacePrompt] = useState<string>('beautiful woman, perfect skin, detailed facial features');
const [faceDenoiseStr, setFaceDenoiseStr] = useState<string>('0.25');
// 处理状态
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const [progress, setProgress] = useState<ImageEnhancementProgress | null>(null);
const [results, setResults] = useState<ImageEnhancementResult[] | null>(null);
const [batchResults, setBatchResults] = useState<BatchImageEnhancementResult | null>(null);
// 当前模式single 或 batch
const [mode, setMode] = useState<'single' | 'batch'>('single');
// 选择单张图片
const selectSingleImage = async () => {
try {
const selected = await open({
multiple: false,
filters: [
{
name: 'Image Files',
extensions: ['png', 'jpg', 'jpeg', 'bmp', 'tiff', 'webp']
}
]
});
if (selected && typeof selected === 'string') {
setSingleImagePath(selected);
// 自动生成输出路径
const pathParts = selected.split('.');
const extension = pathParts.pop();
const basePath = pathParts.join('.');
setSingleOutputPath(`${basePath}_enhanced.${extension}`);
}
} catch (error) {
console.error('选择图片失败:', error);
}
};
// 选择单张图片输出路径
const selectSingleOutputPath = async () => {
try {
const selected = await open({
multiple: false,
filters: [
{
name: 'Image Files',
extensions: ['png', 'jpg', 'jpeg', 'bmp', 'tiff', 'webp']
}
]
});
if (selected && typeof selected === 'string') {
setSingleOutputPath(selected);
}
} catch (error) {
console.error('选择输出路径失败:', error);
}
};
// 选择批量输入目录
const selectBatchInputDir = async () => {
try {
const selected = await open({
directory: true,
multiple: false
});
if (selected && typeof selected === 'string') {
setBatchInputDir(selected);
}
} catch (error) {
console.error('选择输入目录失败:', error);
}
};
// 选择批量输出目录
const selectBatchOutputDir = async () => {
try {
const selected = await open({
directory: true,
multiple: false
});
if (selected && typeof selected === 'string') {
setBatchOutputDir(selected);
}
} catch (error) {
console.error('选择输出目录失败:', error);
}
};
// 处理单张图片
const processSingleImage = async () => {
if (!singleImagePath || !singleOutputPath) {
alert('请选择输入图片和输出路径');
return;
}
setIsProcessing(true);
setResults(null);
setProgress(null);
try {
// 监听进度事件
const unlisten = await listen<ImageEnhancementProgress>('image-enhancement-progress', (event) => {
setProgress(event.payload);
});
const result = await invoke<ImageEnhancementResult>('ai_model_face_hair_fix_single_image', {
inputImage: singleImagePath,
serverUrl,
facePrompt,
faceDenoise: faceDenoiseStr,
outputImage: singleOutputPath
});
setResults([result]);
// 清理事件监听器
unlisten();
} catch (error) {
console.error('处理失败:', error);
alert(`处理失败: ${error}`);
} finally {
setIsProcessing(false);
}
};
// 批量处理图片
const processBatchImages = async () => {
if (!batchInputDir || !batchOutputDir) {
alert('请选择输入目录和输出目录');
return;
}
setIsProcessing(true);
setBatchResults(null);
setProgress(null);
try {
// 监听进度事件
const unlisten = await listen<ImageEnhancementProgress>('image-enhancement-progress', (event) => {
setProgress(event.payload);
});
const result = await invoke<BatchImageEnhancementResult>('ai_model_face_hair_fix_batch_images', {
inputDir: batchInputDir,
serverUrl,
facePrompt,
faceDenoise: faceDenoiseStr,
outputDir: batchOutputDir
});
setBatchResults(result);
// 清理事件监听器
unlisten();
} catch (error) {
console.error('批量处理失败:', error);
alert(`批量处理失败: ${error}`);
} finally {
setIsProcessing(false);
}
};
// 重置状态
const resetState = () => {
setProgress(null);
setResults(null);
setBatchResults(null);
setIsProcessing(false);
};
return (
<div className="p-6 max-w-6xl mx-auto">
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 mb-2 flex items-center">
<ImagePlus className="mr-3 text-blue-600" size={28} />
AI模型面部头发修复工具
</h1>
<p className="text-gray-600">
ComfyUI的专业AI模型面部头发修复工具
</p>
</div>
{/* 模式选择 */}
<div className="mb-6">
<div className="flex space-x-4">
<button
onClick={() => { setMode('single'); resetState(); }}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
mode === 'single'
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
<ImageIcon className="inline mr-2" size={16} />
</button>
<button
onClick={() => { setMode('batch'); resetState(); }}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
mode === 'batch'
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
<Folder className="inline mr-2" size={16} />
</button>
</div>
</div>
{/* 服务器设置 */}
<div className="bg-white rounded-lg shadow-sm border p-6 mb-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<Settings className="mr-2 text-gray-600" size={20} />
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
ComfyUI服务器地址
</label>
<input
type="text"
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="http://192.168.0.193:8188"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
(0.0-1.0)
</label>
<input
type="text"
value={faceDenoiseStr}
onChange={(e) => setFaceDenoiseStr(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="0.25"
/>
</div>
</div>
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<textarea
value={facePrompt}
onChange={(e) => setFacePrompt(e.target.value)}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="beautiful woman, perfect skin, detailed facial features"
/>
</div>
</div>
{/* 单张图片处理 */}
{mode === 'single' && (
<div className="bg-white rounded-lg shadow-sm border p-6 mb-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<ImageIcon className="mr-2 text-gray-600" size={20} />
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<div className="flex space-x-2">
<input
type="text"
value={singleImagePath}
onChange={(e) => setSingleImagePath(e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="选择要处理的图片文件"
/>
<button
onClick={selectSingleImage}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center"
>
<Upload className="mr-2" size={16} />
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<div className="flex space-x-2">
<input
type="text"
value={singleOutputPath}
onChange={(e) => setSingleOutputPath(e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="增强后图片的保存路径"
/>
<button
onClick={selectSingleOutputPath}
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors flex items-center"
>
<Download className="mr-2" size={16} />
</button>
</div>
</div>
<div className="flex justify-end">
<button
onClick={processSingleImage}
disabled={isProcessing || !singleImagePath || !singleOutputPath}
className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors flex items-center"
>
{isProcessing ? (
<Loader2 className="mr-2 animate-spin" size={16} />
) : (
<Play className="mr-2" size={16} />
)}
{isProcessing ? '处理中...' : '开始处理'}
</button>
</div>
</div>
</div>
)}
{/* 批量处理 */}
{mode === 'batch' && (
<div className="bg-white rounded-lg shadow-sm border p-6 mb-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<Folder className="mr-2 text-gray-600" size={20} />
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<div className="flex space-x-2">
<input
type="text"
value={batchInputDir}
onChange={(e) => setBatchInputDir(e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="包含图片文件的目录"
/>
<button
onClick={selectBatchInputDir}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center"
>
<FolderOpen className="mr-2" size={16} />
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<div className="flex space-x-2">
<input
type="text"
value={batchOutputDir}
onChange={(e) => setBatchOutputDir(e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="增强后图片的保存目录"
/>
<button
onClick={selectBatchOutputDir}
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors flex items-center"
>
<FolderOpen className="mr-2" size={16} />
</button>
</div>
</div>
<div className="flex justify-end">
<button
onClick={processBatchImages}
disabled={isProcessing || !batchInputDir || !batchOutputDir}
className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors flex items-center"
>
{isProcessing ? (
<Loader2 className="mr-2 animate-spin" size={16} />
) : (
<Play className="mr-2" size={16} />
)}
{isProcessing ? '批量处理中...' : '开始批量处理'}
</button>
</div>
</div>
</div>
)}
{/* 进度显示 */}
{progress && (
<div className="bg-white rounded-lg shadow-sm border p-6 mb-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<Loader2 className="mr-2 text-blue-600 animate-spin" size={20} />
</h2>
<div className="space-y-4">
<div>
<div className="flex justify-between text-sm text-gray-600 mb-1">
<span>{progress.status}</span>
<span>{progress.current}/{progress.total} ({progress.percentage.toFixed(1)}%)</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${progress.percentage}%` }}
/>
</div>
</div>
{progress.current_file && (
<div className="text-sm text-gray-600">
<span className="font-medium">:</span> {progress.current_file}
</div>
)}
</div>
</div>
)}
{/* 单张图片结果 */}
{results && mode === 'single' && (
<div className="bg-white rounded-lg shadow-sm border p-6 mb-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<CheckCircle className="mr-2 text-green-600" size={20} />
</h2>
{results.map((result, index) => (
<div key={index} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center">
{result.success ? (
<CheckCircle className="mr-2 text-green-600" size={16} />
) : (
<XCircle className="mr-2 text-red-600" size={16} />
)}
<span className={`font-medium ${result.success ? 'text-green-700' : 'text-red-700'}`}>
{result.success ? '处理成功' : '处理失败'}
</span>
</div>
<span className="text-sm text-gray-500">
: {result.execution_time.toFixed(2)}
</span>
</div>
<div className="text-sm text-gray-600 space-y-1">
<div><span className="font-medium">:</span> {result.input_path}</div>
{result.output_path && (
<div><span className="font-medium">:</span> {result.output_path}</div>
)}
<div><span className="font-medium">:</span> {result.message}</div>
{result.error && (
<div className="text-red-600"><span className="font-medium">:</span> {result.error}</div>
)}
</div>
</div>
))}
</div>
)}
{/* 批量处理结果 */}
{batchResults && mode === 'batch' && (
<div className="bg-white rounded-lg shadow-sm border p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
{batchResults.success ? (
<CheckCircle className="mr-2 text-green-600" size={20} />
) : (
<AlertCircle className="mr-2 text-yellow-600" size={20} />
)}
</h2>
<div className="mb-4 p-4 bg-gray-50 rounded-lg">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<span className="font-medium text-gray-700">:</span>
<span className="ml-2 text-gray-900">{batchResults.total_processed}</span>
</div>
<div>
<span className="font-medium text-green-700">:</span>
<span className="ml-2 text-green-900">{batchResults.successful_count}</span>
</div>
<div>
<span className="font-medium text-red-700">:</span>
<span className="ml-2 text-red-900">{batchResults.failed_count}</span>
</div>
<div>
<span className="font-medium text-gray-700">:</span>
<span className="ml-2 text-gray-900">{batchResults.total_execution_time.toFixed(2)}</span>
</div>
</div>
<div className="mt-2 text-sm text-gray-600">
{batchResults.message}
</div>
</div>
<div className="space-y-2 max-h-96 overflow-y-auto">
{batchResults.results.map((result, index) => (
<div key={index} className="border rounded-lg p-3">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center">
{result.success ? (
<CheckCircle className="mr-2 text-green-600" size={14} />
) : (
<XCircle className="mr-2 text-red-600" size={14} />
)}
<span className={`text-sm font-medium ${result.success ? 'text-green-700' : 'text-red-700'}`}>
{result.success ? '成功' : '失败'}
</span>
</div>
<span className="text-xs text-gray-500">
{result.execution_time.toFixed(2)}
</span>
</div>
<div className="text-xs text-gray-600 space-y-1">
<div className="truncate"><span className="font-medium">:</span> {result.input_path}</div>
{result.output_path && (
<div className="truncate"><span className="font-medium">:</span> {result.output_path}</div>
)}
{result.error && (
<div className="text-red-600 truncate"><span className="font-medium">:</span> {result.error}</div>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
);
};
export default AiModelFaceHairFixTool;

View File

@@ -373,9 +373,9 @@ export class ComfyUIV2Service {
/**
* 调试:列出所有工作流
*/
static async debugListWorkflows(): Promise<WorkflowResponse[]> {
static async debugListWorkflows(): Promise<WorkflowV2[]> {
try {
return await invoke<WorkflowResponse[]>('comfyui_v2_debug_list_workflows');
return await invoke<WorkflowV2[]>('comfyui_v2_debug_list_workflows');
} catch (error) {
console.error('Failed to debug list workflows:', error);
throw new Error(`获取调试工作流列表失败: ${error}`);
@@ -432,6 +432,18 @@ export class ComfyUIV2Service {
}
}
/**
* 更新模板
*/
static async updateTemplate(id: string, request: Partial<CreateTemplateRequest>): Promise<TemplateV2> {
try {
return await invoke<TemplateV2>('comfyui_v2_update_template', { id, request });
} catch (error) {
console.error('Failed to update template:', error);
throw new Error(`更新模板失败: ${error}`);
}
}
/**
* 删除模板
*/

View File

@@ -15,6 +15,8 @@ import type {
TemplateV2,
ExecutionV2,
RealtimeEvent,
ExecutionCompletedEvent,
ExecutionProgressEvent,
} from '../services/comfyuiV2Service';
// ==================== 状态接口定义 ====================

View File

@@ -1,306 +0,0 @@
//! Real Local Image Upload Test
//!
//! This example demonstrates how to upload your actual local image file
//! and use it with the AI Model Face & Hair Detail Fix template.
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use comfyui_sdk::{
ComfyUIClient, ComfyUIClientConfig, ExecutionOptions,
AI_MODEL_FACE_HAIR_FIX_TEMPLATE
};
use comfyui_sdk::utils::SimpleCallbacks;
use serde_json::json;
/// Test with real local image upload
async fn test_with_real_local_image() -> Result<(), Box<dyn std::error::Error>> {
println!("🚀 Starting Real Local Image Upload Test");
// Initialize client
let mut client = ComfyUIClient::new(ComfyUIClientConfig {
base_url: "http://192.168.0.193:8188".to_string(),
..Default::default()
})?;
// Connect to ComfyUI server
println!("📡 Connecting to ComfyUI server...");
client.connect().await?;
println!("✅ Connected successfully!");
// Register the template
println!("📝 Registering AI Model Face & Hair Fix template...");
client.templates().register_from_data(AI_MODEL_FACE_HAIR_FIX_TEMPLATE.clone())?;
let template = client.templates_ref()
.get_by_id(&AI_MODEL_FACE_HAIR_FIX_TEMPLATE.metadata.id)
.ok_or("Failed to register template")?;
println!("✅ Template registered!");
// Your actual local image path
let local_image_path = "20250808-111737.png";
println!("\n📤 Uploading your image: {}", local_image_path);
println!("⏳ This may take a moment depending on file size and network speed...");
match upload_and_execute(&client, template, local_image_path).await {
Ok(_) => println!("✅ Upload and execution completed successfully!"),
Err(e) => {
println!("\n💥 Upload/Execution failed: {}", e);
show_troubleshooting_tips(&e);
}
}
// Disconnect
println!("\n🔌 Disconnecting from ComfyUI server...");
client.disconnect().await?;
println!("👋 Disconnected successfully!");
Ok(())
}
/// Upload image and execute template
async fn upload_and_execute(
client: &ComfyUIClient,
template: &comfyui_sdk::WorkflowTemplate,
image_path: &str,
) -> Result<(), Box<dyn std::error::Error>> {
// Step 1: Upload image to ComfyUI server
println!("\n🔄 Step 1: Uploading image to ComfyUI server...");
if !Path::new(image_path).exists() {
return Err(format!("File not found: {}", image_path).into());
}
let upload_response = client.upload_image(image_path, false).await?;
let uploaded_filename = upload_response.name;
println!("✅ Upload successful! Server filename: {}", uploaded_filename);
// Step 2: Execute AI Model Face & Hair Enhancement
println!("\n🔄 Step 2: Executing AI Model Face & Hair Enhancement...");
// Create execution callbacks
let callbacks = Arc::new(
SimpleCallbacks::new()
.with_progress(|progress| {
let percentage = (progress.progress as f64 / progress.max as f64 * 100.0) as u32;
println!("⏳ Progress: {}% ({}/{}) - Node: {}",
percentage, progress.progress, progress.max, progress.node_id);
})
.with_executing(|node_id| {
println!("🔄 Executing node: {}", node_id);
})
.with_executed(|result| {
println!("✅ Node completed - Prompt ID: {}", result.prompt_id);
})
.with_error(|error| {
println!("❌ Execution error: {}", error.message);
})
);
// Prepare parameters
let mut parameters = HashMap::new();
parameters.insert("input_image".to_string(), json!(uploaded_filename));
parameters.insert("face_prompt".to_string(),
json!("beautiful woman, perfect skin, detailed facial features, professional photography"));
parameters.insert("face_denoise".to_string(), json!("0.25"));
let execution_options = ExecutionOptions {
timeout: Some(std::time::Duration::from_secs(180)), // 3 minutes timeout
priority: None,
};
let result = client.execute_template_with_callbacks(
template,
parameters,
execution_options,
callbacks,
).await?;
if result.success {
println!("\n🎉 AI Model Enhancement completed successfully!");
println!("📊 Total execution time: {}ms ({:.1}s)",
result.execution_time, result.execution_time as f64 / 1000.0);
println!("🆔 Prompt ID: {}", result.prompt_id);
if let Some(outputs) = &result.outputs {
println!("\n📁 Generated outputs:");
println!("{}", serde_json::to_string_pretty(outputs)?);
// Convert outputs to HTTP URLs
let image_urls = client.outputs_to_urls(outputs);
println!("\n🖼️ Enhanced Image URLs:");
for (index, url) in image_urls.iter().enumerate() {
println!(" {}. {}", index + 1, url);
}
println!("\n💡 How to use these URLs:");
println!(" - Copy the URL and paste in your browser to view");
println!(" - Right-click and \"Save As\" to download");
println!(" - Use in your application to display the enhanced image");
}
} else {
println!("\n❌ AI Model Enhancement failed!");
if let Some(error) = &result.error {
println!("Error details: {}", error.message);
}
}
Ok(())
}
/// Show troubleshooting tips based on error
fn show_troubleshooting_tips(error: &Box<dyn std::error::Error>) {
let error_msg = error.to_string();
if error_msg.contains("File not found") || error_msg.contains("No such file") {
println!("\n💡 Troubleshooting Tips:");
println!("1. 🔍 Check if the file path is correct");
println!("2. 📁 Make sure the file exists at the specified location");
println!("3. 🔐 Ensure you have read permissions for the file");
println!("4. 📝 Try using absolute path: /home/user/images/20250808-111737.png");
println!("5. 🖼️ Verify the file is a valid image format (PNG, JPG, etc.)");
} else if error_msg.contains("Failed to upload") || error_msg.contains("HTTP") {
println!("\n💡 Network/Server Issues:");
println!("1. 🌐 Check if ComfyUI server is running and accessible");
println!("2. 📡 Verify network connection to the server");
println!("3. 💾 Check if server has enough disk space");
println!("4. 🔒 Ensure server allows file uploads");
}
}
/// Alternative method using one-step upload and execute (conceptual)
async fn test_one_step_method() -> Result<(), Box<dyn std::error::Error>> {
println!("\n\n🚀 Testing One-Step Method (Upload + Execute)");
let mut client = ComfyUIClient::new(ComfyUIClientConfig {
base_url: "http://192.168.0.193:8188".to_string(),
..Default::default()
})?;
client.connect().await?;
client.templates().register_from_data(AI_MODEL_FACE_HAIR_FIX_TEMPLATE.clone())?;
let template = client.templates_ref()
.get_by_id(&AI_MODEL_FACE_HAIR_FIX_TEMPLATE.metadata.id)
.ok_or("Failed to register template")?;
let local_image_path = "20250808-111737.png";
println!("🔄 One-step upload and execute...");
// For now, we'll implement this as upload + execute since we don't have
// a dedicated one-step method yet
match upload_and_execute_one_step(&client, template, local_image_path).await {
Ok(_) => println!("🎉 One-step method completed successfully!"),
Err(e) => println!("⚠️ One-step method failed: {}", e),
}
client.disconnect().await?;
Ok(())
}
/// One-step upload and execute (simplified version)
async fn upload_and_execute_one_step(
client: &ComfyUIClient,
template: &comfyui_sdk::WorkflowTemplate,
image_path: &str,
) -> Result<(), Box<dyn std::error::Error>> {
// Upload image
let upload_response = client.upload_image(image_path, false).await?;
// Create simple callbacks
let callbacks = Arc::new(
SimpleCallbacks::new()
.with_progress(|progress| {
let percentage = (progress.progress as f64 / progress.max as f64 * 100.0) as u32;
println!("⏳ Progress: {}% - Node: {}", percentage, progress.node_id);
})
);
// Execute template
let mut parameters = HashMap::new();
parameters.insert("input_image".to_string(), json!(upload_response.name));
parameters.insert("face_prompt".to_string(),
json!("stunning model, flawless skin, professional photography, high quality"));
parameters.insert("face_denoise".to_string(), json!("0.3"));
let result = client.execute_template_with_callbacks(
template,
parameters,
ExecutionOptions {
timeout: Some(std::time::Duration::from_secs(180)),
priority: None,
},
callbacks,
).await?;
if result.success {
if let Some(outputs) = &result.outputs {
let image_urls = client.outputs_to_urls(outputs);
println!("🖼️ Result URLs: {:?}", image_urls);
}
}
Ok(())
}
/// Show usage examples
fn show_usage_examples() {
println!("\n📚 Complete Usage Guide for Local Images:");
println!("");
println!("🔧 Basic Setup:");
println!("```rust");
println!("use comfyui_sdk::{{ComfyUIClient, ComfyUIClientConfig, AI_MODEL_FACE_HAIR_FIX_TEMPLATE}};");
println!("");
println!("let mut client = ComfyUIClient::new(ComfyUIClientConfig {{");
println!(" base_url: \"http://your-comfyui-server:8188\".to_string(),");
println!(" ..Default::default()");
println!("}});");
println!("```");
println!("");
println!("📤 Method 1 - Manual Upload:");
println!("```rust");
println!("// Upload image first");
println!("let upload_response = client.upload_image(");
println!(" \"/path/to/your/image.png\", false");
println!(").await?;");
println!("");
println!("// Then execute template");
println!("let result = client.execute_template(template, parameters, options).await?;");
println!("```");
println!("");
println!("🚀 Method 2 - Combined Upload and Execute:");
println!("```rust");
println!("// Upload and execute in sequence");
println!("let upload_response = client.upload_image(image_path, false).await?;");
println!("let mut parameters = HashMap::new();");
println!("parameters.insert(\"input_image\".to_string(), json!(upload_response.name));");
println!("let result = client.execute_template_with_callbacks(");
println!(" template, parameters, options, callbacks");
println!(").await?;");
println!("```");
}
/// Run all tests
async fn run_all_tests() -> Result<(), Box<dyn std::error::Error>> {
show_usage_examples();
test_with_real_local_image().await?;
// Uncomment to test one-step method as well
// test_one_step_method().await?;
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
run_all_tests().await
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_show_usage_examples() {
show_usage_examples();
// This test just ensures the function runs without panicking
}
}

View File

@@ -1,113 +0,0 @@
//! Test URL generation fix
//!
//! This example demonstrates that the double slash bug has been fixed
use std::collections::HashMap;
use serde_json::json;
use comfyui_sdk::{HTTPClient, ComfyUIClientConfig};
fn main() {
println!("🔧 Testing URL Generation Fix");
println!("==============================");
// Test with base URL ending with slash (the problematic case)
let config_with_slash = ComfyUIClientConfig {
base_url: "http://192.168.0.193:8188/".to_string(),
..Default::default()
};
let client_with_slash = HTTPClient::new(config_with_slash).unwrap();
// Test with base URL without ending slash
let config_without_slash = ComfyUIClientConfig {
base_url: "http://192.168.0.193:8188".to_string(),
..Default::default()
};
let client_without_slash = HTTPClient::new(config_without_slash).unwrap();
// Create test outputs (simulating ComfyUI response)
let mut outputs = HashMap::new();
outputs.insert("58".to_string(), json!({
"images": [
{
"filename": "ComfyUI_02046_.png",
"subfolder": "",
"type": "output"
}
]
}));
println!("\n📋 Test Case 1: Base URL with trailing slash");
println!("Base URL: http://192.168.0.193:8188/");
let urls_with_slash = client_with_slash.outputs_to_urls(&outputs);
for url in &urls_with_slash {
println!("Generated URL: {}", url);
if url.contains("//view") {
println!("❌ FAILED: URL contains double slash!");
} else {
println!("✅ PASSED: No double slash found");
}
}
println!("\n📋 Test Case 2: Base URL without trailing slash");
println!("Base URL: http://192.168.0.193:8188");
let urls_without_slash = client_without_slash.outputs_to_urls(&outputs);
for url in &urls_without_slash {
println!("Generated URL: {}", url);
if url.contains("//view") {
println!("❌ FAILED: URL contains double slash!");
} else {
println!("✅ PASSED: No double slash found");
}
}
println!("\n📋 Test Case 3: Multiple images");
let mut multi_outputs = HashMap::new();
multi_outputs.insert("58".to_string(), json!({
"images": [
{
"filename": "image1.png",
"subfolder": "temp",
"type": "output"
},
{
"filename": "image2.jpg",
"subfolder": "",
"type": "temp"
}
]
}));
let multi_urls = client_with_slash.outputs_to_urls(&multi_outputs);
println!("Generated {} URLs:", multi_urls.len());
for (i, url) in multi_urls.iter().enumerate() {
println!(" {}. {}", i + 1, url);
if url.contains("//view") {
println!(" ❌ FAILED: URL contains double slash!");
} else {
println!(" ✅ PASSED: No double slash found");
}
}
println!("\n🎯 Expected vs Actual URLs:");
println!("Expected: http://192.168.0.193:8188/view?filename=ComfyUI_02046_.png&subfolder=&type=output");
if !urls_with_slash.is_empty() {
println!("Actual: {}", urls_with_slash[0]);
if urls_with_slash[0] == "http://192.168.0.193:8188/view?filename=ComfyUI_02046_.png&subfolder=&type=output" {
println!("✅ URLs match perfectly!");
} else {
println!("❌ URLs don't match!");
}
}
println!("\n🔍 Consistency Check:");
if urls_with_slash == urls_without_slash {
println!("✅ URLs are consistent regardless of trailing slash in base URL");
} else {
println!("❌ URLs are inconsistent!");
println!("With slash: {:?}", urls_with_slash);
println!("Without slash: {:?}", urls_without_slash);
}
println!("\n🎉 URL Generation Fix Test Completed!");
println!("The double slash bug has been fixed. URLs now correctly use single slashes.");
}