|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>ModelMuse - AI Model Converter</title> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<script src="https://unpkg.com/feather-icons"></script> |
|
|
<style> |
|
|
.dropzone { |
|
|
border: 2px dashed #9CA3AF; |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
.dropzone-active { |
|
|
border-color: #8B5CF6; |
|
|
background-color: #F3E8FF; |
|
|
} |
|
|
.progress-bar { |
|
|
transition: width 0.3s ease; |
|
|
} |
|
|
@keyframes pulse { |
|
|
0%, 100% { opacity: 1; } |
|
|
50% { opacity: 0.5; } |
|
|
} |
|
|
.pulse { |
|
|
animation: pulse 2s infinite; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="bg-gray-50 min-h-screen"> |
|
|
<div class="container mx-auto px-4 py-8 max-w-5xl"> |
|
|
<header class="mb-8 text-center"> |
|
|
<h1 class="text-4xl font-bold text-purple-700 mb-2">ModelMuse</h1> |
|
|
<p class="text-lg text-gray-600">Your AI Alchemy Wizard for converting Stable Diffusion models to Amuse-compatible ONNX/Olive</p> |
|
|
</header> |
|
|
|
|
|
<div class="bg-white rounded-xl shadow-lg overflow-hidden mb-8"> |
|
|
<div class="bg-gradient-to-r from-purple-600 to-indigo-600 p-4 text-white"> |
|
|
<h2 class="text-xl font-bold">Model Converter</h2> |
|
|
</div> |
|
|
|
|
|
<div class="p-6"> |
|
|
|
|
|
<div class="mb-8"> |
|
|
<h3 class="text-lg font-semibold text-gray-800 mb-3">Input Model</h3> |
|
|
<div id="dropzone" class="dropzone rounded-lg p-8 text-center cursor-pointer mb-4"> |
|
|
<div class="flex flex-col items-center justify-center"> |
|
|
<i data-feather="upload-cloud" class="w-12 h-12 text-gray-400 mb-3"></i> |
|
|
<p class="text-gray-600">Drag & drop your model file here (.ckpt, .safetensors)</p> |
|
|
<p class="text-sm text-gray-500 mt-1">or</p> |
|
|
<button id="browse-btn" class="mt-2 bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition-colors"> |
|
|
Browse Files |
|
|
</button> |
|
|
</div> |
|
|
<input type="file" id="file-input" class="hidden" accept=".ckpt,.safetensors"> |
|
|
</div> |
|
|
<div id="selected-files" class="hidden"> |
|
|
<h4 class="text-sm font-medium text-gray-700 mb-2">Selected Files:</h4> |
|
|
<div id="file-list" class="space-y-2"></div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="mb-8"> |
|
|
<h3 class="text-lg font-semibold text-gray-800 mb-3">Conversion Options</h3> |
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Target Hardware</label> |
|
|
<select class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-purple-500 focus:border-purple-500"> |
|
|
<option value="directml">DirectML (Windows Default)</option> |
|
|
<option value="cuda">CUDA (NVIDIA GPU)</option> |
|
|
<option value="cpu">CPU (OpenVINO)</option> |
|
|
</select> |
|
|
</div> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Precision</label> |
|
|
<select class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-purple-500 focus:border-purple-500"> |
|
|
<option value="fp16">FP16 (Recommended)</option> |
|
|
<option value="fp32">FP32 (Most Compatible)</option> |
|
|
<option value="int8">INT8 (Quantized, Experimental)</option> |
|
|
</select> |
|
|
</div> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Output Directory</label> |
|
|
<div class="flex"> |
|
|
<input type="text" id="output-dir" class="flex-grow border border-gray-300 rounded-l-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-500" placeholder="Select output folder" readonly> |
|
|
<button id="output-dir-btn" class="bg-purple-600 text-white px-4 py-2 rounded-r-lg hover:bg-purple-700 transition-colors"> |
|
|
<i data-feather="folder"></i> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="flex items-end"> |
|
|
<button id="advanced-btn" class="text-purple-600 hover:text-purple-800 flex items-center"> |
|
|
<i data-feather="settings" class="w-4 h-4 mr-2"></i> |
|
|
Advanced Options |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-8"> |
|
|
<div class="flex"> |
|
|
<div class="flex-shrink-0"> |
|
|
<i data-feather="alert-triangle" class="w-5 h-5 text-yellow-400"></i> |
|
|
</div> |
|
|
<div class="ml-3"> |
|
|
<h3 class="text-sm font-medium text-yellow-800">License Compliance</h3> |
|
|
<div class="mt-2 text-sm text-yellow-700"> |
|
|
<p>You may only convert models for which you have the appropriate rights. Please check the model license before proceeding.</p> |
|
|
</div> |
|
|
<div class="mt-3"> |
|
|
<div class="flex items-center"> |
|
|
<input id="license-checkbox" type="checkbox" class="h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded"> |
|
|
<label for="license-checkbox" class="ml-2 block text-sm text-gray-700"> |
|
|
I confirm I have the rights to convert this model |
|
|
</label> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="flex justify-between"> |
|
|
<button id="cli-btn" class="flex items-center text-gray-600 hover:text-gray-800"> |
|
|
<i data-feather="terminal" class="w-4 h-4 mr-2"></i> |
|
|
CLI Command Builder |
|
|
</button> |
|
|
<button id="convert-btn" class="bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-6 py-3 rounded-lg font-medium hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed" disabled> |
|
|
Convert Model |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="progress-section" class="bg-white rounded-xl shadow-lg overflow-hidden hidden mb-8"> |
|
|
<div class="bg-gradient-to-r from-purple-600 to-indigo-600 p-4 text-white"> |
|
|
<h2 class="text-xl font-bold">Conversion Progress</h2> |
|
|
</div> |
|
|
<div class="p-6"> |
|
|
<div class="mb-4"> |
|
|
<div class="flex justify-between mb-1"> |
|
|
<span class="text-sm font-medium text-gray-700">Processing</span> |
|
|
<span id="progress-percent" class="text-sm font-medium text-gray-700">0%</span> |
|
|
</div> |
|
|
<div class="w-full bg-gray-200 rounded-full h-2.5"> |
|
|
<div id="progress-bar" class="progress-bar bg-purple-600 h-2.5 rounded-full" style="width: 0%"></div> |
|
|
</div> |
|
|
</div> |
|
|
<div id="progress-steps" class="space-y-3"> |
|
|
|
|
|
</div> |
|
|
<div id="logs-container" class="mt-4 bg-gray-50 p-3 rounded-lg max-h-48 overflow-y-auto hidden"> |
|
|
<pre id="conversion-logs" class="text-xs font-mono text-gray-700"></pre> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="results-section" class="bg-white rounded-xl shadow-lg overflow-hidden hidden"> |
|
|
<div class="bg-gradient-to-r from-green-600 to-emerald-600 p-4 text-white"> |
|
|
<h2 class="text-xl font-bold">Conversion Complete</h2> |
|
|
</div> |
|
|
<div class="p-6"> |
|
|
<div class="flex items-center mb-4"> |
|
|
<div class="bg-green-100 p-3 rounded-full mr-4"> |
|
|
<i data-feather="check-circle" class="w-6 h-6 text-green-600"></i> |
|
|
</div> |
|
|
<div> |
|
|
<h3 class="text-lg font-medium text-gray-800">Successfully converted model</h3> |
|
|
<p class="text-sm text-gray-600">Your model is now ready for use with Amuse</p> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6"> |
|
|
<div class="border border-gray-200 rounded-lg p-4"> |
|
|
<h4 class="text-sm font-medium text-gray-700 mb-2">Output Files</h4> |
|
|
<ul id="output-files" class="text-sm text-gray-600 space-y-1"> |
|
|
<li class="flex items-center"><i data-feather="file" class="w-4 h-4 mr-2 text-purple-500"></i> model.onnx</li> |
|
|
<li class="flex items-center"><i data-feather="file" class="w-4 h-4 mr-2 text-purple-500"></i> metadata.json</li> |
|
|
<li class="flex items-center"><i data-feather="file" class="w-4 h-4 mr-2 text-purple-500"></i> olive_config.json</li> |
|
|
</ul> |
|
|
</div> |
|
|
<div class="border border-gray-200 rounded-lg p-4"> |
|
|
<h4 class="text-sm font-medium text-gray-700 mb-2">Model Info</h4> |
|
|
<dl class="text-sm text-gray-600 space-y-1"> |
|
|
<div class="flex justify-between"> |
|
|
<dt>Original Model:</dt> |
|
|
<dd id="original-model" class="font-medium">model.safetensors</dd> |
|
|
</div> |
|
|
<div class="flex justify-between"> |
|
|
<dt>Format:</dt> |
|
|
<dd id="output-format" class="font-medium">ONNX + Olive</dd> |
|
|
</div> |
|
|
<div class="flex justify-between"> |
|
|
<dt>Precision:</dt> |
|
|
<dd id="output-precision" class="font-medium">FP16</dd> |
|
|
</div> |
|
|
<div class="flex justify-between"> |
|
|
<dt>Target:</dt> |
|
|
<dd id="output-target" class="font-medium">DirectML</dd> |
|
|
</div> |
|
|
</dl> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="flex justify-end space-x-3"> |
|
|
<button id="open-folder-btn" class="flex items-center border border-gray-300 px-4 py-2 rounded-lg text-gray-700 hover:bg-gray-50"> |
|
|
<i data-feather="folder" class="w-4 h-4 mr-2"></i> |
|
|
Open Output Folder |
|
|
</button> |
|
|
<button id="create-installer-btn" class="flex items-center bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700"> |
|
|
<i data-feather="package" class="w-4 h-4 mr-2"></i> |
|
|
Create Windows Installer |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="cli-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> |
|
|
<div class="bg-white rounded-lg shadow-xl w-full max-w-2xl"> |
|
|
<div class="border-b border-gray-200 px-6 py-4"> |
|
|
<h3 class="text-lg font-semibold text-gray-800">CLI Command Builder</h3> |
|
|
</div> |
|
|
<div class="p-6"> |
|
|
<p class="text-sm text-gray-600 mb-4">Use this command to run the conversion from command line:</p> |
|
|
<div class="bg-gray-800 rounded-lg p-4 mb-4"> |
|
|
<pre id="cli-command" class="text-green-400 font-mono text-sm overflow-x-auto">modelmuse --input model.safetensors --output ./output --target directml --precision fp16</pre> |
|
|
</div> |
|
|
<p class="text-xs text-gray-500 mb-4">Note: You can also run batch conversions by specifying multiple input files or directories.</p> |
|
|
<div class="flex justify-end"> |
|
|
<button id="copy-cli-btn" class="bg-purple-600 text-white px-4 py-2 rounded-lg mr-3 hover:bg-purple-700"> |
|
|
Copy Command |
|
|
</button> |
|
|
<button id="close-cli-btn" class="border border-gray-300 px-4 py-2 rounded-lg text-gray-700 hover:bg-gray-50"> |
|
|
Close |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
feather.replace(); |
|
|
|
|
|
|
|
|
const dropzone = document.getElementById('dropzone'); |
|
|
const fileInput = document.getElementById('file-input'); |
|
|
const browseBtn = document.getElementById('browse-btn'); |
|
|
const selectedFilesDiv = document.getElementById('selected-files'); |
|
|
const fileList = document.getElementById('file-list'); |
|
|
const licenseCheckbox = document.getElementById('license-checkbox'); |
|
|
const convertBtn = document.getElementById('convert-btn'); |
|
|
const progressSection = document.getElementById('progress-section'); |
|
|
const resultsSection = document.getElementById('results-section'); |
|
|
const cliBtn = document.getElementById('cli-btn'); |
|
|
const cliModal = document.getElementById('cli-modal'); |
|
|
const closeCliBtn = document.getElementById('close-cli-btn'); |
|
|
const copyCliBtn = document.getElementById('copy-cli-btn'); |
|
|
|
|
|
|
|
|
browseBtn.addEventListener('click', () => fileInput.click()); |
|
|
|
|
|
fileInput.addEventListener('change', (e) => { |
|
|
handleFiles(e.target.files); |
|
|
}); |
|
|
|
|
|
dropzone.addEventListener('dragover', (e) => { |
|
|
e.preventDefault(); |
|
|
dropzone.classList.add('dropzone-active'); |
|
|
}); |
|
|
|
|
|
['dragleave', 'dragend'].forEach(type => { |
|
|
dropzone.addEventListener(type, () => { |
|
|
dropzone.classList.remove('dropzone-active'); |
|
|
}); |
|
|
}); |
|
|
|
|
|
dropzone.addEventListener('drop', (e) => { |
|
|
e.preventDefault(); |
|
|
dropzone.classList.remove('dropzone-active'); |
|
|
|
|
|
if (e.dataTransfer.files.length) { |
|
|
handleFiles(e.dataTransfer.files); |
|
|
} |
|
|
}); |
|
|
|
|
|
licenseCheckbox.addEventListener('change', () => { |
|
|
convertBtn.disabled = !licenseCheckbox.checked; |
|
|
}); |
|
|
|
|
|
convertBtn.addEventListener('click', () => { |
|
|
startConversion(); |
|
|
}); |
|
|
|
|
|
cliBtn.addEventListener('click', () => { |
|
|
cliModal.classList.remove('hidden'); |
|
|
}); |
|
|
|
|
|
closeCliBtn.addEventListener('click', () => { |
|
|
cliModal.classList.add('hidden'); |
|
|
}); |
|
|
|
|
|
copyCliBtn.addEventListener('click', () => { |
|
|
const cliCommand = document.getElementById('cli-command').textContent; |
|
|
navigator.clipboard.writeText(cliCommand).then(() => { |
|
|
copyCliBtn.textContent = 'Copied!'; |
|
|
setTimeout(() => { |
|
|
copyCliBtn.textContent = 'Copy Command'; |
|
|
}, 2000); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
function handleFiles(files) { |
|
|
fileList.innerHTML = ''; |
|
|
|
|
|
for (const file of files) { |
|
|
const fileItem = document.createElement('div'); |
|
|
fileItem.className = 'flex items-center justify-between p-2 bg-gray-50 rounded-lg'; |
|
|
fileItem.innerHTML = ` |
|
|
<div class="flex items-center"> |
|
|
<i data-feather="file" class="w-4 h-4 mr-2 text-purple-500"></i> |
|
|
<span class="text-sm">${file.name}</span> |
|
|
</div> |
|
|
<span class="text-xs text-gray-500">${formatFileSize(file.size)}</span> |
|
|
`; |
|
|
fileList.appendChild(fileItem); |
|
|
} |
|
|
|
|
|
selectedFilesDiv.classList.remove('hidden'); |
|
|
feather.replace(); |
|
|
} |
|
|
|
|
|
function formatFileSize(bytes) { |
|
|
if (bytes === 0) return '0 Bytes'; |
|
|
const k = 1024; |
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB']; |
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k)); |
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
|
|
} |
|
|
|
|
|
function startConversion() { |
|
|
progressSection.classList.remove('hidden'); |
|
|
|
|
|
|
|
|
const progressBar = document.getElementById('progress-bar'); |
|
|
const progressPercent = document.getElementById('progress-percent'); |
|
|
const progressSteps = document.getElementById('progress-steps'); |
|
|
|
|
|
const steps = [ |
|
|
{ name: 'Detecting model type', icon: 'search' }, |
|
|
{ name: 'Loading model weights', icon: 'loader' }, |
|
|
{ name: 'Exporting to ONNX', icon: 'download' }, |
|
|
{ name: 'Running Olive optimization', icon: 'cpu' }, |
|
|
{ name: 'Validating outputs', icon: 'check-circle' }, |
|
|
{ name: 'Saving artifacts', icon: 'save' } |
|
|
]; |
|
|
|
|
|
progressSteps.innerHTML = steps.map(step => ` |
|
|
<div class="flex items-center"> |
|
|
<div class="flex-shrink-0 mr-3"> |
|
|
<i data-feather="${step.icon}" class="w-4 h-4 text-purple-500"></i> |
|
|
</div> |
|
|
<div class="flex-grow"> |
|
|
<p class="text-sm font-medium text-gray-700">${step.name}</p> |
|
|
<div class="w-full bg-gray-200 rounded-full h-1.5 mt-1"> |
|
|
<div class="bg-purple-600 h-1.5 rounded-full progress-step" style="width: 0%"></div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
`).join(''); |
|
|
|
|
|
feather.replace(); |
|
|
|
|
|
let progress = 0; |
|
|
const interval = setInterval(() => { |
|
|
progress += Math.random() * 5; |
|
|
if (progress > 100) progress = 100; |
|
|
|
|
|
progressBar.style.width = `${progress}%`; |
|
|
progressPercent.textContent = `${Math.floor(progress)}%`; |
|
|
|
|
|
const stepProgresses = document.querySelectorAll('.progress-step'); |
|
|
steps.forEach((_, i) => { |
|
|
const stepProgress = Math.min(100, Math.max(0, (progress - (i * (100 / steps.length))) * steps.length)); |
|
|
if (stepProgresses[i]) { |
|
|
stepProgresses[i].style.width = `${stepProgress}%`; |
|
|
} |
|
|
}); |
|
|
|
|
|
if (progress === 100) { |
|
|
clearInterval(interval); |
|
|
setTimeout(() => { |
|
|
progressSection.classList.add('hidden'); |
|
|
resultsSection.classList.remove('hidden'); |
|
|
}, 500); |
|
|
} |
|
|
}, 300); |
|
|
} |
|
|
</script> |
|
|
</body> |
|
|
</html> |