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:
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>();
|
||||
|
||||
// 自动刷新执行列表
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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: '图片说话/唱歌',
|
||||
|
||||
609
apps/desktop/src/pages/tools/AiModelFaceHairFixTool.tsx
Normal file
609
apps/desktop/src/pages/tools/AiModelFaceHairFixTool.tsx
Normal 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;
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除模板
|
||||
*/
|
||||
|
||||
@@ -15,6 +15,8 @@ import type {
|
||||
TemplateV2,
|
||||
ExecutionV2,
|
||||
RealtimeEvent,
|
||||
ExecutionCompletedEvent,
|
||||
ExecutionProgressEvent,
|
||||
} from '../services/comfyuiV2Service';
|
||||
|
||||
// ==================== 状态接口定义 ====================
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
Reference in New Issue
Block a user