// Однократный перенос старых прогонов text2img в ImageBatches. // ComfyUI (SaveImage) записывает в каждый PNG граф запуска (текстовый чанк "prompt") — из него // восстанавливается пользовательский ввод; картинки одного батча — подряд идущие файлы с одинаковым графом. import fs from 'node:fs'; import path from 'node:path'; import zlib from 'node:zlib'; import { COMFY_OUTPUT } from './config.ts'; import { attachImages, batchOfImage, getMeta, insertTextBatch, setMeta, transaction } from './db.ts'; import { inputFromGraph } from './workflows.ts'; const KEY = 'migration.imageBatches.v1'; const SRC_DIR = 'img2glb_src'; /** Картинки одного батча сохраняются почти одновременно. */ const SAME_BATCH_MS = 15_000; /** Текстовые чанки PNG (tEXt, iTXt, zTXt). */ function pngTexts(file: string): Record { const b = fs.readFileSync(file); const out: Record = {}; let o = 8; while (o + 8 <= b.length) { const len = b.readUInt32BE(o); const type = b.toString('latin1', o + 4, o + 8); const d = b.subarray(o + 8, o + 8 + len); const nul = d.indexOf(0); if (type === 'tEXt') { out[d.toString('latin1', 0, nul)] = d.toString('latin1', nul + 1); } else if (type === 'zTXt') { out[d.toString('latin1', 0, nul)] = zlib.inflateSync(d.subarray(nul + 2)).toString('latin1'); } else if (type === 'iTXt') { const compressed = d[nul + 1] === 1; let p = d.indexOf(0, nul + 3) + 1; // после языка p = d.indexOf(0, p) + 1; // после переведённого ключа const text = d.subarray(p); out[d.toString('latin1', 0, nul)] = (compressed ? zlib.inflateSync(text) : text).toString('utf8'); } else if (type === 'IEND') { break; } o += 12 + len; } return out; } export function migrateOldRuns() { if (getMeta(KEY)) return; const dir = path.join(COMFY_OUTPUT, SRC_DIR); let names: string[] = []; try { names = fs.readdirSync(dir).filter(n => /\.png$/i.test(n)); } catch { /* папки нет — переносить нечего */ } const files = names .map(name => ({ name, mtime: fs.statSync(path.join(dir, name)).mtimeMs })) .filter(f => batchOfImage({ filename: f.name, subfolder: SRC_DIR, type: 'output' }) == null) .sort((a, b) => a.mtime - b.mtime || a.name.localeCompare(b.name)); // группировка: одинаковый граф, подряд, в пределах SAME_BATCH_MS от первой картинки const groups: { graph: string; first: number; names: string[] }[] = []; let skipped = 0; for (const f of files) { let graph: string | undefined; try { graph = pngTexts(path.join(dir, f.name)).prompt; } catch { /* битый PNG */ } if (!graph) { skipped++; continue; } const last = groups[groups.length - 1]; if (last && last.graph === graph && f.mtime - last.first <= SAME_BATCH_MS) last.names.push(f.name); else groups.push({ graph, first: f.mtime, names: [f.name] }); } let created = 0; transaction(() => { for (const g of groups) { let parsed; try { parsed = inputFromGraph(JSON.parse(g.graph)); } catch { parsed = null; } if (!parsed) { skipped += g.names.length; continue; } const id = insertTextBatch(parsed.input, { created: Math.round(g.first), state: 'done', seedUsed: parsed.seed, prompt: parsed.prompt }); attachImages(id, g.names.map(filename => ({ filename, subfolder: SRC_DIR, type: 'output' }))); created++; } setMeta(KEY, new Date().toISOString()); }); console.log(`Перенос старых прогонов: батчей ${created}, картинок ${files.length - skipped}` + (skipped ? `, пропущено ${skipped}` : '')); }