Files
jammyfu-ComfyUI_PaintingCod…/docs/fix/workflow_fixer.html
T

839 lines
33 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Workflow Fixer</title>
<style>
/* 添加深色模式变量 */
:root {
--bg-color: #f5f5f5;
--container-bg: white;
--text-color: #333;
--border-color: #ccc;
--hover-bg: #eeeeee;
--success-color: #4CAF50;
--success-hover: #45a049;
--error-color: #ff4444;
--file-item-bg: #f5f5f5;
--file-item-hover: #eeeeee;
--drop-zone-bg: #f8f8f8;
--drop-zone-border: #ccc;
--drop-zone-hover: #4CAF50;
--drop-zone-hover-bg: #e8f5e9;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #1a1a1a;
--container-bg: #2d2d2d;
--text-color: #e0e0e0;
--border-color: #404040;
--hover-bg: #3d3d3d;
--success-color: #5cb85c;
--success-hover: #4cae4c;
--error-color: #ff6b6b;
--file-item-bg: #333333;
--file-item-hover: #404040;
--drop-zone-bg: #2d2d2d;
--drop-zone-border: #404040;
--drop-zone-hover: #5cb85c;
--drop-zone-hover-bg: #2d3b2d;
}
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.container {
background-color: var(--container-bg);
color: var(--text-color);
padding: 20px;
border-radius: 0 0 8px 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin: 0 auto;
max-width: 800px;
}
.file-input {
margin: 20px 0;
}
.file-input input[type="file"] {
display: none;
}
.file-input label {
display: block;
margin-bottom: 10px;
color: var(--text-color);
}
.file-input label:hover {
background-color: var(--hover-bg);
}
button {
background-color: var(--success-color);
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:hover {
background-color: var(--success-hover);
}
button:disabled {
background-color: var(--border-color);
cursor: not-allowed;
}
.error {
color: var(--error-color);
margin: 10px 0;
}
.success {
color: var(--success-color);
margin: 10px 0;
}
.file-name {
margin: 10px 0;
color: var(--text-color);
}
/* 添加语言切换按钮样式 */
.lang-switch {
position: absolute;
top: 20px;
right: 20px;
}
.lang-btn {
background-color: #2196F3;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.lang-btn:hover {
background-color: #1976D2;
}
/* 添加下拉菜单样式 */
.fix-type-select {
margin: 20px 0;
width: 100%;
}
select {
width: 100%;
padding: 10px;
border-radius: 4px;
border: 1px solid var(--border-color);
background-color: var(--container-bg);
color: var(--text-color);
font-size: 16px;
}
select:focus {
outline: none;
border-color: var(--success-color);
}
/* 添加路径转换选项样式 */
.path-options {
margin: 15px 0;
padding: 10px;
border: 1px solid var(--border-color);
border-radius: 4px;
display: none; /* 默认隐藏 */
background-color: var(--container-bg);
}
.path-options.show {
display: block;
}
.path-option {
margin: 10px 0;
}
.path-option label {
display: inline-flex;
align-items: center;
cursor: pointer;
margin-right: 20px;
}
.path-option input[type="radio"] {
margin-right: 8px;
}
/* 修改头图容器样式 */
.header-image {
text-align: center;
margin: 20px auto 0; /* 上边距20px,左右自动居中,下边距0 */
max-width: 800px; /* 与container保持相同宽度 */
padding: 20px 20px 0; /* 上左右内边距20px,下内边距0 */
}
.header-image img {
max-width: 100%;
height: auto;
border-radius: 8px 8px 0 0;
box-shadow: 0 -2px 4px rgba(0,0,0,0.1);
transition: opacity 0.3s ease; /* 添加过渡效果 */
}
.header-image a {
display: block; /* 使链接块级化,完整覆盖图片区域 */
text-decoration: none; /* 移除链接下划线 */
cursor: pointer; /* 显示手型光标 */
}
.header-image a:hover img {
opacity: 0.9; /* 鼠标悬停时轻微透明效果 */
transition: opacity 0.3s ease; /* 平滑过渡效果 */
}
/* 整体包装容器 */
.wrapper {
background-color: var(--container-bg);
max-width: 800px;
margin: 20px auto;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* 添加拖拽区域样式 */
.file-drop-zone {
border: 2px dashed var(--drop-zone-border);
border-radius: 4px;
padding: 30px; /* 增加内边距使区域更大 */
text-align: center;
background-color: var(--drop-zone-bg);
cursor: pointer;
transition: all 0.3s ease;
color: var(--text-color);
}
.file-drop-zone.dragover {
border-color: var(--drop-zone-hover);
background-color: var(--drop-zone-hover-bg);
}
.file-drop-zone:hover {
border-color: var(--drop-zone-hover);
}
#dragText {
font-size: 16px; /* 调整文字大小 */
color: var(--text-color); /* 调整文字颜色 */
margin: 0; /* 移除外边距 */
}
.selected-files {
margin: 10px 0;
}
.file-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px;
margin: 5px 0;
background-color: var(--file-item-bg);
border-radius: 4px;
transition: all 0.3s ease;
color: var(--text-color);
}
.file-item:hover {
background-color: var(--file-item-hover);
}
.file-name {
flex-grow: 1;
margin-right: 10px;
}
.delete-btn {
cursor: pointer;
color: var(--text-color);
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s ease;
}
.delete-btn:hover {
color: var(--error-color);
background-color: rgba(255, 68, 68, 0.1);
}
/* 添加平滑过渡效果 */
* {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
</style>
</head>
<body>
<div class="wrapper">
<!-- 头图容器 -->
<div class="header-image">
<a href="https://github.com/jammyfu/ComfyUI_PaintingCoderUtils" target="_blank" title="Visit PaintingCoder on GitHub">
<img src="https://raw.githubusercontent.com/jammyfu/ComfyUI_PaintingCoderUtils/main/docs/images/workflow_fix_top_banner01.jpg" alt="Painting Coder Logo">
</a>
</div>
<!-- 主内容容器 -->
<div class="container">
<h1 id="title">PaintingCoder Workflow Fixer</h1>
<p id="description">This tool helps fix old workflow JSON files by adding the PaintingCoder namespace to node types.</p>
<!-- 添加功能选择下拉菜单 -->
<div class="fix-type-select">
<select id="fixType" onchange="updateUI()">
<option value="namespace">Fix PaintingCoder old version (<0.3.0) workflow</option>
<option value="path">Fix Path Separator</option>
</select>
</div>
<!-- 添加路径转换选项 -->
<div id="pathOptions" class="path-options">
<div class="path-option">
<label>
<input type="radio" name="pathType" value="auto" checked>
<span id="autoLabel">Auto Detect</span>
</label>
<label>
<input type="radio" name="pathType" value="toWindows">
<span id="toWindowsLabel">To Windows Format</span>
</label>
<label>
<input type="radio" name="pathType" value="toUnix">
<span id="toUnixLabel">To Unix Format (Linux/Mac)</span>
</label>
</div>
</div>
<div class="file-input">
<div id="dropZone" class="file-drop-zone">
<div id="dragText">Choose Workflow File or drag and drop file here</div>
<input type="file" id="fileInput" accept=".json" multiple onchange="handleFileSelect(event)">
</div>
<div id="selectedFiles" class="selected-files"></div>
</div>
<div>
<button id="fixButton" onclick="fixAndDownload()" disabled>Fix & Download</button>
</div>
<div id="error" class="error"></div>
<div id="success" class="success"></div>
</div>
</div>
<div class="lang-switch">
<button class="lang-btn" onclick="toggleLanguage()" id="langBtn">中文</button>
</div>
<!-- 页脚 -->
<footer style="text-align: center; padding: 20px; margin-top: 30px; border-top: 1px solid var(--border-color);">
<p id="version" style="color: var(--text-color); font-size: 0.9em; margin: 0;">Version 0.2.0</p>
<p style="margin-bottom: 5px;">© 2025 jammyfu. All rights reserved.</p>
</footer>
<script>
// 语言配置
const translations = {
en: {
title: "PaintingCoder Workflow Fixer",
description: {
namespace: "This tool helps fix old workflow JSON files by adding the PaintingCoder namespace to node types.",
path: "This tool helps convert path separators between Windows (\\) and Unix-style (Linux/Mac) (/) formats."
},
dragText: "Choose Workflow File or drag and drop file here",
fixButton: "Fix & Download",
selectedFile: "Selected file: ",
errorSelectFile: "Please select a file first.",
successMessage: "File fixed and saved as ",
switchLang: "中文",
fixTypeNamespace: "Fix PaintingCoder old version (<0.3.0) workflow",
fixTypePath: "Fix Path Separator",
pathOptions: {
auto: "Auto Detect",
toWindows: "To Windows Format (\\)",
toUnix: "To Unix Format (Linux/Mac) (/)"
},
errorJsonFile: "Please select a JSON file.",
selectedFiles: "Selected files:",
noFilesSelected: "No files selected",
removeFile: "Remove"
},
zh: {
title: "PaintingCoder 工作流修复工具",
description: {
namespace: "此工具可以通过添加 PaintingCoder 命名空间来修复旧的工作流 JSON 文件。",
path: "此工具可以在 Windows (\\) 和 Unix (Linux/Mac) (/) 格式之间转换路径分隔符。"
},
dragText: "选择工作流文件或将文件拖放到此处",
fixButton: "修复并下载",
selectedFile: "已选择文件:",
errorSelectFile: "请先选择一个文件。",
successMessage: "文件已修复并保存为 ",
switchLang: "English",
fixTypeNamespace: "修复PaintingCoder旧版本(<0.3.0)工作流",
fixTypePath: "修复路径分隔符",
pathOptions: {
auto: "自动检测",
toWindows: "转换为 Windows 格式 (\\)",
toUnix: "转换为 Unix 格式 (Linux/Mac) (/)"
},
errorJsonFile: "请选择JSON文件。",
selectedFiles: "已选择文件:",
noFilesSelected: "未选择文件",
removeFile: "移除"
}
};
let currentLanguage = 'en';
function toggleLanguage() {
currentLanguage = currentLanguage === 'en' ? 'zh' : 'en';
updateLanguage();
}
function updateLanguage() {
const t = translations[currentLanguage];
document.getElementById('title').textContent = t.title;
updateDescription();
document.getElementById('dragText').textContent = t.dragText;
document.getElementById('fixButton').textContent = t.fixButton;
document.getElementById('langBtn').textContent = t.switchLang;
// 更新下拉菜单选项
const select = document.getElementById('fixType');
select.options[0].text = t.fixTypeNamespace;
select.options[1].text = t.fixTypePath;
// 更新动态内容
if (currentFile) {
document.getElementById('fileName').textContent =
`${t.selectedFile}${currentFile.name}`;
}
// 更新错误和成功消息
const errorEl = document.getElementById('error');
if (errorEl.textContent === translations.en.errorSelectFile) {
errorEl.textContent = t.errorSelectFile;
} else if (errorEl.textContent === translations.en.errorJsonFile) {
errorEl.textContent = t.errorJsonFile;
}
const successEl = document.getElementById('success');
if (successEl.textContent.startsWith(translations.en.successMessage)) {
const fileName = successEl.textContent.replace(translations.en.successMessage, '');
successEl.textContent = t.successMessage + fileName;
}
// 更新路径选项文本
document.getElementById('autoLabel').textContent = t.pathOptions.auto;
document.getElementById('toWindowsLabel').textContent = t.pathOptions.toWindows;
document.getElementById('toUnixLabel').textContent = t.pathOptions.toUnix;
updateFileList(); // 更新文件列表的语言
// 更新头图
const headerImage = document.querySelector('.header-image img');
if (currentLanguage === 'zh') {
headerImage.src = "https://raw.githubusercontent.com/jammyfu/ComfyUI_PaintingCoderUtils/main/docs/images/workflow_fix_top_banner01.jpg";
} else {
headerImage.src = "https://raw.githubusercontent.com/jammyfu/ComfyUI_PaintingCoderUtils/main/docs/images/workflow_fix_top_banner01.jpg";
}
}
// 需要添加命名空间的节点映射
const nodeMapping = {
"MaskPreview": "PaintingCoder::MaskPreview",
"ImageSizeCreator": "PaintingCoder::ImageSizeCreator",
"ImageToBase64": "PaintingCoder::ImageToBase64",
"ImageLatentCreator": "PaintingCoder::ImageLatentCreator",
"DynamicImageCombiner": "PaintingCoder::DynamicImageCombiner",
"DynamicMaskCombiner": "PaintingCoder::DynamicMaskCombiner",
"ImageResolutionAdjuster": "PaintingCoder::ImageResolutionAdjuster",
"TextCombiner": "PaintingCoder::TextCombiner",
"ShowTextPlus": "PaintingCoder::ShowTextPlus",
"SimpleTextInput": "PaintingCoder::SimpleTextInput",
"MultilineTextInput": "PaintingCoder::MultilineTextInput",
"RemoveEmptyLinesAndLeadingSpaces": "PaintingCoder::RemoveEmptyLinesAndLeadingSpaces",
"ImageSwitch": "PaintingCoder::ImageSwitch",
"TextSwitch": "PaintingCoder::TextSwitch",
"MaskSwitch": "PaintingCoder::MaskSwitch",
"LatentSwitch": "PaintingCoder::LatentSwitch",
"WebImageLoader": "PaintingCoder::WebImageLoader"
};
let currentFile = null;
let selectedFiles = []; // 存储选中的文件
// 添加拖拽相关函数
const dropZone = document.getElementById('dropZone');
const dragText = document.getElementById('dragText');
// 添加拖拽事件处理
function handleDragOver(e) {
e.preventDefault();
e.stopPropagation();
dropZone.classList.add('dragover');
}
function handleDragLeave(e) {
e.preventDefault();
e.stopPropagation();
dropZone.classList.remove('dragover');
}
function handleDrop(e) {
e.preventDefault();
e.stopPropagation();
dropZone.classList.remove('dragover');
const files = e.dataTransfer.files;
for (let file of files) {
if (file.name.endsWith('.json')) {
// 检查是否已存在相同文件
if (!selectedFiles.some(f => f.name === file.name)) {
selectedFiles.push(file);
}
} else {
const t = translations[currentLanguage];
document.getElementById('error').textContent = t.errorJsonFile;
}
}
updateFileList();
}
// 统一处理文件的函数
function handleFile(file) {
currentFile = file;
const t = translations[currentLanguage];
document.getElementById('fileName').textContent = `${t.selectedFile}${file.name}`;
document.getElementById('fixButton').disabled = false;
document.getElementById('error').textContent = '';
document.getElementById('success').textContent = '';
}
// 修改文件选择处理函数
function handleFileSelect(event) {
const files = event.target.files;
for (let file of files) {
if (file.name.endsWith('.json')) {
// 检查是否已存在相同文件
if (!selectedFiles.some(f => f.name === file.name)) {
selectedFiles.push(file);
}
} else {
const t = translations[currentLanguage];
document.getElementById('error').textContent = t.errorJsonFile;
}
}
updateFileList();
event.target.value = ''; // 清空input,允许重复选择相同文件
}
// 修改点击事件处理
document.addEventListener('DOMContentLoaded', function() {
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
// 整个虚线框区域点击触发文件选择
dropZone.addEventListener('click', function() {
fileInput.click();
});
// 拖拽事件监听
dropZone.addEventListener('dragover', handleDragOver);
dropZone.addEventListener('dragleave', handleDragLeave);
dropZone.addEventListener('drop', handleDrop);
updateFileList();
});
function updateDescription() {
const fixType = document.getElementById('fixType').value;
const t = translations[currentLanguage];
document.getElementById('description').textContent = t.description[fixType];
}
function updateUI() {
const fixType = document.getElementById('fixType').value;
updateDescription();
// 显示/隐藏路径选项
const pathOptions = document.getElementById('pathOptions');
pathOptions.classList.toggle('show', fixType === 'path');
}
function updateFileList() {
const filesContainer = document.getElementById('selectedFiles');
const t = translations[currentLanguage];
if (selectedFiles.length === 0) {
filesContainer.innerHTML = `<div class="file-item"><span class="file-name">${t.noFilesSelected}</span></div>`;
document.getElementById('fixButton').disabled = true;
return;
}
filesContainer.innerHTML = selectedFiles.map((file, index) => `
<div class="file-item">
<span class="file-name">${file.name}</span>
<span class="delete-btn" onclick="removeFile(${index})">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
</span>
</div>
`).join('');
document.getElementById('fixButton').disabled = false;
}
function removeFile(index) {
selectedFiles.splice(index, 1);
updateFileList();
}
// 定义需要进行路径转换的节点类型
const PATH_CONVERSION_NODES = new Set([
"CheckpointLoaderSimple",
"LoraLoader",
"ControlNetLoader",
"UNETLoader",
"VAELoader",
"CLIPLoader",
"DiffControlNetLoader",
"ModelLoader",
"LoadDiffusionModel",
"CheckpointLoader",
"DiffusersLoader",
"LoraLoaderModelOnly",
"VAEDecode",
"VAEEncode",
"CLIPVisionLoader",
"StyleModelLoader",
"UpscaleModelLoader",
"Load LoRA",
"Lora List",
"LoRA Stacker",
"GridLoras",
"CheckpointLoader|pysssss",
"LoraLoader|pysssss",
"LoraSave",
"LoraLoader",
"DualCLIPLoader",
"DualUNETLoader",
"DualVAELoader",
"DualCLIPVisionLoader",
"DualStyleModelLoader",
"DualUpscaleModelLoader",
"LoraLoader|pysssss",
"LoraSave|pysssss",
"LoraSave",
"LoraList",
"LoRA Stacker",
"GridLoras",
"LoraSave|pysssss",
"LoraList|pysssss",
"LoRA Stacker|pysssss",
"GridLoras|pysssss"
]);
function isMarkdownOrBase64File(filename) {
return filename.endsWith('.md') ||
filename.endsWith('.markdown') ||
filename.includes('base64') ||
filename.includes('data:image');
}
function detectPathFormat(workflow) {
let unixCount = 0;
let windowsCount = 0;
Object.keys(workflow.nodes).forEach(nodeId => {
const node = workflow.nodes[nodeId];
const nodeType = node.type || node.class_type;
if (PATH_CONVERSION_NODES.has(nodeType) && node.widgets_values) {
node.widgets_values.forEach(value => {
if (typeof value === 'string' && !value.includes('base64')) {
const unixSlashes = (value.match(/\//g) || []).length;
const windowsSlashes = (value.match(/\\/g) || []).length;
if (unixSlashes > windowsSlashes) {
unixCount++;
} else if (windowsSlashes > unixSlashes) {
windowsCount++;
}
} else if (typeof value === 'object' && value?.content) {
if (!value.content.includes('base64')) {
const path = value.content;
const unixSlashes = (path.match(/\//g) || []).length;
const windowsSlashes = (path.match(/\\/g) || []).length;
if (unixSlashes > windowsSlashes) {
unixCount++;
} else if (windowsSlashes > unixSlashes) {
windowsCount++;
}
}
}
});
}
});
if (unixCount > windowsCount) {
return 'unix';
} else if (windowsCount > unixCount) {
return 'windows';
}
return null;
}
function fixAndDownload() {
const t = translations[currentLanguage];
if (selectedFiles.length === 0) {
document.getElementById('error').textContent = t.errorSelectFile;
return;
}
selectedFiles.forEach(file => {
if (isMarkdownOrBase64File(file.name)) {
console.log(`Skipping file: ${file.name} (markdown or base64)`);
return;
}
const reader = new FileReader();
reader.onload = function(e) {
try {
const workflow = JSON.parse(e.target.result);
const fixType = document.getElementById('fixType').value;
let conversionDirection = '';
if (fixType === 'path') {
const selectedPathType = document.querySelector('input[name="pathType"]:checked');
conversionDirection = selectedPathType ? selectedPathType.value : 'auto';
if (conversionDirection === 'auto') {
// 检测当前文件的路径格式
const currentFormat = detectPathFormat(workflow);
// 如果检测到当前格式,就转换为另一种格式
if (currentFormat === 'unix') {
conversionDirection = 'windows';
} else if (currentFormat === 'windows') {
conversionDirection = 'unix';
} else {
// 如果无法检测,则转换为unix格式(标准格式)
conversionDirection = 'unix';
}
console.log(`Auto detected format: ${currentFormat}, converting to: ${conversionDirection}`);
} else if (conversionDirection === 'toWindows') {
conversionDirection = 'windows';
} else if (conversionDirection === 'toUnix') {
conversionDirection = 'unix';
}
}
const fixedWorkflow = fixWorkflow(workflow, fixType, conversionDirection);
let newFileName = file.name;
if (fixType === 'namespace') {
newFileName = file.name.replace('.json', '_fixed.json');
} else if (fixType === 'path') {
newFileName = file.name.replace('.json', `_${conversionDirection}.json`);
}
const blob = new Blob([JSON.stringify(fixedWorkflow, null, 2)], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = newFileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
document.getElementById('success').textContent = t.successMessage + newFileName;
document.getElementById('error').textContent = '';
} catch (e) {
document.getElementById('error').textContent = `Error: ${e.message}`;
document.getElementById('success').textContent = '';
}
};
reader.readAsText(file);
});
}
// 添加深色模式检测和监听
function updateTheme() {
// 检测系统深色模式
const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', isDarkMode ? 'dark' : 'light');
}
// 监听系统主题变化
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme);
// 初始化主题
document.addEventListener('DOMContentLoaded', function() {
updateTheme();
updateUI();
});
function fixWorkflow(workflow, fixType, conversionDirection) {
try {
if (fixType === 'path') {
Object.keys(workflow.nodes).forEach(nodeId => {
const node = workflow.nodes[nodeId];
const nodeType = node.type || node.class_type;
// 如果是需要处理的节点类型
if (PATH_CONVERSION_NODES.has(nodeType)) {
// 处理 widgets_values 数组中的所有值
if (node.widgets_values && Array.isArray(node.widgets_values)) {
node.widgets_values = node.widgets_values.map(value => {
if (typeof value === 'string' && !value.includes('base64')) {
// 转换路径分隔符
if (conversionDirection === 'unix') {
return value.replace(/\\/g, '/');
} else if (conversionDirection === 'windows') {
return value.replace(/\//g, '\\');
}
} else if (typeof value === 'object' && value?.content) {
// 处理 pysssss 节点的特殊格式
if (!value.content.includes('base64')) {
const newContent = conversionDirection === 'unix'
? value.content.replace(/\\/g, '/')
: value.content.replace(/\//g, '\\');
return { ...value, content: newContent };
}
}
return value;
});
}
}
});
} else if (fixType === 'namespace') {
// ... 保持原有的命名空间修复逻辑不变 ...
}
return workflow;
} catch (e) {
throw new Error(`修复工作流时出错: ${e.message}`);
}
}
</script>
</body>
</html>