feat(gui): load and parse most parameters from image, text, or json
This commit is contained in:
parent
10fa36c1c1
commit
079d4aa602
|
@ -203,9 +203,9 @@ def save_image(
|
||||||
exif = PngImagePlugin.PngInfo()
|
exif = PngImagePlugin.PngInfo()
|
||||||
|
|
||||||
if params is not None:
|
if params is not None:
|
||||||
exif.add_text("make", "onnx-web")
|
exif.add_text("Make", "onnx-web")
|
||||||
exif.add_text(
|
exif.add_text(
|
||||||
"maker note",
|
"Maker Note",
|
||||||
dumps(
|
dumps(
|
||||||
json_params(
|
json_params(
|
||||||
[output],
|
[output],
|
||||||
|
@ -217,9 +217,9 @@ def save_image(
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
exif.add_text("model", server.server_version)
|
exif.add_text("Model", server.server_version)
|
||||||
exif.add_text(
|
exif.add_text(
|
||||||
"parameters",
|
"Parameters",
|
||||||
str_params(server, params, size, inversions=inversions, loras=loras),
|
str_params(server, params, size, inversions=inversions, loras=loras),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
"@types/lodash": "^4.14.192",
|
"@types/lodash": "^4.14.192",
|
||||||
"@types/node": "^18.15.11",
|
"@types/node": "^18.15.11",
|
||||||
"browser-bunyan": "^1.8.0",
|
"browser-bunyan": "^1.8.0",
|
||||||
|
"exifreader": "^4.13.0",
|
||||||
"i18next": "^22.4.14",
|
"i18next": "^22.4.14",
|
||||||
"i18next-browser-languagedetector": "^7.0.1",
|
"i18next-browser-languagedetector": "^7.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
|
|
@ -1,28 +1,26 @@
|
||||||
import * as React from 'react';
|
|
||||||
import { useContext } from 'react';
|
|
||||||
import { doesExist, mustExist } from '@apextoaster/js-utils';
|
import { doesExist, mustExist } from '@apextoaster/js-utils';
|
||||||
import { useStore } from 'zustand';
|
import { Delete as DeleteIcon, ImageSearch, Save as SaveIcon } from '@mui/icons-material';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import {
|
import {
|
||||||
|
Autocomplete,
|
||||||
Button,
|
Button,
|
||||||
IconButton,
|
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTitle,
|
|
||||||
DialogContent,
|
|
||||||
DialogActions,
|
DialogActions,
|
||||||
TextField,
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
IconButton,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
Autocomplete,
|
|
||||||
Stack,
|
Stack,
|
||||||
|
TextField,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import * as ExifReader from 'exifreader';
|
||||||
Delete as DeleteIcon,
|
import * as React from 'react';
|
||||||
Save as SaveIcon,
|
import { useContext } from 'react';
|
||||||
} from '@mui/icons-material';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useStore } from 'zustand';
|
||||||
|
|
||||||
|
import { BaseImgParams, Txt2ImgParams } from '../client/types.js';
|
||||||
import { StateContext } from '../state.js';
|
import { StateContext } from '../state.js';
|
||||||
import { BaseImgParams } from '../client/types.js';
|
|
||||||
|
|
||||||
export interface ProfilesProps {
|
export interface ProfilesProps {
|
||||||
params: BaseImgParams;
|
params: BaseImgParams;
|
||||||
|
@ -78,6 +76,29 @@ export function Profiles(props: ProfilesProps) {
|
||||||
<Button type="button" variant="contained" onClick={() => setDialogOpen(true)}>
|
<Button type="button" variant="contained" onClick={() => setDialogOpen(true)}>
|
||||||
<SaveIcon />
|
<SaveIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button component='label' variant="contained">
|
||||||
|
<ImageSearch />
|
||||||
|
<input
|
||||||
|
hidden
|
||||||
|
accept={'*.json,*.jpg,*.jpeg,*.png,*.txt'}
|
||||||
|
type='file'
|
||||||
|
onChange={(event) => {
|
||||||
|
const { files } = event.target;
|
||||||
|
if (doesExist(files) && files.length > 0) {
|
||||||
|
const file = mustExist(files[0]);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
loadParamsFromFile(file).then((newParams) => {
|
||||||
|
if (doesExist(props.setParams) && doesExist(newParams)) {
|
||||||
|
props.setParams({
|
||||||
|
...props.params,
|
||||||
|
...newParams,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
onChange={(event, value) => {
|
onChange={(event, value) => {
|
||||||
|
@ -124,3 +145,95 @@ export function Profiles(props: ProfilesProps) {
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function loadParamsFromFile(file: File): Promise<Partial<Txt2ImgParams>> {
|
||||||
|
const parts = file.name.toLocaleLowerCase().split('.');
|
||||||
|
const ext = parts[parts.length - 1];
|
||||||
|
|
||||||
|
switch (ext) {
|
||||||
|
case 'jpg':
|
||||||
|
case 'jpeg':
|
||||||
|
case 'png':
|
||||||
|
return parseImageParams(file);
|
||||||
|
case 'json':
|
||||||
|
{
|
||||||
|
const params = JSON.parse(await file.text());
|
||||||
|
return params.params as Txt2ImgParams;
|
||||||
|
}
|
||||||
|
case 'txt':
|
||||||
|
default:
|
||||||
|
return parseAutoComment(await file.text());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parseImageParams(file: File): Promise<Partial<Txt2ImgParams>> {
|
||||||
|
const tags = await ExifReader.load(file);
|
||||||
|
|
||||||
|
// handle lowercase variation from my earlier mistakes
|
||||||
|
const makerNote = tags.MakerNote || tags['maker note'];
|
||||||
|
// eslint-disable-next-line dot-notation, @typescript-eslint/strict-boolean-expressions
|
||||||
|
const userComment = tags.UserComment || tags['Parameters'] || tags['parameters'];
|
||||||
|
|
||||||
|
if (doesExist(makerNote) && isProbablyJSON(makerNote.value)) {
|
||||||
|
const params = JSON.parse(makerNote.value);
|
||||||
|
return params.params as Txt2ImgParams; // TODO: enforce schema and do some error handling
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doesExist(userComment) && typeof userComment.value === 'string') {
|
||||||
|
return parseAutoComment(userComment.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isProbablyJSON(maybeJSON: unknown): maybeJSON is string {
|
||||||
|
return typeof maybeJSON === 'string' && maybeJSON[0] === '{' && maybeJSON[maybeJSON.length - 1] === '}';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NEGATIVE_PROMPT_TAG = 'Negative prompt:';
|
||||||
|
|
||||||
|
export function parseAutoComment(comment: string): Partial<Txt2ImgParams> {
|
||||||
|
const lines = comment.split('\n');
|
||||||
|
const [prompt, maybeNegative, ...otherLines] = lines;
|
||||||
|
|
||||||
|
const params: Partial<Txt2ImgParams> = {
|
||||||
|
prompt,
|
||||||
|
};
|
||||||
|
|
||||||
|
// check if maybeNegative is the negative prompt
|
||||||
|
if (maybeNegative.startsWith(NEGATIVE_PROMPT_TAG)) {
|
||||||
|
params.negativePrompt = maybeNegative.substring(NEGATIVE_PROMPT_TAG.length).trim();
|
||||||
|
} else {
|
||||||
|
otherLines.unshift(maybeNegative);
|
||||||
|
}
|
||||||
|
|
||||||
|
// join rest and split on commas
|
||||||
|
const other = otherLines.join(' ');
|
||||||
|
const otherParams = other.split(',');
|
||||||
|
|
||||||
|
for (const param of otherParams) {
|
||||||
|
const [key, value] = param.split(':');
|
||||||
|
|
||||||
|
switch (key.toLocaleLowerCase().trim()) {
|
||||||
|
case 'steps':
|
||||||
|
params.steps = parseInt(value, 10);
|
||||||
|
break;
|
||||||
|
case 'sampler':
|
||||||
|
params.scheduler = value;
|
||||||
|
break;
|
||||||
|
case 'cfg scale':
|
||||||
|
params.cfg = parseInt(value, 10);
|
||||||
|
break;
|
||||||
|
case 'seed':
|
||||||
|
params.seed = parseInt(value, 10);
|
||||||
|
break;
|
||||||
|
case 'size':
|
||||||
|
// TODO: parse size
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// unknown param
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
|
@ -776,6 +776,11 @@
|
||||||
"@typescript-eslint/types" "5.59.0"
|
"@typescript-eslint/types" "5.59.0"
|
||||||
eslint-visitor-keys "^3.3.0"
|
eslint-visitor-keys "^3.3.0"
|
||||||
|
|
||||||
|
"@xmldom/xmldom@^0.8.8":
|
||||||
|
version "0.8.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.9.tgz#b6ef7457e826be8049667ae673eda7876eb049be"
|
||||||
|
integrity sha512-4VSbbcMoxc4KLjb1gs96SRmi7w4h1SF+fCoiK0XaQX62buCc1G5d0DC5bJ9xJBNPDSVCmIrcl8BiYxzjrqaaJA==
|
||||||
|
|
||||||
"@xobotyi/scrollbar-width@^1.9.5":
|
"@xobotyi/scrollbar-width@^1.9.5":
|
||||||
version "1.9.5"
|
version "1.9.5"
|
||||||
resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d"
|
resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d"
|
||||||
|
@ -1563,6 +1568,13 @@ esutils@^2.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
||||||
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
||||||
|
|
||||||
|
exifreader@^4.13.0:
|
||||||
|
version "4.13.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/exifreader/-/exifreader-4.13.0.tgz#f380b33cfc85630a0dbd56edd41e28710a9e9679"
|
||||||
|
integrity sha512-IhJBpyXDLbCdgzVHkthadOvrMiZOR2XS7POVp0b5JoVfScRoCJ6YazZ+stTkbDTE5TRTP44bE5RKsujckAs45Q==
|
||||||
|
optionalDependencies:
|
||||||
|
"@xmldom/xmldom" "^0.8.8"
|
||||||
|
|
||||||
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||||
version "3.1.3"
|
version "3.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||||
|
|
Loading…
Reference in New Issue