From 703402eed87d490e78d15136a64bf4d7f6f2238c Mon Sep 17 00:00:00 2001 From: shinshin86 Date: Sat, 16 Sep 2023 15:10:10 +0900 Subject: [PATCH 01/10] feat: add getInfotext and getInfotextJson --- src/index.ts | 33 +++++++++++++++++------ test/index.test.ts | 67 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 91 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index ad20a36..17de263 100644 --- a/src/index.ts +++ b/src/index.ts @@ -83,13 +83,28 @@ function pythonBoolToJsBool(str: string): boolean | null { return str.toLowerCase() === "true" ? true : str.toLowerCase() === "false" ? false : null; } + +/** + * @deprecated Use `getInfotext` or `getInfotextJson` instead. + */ async function getPngInfo(arrayBuffer: ArrayBuffer, options?: Options): Promise { + const infotext = await getInfotext(arrayBuffer); + + const isFormatJson = !options || !options.format || options.format === 'json'; + if (isFormatJson) { + return getPngInfoJson(infotext); + } else if (options.format === 'text') { + return infotext; + } +} + +async function getInfotext(arrayBuffer: ArrayBuffer): Promise { const buffer = new Uint8Array(arrayBuffer); const fileSignature = buffer.slice(0, 8); const pngSignature = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]); const crcSize = 4; - let infoString = ""; + let infotext = ""; if (!fileSignature.every((value, index) => value === pngSignature[index])) { return ""; @@ -117,7 +132,7 @@ async function getPngInfo(arrayBuffer: ArrayBuffer, options?: Options): Promise< } // Decode data - infoString = chunkType === 'tEXt' ? new TextDecoder("iso-8859-1").decode(data) : new TextDecoder("utf-8").decode(data); + infotext = chunkType === 'tEXt' ? new TextDecoder("iso-8859-1").decode(data) : new TextDecoder("utf-8").decode(data); break; } } else { @@ -127,13 +142,13 @@ async function getPngInfo(arrayBuffer: ArrayBuffer, options?: Options): Promise< position += crcSize; } + return infotext +} - const isFormatJson = !options || !options.format || options.format === 'json'; - if (isFormatJson) { - return getPngInfoJson(infoString); - } else if (options.format === 'text') { - return infoString; - } +async function getInfotextJson(arrayBuffer: ArrayBuffer): Promise { + const infotext = await getInfotext(arrayBuffer); + + return getPngInfoJson(infotext); } async function getPngInfoJson(infoString: string): Promise { @@ -301,6 +316,8 @@ function convertToCamelCase(input: string): string { export { getPngInfo, + getInfotext, + getInfotextJson, getOriginalKeyNames, PngInfoObject, OriginalKeyPngInfoObject, diff --git a/test/index.test.ts b/test/index.test.ts index 35604bb..e7e76f9 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,4 +1,4 @@ -import { getPngInfo, getOriginalKeyNames, PngInfoObject, OriginalKeyPngInfoObject, LoraHash } from '../src/index'; +import { getPngInfo, getOriginalKeyNames, PngInfoObject, OriginalKeyPngInfoObject, LoraHash, getInfotext, getInfotextJson } from '../src/index'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; @@ -121,6 +121,71 @@ describe('getPngInfo', () => { }) }); +describe('getInfotext', () => { + it('should correctly parse to a infotext', async () => { + const buf = await readFile(path.join(__dirname, "test-image.png")); + const result = await getInfotext(buf); + expect(typeof result).toBe("string"); + }) +}); + +describe('getInfotextJson', () => { + it('should correctly parse a json string', async () => { + const buf = await readFile(path.join(__dirname, "test-image.png")); + const result = await getInfotextJson(buf); + expect(typeof result).toBe('object'); + + if (typeof result === 'object') { + expect(result).toHaveProperty('steps'); + expect(isNaN(Number(result.steps))).toBe(false) + + expect(result).toHaveProperty('sampler'); + + expect(result).toHaveProperty('cfgScale'); + expect(isNaN(Number(result.cfgScale))).toBe(false) + + expect(result).toHaveProperty('seed'); + expect(isNaN(Number(result.seed))).toBe(false) + + expect(result).toHaveProperty('size'); + expect(result.size).toMatch(/^\d+x\d+$/); + + expect(result).toHaveProperty('modelHash'); + + expect(result).toHaveProperty('model'); + + // Check to Lora hashes + const loraHashes = result.loraHashes; + expect(Array.isArray(loraHashes)).toBe(true); + loraHashes.forEach(hash => { + expect(typeof hash).toBe("object"); + + // Expect each object has exactly one key-value pair + const keys = Object.keys(hash); + expect(keys.length).toBe(1); + const values = Object.values(hash); + expect(values.length).toBe(1); + }); + + expect(result).toHaveProperty('version'); + + // Check to prompt + expect(Array.isArray(result.prompt)).toBe(true); + const prompt = result.prompt || []; + prompt.forEach(word => { + expect(typeof word).toBe("string"); + }); + + // Check to Negative prompt + expect(Array.isArray(result.negativePrompt)).toBe(true); + const negativePrompt = result.negativePrompt || [] + negativePrompt.forEach(word => { + expect(typeof word).toBe("string"); + }); + } + }) +}); + describe('getOriginalKeyNames', () => { it('should correctly map keys of the PngInfoObject to original keys', () => { const loraHashes: Array = [{ test1: "aaaaaaaaaaaa" }, { test2: "aaaaaaaaaaaa" }]; From b0275cbbce58fc25914c5b430970612d13677e70 Mon Sep 17 00:00:00 2001 From: shinshin86 Date: Sun, 17 Sep 2023 09:42:56 +0900 Subject: [PATCH 02/10] feat: add functions of convertInfotextToJson and convertJsonToInfotext --- src/index.ts | 52 ++++++++++++++++++++++++++++++++-- test/index.test.ts | 69 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 114 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index 17de263..6764a94 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,8 +4,20 @@ interface Options { format?: FormatOptions; } -const keyMapping = {} +// Values that are always used are stored from the beginning +const keyMapping = { + negativePrompt: "Negative prompt", + steps: "Steps", + sampler: "Sampler", + cfgScale: "CFG scale", + seed: "Seed", + size: "Size", + modelHash: "Model hash", + model: "Model", + version: "Version", +} +// Values that are always used are stored from the beginning const controlNetKeyMapping = { module: "Module", model: "Model", @@ -32,7 +44,7 @@ type PngInfoObject = { size: string, modelHash: string, model: string, - loraHashes: Array, + loraHashes?: Array, version: string, prompt: Array, negativePrompt: Array, @@ -151,7 +163,7 @@ async function getInfotextJson(arrayBuffer: ArrayBuffer): Promise return getPngInfoJson(infotext); } -async function getPngInfoJson(infoString: string): Promise { +function getPngInfoJson(infoString: string): PngInfoObject { const lines = infoString.split("\n"); let phase = 0; let prompt = ""; @@ -314,11 +326,45 @@ function convertToCamelCase(input: string): string { return keyMapping[input]; } +function convertInfotextToJson (infotext: string): PngInfoObject { + return getPngInfoJson(infotext); +} + +function listPropertiesExcept(obj) { + let result = ''; + for (const key in obj) { + if (key !== 'prompt' && key !== 'negativePrompt' && obj[key]) { + result += `${keyMapping[key]}: ${obj[key]}, `; + } + } + + return result.slice(0, -2); + } + +function convertJsonToInfotext(json: PngInfoObject): string { + let infotext = json?.prompt.join(", ") || ''; + + if(!!json?.negativePrompt.length) { + infotext += '\n'; + infotext += "Negative prompt: " + json?.negativePrompt.join(", "); + } + + const otherKeyAndValues = listPropertiesExcept(json); + if(otherKeyAndValues) { + infotext += '\n'; + infotext += otherKeyAndValues; + } + + return infotext; +} + export { getPngInfo, getInfotext, getInfotextJson, getOriginalKeyNames, + convertInfotextToJson, + convertJsonToInfotext, PngInfoObject, OriginalKeyPngInfoObject, LoraHash, diff --git a/test/index.test.ts b/test/index.test.ts index e7e76f9..fe9361e 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,4 +1,4 @@ -import { getPngInfo, getOriginalKeyNames, PngInfoObject, OriginalKeyPngInfoObject, LoraHash, getInfotext, getInfotextJson } from '../src/index'; +import { getPngInfo, getOriginalKeyNames, PngInfoObject, OriginalKeyPngInfoObject, LoraHash, getInfotext, getInfotextJson, convertInfotextToJson, convertJsonToInfotext } from '../src/index'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; @@ -118,14 +118,18 @@ describe('getPngInfo', () => { const buf = await readFile(path.join(__dirname, "test-image.png")); const result = await getPngInfo(buf, { format: 'text' }); expect(typeof result).toBe("string"); + const lines = result.split('\n'); + expect(lines.length).toBe(3); }) }); describe('getInfotext', () => { it('should correctly parse to a infotext', async () => { const buf = await readFile(path.join(__dirname, "test-image.png")); - const result = await getInfotext(buf); - expect(typeof result).toBe("string"); + const infotext = await getInfotext(buf); + expect(typeof infotext).toBe("string"); + const lines = infotext.split('\n'); + expect(lines.length).toBe(3); }) }); @@ -157,7 +161,7 @@ describe('getInfotextJson', () => { // Check to Lora hashes const loraHashes = result.loraHashes; expect(Array.isArray(loraHashes)).toBe(true); - loraHashes.forEach(hash => { + loraHashes?.forEach(hash => { expect(typeof hash).toBe("object"); // Expect each object has exactly one key-value pair @@ -307,3 +311,60 @@ describe('getOriginalKeyNames', () => { expect(getOriginalKeyNames(pngInfo)).toEqual(expected); }); }); + +describe('convertInfotextToJson', () => { + it('should correctly convert infotext to json', () => { + const infotext = `test, prompt +Negative prompt: test, negative, prompt +Steps: 20, Sampler: DPM++ 2M Karras, CFG scale: 7, Seed: 1234567890, Size: 512x512, Model hash: 6ce0161689, Model: v1-5-pruned-emaonly, Version: v1.6.0`; + const json = convertInfotextToJson(infotext); + + expect(Array.isArray(json.prompt)).toBe(true); + expect(json.prompt.length).toBe(2); + expect(json.prompt[0]).toBe('test'); + expect(json.prompt[1]).toBe('prompt'); + + expect(Array.isArray(json.negativePrompt)).toBe(true); + expect(json.negativePrompt.length).toBe(3); + expect(json.negativePrompt[0]).toBe('test'); + expect(json.negativePrompt[1]).toBe('negative'); + expect(json.negativePrompt[2]).toBe('prompt'); + + expect(Number(json.steps)).toBe(20); + expect(json.sampler).toBe("DPM++ 2M Karras"); + expect(Number(json.cfgScale)).toBe(7); + expect(Number(json.seed)).toBe(1234567890); + expect(json.size).toBe("512x512"); + expect(json.modelHash).toBe('6ce0161689'); + expect(json.model).toBe('v1-5-pruned-emaonly'); + expect(json.version).toBe('v1.6.0'); + }) +}) + +describe('convertJsonToInfotext', () => { + it('should correctly convert json to infotext', () => { + const json: PngInfoObject = { + prompt: ['test', 'prompt'], + negativePrompt: ["test", "negative", "prompt"], + steps: 20, + sampler: 'DPM++ 2M Karras', + cfgScale: 7, + seed: 1234567890, + size: "512x512", + modelHash: "6ce0161689", + model: "v1-5-pruned-emaonly", + version: "v1.6.0", + } + + const infotext = convertJsonToInfotext(json); + expect(typeof infotext).toBe("string"); + const lines = infotext.split('\n'); + expect(lines.length).toBe(3); + + + const expectedInfotext = `test, prompt +Negative prompt: test, negative, prompt +Steps: 20, Sampler: DPM++ 2M Karras, CFG scale: 7, Seed: 1234567890, Size: 512x512, Model hash: 6ce0161689, Model: v1-5-pruned-emaonly, Version: v1.6.0`; + expect(infotext).toBe(expectedInfotext); + }) +}) \ No newline at end of file From 7a5f0f0c7696b9446da9e4de5fe3e8ae5d3b867f Mon Sep 17 00:00:00 2001 From: shinshin86 Date: Sun, 17 Sep 2023 09:55:20 +0900 Subject: [PATCH 03/10] [BREAKING CHANGE] Remove deprecated functions and unnecessary functions --- src/index.ts | 93 ----------------- test/index.test.ts | 244 +-------------------------------------------- 2 files changed, 1 insertion(+), 336 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6764a94..34453b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,3 @@ -type FormatOptions = 'json' | 'text'; - -interface Options { - format?: FormatOptions; -} - // Values that are always used are stored from the beginning const keyMapping = { negativePrompt: "Negative prompt", @@ -17,25 +11,10 @@ const keyMapping = { version: "Version", } -// Values that are always used are stored from the beginning -const controlNetKeyMapping = { - module: "Module", - model: "Model", - weight: "Weight", - resizeMode: "Resize Mode", - lowVram: "Low Vram", - processorRes: "Processor Res", - guidanceStart: "Guidance Start", - guidanceEnd: "Guidance End", - pixelPerfect: "Pixel Perfect", - controlMode: "Control Mode", -} - type LoraHash = { [key: string]: string } - type PngInfoObject = { steps: number, sampler: string, @@ -53,22 +32,6 @@ type PngInfoObject = { [key: string]: any, } -type OriginalKeyPngInfoObject = { - "Steps": number, - "Sampler": string, - "CFG scale": number, - "Seed": number, - "Size": string, - "Model hash": string, - "Model": string, - "Lora hashes": Array, - "Version": string, - "Prompt": Array, - "Negative prompt": Array, - // Because there are a myriad of keys depending on the functionality provided, we allow arbitrary properties - [key: string]: any, -} - type ControlNetInfoObject = { key: string, // ControlNet 0, ControlNet 1... module: string, @@ -95,21 +58,6 @@ function pythonBoolToJsBool(str: string): boolean | null { return str.toLowerCase() === "true" ? true : str.toLowerCase() === "false" ? false : null; } - -/** - * @deprecated Use `getInfotext` or `getInfotextJson` instead. - */ -async function getPngInfo(arrayBuffer: ArrayBuffer, options?: Options): Promise { - const infotext = await getInfotext(arrayBuffer); - - const isFormatJson = !options || !options.format || options.format === 'json'; - if (isFormatJson) { - return getPngInfoJson(infotext); - } else if (options.format === 'text') { - return infotext; - } -} - async function getInfotext(arrayBuffer: ArrayBuffer): Promise { const buffer = new Uint8Array(arrayBuffer); const fileSignature = buffer.slice(0, 8); @@ -271,44 +219,6 @@ function getPngInfoJson(infoString: string): PngInfoObject { return data as PngInfoObject; } -function getOriginalKeyNames(obj: PngInfoObject): OriginalKeyPngInfoObject { - const reversedKeyMapping: { [key: string]: string } = Object.keys(keyMapping).reduce((acc: { [key: string]: string }, key: string) => { - const camelCasedKey = convertToCamelCase(key) - acc[camelCasedKey] = key; - return acc; - }, {}) - - const newObj: Partial = {}; - for (const key in obj) { - if (reversedKeyMapping[key]) { - newObj[reversedKeyMapping[key]] = obj[key]; - } else { - newObj[key] = obj[key]; - } - } - - // Handling controlNetList transformation - if (newObj.controlNetList) { - obj.controlNetList.forEach((controlNet: ControlNetInfoObject) => { - const controlNetObj: { [key: string]: any } = {}; - for (const controlNetKey in controlNet) { - // The item "key" is not necessary - if (controlNetKey === "key") { - continue; - } - - controlNetObj[controlNetKeyMapping[controlNetKey]] = controlNet[controlNetKey]; - } - newObj[controlNet.key] = controlNetObj; - }); - - // Removing the original controlNetList property as it is not needed in the new object - delete newObj.controlNetList; - } - - return newObj as OriginalKeyPngInfoObject; -} - function convertToCamelCase(input: string): string { if (!keyMapping[input]) { const convertedInput = input @@ -359,13 +269,10 @@ function convertJsonToInfotext(json: PngInfoObject): string { } export { - getPngInfo, getInfotext, getInfotextJson, - getOriginalKeyNames, convertInfotextToJson, convertJsonToInfotext, PngInfoObject, - OriginalKeyPngInfoObject, LoraHash, } \ No newline at end of file diff --git a/test/index.test.ts b/test/index.test.ts index fe9361e..b1d8913 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,128 +1,8 @@ -import { getPngInfo, getOriginalKeyNames, PngInfoObject, OriginalKeyPngInfoObject, LoraHash, getInfotext, getInfotextJson, convertInfotextToJson, convertJsonToInfotext } from '../src/index'; +import { PngInfoObject, getInfotext, getInfotextJson, convertInfotextToJson, convertJsonToInfotext } from '../src/index'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; -describe('getPngInfo', () => { - it('should correctly parse a json string', async () => { - const buf = await readFile(path.join(__dirname, "test-image.png")); - const result = await getPngInfo(buf); - expect(typeof result).toBe('object'); - - if (typeof result === 'object') { - expect(result).toHaveProperty('steps'); - expect(isNaN(Number(result.steps))).toBe(false) - - expect(result).toHaveProperty('sampler'); - - expect(result).toHaveProperty('cfgScale'); - expect(isNaN(Number(result.cfgScale))).toBe(false) - - expect(result).toHaveProperty('seed'); - expect(isNaN(Number(result.seed))).toBe(false) - - expect(result).toHaveProperty('size'); - expect(result.size).toMatch(/^\d+x\d+$/); - - expect(result).toHaveProperty('modelHash'); - - expect(result).toHaveProperty('model'); - - // Check to Lora hashes - const loraHashes = result.loraHashes; - expect(Array.isArray(loraHashes)).toBe(true); - loraHashes.forEach(hash => { - expect(typeof hash).toBe("object"); - - // Expect each object has exactly one key-value pair - const keys = Object.keys(hash); - expect(keys.length).toBe(1); - const values = Object.values(hash); - expect(values.length).toBe(1); - }); - - expect(result).toHaveProperty('version'); - - // Check to prompt - expect(Array.isArray(result.prompt)).toBe(true); - const prompt = result.prompt || []; - prompt.forEach(word => { - expect(typeof word).toBe("string"); - }); - - // Check to Negative prompt - expect(Array.isArray(result.negativePrompt)).toBe(true); - const negativePrompt = result.negativePrompt || [] - negativePrompt.forEach(word => { - expect(typeof word).toBe("string"); - }); - } - }) - - it('should correctly parse a json string (format option: json)', async () => { - const buf = await readFile(path.join(__dirname, "test-image.png")); - const result = await getPngInfo(buf, { format: 'json' }); - expect(typeof result).toBe('object'); - - if (typeof result === 'object') { - expect(result).toHaveProperty('steps'); - expect(isNaN(Number(result.steps))).toBe(false) - - expect(result).toHaveProperty('sampler'); - - expect(result).toHaveProperty('cfgScale'); - expect(isNaN(Number(result.cfgScale))).toBe(false) - - expect(result).toHaveProperty('seed'); - expect(isNaN(Number(result.seed))).toBe(false) - - expect(result).toHaveProperty('size'); - expect(result.size).toMatch(/^\d+x\d+$/); - - expect(result).toHaveProperty('modelHash'); - - expect(result).toHaveProperty('model'); - - // Check to Lora hashes - const loraHashes = result.loraHashes; - expect(Array.isArray(loraHashes)).toBe(true); - loraHashes.forEach(hash => { - expect(typeof hash).toBe("object"); - - // Expect each object has exactly one key-value pair - const keys = Object.keys(hash); - expect(keys.length).toBe(1); - const values = Object.values(hash); - expect(values.length).toBe(1); - }); - - expect(result).toHaveProperty('version'); - - // Check to prompt - expect(Array.isArray(result.prompt)).toBe(true); - const prompt = result.prompt || []; - prompt.forEach(word => { - expect(typeof word).toBe("string"); - }); - - // Check to Negative prompt - expect(Array.isArray(result.negativePrompt)).toBe(true); - const negativePrompt = result.negativePrompt || [] - negativePrompt.forEach(word => { - expect(typeof word).toBe("string"); - }); - } - }) - - it('should correctly parse to a plain text (format option: text)', async () => { - const buf = await readFile(path.join(__dirname, "test-image.png")); - const result = await getPngInfo(buf, { format: 'text' }); - expect(typeof result).toBe("string"); - const lines = result.split('\n'); - expect(lines.length).toBe(3); - }) -}); - describe('getInfotext', () => { it('should correctly parse to a infotext', async () => { const buf = await readFile(path.join(__dirname, "test-image.png")); @@ -190,128 +70,6 @@ describe('getInfotextJson', () => { }) }); -describe('getOriginalKeyNames', () => { - it('should correctly map keys of the PngInfoObject to original keys', () => { - const loraHashes: Array = [{ test1: "aaaaaaaaaaaa" }, { test2: "aaaaaaaaaaaa" }]; - const pngInfo: PngInfoObject = { - steps: 30, - sampler: "test", - cfgScale: 7, - seed: 3762574169, - size: "512x512", - modelHash: "aaaaaaaaaa", - model: "testModel", - loraHashes, - version: "v1.5.0", - prompt: ["prompt1", "prompt2", "", ""], - negativePrompt: ["negativePrompt1", "negativePrompt2"], - controlNetList: [ - { - key: 'ControlNet 0', - module: 'controlnet_module', - model: 'controlnet_model [abcdef12]', - weight: '1', - resizeMode: 'Crop and Resize', - lowVram: 'False', - processorRes: 512, - guidanceStart: '0', - guidanceEnd: '1', - pixelPerfect: false, - controlMode: 'Balanced' - }, - { - key: 'ControlNet 1', - module: 'controlnet_module', - model: 'controlnet_model [abcdef12]', - weight: '1', - resizeMode: 'Crop and Resize', - lowVram: 'False', - processorRes: 512, - guidanceStart: '0', - guidanceEnd: '1', - pixelPerfect: true, - controlMode: 'Balanced' - }, - ] - }; - - const expected: OriginalKeyPngInfoObject = { - "Steps": 30, - "Sampler": "test", - "CFG scale": 7, - "Seed": 3762574169, - "Size": "512x512", - "Model hash": "aaaaaaaaaa", - "Model": "testModel", - "Lora hashes": [{ test1: "aaaaaaaaaaaa" }, { test2: "aaaaaaaaaaaa" }], - "Version": "v1.5.0", - "Prompt": ["prompt1", "prompt2", "", ""], - "Negative prompt": ["negativePrompt1", "negativePrompt2"], - "ControlNet 0": { - "Module": "controlnet_module", - "Model": "controlnet_model [abcdef12]", - "Weight": "1", - "Resize Mode": "Crop and Resize", - "Low Vram": "False", - "Processor Res": 512, - "Guidance Start": "0", - "Guidance End": "1", - "Pixel Perfect": false, - "Control Mode": "Balanced" - }, - "ControlNet 1": { - "Module": "controlnet_module", - "Model": "controlnet_model [abcdef12]", - "Weight": "1", - "Resize Mode": "Crop and Resize", - "Low Vram": "False", - "Processor Res": 512, - "Guidance Start": "0", - "Guidance End": "1", - "Pixel Perfect": true, - "Control Mode": "Balanced" - } - }; - - expect(getOriginalKeyNames(pngInfo)).toEqual(expected); - }); - - it('should not change keys that do not exist in keyMapping', () => { - const loraHashes: Array = [{ test1: "aaaaaaaaaaaa" }, { test2: "aaaaaaaaaaaa" }]; - const pngInfo: PngInfoObject & { extraKey: string } = { - steps: 30, - sampler: "test", - cfgScale: 7, - seed: 3762574169, - size: "512x512", - modelHash: "aaaaaaaaaa", - model: "testModel", - loraHashes, - version: "v1.5.0", - prompt: ["prompt1", "prompt2", "", ""], - negativePrompt: ["negativePrompt1", "negativePrompt2"], - extraKey: "extraValue" - }; - - const expected: OriginalKeyPngInfoObject & { extraKey: string } = { - "Steps": 30, - "Sampler": "test", - "CFG scale": 7, - "Seed": 3762574169, - "Size": "512x512", - "Model hash": "aaaaaaaaaa", - "Model": "testModel", - "Lora hashes": [{ test1: "aaaaaaaaaaaa" }, { test2: "aaaaaaaaaaaa" }], - "Version": "v1.5.0", - "Prompt": ["prompt1", "prompt2", "", ""], - "Negative prompt": ["negativePrompt1", "negativePrompt2"], - extraKey: "extraValue" - }; - - expect(getOriginalKeyNames(pngInfo)).toEqual(expected); - }); -}); - describe('convertInfotextToJson', () => { it('should correctly convert infotext to json', () => { const infotext = `test, prompt From efcf7b13ba09da5a3fe5383b3fd997b5fb9e043f Mon Sep 17 00:00:00 2001 From: shinshin86 Date: Sun, 17 Sep 2023 11:44:14 +0900 Subject: [PATCH 04/10] Fix convert ControlNet --- src/index.ts | 52 +++++++++++++++++++++++++++++++------ test/index.test.ts | 64 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 104 insertions(+), 12 deletions(-) diff --git a/src/index.ts b/src/index.ts index 34453b8..e972100 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,20 @@ const keyMapping = { version: "Version", } +// Values that are always used are stored from the beginning +const controlNetKeyMapping = { + module: "Module", + model: "Model", + weight: "Weight", + resizeMode: "Resize Mode", + lowVram: "Low Vram", + processorRes: "Processor Res", + guidanceStart: "Guidance Start", + guidanceEnd: "Guidance End", + pixelPerfect: "Pixel Perfect", + controlMode: "Control Mode", +} + type LoraHash = { [key: string]: string } @@ -36,12 +50,12 @@ type ControlNetInfoObject = { key: string, // ControlNet 0, ControlNet 1... module: string, model: string, - weight: string, + weight: number, resizeMode: string, - lowVram: string, + lowVram: boolean, processorRes: number, - guidanceStart: string, - guidanceEnd: string, + guidanceStart: number, + guidanceEnd: number, pixelPerfect: boolean, controlMode: string, // Because there are a myriad of keys depending on the functionality provided, we allow arbitrary properties @@ -185,10 +199,9 @@ function getPngInfoJson(infoString: string): PngInfoObject { while(controlNetMatch !== null) { const controlNetKey = controlNetMatch[1].trim(); const controlNetValue = controlNetMatch[2].trim(); - - if(controlNetKey === "Processor Res") { + if(["Weight","Processor Res", "Guidance Start", "Guidance End"].includes(controlNetKey)) { controlNetObj[convertToCamelCase(controlNetKey)] = Number(controlNetValue); - } else if (controlNetKey === "Pixel Perfect") { + } else if (["Low Vram", "Pixel Perfect"].includes(controlNetKey)) { const jsBool = pythonBoolToJsBool(controlNetValue); if(jsBool !== null) { @@ -243,7 +256,30 @@ function convertInfotextToJson (infotext: string): PngInfoObject { function listPropertiesExcept(obj) { let result = ''; for (const key in obj) { - if (key !== 'prompt' && key !== 'negativePrompt' && obj[key]) { + if(key === "controlNetList") { + const controlNetObjList =obj[key]; + if (!controlNetObjList || !Array.isArray(controlNetObjList)){ + continue; + } + + for (const controlNetObj of controlNetObjList) { + const controlNetKey = controlNetObj.key; + delete controlNetObj.key; + + const controlNetValueList = Object.keys(controlNetObj).map((key) => { + let controlNetValue = controlNetObj[key] + + // true -> True, false -> False + if(["lowVram", "pixelPerfect"].includes(key)) { + controlNetValue = controlNetObj[key].toString().charAt(0).toUpperCase() + controlNetObj[key].toString().slice(1); + } + + return `${controlNetKeyMapping[key]}: ${controlNetValue}` + }); + + result += `${controlNetKey}: "${controlNetValueList.join(', ')}", ` + } + } else if (key !== 'prompt' && key !== 'negativePrompt' && obj[key]) { result += `${keyMapping[key]}: ${obj[key]}, `; } } diff --git a/test/index.test.ts b/test/index.test.ts index b1d8913..3086f84 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -74,7 +74,7 @@ describe('convertInfotextToJson', () => { it('should correctly convert infotext to json', () => { const infotext = `test, prompt Negative prompt: test, negative, prompt -Steps: 20, Sampler: DPM++ 2M Karras, CFG scale: 7, Seed: 1234567890, Size: 512x512, Model hash: 6ce0161689, Model: v1-5-pruned-emaonly, Version: v1.6.0`; +Steps: 20, Sampler: DPM++ 2M Karras, CFG scale: 7, Seed: 1234567890, Size: 512x512, Model hash: 6ce0161689, Model: v1-5-pruned-emaonly, ControlNet 0: "Module: dw_openpose_full, Model: control_v11p_sd15_openpose [12345678], Weight: 1, Resize Mode: Crop and Resize, Low Vram: False, Processor Res: 512, Guidance Start: 0, Guidance End: 1, Pixel Perfect: False, Control Mode: Balanced", ControlNet 1: "Module: dw_openpose_full, Model: control_v11p_sd15_openpose [12345678], Weight: 1, Resize Mode: Crop and Resize, Low Vram: False, Processor Res: 512, Guidance Start: 0, Guidance End: 1, Pixel Perfect: False, Control Mode: Balanced", Version: v1.6.0, `; const json = convertInfotextToJson(infotext); expect(Array.isArray(json.prompt)).toBe(true); @@ -96,6 +96,35 @@ Steps: 20, Sampler: DPM++ 2M Karras, CFG scale: 7, Seed: 1234567890, Size: 512x5 expect(json.modelHash).toBe('6ce0161689'); expect(json.model).toBe('v1-5-pruned-emaonly'); expect(json.version).toBe('v1.6.0'); + + expect(json.controlNetList?.length).toBe(2); + if(Array.isArray(json.controlNetList)) { + const controlNetObj0 = json.controlNetList[0]; + expect(controlNetObj0?.key).toBe("ControlNet 0"); + expect(controlNetObj0?.module).toBe("dw_openpose_full"); + expect(controlNetObj0?.model).toBe("control_v11p_sd15_openpose [12345678]"); + expect(controlNetObj0?.weight).toBe(1); + expect(controlNetObj0?.resizeMode).toBe("Crop and Resize"); + expect(controlNetObj0?.lowVram).toBe(false); + expect(controlNetObj0?.processorRes).toBe(512); + expect(controlNetObj0?.guidanceStart).toBe(0); + expect(controlNetObj0?.guidanceEnd).toBe(1); + expect(controlNetObj0?.pixelPerfect).toBe(false); + expect(controlNetObj0?.controlMode).toBe("Balanced"); + + const controlNetObj1 = json.controlNetList[1]; + expect(controlNetObj1?.key).toBe("ControlNet 1"); + expect(controlNetObj1?.module).toBe("dw_openpose_full"); + expect(controlNetObj1?.model).toBe("control_v11p_sd15_openpose [12345678]"); + expect(controlNetObj1?.weight).toBe(1); + expect(controlNetObj1?.resizeMode).toBe("Crop and Resize"); + expect(controlNetObj1?.lowVram).toBe(false); + expect(controlNetObj1?.processorRes).toBe(512); + expect(controlNetObj1?.guidanceStart).toBe(0); + expect(controlNetObj1?.guidanceEnd).toBe(1); + expect(controlNetObj1?.pixelPerfect).toBe(false); + expect(controlNetObj1?.controlMode).toBe("Balanced"); + } }) }) @@ -112,6 +141,34 @@ describe('convertJsonToInfotext', () => { modelHash: "6ce0161689", model: "v1-5-pruned-emaonly", version: "v1.6.0", + controlNetList: [ + { + key: 'ControlNet 0', + module: 'dw_openpose_full', + model: 'control_v11p_sd15_openpose [12345678]', + weight: 1, + resizeMode: 'Crop and Resize', + lowVram: false, + processorRes: 512, + guidanceStart: 0, + guidanceEnd: 1, + pixelPerfect: false, + controlMode: 'Balanced' + }, + { + key: 'ControlNet 1', + module: 'dw_openpose_full', + model: 'control_v11p_sd15_openpose [12345678]', + weight: 1, + resizeMode: 'Crop and Resize', + lowVram: false, + processorRes: 512, + guidanceStart: 0, + guidanceEnd: 1, + pixelPerfect: false, + controlMode: 'Balanced' + } + ] } const infotext = convertJsonToInfotext(json); @@ -119,10 +176,9 @@ describe('convertJsonToInfotext', () => { const lines = infotext.split('\n'); expect(lines.length).toBe(3); - const expectedInfotext = `test, prompt Negative prompt: test, negative, prompt -Steps: 20, Sampler: DPM++ 2M Karras, CFG scale: 7, Seed: 1234567890, Size: 512x512, Model hash: 6ce0161689, Model: v1-5-pruned-emaonly, Version: v1.6.0`; - expect(infotext).toBe(expectedInfotext); +Steps: 20, Sampler: DPM++ 2M Karras, CFG scale: 7, Seed: 1234567890, Size: 512x512, Model hash: 6ce0161689, Model: v1-5-pruned-emaonly, Version: v1.6.0, ControlNet 0: "Module: dw_openpose_full, Model: control_v11p_sd15_openpose [12345678], Weight: 1, Resize Mode: Crop and Resize, Low Vram: False, Processor Res: 512, Guidance Start: 0, Guidance End: 1, Pixel Perfect: False, Control Mode: Balanced", ControlNet 1: "Module: dw_openpose_full, Model: control_v11p_sd15_openpose [12345678], Weight: 1, Resize Mode: Crop and Resize, Low Vram: False, Processor Res: 512, Guidance Start: 0, Guidance End: 1, Pixel Perfect: False, Control Mode: Balanced"`; + expect(infotext).toEqual(expectedInfotext); }) }) \ No newline at end of file From ae587ccba09d68ab106a59f7e2baca82e98906b8 Mon Sep 17 00:00:00 2001 From: shinshin86 Date: Sun, 17 Sep 2023 11:46:58 +0900 Subject: [PATCH 05/10] Update README --- README.md | 91 ++++++++++++++++++++++--------------------------------- 1 file changed, 37 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 9b126df..72e4d54 100644 --- a/README.md +++ b/README.md @@ -7,36 +7,44 @@ This library provides functionality to extract information stored within PNG images generated by Stable Diffusion Web UI. -## Usage +It also provides functions to mutually convert between infotext and JSON formats. + +## Usage(extract infotext in PNG images) + +You can use `getInfotext` or `getInfotextJson` to extract infotext embedded in PNG images. Support `CJS/ESM/UMD` ### CommonJS ```javascript -const { getPngInfo } = require('chilled-lemon'); +const { getInfotext, getInfotextJson } = require('chilled-lemon'); const { readFile } = require('node:fs/promises'); (async () => { const buf = await readFile('./test.png'); - const jsonOutput = await getPngInfo(buf); - console.log(jsonOutput); - const textOutput = await getPngInfo(buf, { format: 'text' }); - console.log(textOutput); + + const infotext = await getInfotext(buf); + console.log(infotext); + + const json = await getInfotextJson(buf); + console.log(json); })(); ``` ### ES Modules ```javascript -import { getPngInfo } from 'chilled-lemon'; +import { getInfotext, getInfotextJson } from 'chilled-lemon'; import { readFile } from 'node:fs/promises'; const buf = await readFile('./test.png'); -const jsonOutput = await getPngInfo(buf); -console.log(jsonOutput); -const textOutput = await getPngInfo(buf, { format: 'text' }); -console.log(textOutput); + +const infotext = await getInfotext(buf); +console.log(infotext); + +const json = await getInfotextJson(buf); +console.log(json); ``` @@ -56,7 +64,7 @@ console.log(textOutput);