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()
|
||||
|
||||
if params is not None:
|
||||
exif.add_text("make", "onnx-web")
|
||||
exif.add_text("Make", "onnx-web")
|
||||
exif.add_text(
|
||||
"maker note",
|
||||
"Maker Note",
|
||||
dumps(
|
||||
json_params(
|
||||
[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(
|
||||
"parameters",
|
||||
"Parameters",
|
||||
str_params(server, params, size, inversions=inversions, loras=loras),
|
||||
)
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"@types/lodash": "^4.14.192",
|
||||
"@types/node": "^18.15.11",
|
||||
"browser-bunyan": "^1.8.0",
|
||||
"exifreader": "^4.13.0",
|
||||
"i18next": "^22.4.14",
|
||||
"i18next-browser-languagedetector": "^7.0.1",
|
||||
"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 { useStore } from 'zustand';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Delete as DeleteIcon, ImageSearch, Save as SaveIcon } from '@mui/icons-material';
|
||||
import {
|
||||
Autocomplete,
|
||||
Button,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Autocomplete,
|
||||
Stack,
|
||||
TextField,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Delete as DeleteIcon,
|
||||
Save as SaveIcon,
|
||||
} from '@mui/icons-material';
|
||||
import * as ExifReader from 'exifreader';
|
||||
import * as React from 'react';
|
||||
import { useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { BaseImgParams, Txt2ImgParams } from '../client/types.js';
|
||||
import { StateContext } from '../state.js';
|
||||
import { BaseImgParams } from '../client/types.js';
|
||||
|
||||
export interface ProfilesProps {
|
||||
params: BaseImgParams;
|
||||
|
@ -78,6 +76,29 @@ export function Profiles(props: ProfilesProps) {
|
|||
<Button type="button" variant="contained" onClick={() => setDialogOpen(true)}>
|
||||
<SaveIcon />
|
||||
</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>
|
||||
)}
|
||||
onChange={(event, value) => {
|
||||
|
@ -124,3 +145,95 @@ export function Profiles(props: ProfilesProps) {
|
|||
</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"
|
||||
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":
|
||||
version "1.9.5"
|
||||
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"
|
||||
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:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
|
|
Loading…
Reference in New Issue