lots of updates
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo } from 'react'
|
||||
import { useRecoilState } from 'recoil'
|
||||
import { nanoid } from 'nanoid'
|
||||
import useInputImage from './hooks/useInputImage'
|
||||
import LandingPage from './components/LandingPage/LandingPage'
|
||||
import { themeState } from './components/Header/ThemeChanger'
|
||||
import Workspace from './components/Workspace'
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Rect, Settings } from '../store/Atoms'
|
||||
import { dataURItoBlob } from '../utils'
|
||||
import { dataURItoBlob, srcToFile } from '../utils'
|
||||
|
||||
export const API_ENDPOINT = `${process.env.REACT_APP_INPAINTING_URL}`
|
||||
|
||||
@@ -57,6 +57,7 @@ export default async function inpaint(
|
||||
fd.append('sdSampler', settings.sdSampler.toString())
|
||||
fd.append('sdSeed', seed ? seed.toString() : '-1')
|
||||
fd.append('sdMatchHistograms', settings.sdMatchHistograms ? 'true' : 'false')
|
||||
fd.append('sdScale', (settings.sdScale / 100).toString())
|
||||
|
||||
fd.append('cv2Radius', settings.cv2Radius.toString())
|
||||
fd.append('cv2Flag', settings.cv2Flag.toString())
|
||||
@@ -198,3 +199,27 @@ export async function getMedias() {
|
||||
const errMsg = await res.text()
|
||||
throw new Error(errMsg)
|
||||
}
|
||||
|
||||
export async function downloadToOutput(
|
||||
image: HTMLImageElement,
|
||||
filename: string,
|
||||
mimeType: string
|
||||
) {
|
||||
const file = await srcToFile(image.src, filename, mimeType)
|
||||
const fd = new FormData()
|
||||
fd.append('image', file)
|
||||
fd.append('filename', filename)
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_ENDPOINT}/save_image`, {
|
||||
method: 'POST',
|
||||
body: fd,
|
||||
})
|
||||
if (!res.ok) {
|
||||
const errMsg = await res.text()
|
||||
throw new Error(errMsg)
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Something went wrong: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,10 @@ import {
|
||||
} from 'react-zoom-pan-pinch'
|
||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||
import { useWindowSize, useKey, useKeyPressEvent } from 'react-use'
|
||||
import inpaint, { postInteractiveSeg } from '../../adapters/inpainting'
|
||||
import inpaint, {
|
||||
downloadToOutput,
|
||||
postInteractiveSeg,
|
||||
} from '../../adapters/inpainting'
|
||||
import Button from '../shared/Button'
|
||||
import Slider from './Slider'
|
||||
import SizeSelector from './SizeSelector'
|
||||
@@ -34,7 +37,10 @@ import {
|
||||
} from '../../utils'
|
||||
import {
|
||||
croperState,
|
||||
enableFileManagerState,
|
||||
fileState,
|
||||
imageHeightState,
|
||||
imageWidthState,
|
||||
interactiveSegClicksState,
|
||||
isInpaintingState,
|
||||
isInteractiveSegRunningState,
|
||||
@@ -173,6 +179,10 @@ export default function Editor() {
|
||||
const [redoRenders, setRedoRenders] = useState<HTMLImageElement[]>([])
|
||||
const [redoCurLines, setRedoCurLines] = useState<Line[]>([])
|
||||
const [redoLineGroups, setRedoLineGroups] = useState<LineGroup[]>([])
|
||||
const enableFileManager = useRecoilValue(enableFileManagerState)
|
||||
|
||||
const [imageWidth, setImageWidth] = useRecoilState(imageWidthState)
|
||||
const [imageHeight, setImageHeight] = useRecoilState(imageHeightState)
|
||||
|
||||
const draw = useCallback(
|
||||
(render: HTMLImageElement, lineGroup: LineGroup) => {
|
||||
@@ -524,6 +534,9 @@ export default function Editor() {
|
||||
const rW = windowSize.width / original.naturalWidth
|
||||
const rH = (windowSize.height - TOOLBAR_SIZE) / original.naturalHeight
|
||||
|
||||
setImageWidth(original.naturalWidth)
|
||||
setImageHeight(original.naturalHeight)
|
||||
|
||||
let s = 1.0
|
||||
if (rW < 1 || rH < 1) {
|
||||
s = Math.min(rW, rH)
|
||||
@@ -1054,6 +1067,27 @@ export default function Editor() {
|
||||
if (file === undefined) {
|
||||
return
|
||||
}
|
||||
if (enableFileManager && renders.length > 0) {
|
||||
try {
|
||||
downloadToOutput(renders[renders.length - 1], file.name, file.type)
|
||||
setToastState({
|
||||
open: true,
|
||||
desc: `Save image success`,
|
||||
state: 'success',
|
||||
duration: 2000,
|
||||
})
|
||||
} catch (e: any) {
|
||||
setToastState({
|
||||
open: true,
|
||||
desc: e.message ? e.message : e.toString(),
|
||||
state: 'error',
|
||||
duration: 2000,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: download to output directory
|
||||
const name = file.name.replace(/(\.[\w\d_-]+)$/i, '_cleanup$1')
|
||||
const curRender = renders[renders.length - 1]
|
||||
downloadImage(curRender.currentSrc, name)
|
||||
|
||||
@@ -6,14 +6,23 @@ type SliderProps = {
|
||||
min?: number
|
||||
max?: number
|
||||
onChange: (value: number) => void
|
||||
onClick: () => void
|
||||
onClick?: () => void
|
||||
width?: number
|
||||
}
|
||||
|
||||
export default function Slider(props: SliderProps) {
|
||||
const { value, onChange, onClick, label, min, max } = props
|
||||
const { value, onChange, onClick, label, min, max, width } = props
|
||||
const styles: any = {}
|
||||
if (width !== undefined) {
|
||||
styles.width = width
|
||||
}
|
||||
|
||||
const step = ((max || 100) - (min || 0)) / 100
|
||||
|
||||
const onMouseUp = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.currentTarget?.blur()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="editor-brush-slider">
|
||||
<span>{label}</span>
|
||||
@@ -29,6 +38,8 @@ export default function Slider(props: SliderProps) {
|
||||
onChange(parseInt(ev.currentTarget.value, 10))
|
||||
}}
|
||||
onClick={onClick}
|
||||
style={styles}
|
||||
onMouseUp={onMouseUp}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import React, {
|
||||
useState,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||
import { useRecoilState } from 'recoil'
|
||||
import PhotoAlbum, { RenderPhoto } from 'react-photo-album'
|
||||
import * as ScrollArea from '@radix-ui/react-scroll-area'
|
||||
import Modal from '../shared/Modal'
|
||||
@@ -127,7 +127,7 @@ export default function FileManager(props: Props) {
|
||||
ref={onRefChange}
|
||||
>
|
||||
<PhotoAlbum
|
||||
layout="columns"
|
||||
layout="masonry"
|
||||
photos={photos}
|
||||
renderPhoto={renderPhoto}
|
||||
spacing={8}
|
||||
|
||||
@@ -21,7 +21,7 @@ function SettingBlock(props: SettingBlockProps) {
|
||||
<div className={`setting-block ${className}`}>
|
||||
<div className={contentClass}>
|
||||
<div className="setting-block-content-title">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
{desc ? (
|
||||
<Tooltip content={<div style={{ maxWidth: 400 }}>{desc}</div>}>
|
||||
<span>{title}</span>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import React, { FormEvent } from 'react'
|
||||
|
||||
import React from 'react'
|
||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||
import { FolderOpenIcon } from '@heroicons/react/24/outline'
|
||||
import * as Tabs from '@radix-ui/react-tabs'
|
||||
import {
|
||||
isPaintByExampleState,
|
||||
isSDState,
|
||||
@@ -14,10 +11,6 @@ import HDSettingBlock from './HDSettingBlock'
|
||||
import ModelSettingBlock from './ModelSettingBlock'
|
||||
import DownloadMaskSettingBlock from './DownloadMaskSettingBlock'
|
||||
import useHotKey from '../../hooks/useHotkey'
|
||||
import SettingBlock from './SettingBlock'
|
||||
import { Switch, SwitchThumb } from '../shared/Switch'
|
||||
import Button from '../shared/Button'
|
||||
import TextInput from '../shared/Input'
|
||||
|
||||
declare module 'react' {
|
||||
interface InputHTMLAttributes<T> extends HTMLAttributes<T> {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from 'react'
|
||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||
import { appState, croperState, settingState } from '../../store/Atoms'
|
||||
import Slider from '../Editor/Slider'
|
||||
import SettingBlock from '../Settings/SettingBlock'
|
||||
|
||||
const ImageResizeScale = () => {
|
||||
const [setting, setSettingState] = useRecoilState(settingState)
|
||||
const app = useRecoilValue(appState)
|
||||
const croper = useRecoilValue(croperState)
|
||||
|
||||
const handleSliderChange = (value: number) => {
|
||||
setSettingState(old => {
|
||||
return { ...old, sdScale: value }
|
||||
})
|
||||
}
|
||||
|
||||
const scaledWidth = () => {
|
||||
let width = app.imageWidth
|
||||
if (setting.showCroper) {
|
||||
width = croper.width
|
||||
}
|
||||
return Math.round((width * setting.sdScale) / 100)
|
||||
}
|
||||
|
||||
const scaledHeight = () => {
|
||||
let height = app.imageHeight
|
||||
if (setting.showCroper) {
|
||||
height = croper.height
|
||||
}
|
||||
return Math.round((height * setting.sdScale) / 100)
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingBlock
|
||||
className="sub-setting-block"
|
||||
title="Resize"
|
||||
titleSuffix={
|
||||
<div
|
||||
style={{ width: 86 }}
|
||||
>{`(${scaledWidth()}x${scaledHeight()})`}</div>
|
||||
}
|
||||
desc="Resize the image before inpainting, the area outside the mask will not lose quality."
|
||||
input={
|
||||
<Slider
|
||||
label=""
|
||||
width={70}
|
||||
min={50}
|
||||
max={100}
|
||||
value={setting.sdScale}
|
||||
onChange={handleSliderChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ImageResizeScale
|
||||
@@ -14,6 +14,7 @@ import { Switch, SwitchThumb } from '../shared/Switch'
|
||||
import Button from '../shared/Button'
|
||||
import emitter, { EVENT_PAINT_BY_EXAMPLE } from '../../event'
|
||||
import { useImage } from '../../utils'
|
||||
import ImageResizeScale from './ImageResizeScale'
|
||||
|
||||
const INPUT_WIDTH = 30
|
||||
|
||||
@@ -68,22 +69,6 @@ const PESidePanel = () => {
|
||||
</PopoverPrimitive.Trigger>
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content className="side-panel-content">
|
||||
<SettingBlock
|
||||
title="Croper"
|
||||
input={
|
||||
<Switch
|
||||
checked={setting.showCroper}
|
||||
onCheckedChange={value => {
|
||||
setSettingState(old => {
|
||||
return { ...old, showCroper: value }
|
||||
})
|
||||
}}
|
||||
>
|
||||
<SwitchThumb />
|
||||
</Switch>
|
||||
}
|
||||
/>
|
||||
|
||||
<NumberInputSetting
|
||||
title="Steps"
|
||||
width={INPUT_WIDTH}
|
||||
@@ -97,6 +82,8 @@ const PESidePanel = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<ImageResizeScale />
|
||||
|
||||
<NumberInputSetting
|
||||
title="Guidance Scale"
|
||||
width={INPUT_WIDTH}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FormEvent, useState } from 'react'
|
||||
import React, { FormEvent } from 'react'
|
||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||
import * as PopoverPrimitive from '@radix-ui/react-popover'
|
||||
import { useToggle } from 'react-use'
|
||||
@@ -15,6 +15,7 @@ import Selector from '../shared/Selector'
|
||||
import { Switch, SwitchThumb } from '../shared/Switch'
|
||||
import TextAreaInput from '../shared/Textarea'
|
||||
import emitter, { EVENT_PROMPT } from '../../event'
|
||||
import ImageResizeScale from './ImageResizeScale'
|
||||
|
||||
const INPUT_WIDTH = 30
|
||||
|
||||
@@ -71,6 +72,9 @@ const SidePanel = () => {
|
||||
</Switch>
|
||||
}
|
||||
/>
|
||||
|
||||
<ImageResizeScale />
|
||||
|
||||
{/*
|
||||
<NumberInputSetting
|
||||
title="Num Samples"
|
||||
@@ -98,21 +102,6 @@ const SidePanel = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* <NumberInputSetting
|
||||
title="Strength"
|
||||
width={INPUT_WIDTH}
|
||||
allowFloat
|
||||
value={`${setting.sdStrength}`}
|
||||
desc="TODO"
|
||||
onValue={value => {
|
||||
const val = value.length === 0 ? 0 : parseFloat(value)
|
||||
console.log(val)
|
||||
setSettingState(old => {
|
||||
return { ...old, sdStrength: val }
|
||||
})
|
||||
}}
|
||||
/> */}
|
||||
|
||||
<NumberInputSetting
|
||||
title="Guidance Scale"
|
||||
width={INPUT_WIDTH}
|
||||
|
||||
@@ -35,6 +35,8 @@ export interface Rect {
|
||||
|
||||
interface AppState {
|
||||
file: File | undefined
|
||||
imageHeight: number
|
||||
imageWidth: number
|
||||
disableShortCuts: boolean
|
||||
isInpainting: boolean
|
||||
isDisableModelSwitch: boolean
|
||||
@@ -49,6 +51,8 @@ export const appState = atom<AppState>({
|
||||
key: 'appState',
|
||||
default: {
|
||||
file: undefined,
|
||||
imageHeight: 0,
|
||||
imageWidth: 0,
|
||||
disableShortCuts: false,
|
||||
isInpainting: false,
|
||||
isDisableModelSwitch: false,
|
||||
@@ -82,6 +86,30 @@ export const isInpaintingState = selector({
|
||||
},
|
||||
})
|
||||
|
||||
export const imageHeightState = selector({
|
||||
key: 'imageHeightState',
|
||||
get: ({ get }) => {
|
||||
const app = get(appState)
|
||||
return app.imageHeight
|
||||
},
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const app = get(appState)
|
||||
set(appState, { ...app, imageHeight: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const imageWidthState = selector({
|
||||
key: 'imageWidthState',
|
||||
get: ({ get }) => {
|
||||
const app = get(appState)
|
||||
return app.imageWidth
|
||||
},
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const app = get(appState)
|
||||
set(appState, { ...app, imageWidth: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const showFileManagerState = selector({
|
||||
key: 'showFileManager',
|
||||
get: ({ get }) => {
|
||||
@@ -121,6 +149,12 @@ export const fileState = selector({
|
||||
isInteractiveSeg: false,
|
||||
isInteractiveSegRunning: false,
|
||||
})
|
||||
|
||||
const setting = get(settingState)
|
||||
set(settingState, {
|
||||
...setting,
|
||||
sdScale: 100,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -282,6 +316,7 @@ export interface Settings {
|
||||
sdSeedFixed: boolean // true: use sdSeed, false: random generate seed on backend
|
||||
sdNumSamples: number
|
||||
sdMatchHistograms: boolean
|
||||
sdScale: number
|
||||
|
||||
// For OpenCV2
|
||||
cv2Radius: number
|
||||
@@ -409,6 +444,7 @@ export const settingStateDefault: Settings = {
|
||||
sdSeedFixed: true,
|
||||
sdNumSamples: 1,
|
||||
sdMatchHistograms: false,
|
||||
sdScale: 100,
|
||||
|
||||
// CV2
|
||||
cv2Radius: 5,
|
||||
|
||||
Reference in New Issue
Block a user