Urabewe-LoRA-Collection / dataset_builder /musubi_dataset_builder.html
Urabewe's picture
Upload dataset_builder/musubi_dataset_builder.html
c721c42 verified
Raw
History Blame Contribute Delete
32.2 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>musubi-tuner dataset builder</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<style>
:root{
--bg:#14171a; --panel:#1c2024; --panel-2:#22262b; --line:#2e343a;
--text:#d8dee3; --dim:#7d8891; --accent:#62c9a6; --accent-dim:#3a6b58;
--warn:#e0a458; --danger:#d9776a;
--mono:"IBM Plex Mono","SF Mono","JetBrains Mono",Menlo,Consolas,monospace;
}
* { box-sizing:border-box; }
body { margin:0; background:var(--bg); color:var(--text); font-family:var(--mono); font-size:13px; }
.app { display:grid; grid-template-columns:300px 1fr; height:100vh; }
@media (max-width:860px){ .app{ grid-template-columns:1fr; height:auto; } }
/* ---- sidebar ---- */
.sidebar { background:var(--panel); border-right:1px solid var(--line); overflow-y:auto; padding:18px 16px 40px; }
h1 { font-size:14px; margin:0 0 3px; letter-spacing:.02em; }
.subtitle { color:var(--dim); font-size:10.5px; margin:0 0 18px; line-height:1.5; }
.section { border:1px solid var(--line); background:var(--panel-2); border-radius:6px; padding:14px 14px; margin-bottom:14px; }
.section-title { font-size:10px; text-transform:uppercase; letter-spacing:.08em; color:var(--accent); margin:0 0 12px; display:flex; align-items:center; gap:8px; }
.section-title::after{ content:""; flex:1; height:1px; background:var(--line); }
label { display:block; font-size:10.5px; color:var(--dim); margin-bottom:4px; }
.field{ margin-bottom:10px; }
.row{ display:grid; grid-template-columns:1fr 1fr; gap:10px; }
input[type=text], input[type=number], select, textarea {
width:100%; background:#181b1e; border:1px solid var(--line); color:var(--text);
border-radius:4px; padding:6px 8px; font-family:var(--mono); font-size:12px; outline:none;
}
input:focus, select:focus, textarea:focus{ border-color:var(--accent-dim); }
input::placeholder{ color:#4d565f; }
.hint{ color:#4d565f; font-size:9.5px; margin-top:3px; line-height:1.4; }
.checkrow{ display:flex; align-items:center; gap:7px; margin-bottom:9px; }
.checkrow input{ accent-color:var(--accent); width:14px; height:14px; }
.checkrow label{ margin:0; color:var(--text); font-size:11.5px; }
select{
appearance:none;
background-image:linear-gradient(45deg,transparent 50%,var(--dim) 50%),linear-gradient(135deg,var(--dim) 50%,transparent 50%);
background-position:calc(100% - 14px) center, calc(100% - 9px) center;
background-size:5px 5px,5px 5px; background-repeat:no-repeat; padding-right:26px;
}
.dataset-tab{
display:flex; align-items:center; gap:8px; padding:8px 10px; border-radius:5px;
border:1px solid var(--line); margin-bottom:6px; cursor:pointer; font-size:11.5px;
}
.dataset-tab.active{ border-color:var(--accent-dim); background:#1a2420; }
.dataset-tab .badge{ font-size:9px; text-transform:uppercase; letter-spacing:.05em; padding:1px 6px; border-radius:3px; background:var(--accent); color:var(--bg); font-weight:600; }
.dataset-tab .badge.video{ background:#8fa8e0; }
.dataset-tab .name{ flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.dataset-tab .count{ color:var(--dim); font-size:10px; }
.dataset-tab .del{ color:var(--dim); font-size:12px; padding:0 3px; }
.dataset-tab .del:hover{ color:var(--danger); }
.add-btn{ width:100%; background:none; border:1px dashed var(--line); color:var(--dim); border-radius:5px; padding:8px; cursor:pointer; font-family:var(--mono); font-size:11.5px; margin-top:2px; }
.add-btn:hover{ border-color:var(--accent-dim); color:var(--accent); }
/* ---- main ---- */
.main{ display:flex; flex-direction:column; min-height:0; }
.toolbar{ display:flex; align-items:center; gap:10px; padding:12px 18px; border-bottom:1px solid var(--line); background:var(--panel); }
.toolbar .title{ font-size:12px; color:var(--dim); }
.toolbar .title strong{ color:var(--text); }
.spacer{ flex:1; }
.tbtn{ background:var(--panel-2); border:1px solid var(--line); color:var(--text); border-radius:4px; padding:6px 12px; font-size:11px; cursor:pointer; font-family:var(--mono); }
.tbtn:hover{ border-color:var(--accent-dim); color:var(--accent); }
.tbtn.primary{ background:var(--accent); color:#0f1412; border-color:var(--accent); font-weight:600; }
.tbtn.primary:hover{ opacity:.9; color:#0f1412; }
.content{ flex:1; overflow-y:auto; padding:18px; position:relative; }
.dropzone{
border:2px dashed var(--line); border-radius:10px; min-height:260px;
display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px;
color:var(--dim); text-align:center; padding:30px;
}
.dropzone.drag{ border-color:var(--accent); background:rgba(98,201,166,0.06); color:var(--accent); }
.dropzone .big{ font-size:13px; }
.dropzone .small{ font-size:10.5px; color:#4d565f; }
.dz-actions{ display:flex; gap:8px; margin-top:6px; }
.grid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(130px,1fr)); gap:10px; }
.thumb{
position:relative; aspect-ratio:1/1; border-radius:6px; overflow:hidden; cursor:pointer;
border:1px solid var(--line); background:#0f1214;
}
.thumb img, .thumb video{ width:100%; height:100%; object-fit:cover; display:block; }
.thumb .cap-flag{
position:absolute; bottom:0; left:0; right:0; padding:4px 6px; font-size:9px;
background:linear-gradient(transparent, rgba(0,0,0,.85)); color:#fff; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.thumb .no-cap{ color:#e0a458; }
.thumb .vidmark{ position:absolute; top:5px; right:5px; background:rgba(0,0,0,.6); color:#fff; font-size:9px; padding:1px 5px; border-radius:3px; }
.thumb:hover{ border-color:var(--accent-dim); }
/* ---- lightbox ---- */
.lightbox{ position:fixed; inset:0; background:rgba(8,9,10,.92); z-index:50; display:none; }
.lightbox.open{ display:flex; }
.lb-inner{ margin:auto; display:flex; width:92%; height:88%; gap:18px; }
.lb-media{ flex:1.4; display:flex; align-items:center; justify-content:center; background:#0b0d0e; border-radius:8px; overflow:hidden; border:1px solid var(--line); position:relative; }
.lb-media img, .lb-media video{ max-width:100%; max-height:100%; object-fit:contain; }
.lb-nav{ position:absolute; top:50%; transform:translateY(-50%); background:rgba(0,0,0,.5); border:none; color:#fff; font-size:20px; width:38px; height:38px; border-radius:50%; cursor:pointer; }
.lb-nav.prev{ left:12px; } .lb-nav.next{ right:12px; }
.lb-panel{ flex:1; background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:18px; display:flex; flex-direction:column; }
.lb-panel .fname{ font-size:11.5px; color:var(--dim); margin-bottom:10px; word-break:break-all; }
.lb-panel textarea{ flex:1; resize:none; font-size:13px; line-height:1.6; }
.lb-panel .lb-footer{ display:flex; justify-content:space-between; align-items:center; margin-top:12px; }
.lb-close{ position:absolute; top:16px; right:22px; background:none; border:none; color:var(--dim); font-size:26px; cursor:pointer; }
.lb-close:hover{ color:var(--text); }
.lb-count{ color:var(--dim); font-size:11px; }
/* ---- export drawer ---- */
.drawer{ border-top:1px solid var(--line); background:#0f1214; max-height:0; overflow:hidden; transition:max-height .18s ease; }
.drawer.open{ max-height:420px; }
.drawer-inner{ padding:16px 18px; display:flex; gap:16px; }
pre#output{ flex:1.4; margin:0; background:#0b0d0e; border:1px solid var(--line); border-radius:6px; padding:14px 16px; font-size:11.5px; line-height:1.6; max-height:380px; overflow:auto; white-space:pre-wrap; word-break:break-word; }
.c-comment{ color:#5c6670; } .c-section{ color:var(--accent); } .c-key{ color:#8fb3e0; }
.c-str{ color:var(--warn); } .c-num{ color:#b48ee0; } .c-bool{ color:#d9776a; }
.export-side{ flex:1; display:flex; flex-direction:column; gap:8px; }
.export-side .warn{ font-size:10.5px; color:var(--dim); background:var(--panel-2); border:1px solid var(--line); border-radius:6px; padding:10px 12px; }
.export-side .warn strong{ color:var(--warn); }
.export-note{ font-size:9.5px; color:#4d565f; line-height:1.5; }
input[type=file]{ display:none; }
</style>
</head>
<body>
<div class="app">
<div class="sidebar">
<h1>musubi-tuner dataset builder</h1>
<p class="subtitle">Drop images/videos in, click each one to write its caption, then export dataset_config.toml + caption files.</p>
<div class="section">
<div class="section-title">general</div>
<div class="row">
<div class="field"><label>resolution width</label><input type="text" id="res_w" value="960"></div>
<div class="field"><label>resolution height</label><input type="text" id="res_h" value="544"></div>
</div>
<div class="row">
<div class="field"><label>caption extension</label><input type="text" id="caption_ext" value=".txt"></div>
<div class="field"><label>batch size</label><input type="text" id="batch_size" value="1"></div>
</div>
<div class="field"><label>num_repeats</label><input type="text" id="num_repeats" value="1"></div>
<div class="checkrow"><input type="checkbox" id="enable_bucket" checked><label for="enable_bucket">enable_bucket</label></div>
<div class="checkrow"><input type="checkbox" id="bucket_no_upscale"><label for="bucket_no_upscale">bucket_no_upscale</label></div>
</div>
<div class="section">
<div class="section-title">datasets</div>
<div id="dataset_list"></div>
<button class="add-btn" id="add_dataset_btn">+ new dataset</button>
</div>
<div class="section" id="active_settings_section" style="display:none;">
<div class="section-title">selected dataset</div>
<div class="field">
<label>path on your machine (browsers can't see real folder paths — type it in)</label>
<input type="text" id="ds_path" placeholder="/path/to/folder">
</div>
<div class="field">
<label>cache_directory <span class="hint" style="display:inline">(required if using jsonl; otherwise optional)</span></label>
<input type="text" id="ds_cache" placeholder="/path/to/cache_dir">
</div>
<div class="field">
<label>control_directory (optional — edit/control models)</label>
<input type="text" id="ds_control" placeholder="leave blank if unused">
</div>
<div class="row">
<div class="field"><label>type</label>
<select id="ds_type">
<option value="image">image</option>
<option value="video">video</option>
</select>
</div>
<div class="field"><label>num_repeats override</label><input type="text" id="ds_repeats" placeholder="inherit"></div>
</div>
<div id="video_fields" style="display:none;">
<div class="field"><label>target_frames (comma-separated)</label><input type="text" id="ds_target_frames" value="1, 25, 45"></div>
<div class="field"><label>frame_extraction</label>
<select id="ds_frame_extraction">
<option value="head">head</option>
<option value="chunk">chunk</option>
<option value="slide">slide</option>
<option value="uniform">uniform</option>
<option value="full">full</option>
</select>
</div>
<div class="field" id="ds_stride_field" style="display:none;"><label>frame_stride</label><input type="text" id="ds_frame_stride" value="10"></div>
<div class="field" id="ds_sample_field" style="display:none;"><label>frame_sample</label><input type="text" id="ds_frame_sample" value="4"></div>
</div>
</div>
</div>
<div class="main">
<div class="toolbar">
<span class="title" id="toolbar_title">no dataset selected</span>
<div class="spacer"></div>
<button class="tbtn" id="toggle_drawer_btn">preview &amp; export</button>
</div>
<div class="content" id="content">
<div class="dropzone" id="dropzone">
<div class="big">create a dataset on the left, then drag &amp; drop images or videos here</div>
<div class="small">or drop a whole folder — subfolders are scanned too. matching .txt files are picked up as existing captions.</div>
<div class="dz-actions">
<button class="tbtn" id="choose_folder_btn">choose folder</button>
<button class="tbtn" id="choose_files_btn">choose files</button>
</div>
</div>
<div class="grid" id="grid" style="display:none;"></div>
</div>
<div class="drawer" id="drawer">
<div class="drawer-inner">
<pre id="output"></pre>
<div class="export-side">
<div class="warn" id="warn_box"></div>
<button class="tbtn" id="copy_toml_btn">copy toml</button>
<button class="tbtn" id="download_toml_btn">download toml only</button>
<button class="tbtn primary" id="download_zip_btn">download everything (.zip)</button>
<button class="tbtn" id="save_disk_btn">save captions directly to folder</button>
<div class="export-note">"Save directly to folder" uses your browser's file system access — pick the same folder your images live in. Falls back to the zip if your browser/embed doesn't support it.</div>
</div>
</div>
</div>
</div>
</div>
<input type="file" id="folder_input" webkitdirectory directory multiple>
<input type="file" id="files_input" multiple accept="image/*,video/*">
<div class="lightbox" id="lightbox">
<button class="lb-close" id="lb_close">&times;</button>
<div class="lb-inner">
<div class="lb-media">
<button class="lb-nav prev" id="lb_prev">&#8249;</button>
<div id="lb_media_slot"></div>
<button class="lb-nav next" id="lb_next">&#8250;</button>
</div>
<div class="lb-panel">
<div class="fname" id="lb_fname"></div>
<textarea id="lb_caption" placeholder="type the caption for this file..."></textarea>
<div class="lb-footer">
<span class="lb-count" id="lb_count"></span>
<span class="lb-count" id="lb_saved">saved</span>
</div>
</div>
</div>
</div>
<script>
const IMAGE_EXT = ['jpg','jpeg','png','webp','bmp','gif'];
const VIDEO_EXT = ['mp4','mov','webm','mkv','avi','m4v'];
let datasets = [];
let idSeq = 0;
let activeDatasetId = null;
let lbIndex = null;
function extOf(name){ const p = name.split('.'); return p.length>1 ? p.pop().toLowerCase() : ''; }
function baseName(name){ return name.replace(/\.[^/.]+$/, ''); }
function escapeHtml(s){ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function activeDataset(){ return datasets.find(d => d.id === activeDatasetId) || null; }
function newDataset(name){
idSeq += 1;
return {
id: idSeq, name: name || ('dataset-' + idSeq), type: 'image',
path: '', cache: '', control: '', repeats: '',
target_frames: '1, 25, 45', frame_extraction: 'head', frame_stride: '10', frame_sample: '4',
images: []
};
}
/* ---------- file collection (drag/drop directory traversal + input fallback) ---------- */
function traverseEntry(entry) {
return new Promise((resolve) => {
if (entry.isFile) {
entry.file(file => resolve([file]), () => resolve([]));
} else if (entry.isDirectory) {
const reader = entry.createReader();
let all = [];
function readBatch() {
reader.readEntries(async (entries) => {
if (!entries.length) {
const nested = await Promise.all(all.map(e => traverseEntry(e)));
resolve(nested.flat());
return;
}
all = all.concat(entries);
readBatch();
}, () => resolve([]));
}
readBatch();
} else {
resolve([]);
}
});
}
async function filesFromDataTransfer(dt) {
const items = dt.items;
if (items && items.length && items[0].webkitGetAsEntry) {
const entries = [];
for (let i=0; i<items.length; i++) {
const entry = items[i].webkitGetAsEntry();
if (entry) entries.push(entry);
}
const nested = await Promise.all(entries.map(e => traverseEntry(e)));
return nested.flat();
}
return Array.from(dt.files || []);
}
async function ingestFiles(fileList) {
let ds = activeDataset();
if (!ds) {
ds = newDataset();
datasets.push(ds);
activeDatasetId = ds.id;
}
const files = Array.from(fileList);
const captionTexts = {};
const mediaFiles = [];
await Promise.all(files.map(async f => {
const ext = extOf(f.name);
if (ext === 'txt') {
const text = await f.text();
captionTexts[baseName(f.name)] = text;
} else if (IMAGE_EXT.includes(ext) || VIDEO_EXT.includes(ext)) {
mediaFiles.push(f);
}
}));
let imgCount = 0, vidCount = 0;
mediaFiles.forEach(f => {
const isVideo = VIDEO_EXT.includes(extOf(f.name));
if (isVideo) vidCount++; else imgCount++;
idSeq += 1;
ds.images.push({
id: idSeq,
name: f.name,
file: f,
url: URL.createObjectURL(f),
caption: captionTexts[baseName(f.name)] || '',
isVideo
});
});
if (vidCount > imgCount) ds.type = 'video';
renderAll();
}
/* ---------- rendering ---------- */
function renderDatasetList() {
const wrap = document.getElementById('dataset_list');
wrap.innerHTML = '';
datasets.forEach(ds => {
const tab = document.createElement('div');
tab.className = 'dataset-tab' + (ds.id === activeDatasetId ? ' active' : '');
tab.innerHTML = `
<span class="badge ${ds.type === 'video' ? 'video' : ''}">${ds.type}</span>
<span class="name">${escapeHtml(ds.name)}</span>
<span class="count">${ds.images.length}</span>
<span class="del" title="remove dataset">&times;</span>
`;
tab.addEventListener('click', (e) => {
if (e.target.classList.contains('del')) {
datasets = datasets.filter(d => d.id !== ds.id);
if (activeDatasetId === ds.id) activeDatasetId = datasets.length ? datasets[0].id : null;
renderAll();
return;
}
activeDatasetId = ds.id;
renderAll();
});
wrap.appendChild(tab);
});
}
function renderActiveSettings() {
const section = document.getElementById('active_settings_section');
const ds = activeDataset();
document.getElementById('toolbar_title').innerHTML = ds
? `<strong>${escapeHtml(ds.name)}</strong> &nbsp;·&nbsp; ${ds.images.length} file${ds.images.length===1?'':'s'}`
: 'no dataset selected';
if (!ds) { section.style.display = 'none'; return; }
section.style.display = 'block';
document.getElementById('ds_path').value = ds.path;
document.getElementById('ds_cache').value = ds.cache;
document.getElementById('ds_control').value = ds.control;
document.getElementById('ds_type').value = ds.type;
document.getElementById('ds_repeats').value = ds.repeats;
document.getElementById('ds_target_frames').value = ds.target_frames;
document.getElementById('ds_frame_extraction').value = ds.frame_extraction;
document.getElementById('ds_frame_stride').value = ds.frame_stride;
document.getElementById('ds_frame_sample').value = ds.frame_sample;
document.getElementById('video_fields').style.display = ds.type === 'video' ? 'block' : 'none';
document.getElementById('ds_stride_field').style.display = ds.frame_extraction === 'slide' ? 'block' : 'none';
document.getElementById('ds_sample_field').style.display = ds.frame_extraction === 'uniform' ? 'block' : 'none';
}
function renderGrid() {
const ds = activeDataset();
const dropzone = document.getElementById('dropzone');
const grid = document.getElementById('grid');
if (!ds || ds.images.length === 0) {
dropzone.style.display = 'flex';
grid.style.display = 'none';
dropzone.querySelector('.big').textContent = ds
? 'drag & drop images or videos here for "' + ds.name + '"'
: 'create a dataset on the left, then drag & drop images or videos here';
return;
}
dropzone.style.display = 'none';
grid.style.display = 'grid';
grid.innerHTML = '';
ds.images.forEach((img, idx) => {
const cell = document.createElement('div');
cell.className = 'thumb';
if (img.isVideo) {
cell.innerHTML = `<video src="${img.url}" muted preload="metadata"></video><span class="vidmark">video</span>`;
} else {
cell.innerHTML = `<img src="${img.url}" loading="lazy">`;
}
const flag = document.createElement('div');
flag.className = 'cap-flag' + (img.caption ? '' : ' no-cap');
flag.textContent = img.caption ? img.caption : 'no caption yet';
cell.appendChild(flag);
cell.addEventListener('click', () => openLightbox(idx));
grid.appendChild(cell);
});
}
function renderAll() {
renderDatasetList();
renderActiveSettings();
renderGrid();
updateToml();
}
/* ---------- lightbox ---------- */
function openLightbox(idx) {
lbIndex = idx;
document.getElementById('lightbox').classList.add('open');
renderLightbox();
}
function closeLightbox() {
saveLightboxCaption();
document.getElementById('lightbox').classList.remove('open');
lbIndex = null;
renderGrid();
updateToml();
}
function saveLightboxCaption() {
const ds = activeDataset();
if (!ds || lbIndex === null) return;
const img = ds.images[lbIndex];
if (!img) return;
img.caption = document.getElementById('lb_caption').value;
}
function renderLightbox() {
const ds = activeDataset();
if (!ds || lbIndex === null) return;
const img = ds.images[lbIndex];
const slot = document.getElementById('lb_media_slot');
slot.innerHTML = img.isVideo
? `<video src="${img.url}" controls autoplay muted loop></video>`
: `<img src="${img.url}">`;
document.getElementById('lb_fname').textContent = img.name;
document.getElementById('lb_caption').value = img.caption;
document.getElementById('lb_count').textContent = (lbIndex+1) + ' / ' + ds.images.length;
}
function navLightbox(delta) {
const ds = activeDataset();
if (!ds) return;
saveLightboxCaption();
lbIndex = (lbIndex + delta + ds.images.length) % ds.images.length;
renderLightbox();
renderGrid();
}
document.getElementById('lb_close').onclick = closeLightbox;
document.getElementById('lb_prev').onclick = () => navLightbox(-1);
document.getElementById('lb_next').onclick = () => navLightbox(1);
document.getElementById('lb_caption').addEventListener('input', () => {
document.getElementById('lb_saved').style.opacity = '0';
});
document.getElementById('lb_caption').addEventListener('blur', saveLightboxCaption);
document.addEventListener('keydown', (e) => {
if (!document.getElementById('lightbox').classList.contains('open')) return;
if (document.activeElement === document.getElementById('lb_caption')) {
if (e.key === 'Escape') { document.getElementById('lb_caption').blur(); }
return;
}
if (e.key === 'Escape') closeLightbox();
if (e.key === 'ArrowLeft') navLightbox(-1);
if (e.key === 'ArrowRight') navLightbox(1);
});
/* ---------- toml build ---------- */
function tomlStr(v){ return '"' + String(v).replace(/\\/g,'\\\\').replace(/"/g,'\\"') + '"'; }
function highlight(raw) {
return raw.split('\n').map(line => {
if (/^\s*#/.test(line)) return '<span class="c-comment">' + escapeHtml(line) + '</span>';
if (/^\[\[?[\w.]+\]?\]$/.test(line)) return '<span class="c-section">' + escapeHtml(line) + '</span>';
const kv = line.match(/^(\s*)([\w_]+)(\s*=\s*)(.+)$/);
if (kv) {
const [, indent, key, eq, val] = kv;
let vClass = 'c-str';
if (/^-?\d+(\.\d+)?$/.test(val.trim())) vClass = 'c-num';
else if (/^(true|false)$/.test(val.trim())) vClass = 'c-bool';
else if (/^\[.*\]$/.test(val.trim())) vClass = 'c-num';
return indent + '<span class="c-key">' + escapeHtml(key) + '</span>' + escapeHtml(eq) + '<span class="'+vClass+'">' + escapeHtml(val) + '</span>';
}
return escapeHtml(line);
}).join('\n');
}
function buildToml() {
const resW = document.getElementById('res_w').value.trim();
const resH = document.getElementById('res_h').value.trim();
const captionExt = document.getElementById('caption_ext').value.trim() || '.txt';
const batchSize = document.getElementById('batch_size').value.trim();
const numRepeats = document.getElementById('num_repeats').value.trim();
const enableBucket = document.getElementById('enable_bucket').checked;
const bucketNoUpscale = document.getElementById('bucket_no_upscale').checked;
let lines = [];
lines.push('# general configurations — apply to all datasets unless overridden below');
lines.push('[general]');
if (resW && resH) lines.push(`resolution = [${resW}, ${resH}]`);
lines.push(`caption_extension = ${tomlStr(captionExt)}`);
if (batchSize) lines.push(`batch_size = ${batchSize}`);
if (numRepeats) lines.push(`num_repeats = ${numRepeats}`);
lines.push(`enable_bucket = ${enableBucket}`);
lines.push(`bucket_no_upscale = ${bucketNoUpscale}`);
lines.push('');
const warnings = [];
if (datasets.length === 0) warnings.push('No datasets yet — create one and drop in some files.');
datasets.forEach(ds => {
lines.push('[[datasets]]');
if (!ds.path) warnings.push(`"${ds.name}": path is empty — fill in the actual folder path on your machine.`);
const dirKey = ds.type === 'video' ? 'video_directory' : 'image_directory';
lines.push(`${dirKey} = ${tomlStr(ds.path || ('/path/to/' + ds.name))}`);
if (ds.cache) lines.push(`cache_directory = ${tomlStr(ds.cache)}`);
if (ds.control) lines.push(`control_directory = ${tomlStr(ds.control)}`);
if (ds.repeats) lines.push(`num_repeats = ${ds.repeats}`);
if (ds.type === 'video') {
const frames = ds.target_frames.split(',').map(s => s.trim()).filter(Boolean).join(', ');
if (frames) lines.push(`target_frames = [${frames}]`);
lines.push(`frame_extraction = ${tomlStr(ds.frame_extraction)}`);
if (ds.frame_extraction === 'slide' && ds.frame_stride) lines.push(`frame_stride = ${ds.frame_stride}`);
if (ds.frame_extraction === 'uniform' && ds.frame_sample) lines.push(`frame_sample = ${ds.frame_sample}`);
}
const uncaptioned = ds.images.filter(i => !i.caption).length;
if (uncaptioned) warnings.push(`"${ds.name}": ${uncaptioned} of ${ds.images.length} file(s) have no caption yet.`);
lines.push('');
});
const caches = datasets.map(d => d.cache).filter(Boolean);
const seen = new Set();
caches.forEach(c => { if (seen.has(c)) warnings.push(`Cache directory "${c}" is used by more than one dataset.`); seen.add(c); });
return { text: lines.join('\n'), warnings, captionExt };
}
function updateToml() {
const { text, warnings } = buildToml();
document.getElementById('output').innerHTML = highlight(text);
const box = document.getElementById('warn_box');
box.innerHTML = warnings.length
? '<strong>check before running:</strong><br>' + warnings.map(w => '• ' + escapeHtml(w)).join('<br>')
: 'looks good — no missing paths or captions detected.';
window._currentToml = text;
}
/* ---------- wiring: general + active dataset fields ---------- */
['res_w','res_h','caption_ext','batch_size','num_repeats'].forEach(id => {
document.getElementById(id).addEventListener('input', updateToml);
});
document.getElementById('enable_bucket').addEventListener('change', updateToml);
document.getElementById('bucket_no_upscale').addEventListener('change', updateToml);
function bindDsField(id, prop, isSelect) {
document.getElementById(id).addEventListener(isSelect ? 'change' : 'input', (e) => {
const ds = activeDataset();
if (!ds) return;
ds[prop] = e.target.value;
if (id === 'ds_type' || id === 'ds_frame_extraction') renderActiveSettings();
if (id === 'ds_type') renderDatasetList();
updateToml();
});
}
bindDsField('ds_path','path');
bindDsField('ds_cache','cache');
bindDsField('ds_control','control');
bindDsField('ds_type','type', true);
bindDsField('ds_repeats','repeats');
bindDsField('ds_target_frames','target_frames');
bindDsField('ds_frame_extraction','frame_extraction', true);
bindDsField('ds_frame_stride','frame_stride');
bindDsField('ds_frame_sample','frame_sample');
document.getElementById('add_dataset_btn').onclick = () => {
const ds = newDataset();
datasets.push(ds);
activeDatasetId = ds.id;
renderAll();
};
/* ---------- drag & drop + file inputs ---------- */
const dropzone = document.getElementById('dropzone');
const content = document.getElementById('content');
['dragenter','dragover'].forEach(evt => content.addEventListener(evt, (e) => {
e.preventDefault(); dropzone.classList.add('drag');
}));
['dragleave','drop'].forEach(evt => content.addEventListener(evt, (e) => {
e.preventDefault(); dropzone.classList.remove('drag');
}));
content.addEventListener('drop', async (e) => {
const files = await filesFromDataTransfer(e.dataTransfer);
if (files.length) ingestFiles(files);
});
document.getElementById('choose_folder_btn').onclick = () => document.getElementById('folder_input').click();
document.getElementById('choose_files_btn').onclick = () => document.getElementById('files_input').click();
document.getElementById('folder_input').addEventListener('change', (e) => ingestFiles(e.target.files));
document.getElementById('files_input').addEventListener('change', (e) => ingestFiles(e.target.files));
/* ---------- export drawer ---------- */
const drawer = document.getElementById('drawer');
document.getElementById('toggle_drawer_btn').onclick = () => drawer.classList.toggle('open');
document.getElementById('copy_toml_btn').onclick = () => {
navigator.clipboard.writeText(window._currentToml || '').then(() => {
const b = document.getElementById('copy_toml_btn');
const orig = b.textContent; b.textContent = 'copied'; setTimeout(() => b.textContent = orig, 1200);
});
};
document.getElementById('download_toml_btn').onclick = () => {
downloadBlob(new Blob([window._currentToml || ''], {type:'text/plain'}), 'dataset_config.toml');
};
function downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = filename; a.click();
URL.revokeObjectURL(url);
}
document.getElementById('download_zip_btn').onclick = async () => {
const btn = document.getElementById('download_zip_btn');
const orig = btn.textContent;
btn.textContent = 'zipping...';
const { text, captionExt } = buildToml();
const zip = new JSZip();
zip.file('dataset_config.toml', text);
datasets.forEach(ds => {
const folder = zip.folder(ds.name);
ds.images.forEach(img => {
if (img.caption) folder.file(baseName(img.name) + captionExt, img.caption);
});
});
const blob = await zip.generateAsync({ type: 'blob' });
downloadBlob(blob, 'musubi_dataset_export.zip');
btn.textContent = orig;
};
document.getElementById('save_disk_btn').onclick = async () => {
const btn = document.getElementById('save_disk_btn');
if (!('showDirectoryPicker' in window)) {
alert('Direct folder saving isn\'t supported in this browser/embed. Use "download everything (.zip)" instead — unzip it next to your images.');
return;
}
const { captionExt } = buildToml();
for (const ds of datasets) {
if (!ds.images.some(i => i.caption)) continue;
try {
const dirHandle = await window.showDirectoryPicker({ mode: 'readwrite', id: 'musubi-' + ds.id });
for (const img of ds.images) {
if (!img.caption) continue;
const fileHandle = await dirHandle.getFileHandle(baseName(img.name) + captionExt, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(img.caption);
await writable.close();
}
} catch (err) {
if (err && err.name === 'AbortError') continue;
alert('Couldn\'t write directly to disk here (' + (err && err.message ? err.message : 'permission blocked') + '). Use the zip download instead.');
return;
}
}
btn.textContent = 'saved';
setTimeout(() => btn.textContent = 'save captions directly to folder', 1500);
};
renderAll();
</script>
</body>
</html>