1
0
Fork 0

feat(gui): load and merge server params with config

This commit is contained in:
Sean Sube 2023-01-09 22:59:08 -06:00
parent 03fd728ab0
commit 37efd51341
11 changed files with 146 additions and 146 deletions

View File

@ -2,10 +2,18 @@
"api": { "api": {
"root": "http://127.0.0.1:5000" "root": "http://127.0.0.1:5000"
}, },
"default": { "params": {
"model": "stable-diffusion-onnx-v1-5", "model": {
"platform": "amd", "default": "stable-diffusion-onnx-v1-5"
"scheduler": "euler-a", },
"prompt": "an astronaut eating a hamburger" "platform": {
"default": "amd"
},
"scheduler": {
"default": "euler-a"
},
"prompt": {
"default": "an astronaut eating a hamburger"
}
} }
} }

View File

@ -13,7 +13,9 @@
"@mui/icons-material": "^5.11.0", "@mui/icons-material": "^5.11.0",
"@mui/lab": "^5.0.0-alpha.114", "@mui/lab": "^5.0.0-alpha.114",
"@mui/material": "^5.11.3", "@mui/material": "^5.11.3",
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.18", "@types/node": "^18.11.18",
"lodash": "^4.17.21",
"noicejs": "^5.0.0-3", "noicejs": "^5.0.0-3",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

View File

@ -1,4 +1,5 @@
import { doesExist, NotImplementedError } from '@apextoaster/js-utils'; import { doesExist, NotImplementedError } from '@apextoaster/js-utils';
import { ConfigParams } from '../config';
export interface BaseImgParams { export interface BaseImgParams {
/** /**
@ -57,6 +58,7 @@ export interface ApiResponse {
export interface ApiClient { export interface ApiClient {
models(): Promise<Array<string>>; models(): Promise<Array<string>>;
params(): Promise<ConfigParams>;
platforms(): Promise<Array<string>>; platforms(): Promise<Array<string>>;
schedulers(): Promise<Array<string>>; schedulers(): Promise<Array<string>>;
@ -136,6 +138,11 @@ export function makeClient(root: string, f = fetch): ApiClient {
const res = await f(path); const res = await f(path);
return await res.json() as Array<string>; return await res.json() as Array<string>;
}, },
async params(): Promise<ConfigParams> {
const path = new URL(joinPath('settings', 'params'), root);
const res = await f(path);
return await res.json() as ConfigParams;
},
async schedulers(): Promise<Array<string>> { async schedulers(): Promise<Array<string>> {
const path = new URL(joinPath('settings', 'schedulers'), root); const path = new URL(joinPath('settings', 'schedulers'), root);
const res = await f(path); const res = await f(path);

View File

@ -1,28 +1,30 @@
import { doesExist } from '@apextoaster/js-utils'; import { doesExist } from '@apextoaster/js-utils';
import { Casino } from '@mui/icons-material'; import { Casino } from '@mui/icons-material';
import { IconButton, Stack, TextField } from '@mui/material'; import { Button, Stack, TextField } from '@mui/material';
import * as React from 'react'; import * as React from 'react';
import { BaseImgParams } from '../api/client.js'; import { BaseImgParams } from '../api/client.js';
import { CONFIG_DEFAULTS } from '../config.js'; import { ConfigParams } from '../config.js';
import { NumericField } from './NumericField.js'; import { NumericField } from './NumericField.js';
export interface ImageControlProps { export interface ImageControlProps {
config: ConfigParams;
params: BaseImgParams; params: BaseImgParams;
onChange?: (params: BaseImgParams) => void; onChange?: (params: BaseImgParams) => void;
} }
export function ImageControl(props: ImageControlProps) { export function ImageControl(props: ImageControlProps) {
const { params } = props; const { config, params } = props;
return <Stack spacing={2}> return <Stack spacing={2}>
<Stack direction='row' spacing={4}> <Stack direction='row' spacing={4}>
<NumericField <NumericField
decimal decimal
label='CFG' label='CFG'
min={CONFIG_DEFAULTS.cfg.min} min={config.cfg.min}
max={CONFIG_DEFAULTS.cfg.max} max={config.cfg.max}
step={CONFIG_DEFAULTS.cfg.step} step={config.cfg.step}
value={params.cfg} value={params.cfg}
onChange={(cfg) => { onChange={(cfg) => {
if (doesExist(props.onChange)) { if (doesExist(props.onChange)) {
@ -35,9 +37,9 @@ export function ImageControl(props: ImageControlProps) {
/> />
<NumericField <NumericField
label='Steps' label='Steps'
min={CONFIG_DEFAULTS.steps.min} min={config.steps.min}
max={CONFIG_DEFAULTS.steps.max} max={config.steps.max}
step={CONFIG_DEFAULTS.steps.step} step={config.steps.step}
value={params.steps} value={params.steps}
onChange={(steps) => { onChange={(steps) => {
if (doesExist(props.onChange)) { if (doesExist(props.onChange)) {
@ -50,9 +52,9 @@ export function ImageControl(props: ImageControlProps) {
/> />
<NumericField <NumericField
label='Seed' label='Seed'
min={CONFIG_DEFAULTS.seed.min} min={config.seed.min}
max={CONFIG_DEFAULTS.seed.max} max={config.seed.max}
step={CONFIG_DEFAULTS.seed.step} step={config.seed.step}
value={params.seed} value={params.seed}
onChange={(seed) => { onChange={(seed) => {
if (doesExist(props.onChange)) { if (doesExist(props.onChange)) {
@ -63,17 +65,21 @@ export function ImageControl(props: ImageControlProps) {
} }
}} }}
/> />
<IconButton onClick={() => { <Button
const seed = Math.floor(Math.random() * CONFIG_DEFAULTS.seed.max); variant='outlined'
if (doesExist(props.onChange)) { startIcon={<Casino />}
props.onChange({ onClick={() => {
...params, const seed = Math.floor(Math.random() * config.seed.max);
seed, if (doesExist(props.onChange)) {
}); props.onChange({
} ...params,
}}> seed,
<Casino /> });
</IconButton> }
}}
>
New Seed
</Button>
</Stack> </Stack>
<TextField label='Prompt' variant='outlined' value={params.prompt} onChange={(event) => { <TextField label='Prompt' variant='outlined' value={params.prompt} onChange={(event) => {
if (doesExist(props.onChange)) { if (doesExist(props.onChange)) {

View File

@ -4,7 +4,7 @@ import * as React from 'react';
import { useMutation, useQuery } from 'react-query'; import { useMutation, useQuery } from 'react-query';
import { ApiClient, BaseImgParams } from '../api/client.js'; import { ApiClient, BaseImgParams } from '../api/client.js';
import { Config, CONFIG_DEFAULTS, IMAGE_FILTER, STALE_TIME } from '../config.js'; import { ConfigParams, IMAGE_FILTER, STALE_TIME } from '../config.js';
import { SCHEDULER_LABELS } from '../strings.js'; import { SCHEDULER_LABELS } from '../strings.js';
import { ImageInput } from './ImageInput.js'; import { ImageInput } from './ImageInput.js';
import { ImageCard } from './ImageCard.js'; import { ImageCard } from './ImageCard.js';
@ -17,7 +17,7 @@ const { useState } = React;
export interface Img2ImgProps { export interface Img2ImgProps {
client: ApiClient; client: ApiClient;
config: Config; config: ConfigParams;
model: string; model: string;
platform: string; platform: string;
@ -43,14 +43,14 @@ export function Img2Img(props: Img2ImgProps) {
}); });
const [source, setSource] = useState<File>(); const [source, setSource] = useState<File>();
const [strength, setStrength] = useState(CONFIG_DEFAULTS.strength.default); const [strength, setStrength] = useState(config.strength.default);
const [params, setParams] = useState<BaseImgParams>({ const [params, setParams] = useState<BaseImgParams>({
cfg: CONFIG_DEFAULTS.cfg.default, cfg: config.cfg.default,
seed: CONFIG_DEFAULTS.seed.default, seed: config.seed.default,
steps: CONFIG_DEFAULTS.steps.default, steps: config.steps.default,
prompt: config.default.prompt, prompt: config.prompt.default,
}); });
const [scheduler, setScheduler] = useState(config.default.scheduler); const [scheduler, setScheduler] = useState(config.scheduler.default);
return <Box> return <Box>
<Stack spacing={2}> <Stack spacing={2}>
@ -67,15 +67,15 @@ export function Img2Img(props: Img2ImgProps) {
/> />
</Stack> </Stack>
<ImageInput filter={IMAGE_FILTER} label='Source' onChange={setSource} /> <ImageInput filter={IMAGE_FILTER} label='Source' onChange={setSource} />
<ImageControl params={params} onChange={(newParams) => { <ImageControl config={config} params={params} onChange={(newParams) => {
setParams(newParams); setParams(newParams);
}} /> }} />
<NumericField <NumericField
decimal decimal
label='Strength' label='Strength'
min={CONFIG_DEFAULTS.strength.min} min={config.strength.min}
max={CONFIG_DEFAULTS.strength.max} max={config.strength.max}
step={CONFIG_DEFAULTS.strength.step} step={config.strength.step}
value={strength} value={strength}
onChange={(value) => { onChange={(value) => {
setStrength(value); setStrength(value);

View File

@ -5,7 +5,7 @@ import * as React from 'react';
import { useMutation, useQuery } from 'react-query'; import { useMutation, useQuery } from 'react-query';
import { ApiClient, ApiResponse, BaseImgParams, equalResponse } from '../api/client.js'; import { ApiClient, ApiResponse, BaseImgParams, equalResponse } from '../api/client.js';
import { Config, CONFIG_DEFAULTS, DEFAULT_BRUSH, IMAGE_FILTER, STALE_TIME } from '../config.js'; import { Config, ConfigParams, DEFAULT_BRUSH, IMAGE_FILTER, STALE_TIME } from '../config.js';
import { SCHEDULER_LABELS } from '../strings.js'; import { SCHEDULER_LABELS } from '../strings.js';
import { ImageInput } from './ImageInput.js'; import { ImageInput } from './ImageInput.js';
import { ImageCard } from './ImageCard.js'; import { ImageCard } from './ImageCard.js';
@ -57,7 +57,7 @@ export interface Point {
export interface InpaintProps { export interface InpaintProps {
client: ApiClient; client: ApiClient;
config: Config; config: ConfigParams;
model: string; model: string;
platform: string; platform: string;
@ -166,12 +166,12 @@ export function Inpaint(props: InpaintProps) {
const [mask, setMask] = useState<File>(); const [mask, setMask] = useState<File>();
const [source, setSource] = useState<File>(); const [source, setSource] = useState<File>();
const [params, setParams] = useState<BaseImgParams>({ const [params, setParams] = useState<BaseImgParams>({
cfg: CONFIG_DEFAULTS.cfg.default, cfg: config.cfg.default,
seed: CONFIG_DEFAULTS.seed.default, seed: config.seed.default,
steps: CONFIG_DEFAULTS.steps.default, steps: config.steps.default,
prompt: config.default.prompt, prompt: config.prompt.default,
}); });
const [scheduler, setScheduler] = useState(config.default.scheduler); const [scheduler, setScheduler] = useState(config.scheduler.default);
useEffect(() => { useEffect(() => {
const canvas = mustExist(canvasRef.current); const canvas = mustExist(canvasRef.current);
@ -190,11 +190,11 @@ export function Inpaint(props: InpaintProps) {
function renderCanvas() { function renderCanvas() {
return <canvas return <canvas
ref={canvasRef} ref={canvasRef}
height={CONFIG_DEFAULTS.height.default} height={config.height.default}
width={CONFIG_DEFAULTS.width.default} width={config.width.default}
style={{ style={{
maxHeight: CONFIG_DEFAULTS.height.default, maxHeight: config.height.default,
maxWidth: CONFIG_DEFAULTS.width.default, maxWidth: config.width.default,
}} }}
onClick={(event) => { onClick={(event) => {
const canvas = mustExist(canvasRef.current); const canvas = mustExist(canvasRef.current);
@ -271,22 +271,25 @@ export function Inpaint(props: InpaintProps) {
}} }}
/> />
<Button <Button
variant='outlined'
startIcon={<FormatColorFill />} startIcon={<FormatColorFill />}
onClick={() => floodMask(floodBelow)}> onClick={() => floodMask(floodBelow)}>
Gray to black Gray to black
</Button> </Button>
<Button <Button
variant='outlined'
startIcon={<Gradient />} startIcon={<Gradient />}
onClick={() => grayscaleMask()}> onClick={() => grayscaleMask()}>
Grayscale Grayscale
</Button> </Button>
<Button <Button
variant='outlined'
startIcon={<FormatColorFill />} startIcon={<FormatColorFill />}
onClick={() => floodMask(floodAbove)}> onClick={() => floodMask(floodAbove)}>
Gray to white Gray to white
</Button> </Button>
</Stack> </Stack>
<ImageControl params={params} onChange={(newParams) => { <ImageControl config={config} params={params} onChange={(newParams) => {
setParams(newParams); setParams(newParams);
}} /> }} />
<Button onClick={() => upload.mutate()}>Generate</Button> <Button onClick={() => upload.mutate()}>Generate</Button>

View File

@ -4,7 +4,7 @@ import * as React from 'react';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { ApiClient } from '../api/client.js'; import { ApiClient } from '../api/client.js';
import { Config, STALE_TIME } from '../config.js'; import { Config, ConfigParams, STALE_TIME } from '../config.js';
import { MODEL_LABELS, PLATFORM_LABELS } from '../strings.js'; import { MODEL_LABELS, PLATFORM_LABELS } from '../strings.js';
import { Img2Img } from './Img2Img.js'; import { Img2Img } from './Img2Img.js';
import { Inpaint } from './Inpaint.js'; import { Inpaint } from './Inpaint.js';
@ -15,15 +15,15 @@ const { useState } = React;
export interface OnnxWebProps { export interface OnnxWebProps {
client: ApiClient; client: ApiClient;
config: Config; config: ConfigParams;
} }
export function OnnxWeb(props: OnnxWebProps) { export function OnnxWeb(props: OnnxWebProps) {
const { client, config } = props; const { client, config } = props;
const [tab, setTab] = useState('txt2img'); const [tab, setTab] = useState('txt2img');
const [model, setModel] = useState(config.default.model); const [model, setModel] = useState(config.model.default);
const [platform, setPlatform] = useState(config.default.platform); const [platform, setPlatform] = useState(config.platform.default);
const models = useQuery('models', async () => client.models(), { const models = useQuery('models', async () => client.models(), {
staleTime: STALE_TIME, staleTime: STALE_TIME,

View File

@ -3,7 +3,7 @@ import * as React from 'react';
import { useMutation, useQuery } from 'react-query'; import { useMutation, useQuery } from 'react-query';
import { ApiClient, BaseImgParams } from '../api/client.js'; import { ApiClient, BaseImgParams } from '../api/client.js';
import { Config, CONFIG_DEFAULTS, STALE_TIME } from '../config.js'; import { ConfigParams, STALE_TIME } from '../config.js';
import { SCHEDULER_LABELS } from '../strings.js'; import { SCHEDULER_LABELS } from '../strings.js';
import { ImageCard } from './ImageCard.js'; import { ImageCard } from './ImageCard.js';
import { ImageControl } from './ImageControl.js'; import { ImageControl } from './ImageControl.js';
@ -15,7 +15,7 @@ const { useState } = React;
export interface Txt2ImgProps { export interface Txt2ImgProps {
client: ApiClient; client: ApiClient;
config: Config; config: ConfigParams;
model: string; model: string;
platform: string; platform: string;
@ -40,15 +40,15 @@ export function Txt2Img(props: Txt2ImgProps) {
staleTime: STALE_TIME, staleTime: STALE_TIME,
}); });
const [height, setHeight] = useState(CONFIG_DEFAULTS.height.default); const [height, setHeight] = useState(config.height.default);
const [width, setWidth] = useState(CONFIG_DEFAULTS.width.default); const [width, setWidth] = useState(config.width.default);
const [params, setParams] = useState<BaseImgParams>({ const [params, setParams] = useState<BaseImgParams>({
cfg: CONFIG_DEFAULTS.cfg.default, cfg: config.cfg.default,
seed: CONFIG_DEFAULTS.seed.default, seed: config.seed.default,
steps: CONFIG_DEFAULTS.steps.default, steps: config.steps.default,
prompt: config.default.prompt, prompt: config.prompt.default,
}); });
const [scheduler, setScheduler] = useState(config.default.scheduler); const [scheduler, setScheduler] = useState(config.scheduler.default);
return <Box> return <Box>
<Stack spacing={2}> <Stack spacing={2}>
@ -64,15 +64,15 @@ export function Txt2Img(props: Txt2ImgProps) {
}} }}
/> />
</Stack> </Stack>
<ImageControl params={params} onChange={(newParams) => { <ImageControl config={config} params={params} onChange={(newParams) => {
setParams(newParams); setParams(newParams);
}} /> }} />
<Stack direction='row' spacing={4}> <Stack direction='row' spacing={4}>
<NumericField <NumericField
label='Width' label='Width'
min={CONFIG_DEFAULTS.width.min} min={config.width.min}
max={CONFIG_DEFAULTS.width.max} max={config.width.max}
step={CONFIG_DEFAULTS.width.step} step={config.width.step}
value={width} value={width}
onChange={(value) => { onChange={(value) => {
setWidth(value); setWidth(value);
@ -80,9 +80,9 @@ export function Txt2Img(props: Txt2ImgProps) {
/> />
<NumericField <NumericField
label='Height' label='Height'
min={CONFIG_DEFAULTS.height.min} min={config.height.min}
max={CONFIG_DEFAULTS.height.max} max={config.height.max}
step={CONFIG_DEFAULTS.height.step} step={config.height.step}
value={height} value={height}
onChange={(value) => { onChange={(value) => {
setHeight(value); setHeight(value);

View File

@ -1,17 +1,45 @@
import { Img2ImgParams, STATUS_SUCCESS, Txt2ImgParams } from './api/client.js'; import { Img2ImgParams, STATUS_SUCCESS, Txt2ImgParams } from './api/client.js';
export interface ConfigNumber {
default: number;
min: number;
max: number;
step: number;
}
export interface ConfigString {
default: string;
keys: Array<string>;
}
export type KeyFilter<T extends object> = {
[K in keyof T]: T[K] extends number ? K : T[K] extends string ? K : never;
}[keyof T];
export type ConfigRanges<T extends object> = {
[K in KeyFilter<T>]: T[K] extends number ? ConfigNumber : T[K] extends string ? ConfigString : never;
};
export type ConfigParams = ConfigRanges<Required<Img2ImgParams & Txt2ImgParams>>;
export interface Config { export interface Config {
api: { api: {
root: string; root: string;
}; };
default: { params: {
model: string; model: ConfigString;
platform: string; platform: ConfigString;
scheduler: string; scheduler: ConfigString;
prompt: string; prompt: ConfigString;
}; };
} }
export const DEFAULT_BRUSH = 8;
export const IMAGE_FILTER = '.bmp, .jpg, .jpeg, .png';
export const IMAGE_STEP = 8;
export const IMAGE_MAX = 512;
export const STALE_TIME = 3_000;
export async function loadConfig(): Promise<Config> { export async function loadConfig(): Promise<Config> {
const configPath = new URL('./config.json', window.origin); const configPath = new URL('./config.json', window.origin);
const configReq = await fetch(configPath); const configReq = await fetch(configPath);
@ -21,70 +49,3 @@ export async function loadConfig(): Promise<Config> {
throw new Error('could not load config'); throw new Error('could not load config');
} }
} }
export interface ConfigRange {
default: number;
min: number;
max: number;
step: number;
}
export type KeyFilter<T extends object> = {
[K in keyof T]: T[K] extends number ? K : T[K] extends string ? K : never;
}[keyof T];
export type ConfigRanges<T extends object> = {
[K in KeyFilter<T>]: T[K] extends number ? ConfigRange : T[K] extends string ? string : never;
};
export const DEFAULT_BRUSH = 8;
export const IMAGE_FILTER = '.bmp, .jpg, .jpeg, .png';
export const IMAGE_STEP = 8;
export const IMAGE_MAX = 512;
export const CONFIG_DEFAULTS: ConfigRanges<Required<Img2ImgParams & Txt2ImgParams>> = {
cfg: {
default: 6,
min: 1,
max: 30,
step: 0.1,
},
height: {
default: IMAGE_MAX,
min: IMAGE_STEP,
max: IMAGE_MAX,
step: IMAGE_STEP,
},
model: '',
negativePrompt: '',
platform: '',
prompt: 'an astronaut eating a hamburger',
scheduler: '',
steps: {
default: 25,
min: 1,
max: 200,
step: 1,
},
seed: {
default: -1,
min: -1,
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
max: (2 ** 32) - 1,
step: 1,
},
strength: {
default: 0.5,
min: 0,
max: 1,
step: 0.01,
},
width: {
default: IMAGE_MAX,
min: IMAGE_STEP,
max: IMAGE_MAX,
step: IMAGE_STEP,
},
};
export const STALE_TIME = 3_000;

View File

@ -1,22 +1,25 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { mustExist } from '@apextoaster/js-utils'; import { mustExist } from '@apextoaster/js-utils';
import { merge } from 'lodash';
import * as React from 'react'; import * as React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from 'react-query'; import { QueryClient, QueryClientProvider } from 'react-query';
import { makeClient } from './api/client.js'; import { makeClient } from './api/client.js';
import { OnnxWeb } from './components/OnnxWeb.js'; import { OnnxWeb } from './components/OnnxWeb.js';
import { loadConfig } from './config.js'; import { ConfigParams, loadConfig } from './config.js';
export async function main() { export async function main() {
const config = await loadConfig(); const config = await loadConfig();
const client = makeClient(config.api.root); const client = makeClient(config.api.root);
const params = await client.params();
const merged = merge(params, config.params) as ConfigParams;
const query = new QueryClient(); const query = new QueryClient();
const appElement = mustExist(document.getElementById('app')); const appElement = mustExist(document.getElementById('app'));
const app = ReactDOM.createRoot(appElement); const app = ReactDOM.createRoot(appElement);
app.render(<QueryClientProvider client={query}> app.render(<QueryClientProvider client={query}>
<OnnxWeb client={client} config={config} /> <OnnxWeb client={client} config={merged} />
</QueryClientProvider>); </QueryClientProvider>);
} }

View File

@ -541,6 +541,11 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/lodash@^4.14.191":
version "4.14.191"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa"
integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==
"@types/mocha@^10.0.1": "@types/mocha@^10.0.1":
version "10.0.1" version "10.0.1"
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.1.tgz#2f4f65bb08bc368ac39c96da7b2f09140b26851b" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.1.tgz#2f4f65bb08bc368ac39c96da7b2f09140b26851b"
@ -2028,6 +2033,11 @@ lodash.merge@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
log-symbols@4.1.0: log-symbols@4.1.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"