diff --git a/api/onnx_web/output.py b/api/onnx_web/output.py index 79fc863c..6b051854 100644 --- a/api/onnx_web/output.py +++ b/api/onnx_web/output.py @@ -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), ) diff --git a/gui/package.json b/gui/package.json index f721bb40..527c2639 100644 --- a/gui/package.json +++ b/gui/package.json @@ -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", diff --git a/gui/src/components/Profiles.tsx b/gui/src/components/Profiles.tsx index 99c03c6c..fdf63ad4 100644 --- a/gui/src/components/Profiles.tsx +++ b/gui/src/components/Profiles.tsx @@ -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) { + )} onChange={(event, value) => { @@ -124,3 +145,95 @@ export function Profiles(props: ProfilesProps) { ; } + +export async function loadParamsFromFile(file: File): Promise> { + 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> { + 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 { + const lines = comment.split('\n'); + const [prompt, maybeNegative, ...otherLines] = lines; + + const params: Partial = { + 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; +} diff --git a/gui/yarn.lock b/gui/yarn.lock index 0ab674c3..f93b5e70 100644 --- a/gui/yarn.lock +++ b/gui/yarn.lock @@ -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"