feat: add local image enhancement and lossless mod assets
This commit is contained in:
1 parent
cc71050362
commit
f7f7f5b1b3
29 files changed
+1495
-10
No files matched your search
@@ -0,0 +1,151 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { test } from "node:test";
|
||||
import { build } from "esbuild";
|
||||
import { parseHTML } from "linkedom";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
const out = await build({ stdin: { contents: `export * from './hash'; export * from './presets'; export * from './queue'; export * from './backends/swarm'; export * from './backends/a1111'; export * from './backends/comfy'; export * from '../player/renderer';`,
|
||||
resolveDir: fileURLToPath(new URL("../src/imager/", import.meta.url)), loader: "ts" }, bundle: true, write: false, format: "esm", platform: "node", logLevel: "silent" });
|
||||
const { DEFAULT_PRESET: preset, validatePreset, presetHash, cacheKey, ImageQueue, SwarmBackend, A1111Backend, ComfyBackend, workflow, Renderer, sha256Fallback } = await import(`data:text/javascript;base64,${Buffer.from(out.outputFiles[0].text).toString("base64")}`);
|
||||
const tick = () => new Promise((r) => setTimeout(r, 0));
|
||||
async function until(check) { for (let i = 0; i < 200; i++) { if (check()) return; await tick(); } assert.fail("timed out"); }
|
||||
function store(tasks = []) {
|
||||
const journal = new Map(tasks.map((t) => [t.key, structuredClone(t)])), images = new Map();
|
||||
return { journal, images, get: async (k) => images.get(k), put: async (i) => { images.set(i.key, i); },
|
||||
tasks: async () => [...journal.values()].map((t) => structuredClone(t)), save: async (t) => { journal.set(t.key, structuredClone(t)); } };
|
||||
}
|
||||
const image = (t) => ({ key: t.key, version: t.version, path: t.path, blob: new Blob(['png']), scale: 2, width: 20, height: 10 });
|
||||
|
||||
test("plain HTTP hash fallback matches SHA-256 for empty, Unicode and multiple blocks", () => {
|
||||
for (const text of ['', 'abc', 'Апскейл 🎨', 'x'.repeat(55), 'x'.repeat(56), 'x'.repeat(64), 'x'.repeat(2000)]) {
|
||||
assert.equal(sha256Fallback(new TextEncoder().encode(text)), createHash('sha256').update(text).digest('hex'));
|
||||
}
|
||||
});
|
||||
|
||||
test("preset validation and stable cache identity include source, version, backend and actual parameters", async () => {
|
||||
const hash = await presetHash(preset);
|
||||
assert.equal(hash, await presetHash({ ...preset, name: "Renamed" }));
|
||||
for (const changed of [{ scale: 4 }, { url: "http://127.0.0.1:8000" }, { seed: 1 }, { creativity: .3 }, { backend: "a1111" }]) assert.notEqual(hash, await presetHash({ ...preset, ...changed }));
|
||||
assert.notEqual(cacheKey(1, 'a|b', 'c', hash), cacheKey(1, 'a', 'b|c', hash));
|
||||
assert.notEqual(cacheKey(1, 'a', 'old', hash), cacheKey(1, 'a', 'new', hash));
|
||||
for (const bad of [{ scale: 0 }, { scale: NaN }, { creativity: 2 }, { steps: 1.5 }, { seed: -1 }, { url: 'javascript:alert(1)' }, { backend: "unknown" }]) assert.throws(() => validatePreset({ ...preset, ...bad }));
|
||||
});
|
||||
|
||||
test("one GPU task, concurrent deduplication and cancelling does not overlap remote work", async () => {
|
||||
const s = store(); const releases = []; let active = 0, maximum = 0;
|
||||
const q = new ImageQueue(s, async (t) => { maximum = Math.max(maximum, ++active); await new Promise((r) => releases.push(r)); active--; return image(t); });
|
||||
const [key, duplicate] = await Promise.all([q.enqueue(1, 'a.png', 'a', '/a', preset), q.enqueue(1, 'a.png', 'a', '/a', preset)]);
|
||||
assert.equal(key, duplicate); assert.equal(q.snapshot().length, 1);
|
||||
await until(() => releases.length === 1);
|
||||
const second = await q.enqueue(1, 'b.png', 'b', '/b', preset);
|
||||
q.cancel(key); await tick(); assert.equal(releases.length, 1);
|
||||
releases.shift()(); await until(() => releases.length === 1);
|
||||
releases.shift()(); await until(() => q.snapshot().find((t) => t.key === second).status === 'done');
|
||||
assert.equal(maximum, 1); assert.equal(s.images.has(key), false); assert.equal(s.images.has(second), true);
|
||||
assert.equal(s.journal.get(key).status, 'cancelled');
|
||||
});
|
||||
|
||||
test("failed generation and storage quota are retryable; reload never starts the GPU automatically", async () => {
|
||||
const s = store(); let fail = true, calls = 0;
|
||||
const q = new ImageQueue(s, async (t) => { calls++; if (fail) throw new Error('GPU offline'); return image(t); });
|
||||
const key = await q.enqueue(1, 'a.png', 'a', '/a', preset);
|
||||
await until(() => q.snapshot()[0].status === 'failed'); assert.match(q.snapshot()[0].error, /offline/);
|
||||
fail = false; await q.retry(key); await until(() => q.snapshot()[0].status === 'done'); assert.equal(calls, 2);
|
||||
const task = { ...q.snapshot()[0], status: 'running' };
|
||||
const recovered = new ImageQueue(store([task]), async () => assert.fail('must resume explicitly'));
|
||||
await recovered.restore(); assert.equal(recovered.snapshot()[0].status, 'cancelled');
|
||||
const quota = store(); quota.put = async () => { throw new Error('QuotaExceededError'); };
|
||||
const noSpace = new ImageQueue(quota, async (t) => image(t));
|
||||
await noSpace.enqueue(1, 'x.png', 'x', '/x', preset); await until(() => noSpace.snapshot()[0].status === 'failed');
|
||||
assert.match(noSpace.snapshot()[0].error, /Quota/);
|
||||
});
|
||||
|
||||
test("cached task bypasses GPU and cache loss permits regeneration", async () => {
|
||||
const s = store(); let calls = 0;
|
||||
const q = new ImageQueue(s, async (t) => { calls++; return image(t); });
|
||||
const key = await q.enqueue(1, 'a.png', 'a', '/a', preset); await until(() => q.snapshot()[0].status === 'done');
|
||||
await q.enqueue(1, 'a.png', 'a', '/a', preset); assert.equal(calls, 1);
|
||||
s.images.clear(); await q.enqueue(1, 'a.png', 'a', '/a', preset); await until(() => q.snapshot()[0].status === 'done'); assert.equal(calls, 2);
|
||||
});
|
||||
|
||||
test("Swarm creates a session, refreshes invalid session once and understands current model tuples", async () => {
|
||||
const original = globalThis.fetch; let sessions = 0, lists = 0;
|
||||
globalThis.fetch = async (url, options) => {
|
||||
const body = JSON.parse(options.body);
|
||||
if (url.endsWith('GetNewSession')) { sessions++; return Response.json({ session_id: 'session' + sessions }); }
|
||||
assert.ok(body.session_id);
|
||||
if (++lists === 1) return Response.json({ error: 'Session expired', error_id: 'invalid_session_id' });
|
||||
return Response.json({ list: [{ id: 'refinerupscalemethod', values: ['pixel-lanczos///Lanczos', 'model-4x.pth'] }], models: { 'Stable-Diffusion': [['sdxl.safetensors', 'sdxl']] } });
|
||||
};
|
||||
try { const b = new SwarmBackend('http://127.0.0.1:7801'); const models = await b.models(); assert.equal(sessions, 2); assert.deepEqual(models.models, ['sdxl.safetensors']); assert.equal(models.upscalers[0], 'pixel-lanczos'); }
|
||||
finally { globalThis.fetch = original; }
|
||||
});
|
||||
|
||||
test("Comfy upscale is a pixel-only graph; img2img adds the sampling graph", () => {
|
||||
const upscale = workflow('input.png', { ...preset, upscaler: '4x.pth' }, 100, 50);
|
||||
assert.ok(Object.values(upscale).some((n) => n.class_type === 'ImageUpscaleWithModel'));
|
||||
assert.ok(!Object.values(upscale).some((n) => n.class_type === 'KSampler' || n.class_type === 'VAEEncode'));
|
||||
assert.equal(upscale['4'].inputs.width, 200);
|
||||
const diffusion = workflow('input.png', { ...preset, mode: 'img2img', model: 'sdxl' }, 100, 50);
|
||||
assert.equal(diffusion['9'].inputs.denoise, preset.creativity);
|
||||
assert.deepEqual(diffusion['11'].inputs.images, ['10', 0]);
|
||||
});
|
||||
|
||||
test("backend wire protocols send the source and return downloadable results", async () => {
|
||||
const original = { fetch: globalThis.fetch, FileReader: globalThis.FileReader, createImageBitmap: globalThis.createImageBitmap };
|
||||
globalThis.FileReader = class {
|
||||
readAsDataURL(blob) { blob.arrayBuffer().then((buffer) => { this.result = 'data:image/png;base64,' + Buffer.from(buffer).toString('base64'); this.onload(); }); }
|
||||
};
|
||||
globalThis.createImageBitmap = async () => ({ width: 64, height: 128, close() {} });
|
||||
const calls = [];
|
||||
globalThis.fetch = async (url, options) => {
|
||||
const path = new URL(url).pathname;
|
||||
const body = typeof options?.body === 'string' ? JSON.parse(options.body) : options?.body;
|
||||
calls.push({ path, body });
|
||||
if (String(url).startsWith('data:')) return original.fetch(url);
|
||||
if (path.endsWith('/GetNewSession')) return Response.json({ session_id: 's' });
|
||||
if (path.endsWith('/GenerateText2Image')) return Response.json({ images: ['View/generated.png'] });
|
||||
if (path.endsWith('/extra-single-image')) return Response.json({ image: 'AAAA' });
|
||||
if (path.endsWith('/img2img')) return Response.json({ images: ['AAAA'] });
|
||||
if (path.endsWith('/upload/image')) return Response.json({ name: 'source.png', subfolder: '', type: 'temp' });
|
||||
if (path.endsWith('/prompt')) return Response.json({ prompt_id: 'job', node_errors: {} });
|
||||
if (path.endsWith('/history/job')) return Response.json({ job: { outputs: { '11': { images: [{ filename: 'output.png', subfolder: '', type: 'temp' }] } } } });
|
||||
return new Response(new Blob(['result'], { type: 'image/png' }));
|
||||
};
|
||||
try {
|
||||
const source = new Blob(['original'], { type: 'image/png' });
|
||||
const swarm = new SwarmBackend('http://127.0.0.1:7801');
|
||||
assert.equal(await (await swarm.process(source, { ...preset, model: 'sdxl' })).text(), 'result');
|
||||
const swarmBody = calls.find((c) => c.path.endsWith('/GenerateText2Image')).body;
|
||||
assert.equal(swarmBody.refinermethod, 'PostApply'); assert.equal(swarmBody.refinercontrolpercentage, 0); assert.equal(swarmBody.initimagecreativity, 0);
|
||||
assert.equal(swarmBody.width, 64); assert.equal(swarmBody.height, 128); assert.equal(swarmBody.refinerupscale, 2);
|
||||
assert.match(swarmBody.initimage, /^data:image\/png;base64,/); assert.equal(swarmBody.donotsave, true);
|
||||
const a = new A1111Backend('http://127.0.0.1:7860');
|
||||
await a.process(source, { ...preset, upscaler: 'ESRGAN' });
|
||||
assert.equal(calls.find((c) => c.path.endsWith('/extra-single-image')).body.upscaler_1, 'ESRGAN');
|
||||
await a.process(source, { ...preset, mode: 'img2img', model: 'sdxl' });
|
||||
const aBody = calls.find((c) => c.path.endsWith('/img2img')).body;
|
||||
assert.equal(aBody.width, 128); assert.equal(aBody.height, 256); assert.equal(aBody.denoising_strength, preset.creativity); assert.equal(aBody.override_settings.sd_model_checkpoint, 'sdxl');
|
||||
const comfy = new ComfyBackend('http://127.0.0.1:8188');
|
||||
await comfy.process(source, preset);
|
||||
const graph = calls.find((c) => c.path === '/prompt').body.prompt;
|
||||
assert.equal(graph['1'].inputs.image, 'source.png [temp]'); assert.equal(graph['4'].inputs.width, 128);
|
||||
assert.ok(calls.some((c) => c.path === '/view'));
|
||||
} finally { Object.assign(globalThis, original); }
|
||||
});
|
||||
|
||||
test("renderer compensates natural size but leaves explicit and ATL dimensions intact", () => {
|
||||
const { document } = parseHTML('<html><body><main></main></body></html>');
|
||||
const previous = globalThis.document; globalThis.document = document;
|
||||
try {
|
||||
const renderer = new Renderer(document.querySelector('main'), 1920, 1080, { url: () => 'blob:improved', assetScale: () => 2 });
|
||||
const img = renderer.displayable({ file: 'a.png' });
|
||||
Object.defineProperty(img, 'naturalWidth', { value: 800 }); img.dispatchEvent(new document.defaultView.Event('load'));
|
||||
assert.equal(img.width, 400);
|
||||
const sized = renderer.displayable({ file: 'a.png', size: [80, 40] });
|
||||
sized.dispatchEvent(new document.defaultView.Event('load')); assert.equal(sized.style.width, '80px');
|
||||
const atl = renderer.displayable({ file: 'a.png' }); atl.style.width = '100px';
|
||||
atl.dispatchEvent(new document.defaultView.Event('load')); assert.equal(atl.style.width, '100px'); assert.equal(atl.hasAttribute('width'), false);
|
||||
} finally { globalThis.document = previous; }
|
||||
});
|
||||
Reference in new issue
Block a user