Merge pull request #71 from Sanster/add_sd

Stable Diffusion
This commit is contained in:
Qing 2022-09-22 22:54:15 +08:00 committed by GitHub
commit 279f6d2138
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 2550 additions and 248 deletions

View File

@ -28,6 +28,7 @@
1. [ZITS](https://github.com/DQiaole/ZITS_inpainting) 1. [ZITS](https://github.com/DQiaole/ZITS_inpainting)
1. [MAT](https://github.com/fenglinglwb/MAT) 1. [MAT](https://github.com/fenglinglwb/MAT)
1. [FcF](https://github.com/SHI-Labs/FcF-Inpainting) 1. [FcF](https://github.com/SHI-Labs/FcF-Inpainting)
1. [SD1.4](https://github.com/CompVis/stable-diffusion)
- Support CPU & GPU - Support CPU & GPU
- Various inpainting [strategy](#inpainting-strategy) - Various inpainting [strategy](#inpainting-strategy)
- Run as a desktop APP - Run as a desktop APP
@ -41,6 +42,7 @@
| Remove Text | ![text](./assets/unwant_text.jpg) | ![text](./assets/unwant_text_clean.jpg) | | Remove Text | ![text](./assets/unwant_text.jpg) | ![text](./assets/unwant_text_clean.jpg) |
| Remove watermark | ![watermark](./assets/watermark.jpg) | ![watermark_clean](./assets/watermark_cleanup.jpg) | | Remove watermark | ![watermark](./assets/watermark.jpg) | ![watermark_clean](./assets/watermark_cleanup.jpg) |
| Fix old photo | ![oldphoto](./assets/old_photo.jpg) | ![oldphoto_clean](./assets/old_photo_clean.jpg) | | Fix old photo | ![oldphoto](./assets/old_photo.jpg) | ![oldphoto_clean](./assets/old_photo_clean.jpg) |
| Text Driven Inpainting | ![dog](./assets/dog.jpg) | ![fox](./assets/fox.jpg) |
## Quick Start ## Quick Start
@ -54,15 +56,16 @@ lama-cleaner --model=lama --device=cpu --port=8080
Available arguments: Available arguments:
| Name | Description | Default | | Name | Description | Default |
| ---------- | ---------------------------------------------------------------- | -------- | | ----------------- | -------------------------------------------------------------------------------------------------------- | -------- |
| --model | lama/ldm/zits. See details in [Inpaint Model](#inpainting-model) | lama | | --model | lama/ldm/zits/mat/fcf/sd. See details in [Inpaint Model](#inpainting-model) | lama |
| --device | cuda or cpu | cuda | | --hf_access_token | stable-diffusion(sd) model need huggingface access token https://huggingface.co/docs/hub/security-tokens | |
| --port | Port for backend flask web server | 8080 | | --device | cuda or cpu | cuda |
| --gui | Launch lama-cleaner as a desktop application | | | --port | Port for backend flask web server | 8080 |
| --gui_size | Set the window size for the application | 1200 900 | | --gui | Launch lama-cleaner as a desktop application | |
| --input | Path to image you want to load by default | None | | --gui_size | Set the window size for the application | 1200 900 |
| --debug | Enable debug mode for flask web server | | | --input | Path to image you want to load by default | None |
| --debug | Enable debug mode for flask web server | |
## Inpainting Model ## Inpainting Model
@ -73,6 +76,7 @@ Available arguments:
| ZITS | :+1: Better holistic structures compared with previous methods <br/> :neutral_face: Wireframe module is **very** slow on CPU | `Wireframe`: Enable edge and line detect | | ZITS | :+1: Better holistic structures compared with previous methods <br/> :neutral_face: Wireframe module is **very** slow on CPU | `Wireframe`: Enable edge and line detect |
| MAT | TODO | | | MAT | TODO | |
| FcF | :+1: Better structure and texture generation <br/> :neutral_face: Only support fixed size (512x512) input | | | FcF | :+1: Better structure and texture generation <br/> :neutral_face: Only support fixed size (512x512) input | |
| SD1.4 | :+1: SOTA text-to-image diffusion model | |
### LaMa vs LDM ### LaMa vs LDM

BIN
assets/dog.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
assets/fox.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -6,6 +6,7 @@
"dependencies": { "dependencies": {
"@heroicons/react": "^1.0.4", "@heroicons/react": "^1.0.4",
"@radix-ui/react-dialog": "0.1.8-rc.25", "@radix-ui/react-dialog": "0.1.8-rc.25",
"@radix-ui/react-popover": "^1.0.0",
"@radix-ui/react-select": "0.1.2-rc.27", "@radix-ui/react-select": "0.1.2-rc.27",
"@radix-ui/react-switch": "^0.1.5", "@radix-ui/react-switch": "^0.1.5",
"@radix-ui/react-toast": "^0.1.1", "@radix-ui/react-toast": "^0.1.1",
@ -20,14 +21,17 @@
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"cross-env": "7.x", "cross-env": "7.x",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mitt": "^3.0.0",
"nanoid": "^4.0.0", "nanoid": "^4.0.0",
"npm-run-all": "4.x", "npm-run-all": "4.x",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-hotkeys-hook": "^3.4.7",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"react-use": "^17.3.1", "react-use": "^17.3.1",
"react-zoom-pan-pinch": "^2.1.3", "react-zoom-pan-pinch": "^2.1.3",
"recoil": "^0.6.1", "recoil": "^0.6.1",
"socket.io-client": "^4.5.2",
"typescript": "4.x" "typescript": "4.x"
}, },
"scripts": { "scripts": {

View File

@ -1,5 +1,4 @@
import React, { useEffect, useMemo } from 'react' import React, { useEffect, useMemo } from 'react'
import { useKeyPressEvent } from 'react-use'
import { useRecoilState } from 'recoil' import { useRecoilState } from 'recoil'
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
import useInputImage from './hooks/useInputImage' import useInputImage from './hooks/useInputImage'
@ -9,6 +8,7 @@ import Workspace from './components/Workspace'
import { fileState } from './store/Atoms' import { fileState } from './store/Atoms'
import { keepGUIAlive } from './utils' import { keepGUIAlive } from './utils'
import Header from './components/Header/Header' import Header from './components/Header/Header'
import useHotKey from './hooks/useHotkey'
// Keeping GUI Window Open // Keeping GUI Window Open
keepGUIAlive() keepGUIAlive()
@ -24,11 +24,15 @@ function App() {
}, [userInputImage, setFile]) }, [userInputImage, setFile])
// Dark Mode Hotkey // Dark Mode Hotkey
useKeyPressEvent('D', ev => { useHotKey(
ev?.preventDefault() 'shift+d',
const newTheme = theme === 'light' ? 'dark' : 'light' () => {
setTheme(newTheme) const newTheme = theme === 'light' ? 'dark' : 'light'
}) setTheme(newTheme)
},
{},
[theme]
)
useEffect(() => { useEffect(() => {
document.body.setAttribute('data-theme', theme) document.body.setAttribute('data-theme', theme)

View File

@ -1,4 +1,4 @@
import { Settings } from '../store/Atoms' import { Rect, Settings } from '../store/Atoms'
import { dataURItoBlob } from '../utils' import { dataURItoBlob } from '../utils'
export const API_ENDPOINT = `${process.env.REACT_APP_INPAINTING_URL}` export const API_ENDPOINT = `${process.env.REACT_APP_INPAINTING_URL}`
@ -7,7 +7,10 @@ export default async function inpaint(
imageFile: File, imageFile: File,
maskBase64: string, maskBase64: string,
settings: Settings, settings: Settings,
sizeLimit?: string croperRect: Rect,
prompt?: string,
sizeLimit?: string,
seed?: number
) { ) {
// 1080, 2000, Original // 1080, 2000, Original
const fd = new FormData() const fd = new FormData()
@ -30,23 +33,38 @@ export default async function inpaint(
hdSettings.hdStrategyResizeLimit.toString() hdSettings.hdStrategyResizeLimit.toString()
) )
fd.append('prompt', prompt === undefined ? '' : prompt)
fd.append('croperX', croperRect.x.toString())
fd.append('croperY', croperRect.y.toString())
fd.append('croperHeight', croperRect.height.toString())
fd.append('croperWidth', croperRect.width.toString())
fd.append('useCroper', settings.showCroper ? 'true' : 'false')
fd.append('sdMaskBlur', settings.sdMaskBlur.toString())
fd.append('sdStrength', settings.sdStrength.toString())
fd.append('sdSteps', settings.sdSteps.toString())
fd.append('sdGuidanceScale', settings.sdGuidanceScale.toString())
fd.append('sdSampler', settings.sdSampler.toString())
fd.append('sdSeed', seed ? seed.toString() : '-1')
if (sizeLimit === undefined) { if (sizeLimit === undefined) {
fd.append('sizeLimit', '1080') fd.append('sizeLimit', '1080')
} else { } else {
fd.append('sizeLimit', sizeLimit) fd.append('sizeLimit', sizeLimit)
} }
const res = await fetch(`${API_ENDPOINT}/inpaint`, { try {
method: 'POST', const res = await fetch(`${API_ENDPOINT}/inpaint`, {
body: fd, method: 'POST',
}).then(async r => { body: fd,
if (r.ok) { })
return r.blob() if (res.ok) {
const blob = await res.blob()
const newSeed = res.headers.get('x-seed')
return { blob: URL.createObjectURL(blob), seed: newSeed }
} }
} catch {
throw new Error('Something went wrong on server side.') throw new Error('Something went wrong on server side.')
}) }
return URL.createObjectURL(res)
} }
export function switchModel(name: string) { export function switchModel(name: string) {

View File

@ -0,0 +1,167 @@
@use 'sass:math';
$drag-handle-shortside: 12px;
$drag-handle-longside: 40px;
$drag-bar-size: 12px;
$half-handle-shortside: math.div($drag-handle-shortside, 2);
$half-handle-longside: math.div($drag-handle-longside, 2);
$half-drag-bar-size: math.div($drag-bar-size, 2);
.crop-border {
outline-color: var(--yellow-accent);
outline-style: dashed;
}
.info-bar {
position: absolute;
pointer-events: auto;
font-size: 1rem;
padding: 0.2rem 0.8rem;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
color: var(--text-color);
background-color: var(--page-bg);
border-radius: 9999px;
border: var(--editor-toolkit-panel-border);
box-shadow: 0 0 0 1px #0000001a, 0 3px 16px #00000014, 0 2px 6px 1px #00000017;
&:hover {
cursor: move;
}
}
.croper-wrapper {
position: absolute;
height: 100%;
width: 100%;
z-index: 2;
overflow: hidden;
pointer-events: none;
}
.croper {
position: relative;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 2;
pointer-events: none;
// display: flex;
// flex-direction: column;
// align-items: center;
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
}
.drag-bar {
position: absolute;
pointer-events: auto;
// display: none;
&.ord-top {
top: 0;
left: 0;
width: 100%;
height: $drag-bar-size;
margin-top: -$half-drag-bar-size;
cursor: ns-resize;
}
&.ord-right {
right: 0;
top: 0;
width: $drag-bar-size;
height: 100%;
margin-right: -$half-drag-bar-size;
cursor: ew-resize;
}
&.ord-bottom {
bottom: 0;
left: 0;
width: 100%;
height: $drag-bar-size;
margin-bottom: -$half-drag-bar-size;
cursor: ns-resize;
}
&.ord-left {
top: 0;
left: 0;
width: $drag-bar-size;
height: 100%;
margin-left: -$half-drag-bar-size;
cursor: ew-resize;
}
}
.drag-handle {
width: $drag-handle-shortside;
height: $drag-handle-shortside;
z-index: 4;
position: absolute;
display: block;
content: '';
border: 2px solid var(--yellow-accent);
background-color: var(--yellow-accent-light);
pointer-events: auto;
&:hover {
background-color: var(--yellow-accent);
}
&.ord-topleft {
cursor: nw-resize;
top: (-$half-handle-shortside)-1px;
left: (-$half-handle-shortside)-1px;
}
&.ord-topright {
cursor: ne-resize;
top: -($half-handle-shortside)-1px;
right: -($half-handle-shortside)-1px;
}
&.ord-bottomright {
cursor: se-resize;
bottom: -($half-handle-shortside)-1px;
right: -($half-handle-shortside)-1px;
}
&.ord-bottomleft {
cursor: sw-resize;
bottom: -($half-handle-shortside)-1px;
left: -($half-handle-shortside)-1px;
}
&.ord-top,
&.ord-bottom {
left: calc(50% - $half-handle-shortside);
cursor: ns-resize;
}
&.ord-top {
top: (-$half-handle-shortside)-1px;
}
&.ord-bottom {
bottom: -($half-handle-shortside)-1px;
}
&.ord-left,
&.ord-right {
top: calc(50% - $half-handle-shortside);
cursor: ew-resize;
}
&.ord-left {
left: (-$half-handle-shortside)-1px;
}
&.ord-right {
right: -($half-handle-shortside)-1px;
}
}

View File

@ -0,0 +1,381 @@
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/outline'
import React, { useEffect, useState } from 'react'
import { useRecoilState, useRecoilValue } from 'recoil'
import {
croperHeight,
croperWidth,
croperX,
croperY,
isInpaintingState,
} from '../../store/Atoms'
const DOC_MOVE_OPTS = { capture: true, passive: false }
const DRAG_HANDLE_BORDER = 2
const DRAG_HANDLE_SHORT = 12
const DRAG_HANDLE_LONG = 40
interface EVData {
initX: number
initY: number
initHeight: number
initWidth: number
startResizeX: number
startResizeY: number
ord: string // top/right/bottom/left
}
interface Props {
maxHeight: number
maxWidth: number
scale: number
minHeight: number
minWidth: number
}
const Croper = (props: Props) => {
const { minHeight, minWidth, maxHeight, maxWidth, scale } = props
const [x, setX] = useRecoilState(croperX)
const [y, setY] = useRecoilState(croperY)
const [height, setHeight] = useRecoilState(croperHeight)
const [width, setWidth] = useRecoilState(croperWidth)
const isInpainting = useRecoilValue(isInpaintingState)
const [isResizing, setIsResizing] = useState(false)
const [isMoving, setIsMoving] = useState(false)
useEffect(() => {
setX(Math.round((maxWidth - 512) / 2))
setY(Math.round((maxHeight - 512) / 2))
}, [maxHeight, maxWidth, minHeight, minWidth])
const [evData, setEVData] = useState<EVData>({
initX: 0,
initY: 0,
initHeight: 0,
initWidth: 0,
startResizeX: 0,
startResizeY: 0,
ord: 'top',
})
const onDragFocus = () => {
console.log('focus')
}
const checkTopBottomLimit = (newY: number, newHeight: number) => {
if (newY > 0 && newHeight > minHeight && newY + newHeight <= maxHeight) {
return true
}
return false
}
const checkLeftRightLimit = (newX: number, newWidth: number) => {
if (newX > 0 && newWidth > minWidth && newX + newWidth <= maxWidth) {
return true
}
return false
}
const onPointerMove = (e: PointerEvent) => {
if (isInpainting) {
return
}
const curX = e.clientX
const curY = e.clientY
const offsetY = Math.round((curY - evData.startResizeY) / scale)
const offsetX = Math.round((curX - evData.startResizeX) / scale)
const moveTop = () => {
const newHeight = evData.initHeight - offsetY
const newY = evData.initY + offsetY
if (checkTopBottomLimit(newY, newHeight)) {
setHeight(newHeight)
setY(newY)
}
}
const moveBottom = () => {
const newHeight = evData.initHeight + offsetY
if (checkTopBottomLimit(evData.initY, newHeight)) {
setHeight(newHeight)
}
}
const moveLeft = () => {
const newWidth = evData.initWidth - offsetX
const newX = evData.initX + offsetX
if (checkLeftRightLimit(newX, newWidth)) {
setWidth(newWidth)
setX(newX)
}
}
const moveRight = () => {
const newWidth = evData.initWidth + offsetX
if (checkLeftRightLimit(evData.initX, newWidth)) {
setWidth(newWidth)
}
}
if (isResizing) {
switch (evData.ord) {
case 'topleft': {
moveTop()
moveLeft()
break
}
case 'topright': {
moveTop()
moveRight()
break
}
case 'bottomleft': {
moveBottom()
moveLeft()
break
}
case 'bottomright': {
moveBottom()
moveRight()
break
}
case 'top': {
moveTop()
break
}
case 'right': {
moveRight()
break
}
case 'bottom': {
moveBottom()
break
}
case 'left': {
moveLeft()
break
}
default:
break
}
}
if (isMoving) {
const newX = evData.initX + offsetX
const newY = evData.initY + offsetY
if (
checkLeftRightLimit(newX, evData.initWidth) &&
checkTopBottomLimit(newY, evData.initHeight)
) {
setX(newX)
setY(newY)
}
}
}
const onPointerDone = (e: PointerEvent) => {
if (isResizing) {
setIsResizing(false)
}
if (isMoving) {
setIsMoving(false)
}
}
useEffect(() => {
if (isResizing || isMoving) {
document.addEventListener('pointermove', onPointerMove, DOC_MOVE_OPTS)
document.addEventListener('pointerup', onPointerDone, DOC_MOVE_OPTS)
document.addEventListener('pointercancel', onPointerDone, DOC_MOVE_OPTS)
return () => {
document.removeEventListener(
'pointermove',
onPointerMove,
DOC_MOVE_OPTS
)
document.removeEventListener('pointerup', onPointerDone, DOC_MOVE_OPTS)
document.removeEventListener(
'pointercancel',
onPointerDone,
DOC_MOVE_OPTS
)
}
}
}, [isResizing, isMoving, width, height, evData])
const onCropPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
const { ord } = (e.target as HTMLElement).dataset
if (ord) {
setIsResizing(true)
setEVData({
initX: x,
initY: y,
initHeight: height,
initWidth: width,
startResizeX: e.clientX,
startResizeY: e.clientY,
ord,
})
}
}
const createCropSelection = () => {
return (
<div
className="drag-elements"
onFocus={onDragFocus}
onPointerDown={onCropPointerDown}
>
<div
className="drag-bar ord-top"
data-ord="top"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-bar ord-right"
data-ord="right"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-bar ord-bottom"
data-ord="bottom"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-bar ord-left"
data-ord="left"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-handle ord-topleft"
data-ord="topleft"
aria-label="topleft"
tabIndex={0}
role="button"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-handle ord-topright"
data-ord="topright"
aria-label="topright"
tabIndex={0}
role="button"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-handle ord-bottomleft"
data-ord="bottomleft"
aria-label="bottomleft"
tabIndex={0}
role="button"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-handle ord-bottomright"
data-ord="bottomright"
aria-label="bottomright"
tabIndex={0}
role="button"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-handle ord-top"
data-ord="top"
aria-label="top"
tabIndex={0}
role="button"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-handle ord-right"
data-ord="right"
aria-label="right"
tabIndex={0}
role="button"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-handle ord-bottom"
data-ord="bottom"
aria-label="bottom"
tabIndex={0}
role="button"
style={{ transform: `scale(${1 / scale})` }}
/>
<div
className="drag-handle ord-left"
data-ord="left"
aria-label="left"
tabIndex={0}
role="button"
style={{ transform: `scale(${1 / scale})` }}
/>
</div>
)
}
const onInfoBarPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
setIsMoving(true)
setEVData({
initX: x,
initY: y,
initHeight: height,
initWidth: width,
startResizeX: e.clientX,
startResizeY: e.clientY,
ord: '',
})
}
const createInfoBar = () => {
return (
<div
className="info-bar"
onPointerDown={onInfoBarPointerDown}
style={{
transform: `scale(${1 / scale})`,
top: `${10 / scale}px`,
left: `${10 / scale}px`,
}}
>
<div className="crop-size">
{width} x {height}
</div>
</div>
)
}
const createBorder = () => {
return (
<div
className="crop-border"
style={{
height,
width,
outlineWidth: `${DRAG_HANDLE_BORDER / scale}px`,
}}
/>
)
}
return (
<div className="croper-wrapper">
<div className="croper" style={{ height, width, left: x, top: y }}>
{createBorder()}
{createInfoBar()}
{createCropSelection()}
</div>
</div>
)
}
export default Croper

View File

@ -55,7 +55,7 @@
position: fixed; position: fixed;
bottom: 0.5rem; bottom: 0.5rem;
border-radius: 3rem; border-radius: 3rem;
padding: 1rem 3rem; padding: 0.6rem 3rem;
display: grid; display: grid;
grid-template-areas: 'toolkit-size-selector toolkit-brush-slider toolkit-btns'; grid-template-areas: 'toolkit-size-selector toolkit-brush-slider toolkit-btns';
column-gap: 2rem; column-gap: 2rem;

View File

@ -22,7 +22,6 @@ import Button from '../shared/Button'
import Slider from './Slider' import Slider from './Slider'
import SizeSelector from './SizeSelector' import SizeSelector from './SizeSelector'
import { import {
dataURItoBlob,
downloadImage, downloadImage,
isMidClick, isMidClick,
isRightClick, isRightClick,
@ -30,7 +29,19 @@ import {
srcToFile, srcToFile,
useImage, useImage,
} from '../../utils' } from '../../utils'
import { settingState, toastState } from '../../store/Atoms' import {
croperState,
isInpaintingState,
isSDState,
propmtState,
runManuallyState,
seedState,
settingState,
toastState,
} from '../../store/Atoms'
import useHotKey from '../../hooks/useHotkey'
import Croper from '../Croper/Croper'
import emitter, { EVENT_PROMPT } from '../../event'
const TOOLBAR_SIZE = 200 const TOOLBAR_SIZE = 200
const BRUSH_COLOR = '#ffcc00bb' const BRUSH_COLOR = '#ffcc00bb'
@ -74,8 +85,15 @@ function mouseXY(ev: SyntheticEvent) {
export default function Editor(props: EditorProps) { export default function Editor(props: EditorProps) {
const { file } = props const { file } = props
const promptVal = useRecoilValue(propmtState)
const settings = useRecoilValue(settingState) const settings = useRecoilValue(settingState)
const [seedVal, setSeed] = useRecoilState(seedState)
const croperRect = useRecoilValue(croperState)
const [toastVal, setToastState] = useRecoilState(toastState) const [toastVal, setToastState] = useRecoilState(toastState)
const [isInpainting, setIsInpainting] = useRecoilState(isInpaintingState)
const runMannually = useRecoilValue(runManuallyState)
const isSD = useRecoilValue(isSDState)
const [brushSize, setBrushSize] = useState(40) const [brushSize, setBrushSize] = useState(40)
const [original, isOriginalLoaded] = useImage(file) const [original, isOriginalLoaded] = useImage(file)
const [renders, setRenders] = useState<HTMLImageElement[]>([]) const [renders, setRenders] = useState<HTMLImageElement[]>([])
@ -84,13 +102,13 @@ export default function Editor(props: EditorProps) {
return document.createElement('canvas') return document.createElement('canvas')
}) })
const [lineGroups, setLineGroups] = useState<LineGroup[]>([]) const [lineGroups, setLineGroups] = useState<LineGroup[]>([])
const [lastLineGroup, setLastLineGroup] = useState<LineGroup>([])
const [curLineGroup, setCurLineGroup] = useState<LineGroup>([]) const [curLineGroup, setCurLineGroup] = useState<LineGroup>([])
const [{ x, y }, setCoords] = useState({ x: -1, y: -1 }) const [{ x, y }, setCoords] = useState({ x: -1, y: -1 })
const [showBrush, setShowBrush] = useState(false) const [showBrush, setShowBrush] = useState(false)
const [showRefBrush, setShowRefBrush] = useState(false) const [showRefBrush, setShowRefBrush] = useState(false)
const [isPanning, setIsPanning] = useState<boolean>(false) const [isPanning, setIsPanning] = useState<boolean>(false)
const [showOriginal, setShowOriginal] = useState(false) const [showOriginal, setShowOriginal] = useState(false)
const [isInpaintingLoading, setIsInpaintingLoading] = useState(false)
const [scale, setScale] = useState<number>(1) const [scale, setScale] = useState<number>(1)
const [panned, setPanned] = useState<boolean>(false) const [panned, setPanned] = useState<boolean>(false)
const [minScale, setMinScale] = useState<number>(1.0) const [minScale, setMinScale] = useState<number>(1.0)
@ -130,83 +148,28 @@ export default function Editor(props: EditorProps) {
[context, original] [context, original]
) )
const drawLinesOnMask = (_lineGroups: LineGroup[]) => { const drawLinesOnMask = useCallback(
if (!context?.canvas.width || !context?.canvas.height) { (_lineGroups: LineGroup[]) => {
throw new Error('canvas has invalid size') if (!context?.canvas.width || !context?.canvas.height) {
} throw new Error('canvas has invalid size')
maskCanvas.width = context?.canvas.width }
maskCanvas.height = context?.canvas.height maskCanvas.width = context?.canvas.width
const ctx = maskCanvas.getContext('2d') maskCanvas.height = context?.canvas.height
if (!ctx) { const ctx = maskCanvas.getContext('2d')
throw new Error('could not retrieve mask canvas') if (!ctx) {
} throw new Error('could not retrieve mask canvas')
_lineGroups.forEach(lineGroup => {
drawLines(ctx, lineGroup, 'white')
})
}
const runInpainting = async () => {
if (!hadDrawSomething()) {
return
}
const newLineGroups = [...lineGroups, curLineGroup]
setCurLineGroup([])
setIsDraging(false)
setIsInpaintingLoading(true)
if (settings.graduallyInpainting) {
drawLinesOnMask([curLineGroup])
} else {
drawLinesOnMask(newLineGroups)
}
let targetFile = file
if (settings.graduallyInpainting === true && renders.length > 0) {
console.info('gradually inpainting on last result')
const lastRender = renders[renders.length - 1]
targetFile = await srcToFile(lastRender.currentSrc, file.name, file.type)
}
try {
const res = await inpaint(
targetFile,
maskCanvas.toDataURL(),
settings,
sizeLimit.toString()
)
if (!res) {
throw new Error('empty response')
} }
const newRender = new Image()
await loadImage(newRender, res)
const newRenders = [...renders, newRender]
setRenders(newRenders)
draw(newRender, [])
// Only append new LineGroup after inpainting success
setLineGroups(newLineGroups)
// clear redo stack _lineGroups.forEach(lineGroup => {
resetRedoState() drawLines(ctx, lineGroup, 'white')
} catch (e: any) {
setToastState({
open: true,
desc: e.message ? e.message : e.toString(),
state: 'error',
duration: 2000,
}) })
drawOnCurrentRender([]) },
} [context, maskCanvas]
setIsInpaintingLoading(false) )
}
const hadDrawSomething = () => { const hadDrawSomething = useCallback(() => {
return curLineGroup.length !== 0 return curLineGroup.length !== 0
} }, [curLineGroup])
const hadRunInpainting = () => {
return renders.length !== 0
}
const drawOnCurrentRender = useCallback( const drawOnCurrentRender = useCallback(
(lineGroup: LineGroup) => { (lineGroup: LineGroup) => {
@ -219,8 +182,154 @@ export default function Editor(props: EditorProps) {
[original, renders, draw] [original, renders, draw]
) )
const runInpainting = useCallback(
async (prompt?: string, useLastLineGroup?: boolean) => {
// useLastLineGroup 的影响
// 1. 使用上一次的 mask
// 2. 结果替换当前 render
console.log('runInpainting')
let maskLineGroup = []
if (useLastLineGroup === true) {
if (lastLineGroup.length === 0) {
return
}
maskLineGroup = lastLineGroup
} else {
if (!hadDrawSomething()) {
return
}
setLastLineGroup(curLineGroup)
maskLineGroup = curLineGroup
}
const newLineGroups = [...lineGroups, maskLineGroup]
setCurLineGroup([])
setIsDraging(false)
setIsInpainting(true)
if (settings.graduallyInpainting) {
drawLinesOnMask([maskLineGroup])
} else {
drawLinesOnMask(newLineGroups)
}
let targetFile = file
if (settings.graduallyInpainting === true) {
if (useLastLineGroup === true) {
// renders.length == 1 还是用原来的
if (renders.length > 1) {
const lastRender = renders[renders.length - 2]
targetFile = await srcToFile(
lastRender.currentSrc,
file.name,
file.type
)
}
} else if (renders.length > 0) {
console.info('gradually inpainting on last result')
const lastRender = renders[renders.length - 1]
targetFile = await srcToFile(
lastRender.currentSrc,
file.name,
file.type
)
}
}
const sdSeed = settings.sdSeedFixed ? settings.sdSeed : -1
try {
const res = await inpaint(
targetFile,
maskCanvas.toDataURL(),
settings,
croperRect,
prompt,
sizeLimit.toString(),
sdSeed
)
if (!res) {
throw new Error('empty response')
}
const { blob, seed } = res
console.log(seed)
console.log(settings.sdSeedFixed)
if (seed && !settings.sdSeedFixed) {
setSeed(parseInt(seed, 10))
}
const newRender = new Image()
await loadImage(newRender, blob)
if (useLastLineGroup === true) {
const prevRenders = renders.slice(0, -1)
const newRenders = [...prevRenders, newRender]
setRenders(newRenders)
} else {
const newRenders = [...renders, newRender]
setRenders(newRenders)
}
draw(newRender, [])
// Only append new LineGroup after inpainting success
setLineGroups(newLineGroups)
// clear redo stack
resetRedoState()
} catch (e: any) {
setToastState({
open: true,
desc: e.message ? e.message : e.toString(),
state: 'error',
duration: 4000,
})
drawOnCurrentRender([])
}
setIsInpainting(false)
},
[
lineGroups,
curLineGroup,
maskCanvas,
settings.graduallyInpainting,
settings,
croperRect,
sizeLimit,
promptVal,
drawOnCurrentRender,
hadDrawSomething,
drawLinesOnMask,
]
)
useEffect(() => {
emitter.on(EVENT_PROMPT, () => {
if (hadDrawSomething()) {
runInpainting(promptVal)
} else if (lastLineGroup.length !== 0) {
runInpainting(promptVal, true)
} else {
setToastState({
open: true,
desc: 'Please draw mask on picture',
state: 'error',
duration: 1500,
})
}
})
return () => {
emitter.off(EVENT_PROMPT)
}
}, [hadDrawSomething, runInpainting, prompt])
const hadRunInpainting = () => {
return renders.length !== 0
}
const handleMultiStrokeKeyDown = () => { const handleMultiStrokeKeyDown = () => {
if (isInpaintingLoading) { if (isInpainting) {
return return
} }
setIsMultiStrokeKeyPressed(true) setIsMultiStrokeKeyPressed(true)
@ -230,13 +339,13 @@ export default function Editor(props: EditorProps) {
if (!isMultiStrokeKeyPressed) { if (!isMultiStrokeKeyPressed) {
return return
} }
if (isInpaintingLoading) { if (isInpainting) {
return return
} }
setIsMultiStrokeKeyPressed(false) setIsMultiStrokeKeyPressed(false)
if (!settings.runInpaintingManually) { if (!runMannually) {
runInpainting() runInpainting()
} }
} }
@ -246,7 +355,7 @@ export default function Editor(props: EditorProps) {
} }
useKey(predicate, handleMultiStrokeKeyup, { event: 'keyup' }, [ useKey(predicate, handleMultiStrokeKeyup, { event: 'keyup' }, [
isInpaintingLoading, isInpainting,
isMultiStrokeKeyPressed, isMultiStrokeKeyPressed,
hadDrawSomething, hadDrawSomething,
]) ])
@ -257,7 +366,7 @@ export default function Editor(props: EditorProps) {
{ {
event: 'keydown', event: 'keydown',
}, },
[isInpaintingLoading] [isInpainting]
) )
// Draw once the original image is loaded // Draw once the original image is loaded
@ -341,7 +450,7 @@ export default function Editor(props: EditorProps) {
}, [windowSize, resetZoom]) }, [windowSize, resetZoom])
const handleEscPressed = () => { const handleEscPressed = () => {
if (isInpaintingLoading) { if (isInpainting) {
return return
} }
if (isDraging || isMultiStrokeKeyPressed) { if (isDraging || isMultiStrokeKeyPressed) {
@ -361,7 +470,7 @@ export default function Editor(props: EditorProps) {
}, },
[ [
isDraging, isDraging,
isInpaintingLoading, isInpainting,
isMultiStrokeKeyPressed, isMultiStrokeKeyPressed,
resetZoom, resetZoom,
drawOnCurrentRender, drawOnCurrentRender,
@ -404,7 +513,7 @@ export default function Editor(props: EditorProps) {
if (!canvas) { if (!canvas) {
return return
} }
if (isInpaintingLoading) { if (isInpainting) {
return return
} }
if (!isDraging) { if (!isDraging) {
@ -416,13 +525,29 @@ export default function Editor(props: EditorProps) {
return return
} }
if (settings.runInpaintingManually) { if (runMannually) {
setIsDraging(false) setIsDraging(false)
} else { } else {
runInpainting() runInpainting()
} }
} }
const isOutsideCroper = (clickPnt: { x: number; y: number }) => {
if (clickPnt.x < croperRect.x) {
return true
}
if (clickPnt.y < croperRect.y) {
return true
}
if (clickPnt.x > croperRect.x + croperRect.width) {
return true
}
if (clickPnt.y > croperRect.y + croperRect.height) {
return true
}
return false
}
const onMouseDown = (ev: SyntheticEvent) => { const onMouseDown = (ev: SyntheticEvent) => {
if (isPanning) { if (isPanning) {
return return
@ -434,7 +559,7 @@ export default function Editor(props: EditorProps) {
if (!canvas) { if (!canvas) {
return return
} }
if (isInpaintingLoading) { if (isInpainting) {
return return
} }
@ -447,10 +572,14 @@ export default function Editor(props: EditorProps) {
return return
} }
if (isSD && settings.showCroper && isOutsideCroper(mouseXY(ev))) {
return
}
setIsDraging(true) setIsDraging(true)
let lineGroup: LineGroup = [] let lineGroup: LineGroup = []
if (isMultiStrokeKeyPressed || settings.runInpaintingManually) { if (isMultiStrokeKeyPressed || runMannually) {
lineGroup = [...curLineGroup] lineGroup = [...curLineGroup]
} }
lineGroup.push({ size: brushSize, pts: [mouseXY(ev)] }) lineGroup.push({ size: brushSize, pts: [mouseXY(ev)] })
@ -462,6 +591,7 @@ export default function Editor(props: EditorProps) {
if (curLineGroup.length === 0) { if (curLineGroup.length === 0) {
return return
} }
setLastLineGroup([])
const lastLine = curLineGroup.pop()! const lastLine = curLineGroup.pop()!
const newRedoCurLines = [...redoCurLines, lastLine] const newRedoCurLines = [...redoCurLines, lastLine]
@ -478,8 +608,8 @@ export default function Editor(props: EditorProps) {
} }
// save line Group // save line Group
const lastLineGroup = lineGroups.pop()! const latestLineGroup = lineGroups.pop()!
setRedoLineGroups([...redoLineGroups, lastLineGroup]) setRedoLineGroups([...redoLineGroups, latestLineGroup])
// If render is undo, clear strokes // If render is undo, clear strokes
setRedoCurLines([]) setRedoCurLines([])
@ -501,7 +631,7 @@ export default function Editor(props: EditorProps) {
}, [draw, renders, redoRenders, redoLineGroups, lineGroups, original]) }, [draw, renders, redoRenders, redoLineGroups, lineGroups, original])
const undo = () => { const undo = () => {
if (settings.runInpaintingManually && curLineGroup.length !== 0) { if (runMannually && curLineGroup.length !== 0) {
undoStroke() undoStroke()
} else { } else {
undoRender() undoRender()
@ -510,6 +640,7 @@ export default function Editor(props: EditorProps) {
// Handle Cmd+Z // Handle Cmd+Z
const undoPredicate = (event: KeyboardEvent) => { const undoPredicate = (event: KeyboardEvent) => {
// TODO: fix prompt input ctrl+z
const isCmdZ = const isCmdZ =
(event.metaKey || event.ctrlKey) && !event.shiftKey && event.key === 'z' (event.metaKey || event.ctrlKey) && !event.shiftKey && event.key === 'z'
// Handle tab switch // Handle tab switch
@ -524,17 +655,17 @@ export default function Editor(props: EditorProps) {
return false return false
} }
useKey(undoPredicate, undo, undefined, [undoStroke, undoRender]) useKey(undoPredicate, undo, undefined, [undoStroke, undoRender, isSD])
const disableUndo = () => { const disableUndo = () => {
if (isInpaintingLoading) { if (isInpainting) {
return true return true
} }
if (renders.length > 0) { if (renders.length > 0) {
return false return false
} }
if (settings.runInpaintingManually) { if (runMannually) {
if (curLineGroup.length === 0) { if (curLineGroup.length === 0) {
return true return true
} }
@ -575,7 +706,7 @@ export default function Editor(props: EditorProps) {
}, [draw, renders, redoRenders, redoLineGroups, lineGroups, original]) }, [draw, renders, redoRenders, redoLineGroups, lineGroups, original])
const redo = () => { const redo = () => {
if (settings.runInpaintingManually && redoCurLines.length !== 0) { if (runMannually && redoCurLines.length !== 0) {
redoStroke() redoStroke()
} else { } else {
redoRender() redoRender()
@ -600,17 +731,17 @@ export default function Editor(props: EditorProps) {
return false return false
} }
useKey(redoPredicate, redo, undefined, [redoStroke, redoRender]) useKey(redoPredicate, redo, undefined, [redoStroke, redoRender, isSD])
const disableRedo = () => { const disableRedo = () => {
if (isInpaintingLoading) { if (isInpainting) {
return true return true
} }
if (redoRenders.length > 0) { if (redoRenders.length > 0) {
return false return false
} }
if (settings.runInpaintingManually) { if (runMannually) {
if (redoCurLines.length === 0) { if (redoCurLines.length === 0) {
return true return true
} }
@ -688,7 +819,7 @@ export default function Editor(props: EditorProps) {
}, [showBrush, isPanning]) }, [showBrush, isPanning])
// Standard Hotkeys for Brush Size // Standard Hotkeys for Brush Size
useKeyPressEvent('[', () => { useHotKey('[', () => {
setBrushSize(currentBrushSize => { setBrushSize(currentBrushSize => {
if (currentBrushSize > 10) { if (currentBrushSize > 10) {
return currentBrushSize - 10 return currentBrushSize - 10
@ -700,18 +831,23 @@ export default function Editor(props: EditorProps) {
}) })
}) })
useKeyPressEvent(']', () => { useHotKey(']', () => {
setBrushSize(currentBrushSize => { setBrushSize(currentBrushSize => {
return currentBrushSize + 10 return currentBrushSize + 10
}) })
}) })
// Manual Inpainting Hotkey // Manual Inpainting Hotkey
useKeyPressEvent('R', () => { useHotKey(
if (settings.runInpaintingManually && hadDrawSomething()) { 'shift+r',
runInpainting() () => {
} if (runMannually && hadDrawSomething()) {
}) runInpainting()
}
},
{},
[runMannually]
)
// Toggle clean/zoom tool on spacebar. // Toggle clean/zoom tool on spacebar.
useKeyPressEvent( useKeyPressEvent(
@ -792,7 +928,7 @@ export default function Editor(props: EditorProps) {
}} }}
> >
<TransformComponent <TransformComponent
contentClass={isInpaintingLoading ? 'editor-canvas-loading' : ''} contentClass={isInpainting ? 'editor-canvas-loading' : ''}
contentStyle={{ contentStyle={{
visibility: initialCentered ? 'visible' : 'hidden', visibility: initialCentered ? 'visible' : 'hidden',
}} }}
@ -852,10 +988,22 @@ export default function Editor(props: EditorProps) {
/> />
</div> </div>
</div> </div>
{settings.showCroper ? (
<Croper
maxHeight={original.naturalHeight}
maxWidth={original.naturalWidth}
minHeight={Math.min(256, original.naturalHeight)}
minWidth={Math.min(256, original.naturalWidth)}
scale={scale}
/>
) : (
<></>
)}
</TransformComponent> </TransformComponent>
</TransformWrapper> </TransformWrapper>
{showBrush && !isInpaintingLoading && !isPanning && ( {showBrush && !isInpainting && !isPanning && (
<div className="brush-shape" style={getBrushStyle(x, y)} /> <div className="brush-shape" style={getBrushStyle(x, y)} />
)} )}
@ -867,11 +1015,15 @@ export default function Editor(props: EditorProps) {
)} )}
<div className="editor-toolkit-panel"> <div className="editor-toolkit-panel">
<SizeSelector {isSD ? (
onChange={onSizeLimitChange} <></>
originalWidth={original.naturalWidth} ) : (
originalHeight={original.naturalHeight} <SizeSelector
/> onChange={onSizeLimitChange}
originalWidth={original.naturalWidth}
originalHeight={original.naturalHeight}
/>
)}
<Slider <Slider
label="Brush" label="Brush"
min={10} min={10}
@ -977,9 +1129,9 @@ export default function Editor(props: EditorProps) {
/> />
</svg> </svg>
} }
disabled={!hadDrawSomething() || isInpaintingLoading} disabled={!hadDrawSomething() || isInpainting}
onClick={() => { onClick={() => {
if (!isInpaintingLoading && hadDrawSomething()) { if (!isInpainting && hadDrawSomething()) {
runInpainting() runInpainting()
} }
}} }}

View File

@ -1,6 +1,6 @@
header { header {
height: 60px; height: 60px;
padding: 1rem 2rem; padding: 1rem 1.5rem;
position: absolute; position: absolute;
top: 0; top: 0;
display: flex; display: flex;

View File

@ -1,17 +1,19 @@
import { ArrowLeftIcon, UploadIcon } from '@heroicons/react/outline' import { ArrowLeftIcon, UploadIcon } from '@heroicons/react/outline'
import React, { useState } from 'react' import React, { useState } from 'react'
import { useRecoilState } from 'recoil' import { useRecoilState, useRecoilValue } from 'recoil'
import { fileState } from '../../store/Atoms' import { fileState, isSDState } from '../../store/Atoms'
import Button from '../shared/Button' import Button from '../shared/Button'
import Shortcuts from '../Shortcuts/Shortcuts' import Shortcuts from '../Shortcuts/Shortcuts'
import useResolution from '../../hooks/useResolution' import useResolution from '../../hooks/useResolution'
import { ThemeChanger } from './ThemeChanger' import { ThemeChanger } from './ThemeChanger'
import SettingIcon from '../Settings/SettingIcon' import SettingIcon from '../Settings/SettingIcon'
import PromptInput from './PromptInput'
const Header = () => { const Header = () => {
const [file, setFile] = useRecoilState(fileState) const [file, setFile] = useRecoilState(fileState)
const resolution = useResolution() const resolution = useResolution()
const [uploadElemId] = useState(`file-upload-${Math.random().toString()}`) const [uploadElemId] = useState(`file-upload-${Math.random().toString()}`)
const isSD = useRecoilValue(isSDState)
const renderHeader = () => { const renderHeader = () => {
return ( return (
@ -37,6 +39,8 @@ const Header = () => {
</label> </label>
</div> </div>
{isSD && file ? <PromptInput /> : <></>}
<div className="header-icons-wrapper"> <div className="header-icons-wrapper">
<ThemeChanger /> <ThemeChanger />
{file && ( {file && (

View File

@ -0,0 +1,18 @@
.prompt-wrapper {
display: flex;
gap: 12px;
}
.prompt-wrapper input {
all: unset;
border-width: 0;
border-radius: 0.5rem;
min-width: 600px;
padding: 0 0.8rem;
outline: 1px solid var(--border-color);
&:focus-visible {
border-width: 0;
outline: 1px solid var(--yellow-accent);
}
}

View File

@ -0,0 +1,61 @@
import React, { FormEvent, useRef, useState } from 'react'
import { useClickAway } from 'react-use'
import { useRecoilState } from 'recoil'
import emitter, { EVENT_PROMPT } from '../../event'
import { appState, propmtState } from '../../store/Atoms'
import Button from '../shared/Button'
import TextInput from '../shared/Input'
// TODO: show progress in input
const PromptInput = () => {
const [app, setAppState] = useRecoilState(appState)
const [prompt, setPrompt] = useRecoilState(propmtState)
const ref = useRef(null)
const handleOnInput = (evt: FormEvent<HTMLInputElement>) => {
evt.preventDefault()
evt.stopPropagation()
const target = evt.target as HTMLInputElement
setPrompt(target.value)
}
const handleRepaintClick = () => {
if (prompt.length !== 0 && !app.isInpainting) {
emitter.emit(EVENT_PROMPT)
}
}
useClickAway<MouseEvent>(ref, () => {
if (ref?.current) {
const input = ref.current as HTMLInputElement
input.blur()
}
})
const onKeyUp = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleRepaintClick()
}
}
return (
<div className="prompt-wrapper">
<TextInput
ref={ref}
value={prompt}
onInput={handleOnInput}
onKeyUp={onKeyUp}
placeholder="I want to repaint of..."
/>
<Button
border
onClick={handleRepaintClick}
disabled={prompt.length === 0 || app.isInpainting}
>
Dream
</Button>
</div>
)
}
export default PromptInput

View File

@ -1,6 +1,6 @@
import React, { ReactNode } from 'react' import React, { ReactNode } from 'react'
import { useRecoilState } from 'recoil' import { useRecoilState } from 'recoil'
import { AIModel, settingState } from '../../store/Atoms' import { AIModel, SDSampler, settingState } from '../../store/Atoms'
import Selector from '../shared/Selector' import Selector from '../shared/Selector'
import { Switch, SwitchThumb } from '../shared/Switch' import { Switch, SwitchThumb } from '../shared/Switch'
import Tooltip from '../shared/Tooltip' import Tooltip from '../shared/Tooltip'
@ -145,6 +145,8 @@ function ModelSettingBlock() {
return undefined return undefined
case AIModel.FCF: case AIModel.FCF:
return renderFCFModelDesc() return renderFCFModelDesc()
case AIModel.SD14:
return undefined
default: default:
return <></> return <></>
} }
@ -182,6 +184,12 @@ function ModelSettingBlock() {
'https://arxiv.org/abs/2208.03382', 'https://arxiv.org/abs/2208.03382',
'https://github.com/SHI-Labs/FcF-Inpainting' 'https://github.com/SHI-Labs/FcF-Inpainting'
) )
case AIModel.SD14:
return renderModelDesc(
'Stable Diffusion',
'https://ommer-lab.com/research/latent-diffusion-models/',
'https://github.com/CompVis/stable-diffusion'
)
default: default:
return <></> return <></>
} }

View File

@ -1,17 +1,41 @@
import React from 'react' import React, { useRef } from 'react'
import { useClickAway } from 'react-use'
import NumberInput from '../shared/NumberInput' import NumberInput from '../shared/NumberInput'
import SettingBlock from './SettingBlock' import SettingBlock from './SettingBlock'
interface NumberInputSettingProps { interface NumberInputSettingProps {
title: string title: string
allowFloat?: boolean
desc?: string desc?: string
value: string value: string
suffix?: string suffix?: string
width?: number
widthUnit?: string
disable?: boolean
onValue: (val: string) => void onValue: (val: string) => void
} }
function NumberInputSetting(props: NumberInputSettingProps) { function NumberInputSetting(props: NumberInputSettingProps) {
const { title, desc, value, suffix, onValue } = props const {
title,
allowFloat,
desc,
value,
suffix,
onValue,
width,
widthUnit,
disable,
} = props
const ref = useRef(null)
useClickAway<MouseEvent>(ref, () => {
if (ref?.current) {
const input = ref.current as HTMLInputElement
input.blur()
}
})
return ( return (
<SettingBlock <SettingBlock
@ -28,9 +52,12 @@ function NumberInputSetting(props: NumberInputSettingProps) {
}} }}
> >
<NumberInput <NumberInput
style={{ width: '80px' }} allowFloat={allowFloat}
value={`${value}`} style={{ width: `${width}${widthUnit}` }}
value={value}
disabled={disable}
onValue={onValue} onValue={onValue}
ref={ref}
/> />
{suffix && <span>{suffix}</span>} {suffix && <span>{suffix}</span>}
</div> </div>
@ -39,4 +66,11 @@ function NumberInputSetting(props: NumberInputSettingProps) {
) )
} }
NumberInputSetting.defaultProps = {
allowFloat: false,
width: 80,
widthUnit: 'px',
disable: false,
}
export default NumberInputSetting export default NumberInputSetting

View File

@ -1,13 +1,14 @@
import React from 'react' import React from 'react'
import { useRecoilState } from 'recoil' import { useRecoilState, useRecoilValue } from 'recoil'
import { settingState } from '../../store/Atoms' import { isSDState, settingState } from '../../store/Atoms'
import Modal from '../shared/Modal' import Modal from '../shared/Modal'
import ManualRunInpaintingSettingBlock from './ManualRunInpaintingSettingBlock' import ManualRunInpaintingSettingBlock from './ManualRunInpaintingSettingBlock'
import HDSettingBlock from './HDSettingBlock' import HDSettingBlock from './HDSettingBlock'
import ModelSettingBlock from './ModelSettingBlock' import ModelSettingBlock from './ModelSettingBlock'
import GraduallyInpaintingSettingBlock from './GraduallyInpaintingSettingBlock' import GraduallyInpaintingSettingBlock from './GraduallyInpaintingSettingBlock'
import DownloadMaskSettingBlock from './DownloadMaskSettingBlock' import DownloadMaskSettingBlock from './DownloadMaskSettingBlock'
import useHotKey from '../../hooks/useHotkey'
interface SettingModalProps { interface SettingModalProps {
onClose: () => void onClose: () => void
@ -15,6 +16,7 @@ interface SettingModalProps {
export default function SettingModal(props: SettingModalProps) { export default function SettingModal(props: SettingModalProps) {
const { onClose } = props const { onClose } = props
const [setting, setSettingState] = useRecoilState(settingState) const [setting, setSettingState] = useRecoilState(settingState)
const isSD = useRecoilValue(isSDState)
const handleOnClose = () => { const handleOnClose = () => {
setSettingState(old => { setSettingState(old => {
@ -23,6 +25,17 @@ export default function SettingModal(props: SettingModalProps) {
onClose() onClose()
} }
useHotKey(
's',
() => {
setSettingState(old => {
return { ...old, show: !old.show }
})
},
{},
[]
)
return ( return (
<Modal <Modal
onClose={handleOnClose} onClose={handleOnClose}
@ -30,11 +43,12 @@ export default function SettingModal(props: SettingModalProps) {
className="modal-setting" className="modal-setting"
show={setting.show} show={setting.show}
> >
<ManualRunInpaintingSettingBlock /> {isSD ? <></> : <ManualRunInpaintingSettingBlock />}
<GraduallyInpaintingSettingBlock />
{/* <GraduallyInpaintingSettingBlock /> */}
<DownloadMaskSettingBlock /> <DownloadMaskSettingBlock />
<ModelSettingBlock /> <ModelSettingBlock />
<HDSettingBlock /> {isSD ? <></> : <HDSettingBlock />}
</Modal> </Modal>
) )
} }

View File

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import { useKeyPressEvent } from 'react-use'
import { useRecoilState } from 'recoil' import { useRecoilState } from 'recoil'
import useHotKey from '../../hooks/useHotkey'
import { shortcutsState } from '../../store/Atoms' import { shortcutsState } from '../../store/Atoms'
import Button from '../shared/Button' import Button from '../shared/Button'
@ -13,8 +13,7 @@ const Shortcuts = () => {
}) })
} }
useKeyPressEvent('h', ev => { useHotKey('h', () => {
ev?.preventDefault()
shortcutStateHandler() shortcutStateHandler()
}) })

View File

@ -64,7 +64,8 @@ export default function ShortcutsModal() {
<ShortCut content="Decrease Brush Size" keys={['[']} /> <ShortCut content="Decrease Brush Size" keys={['[']} />
<ShortCut content="Increase Brush Size" keys={[']']} /> <ShortCut content="Increase Brush Size" keys={[']']} />
<ShortCut content="Toggle Dark Mode" keys={['Shift', 'D']} /> <ShortCut content="Toggle Dark Mode" keys={['Shift', 'D']} />
<ShortCut content="Toggle Hotkeys Panel" keys={['H']} /> <ShortCut content="Toggle Hotkeys Dialog" keys={['H']} />
<ShortCut content="Toggle Settings Dialog" keys={['S']} />
</div> </div>
</Modal> </Modal>
) )

View File

@ -0,0 +1,57 @@
@use '../../styles/Mixins/' as *;
.side-panel {
position: absolute;
top: 68px;
right: 1.5rem;
padding: 0.3rem 0.3rem;
z-index: 4;
border-radius: 0.8rem;
border-style: solid;
border-color: var(--border-color);
border-width: 1px;
}
.side-panel-trigger {
font-family: 'WorkSans', sans-serif;
font-size: 16px;
border: 0px;
}
.side-panel-content {
position: relative;
font-family: 'WorkSans', sans-serif;
font-size: 14px;
top: 1rem;
right: 1.5rem;
padding: 1rem 1rem;
z-index: 9;
// backdrop-filter: blur(12px);
color: var(--text-color);
background-color: var(--page-bg);
border-radius: 0.8rem;
border-style: solid;
border-color: var(--border-color);
border-width: 1px;
display: flex;
flex-direction: column;
gap: 12px;
.setting-block-content {
gap: 1rem;
}
// input {
// height: 24px;
// // border-radius: 4px;
// }
// button {
// height: 28px;
// // border-radius: 4px;
// }
}

View File

@ -0,0 +1,177 @@
import React, { useState } from 'react'
import { useRecoilState } from 'recoil'
import * as PopoverPrimitive from '@radix-ui/react-popover'
import { useToggle } from 'react-use'
import { SDSampler, settingState } from '../../store/Atoms'
import NumberInputSetting from '../Settings/NumberInputSetting'
import SettingBlock from '../Settings/SettingBlock'
import Selector from '../shared/Selector'
import { Switch, SwitchThumb } from '../shared/Switch'
const INPUT_WIDTH = 30
// TODO: 添加收起来的按钮
const SidePanel = () => {
const [open, toggleOpen] = useToggle(true)
const [setting, setSettingState] = useRecoilState(settingState)
return (
<div className="side-panel">
<PopoverPrimitive.Root open={open}>
<PopoverPrimitive.Trigger
className="btn-primary side-panel-trigger"
onClick={() => toggleOpen()}
>
Stable Diffusion
</PopoverPrimitive.Trigger>
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content className="side-panel-content">
<SettingBlock
title="Show Croper"
input={
<Switch
checked={setting.showCroper}
onCheckedChange={value => {
setSettingState(old => {
return { ...old, showCroper: value }
})
}}
>
<SwitchThumb />
</Switch>
}
/>
{/*
<NumberInputSetting
title="Num Samples"
width={INPUT_WIDTH}
value={`${setting.sdNumSamples}`}
desc=""
onValue={value => {
const val = value.length === 0 ? 0 : parseInt(value, 10)
setSettingState(old => {
return { ...old, sdNumSamples: val }
})
}}
/> */}
<NumberInputSetting
title="Steps"
width={INPUT_WIDTH}
value={`${setting.sdSteps}`}
desc="Large steps result in better result, but more time-consuming"
onValue={value => {
const val = value.length === 0 ? 0 : parseInt(value, 10)
setSettingState(old => {
return { ...old, sdSteps: val }
})
}}
/>
<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}
allowFloat
value={`${setting.sdGuidanceScale}`}
desc="TODO"
onValue={value => {
const val = value.length === 0 ? 0 : parseFloat(value)
setSettingState(old => {
return { ...old, sdGuidanceScale: val }
})
}}
/>
<NumberInputSetting
title="Mask Blur"
width={INPUT_WIDTH}
value={`${setting.sdMaskBlur}`}
desc="TODO"
onValue={value => {
const val = value.length === 0 ? 0 : parseInt(value, 10)
setSettingState(old => {
return { ...old, sdMaskBlur: val }
})
}}
/>
<SettingBlock
className="sub-setting-block"
title="Sampler"
input={
<Selector
width={80}
value={setting.sdSampler as string}
options={Object.values(SDSampler)}
onChange={val => {
const sampler = val as SDSampler
setSettingState(old => {
return { ...old, sdSampler: sampler }
})
}}
/>
}
/>
<SettingBlock
title="Seed"
input={
<div
style={{
display: 'flex',
gap: 0,
justifyContent: 'center',
alignItems: 'center',
}}
>
{/* 每次会从服务器返回更新该值 */}
<NumberInputSetting
title=""
width={80}
value={`${setting.sdSeed}`}
desc=""
disable={!setting.sdSeedFixed}
onValue={value => {
const val = value.length === 0 ? 0 : parseInt(value, 10)
setSettingState(old => {
return { ...old, sdSeed: val }
})
}}
/>
<Switch
checked={setting.sdSeedFixed}
onCheckedChange={value => {
setSettingState(old => {
return { ...old, sdSeedFixed: value }
})
}}
style={{ marginLeft: '8px' }}
>
<SwitchThumb />
</Switch>
</div>
}
/>
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
</PopoverPrimitive.Root>
</div>
)
}
export default SidePanel

View File

@ -1,15 +1,16 @@
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { useRecoilState } from 'recoil' import { useRecoilState, useRecoilValue } from 'recoil'
import Editor from './Editor/Editor' import Editor from './Editor/Editor'
import ShortcutsModal from './Shortcuts/ShortcutsModal' import ShortcutsModal from './Shortcuts/ShortcutsModal'
import SettingModal from './Settings/SettingsModal' import SettingModal from './Settings/SettingsModal'
import Toast from './shared/Toast' import Toast from './shared/Toast'
import { AIModel, settingState, toastState } from '../store/Atoms' import { AIModel, isSDState, settingState, toastState } from '../store/Atoms'
import { import {
currentModel, currentModel,
modelDownloaded, modelDownloaded,
switchModel, switchModel,
} from '../adapters/inpainting' } from '../adapters/inpainting'
import SidePanel from './SidePanel/SidePanel'
interface WorkspaceProps { interface WorkspaceProps {
file: File file: File
@ -18,6 +19,7 @@ interface WorkspaceProps {
const Workspace = ({ file }: WorkspaceProps) => { const Workspace = ({ file }: WorkspaceProps) => {
const [settings, setSettingState] = useRecoilState(settingState) const [settings, setSettingState] = useRecoilState(settingState)
const [toastVal, setToastState] = useRecoilState(toastState) const [toastVal, setToastState] = useRecoilState(toastState)
const isSD = useRecoilValue(isSDState)
const onSettingClose = async () => { const onSettingClose = async () => {
const curModel = await currentModel().then(res => res.text()) const curModel = await currentModel().then(res => res.text())
@ -82,6 +84,7 @@ const Workspace = ({ file }: WorkspaceProps) => {
return ( return (
<> <>
{isSD ? <SidePanel /> : <></>}
<Editor file={file} /> <Editor file={file} />
<SettingModal onClose={onSettingClose} /> <SettingModal onClose={onSettingClose} />
<ShortcutsModal /> <ShortcutsModal />

View File

@ -4,6 +4,7 @@
display: grid; display: grid;
grid-auto-flow: column; grid-auto-flow: column;
column-gap: 1rem; column-gap: 1rem;
background-color: var(--page-bg);
color: var(--btn-text-color); color: var(--btn-text-color);
font-family: 'WorkSans', sans-serif; font-family: 'WorkSans', sans-serif;
width: max-content; width: max-content;
@ -25,6 +26,13 @@
} }
.btn-primary-disabled { .btn-primary-disabled {
background-color: var(--page-bg);
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;
} }
.btn-border {
border-color: var(--btn-border-color);
border-width: 1px;
border-style: solid;
}

View File

@ -1,6 +1,7 @@
import React, { ReactNode } from 'react' import React, { ReactNode } from 'react'
interface ButtonProps { interface ButtonProps {
border?: boolean
disabled?: boolean disabled?: boolean
children?: ReactNode children?: ReactNode
className?: string className?: string
@ -17,6 +18,7 @@ interface ButtonProps {
const Button: React.FC<ButtonProps> = props => { const Button: React.FC<ButtonProps> = props => {
const { const {
children, children,
border,
className, className,
disabled, disabled,
icon, icon,
@ -55,6 +57,7 @@ const Button: React.FC<ButtonProps> = props => {
toolTip ? 'info-tooltip' : '', toolTip ? 'info-tooltip' : '',
tooltipPosition ? `info-tooltip-${tooltipPosition}` : '', tooltipPosition ? `info-tooltip-${tooltipPosition}` : '',
className, className,
border ? `btn-border` : '',
].join(' ')} ].join(' ')}
> >
{icon} {icon}
@ -65,6 +68,7 @@ const Button: React.FC<ButtonProps> = props => {
Button.defaultProps = { Button.defaultProps = {
disabled: false, disabled: false,
border: false,
} }
export default Button export default Button

View File

@ -0,0 +1,43 @@
import React, { FocusEvent, InputHTMLAttributes, RefObject } from 'react'
import { useClickAway } from 'react-use'
import { useRecoilState } from 'recoil'
import { appState } from '../../store/Atoms'
const TextInput = React.forwardRef<
HTMLInputElement,
InputHTMLAttributes<HTMLInputElement>
>((props, ref) => {
const { onFocus, onBlur, ...itemProps } = props
const [_, setAppState] = useRecoilState(appState)
const handleOnFocus = (evt: FocusEvent<any>) => {
setAppState(old => {
return { ...old, disableShortCuts: true }
})
onFocus?.(evt)
}
const handleOnBlur = (evt: FocusEvent<any>) => {
setAppState(old => {
return { ...old, disableShortCuts: false }
})
onBlur?.(evt)
}
return (
<input
{...itemProps}
ref={ref}
type="text"
onFocus={handleOnFocus}
onBlur={handleOnBlur}
onKeyDown={e => {
if (e.key === 'Escape') {
e.currentTarget.blur()
}
}}
/>
)
})
export default TextInput

View File

@ -1,7 +1,9 @@
import { XIcon } from '@heroicons/react/outline' import { XIcon } from '@heroicons/react/outline'
import React, { ReactNode } from 'react' import React, { ReactNode } from 'react'
import { useRecoilState } from 'recoil'
import * as DialogPrimitive from '@radix-ui/react-dialog' import * as DialogPrimitive from '@radix-ui/react-dialog'
import Button from './Button' import Button from './Button'
import { appState } from '../../store/Atoms'
export interface ModalProps { export interface ModalProps {
show: boolean show: boolean
@ -16,10 +18,14 @@ const Modal = React.forwardRef<
ModalProps ModalProps
>((props, forwardedRef) => { >((props, forwardedRef) => {
const { show, children, onClose, className, title } = props const { show, children, onClose, className, title } = props
const [_, setAppState] = useRecoilState(appState)
const onOpenChange = (open: boolean) => { const onOpenChange = (open: boolean) => {
if (!open) { if (!open) {
onClose?.() onClose?.()
setAppState(old => {
return { ...old, disableShortCuts: false }
})
} }
} }

View File

@ -5,8 +5,13 @@
padding: 0 0.8rem; padding: 0 0.8rem;
outline: 1px solid var(--border-color); outline: 1px solid var(--border-color);
height: 32px; height: 32px;
text-align: right;
&:focus-visible { &:focus-visible {
outline: 1px solid var(--yellow-accent); outline: 1px solid var(--yellow-accent);
} }
&:disabled {
color: var(--border-color);
}
} }

View File

@ -1,31 +1,53 @@
import React, { FormEvent, InputHTMLAttributes } from 'react' import React, {
FormEvent,
InputHTMLAttributes,
useEffect,
useState,
} from 'react'
import TextInput from './Input'
interface NumberInputProps extends InputHTMLAttributes<HTMLInputElement> { interface NumberInputProps extends InputHTMLAttributes<HTMLInputElement> {
value: string value: string
allowFloat?: boolean
onValue?: (val: string) => void onValue?: (val: string) => void
} }
const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>( const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
(props: NumberInputProps, forwardedRef) => { (props: NumberInputProps, forwardedRef) => {
const { value, onValue, ...itemProps } = props const { value, allowFloat, onValue, ...itemProps } = props
const [innerValue, setInnerValue] = useState(value)
useEffect(() => {
setInnerValue(value)
}, [value])
const handleOnInput = (evt: FormEvent<HTMLInputElement>) => { const handleOnInput = (evt: FormEvent<HTMLInputElement>) => {
const target = evt.target as HTMLInputElement const target = evt.target as HTMLInputElement
const val = target.value.replace(/\D/g, '') let val = target.value
onValue?.(val) if (allowFloat) {
val = val.replace(/[^0-9.]/g, '').replace(/(\..*?)\..*/g, '$1')
onValue?.(val)
} else {
val = val.replace(/\D/g, '')
onValue?.(val)
}
setInnerValue(val)
} }
return ( return (
<input <TextInput
value={value} value={innerValue}
onInput={handleOnInput} onInput={handleOnInput}
className="number-input" className="number-input"
{...itemProps} {...itemProps}
ref={forwardedRef} ref={forwardedRef}
type="text"
/> />
) )
} }
) )
NumberInput.defaultProps = {
allowFloat: false,
}
export default NumberInput export default NumberInput

View File

@ -51,6 +51,7 @@ const Selector = (props: Props) => {
className="select-trigger" className="select-trigger"
style={{ width }} style={{ width }}
ref={contentRef} ref={contentRef}
onKeyDown={e => e.preventDefault()}
> >
<Select.Value /> <Select.Value />
<Select.Icon> <Select.Icon>

View File

@ -12,6 +12,7 @@ const Switch = React.forwardRef<
{...itemProps} {...itemProps}
ref={forwardedRef} ref={forwardedRef}
className={`switch-root ${className}`} className={`switch-root ${className}`}
onKeyDown={e => e.preventDefault()}
/> />
) )
}) })

View File

@ -1,7 +1,7 @@
.toast-viewpoint { .toast-viewpoint {
position: fixed; position: fixed;
top: 48px; bottom: 48px;
right: 0; right: 1.5rem;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
padding: 25px; padding: 25px;

View File

@ -0,0 +1,7 @@
import mitt from 'mitt'
export const EVENT_PROMPT = 'prompt'
const emitter = mitt()
export default emitter

View File

@ -0,0 +1,22 @@
import { Options, useHotkeys } from 'react-hotkeys-hook'
import { useRecoilValue } from 'recoil'
import { appState } from '../store/Atoms'
const useHotKey = (
keys: string,
callback: any,
options?: Options,
deps?: any[]
) => {
const app = useRecoilValue(appState)
const ref = useHotkeys(
keys,
callback,
{ ...options, enabled: !app.disableShortCuts },
deps
)
return ref
}
export default useHotKey

View File

@ -9,6 +9,7 @@ export enum AIModel {
ZITS = 'zits', ZITS = 'zits',
MAT = 'mat', MAT = 'mat',
FCF = 'fcf', FCF = 'fcf',
SD14 = 'sd1.4',
} }
export const fileState = atom<File | undefined>({ export const fileState = atom<File | undefined>({
@ -16,6 +17,89 @@ export const fileState = atom<File | undefined>({
default: undefined, default: undefined,
}) })
export interface Rect {
x: number
y: number
width: number
height: number
}
interface AppState {
disableShortCuts: boolean
isInpainting: boolean
}
export const appState = atom<AppState>({
key: 'appState',
default: {
disableShortCuts: false,
isInpainting: false,
},
})
export const propmtState = atom<string>({
key: 'promptState',
default: '',
})
export const isInpaintingState = selector({
key: 'isInpainting',
get: ({ get }) => {
const app = get(appState)
return app.isInpainting
},
set: ({ get, set }, newValue: any) => {
const app = get(appState)
set(appState, { ...app, isInpainting: newValue })
},
})
export const croperState = atom<Rect>({
key: 'croperState',
default: {
x: 0,
y: 0,
width: 512,
height: 512,
},
})
export const croperX = selector({
key: 'croperX',
get: ({ get }) => get(croperState).x,
set: ({ get, set }, newValue: any) => {
const rect = get(croperState)
set(croperState, { ...rect, x: newValue })
},
})
export const croperY = selector({
key: 'croperY',
get: ({ get }) => get(croperState).y,
set: ({ get, set }, newValue: any) => {
const rect = get(croperState)
set(croperState, { ...rect, y: newValue })
},
})
export const croperHeight = selector({
key: 'croperHeight',
get: ({ get }) => get(croperState).height,
set: ({ get, set }, newValue: any) => {
const rect = get(croperState)
set(croperState, { ...rect, height: newValue })
},
})
export const croperWidth = selector({
key: 'croperWidth',
get: ({ get }) => get(croperState).width,
set: ({ get, set }, newValue: any) => {
const rect = get(croperState)
set(croperState, { ...rect, width: newValue })
},
})
interface ToastAtomState { interface ToastAtomState {
open: boolean open: boolean
desc: string desc: string
@ -50,6 +134,7 @@ type ModelsHDSettings = { [key in AIModel]: HDSettings }
export interface Settings { export interface Settings {
show: boolean show: boolean
showCroper: boolean
downloadMask: boolean downloadMask: boolean
graduallyInpainting: boolean graduallyInpainting: boolean
runInpaintingManually: boolean runInpaintingManually: boolean
@ -62,6 +147,17 @@ export interface Settings {
// For ZITS // For ZITS
zitsWireframe: boolean zitsWireframe: boolean
// For SD
sdMaskBlur: number
sdMode: SDMode
sdStrength: number
sdSteps: number
sdGuidanceScale: number
sdSampler: SDSampler
sdSeed: number
sdSeedFixed: boolean // true: use sdSeed, false: random generate seed on backend
sdNumSamples: number
} }
const defaultHDSettings: ModelsHDSettings = { const defaultHDSettings: ModelsHDSettings = {
@ -100,10 +196,29 @@ const defaultHDSettings: ModelsHDSettings = {
hdStrategyCropMargin: 128, hdStrategyCropMargin: 128,
enabled: false, enabled: false,
}, },
[AIModel.SD14]: {
hdStrategy: HDStrategy.ORIGINAL,
hdStrategyResizeLimit: 768,
hdStrategyCropTrigerSize: 512,
hdStrategyCropMargin: 128,
enabled: true,
},
}
export enum SDSampler {
ddim = 'ddim',
pndm = 'pndm',
}
export enum SDMode {
text2img = 'text2img',
img2img = 'img2img',
inpainting = 'inpainting',
} }
export const settingStateDefault: Settings = { export const settingStateDefault: Settings = {
show: false, show: false,
showCroper: false,
downloadMask: false, downloadMask: false,
graduallyInpainting: true, graduallyInpainting: true,
runInpaintingManually: false, runInpaintingManually: false,
@ -114,6 +229,17 @@ export const settingStateDefault: Settings = {
ldmSampler: LDMSampler.plms, ldmSampler: LDMSampler.plms,
zitsWireframe: true, zitsWireframe: true,
// SD
sdMaskBlur: 5,
sdMode: SDMode.inpainting,
sdStrength: 0.75,
sdSteps: 50,
sdGuidanceScale: 7.5,
sdSampler: SDSampler.ddim,
sdSeed: 42,
sdSeedFixed: true,
sdNumSamples: 1,
} }
const localStorageEffect = const localStorageEffect =
@ -138,7 +264,7 @@ const localStorageEffect =
) )
} }
const ROOT_STATE_KEY = 'settingsState2' const ROOT_STATE_KEY = 'settingsState3'
// Each atom can reference an array of these atom effect functions which are called in priority order when the atom is initialized // Each atom can reference an array of these atom effect functions which are called in priority order when the atom is initialized
// https://recoiljs.org/docs/guides/atom-effects/#local-storage-persistence // https://recoiljs.org/docs/guides/atom-effects/#local-storage-persistence
export const settingState = atom<Settings>({ export const settingState = atom<Settings>({
@ -147,6 +273,18 @@ export const settingState = atom<Settings>({
effects: [localStorageEffect(ROOT_STATE_KEY)], effects: [localStorageEffect(ROOT_STATE_KEY)],
}) })
export const seedState = selector({
key: 'seed',
get: ({ get }) => {
const settings = get(settingState)
return settings.sdSeed
},
set: ({ get, set }, newValue: any) => {
const settings = get(settingState)
set(settingState, { ...settings, sdSeed: newValue })
},
})
export const hdSettingsState = selector({ export const hdSettingsState = selector({
key: 'hdSettings', key: 'hdSettings',
get: ({ get }) => { get: ({ get }) => {
@ -164,3 +302,20 @@ export const hdSettingsState = selector({
}) })
}, },
}) })
export const isSDState = selector({
key: 'isSD',
get: ({ get }) => {
const settings = get(settingState)
return settings.model === AIModel.SD14
},
})
export const runManuallyState = selector({
key: 'runManuallyState',
get: ({ get }) => {
const settings = get(settingState)
const isSD = get(isSDState)
return settings.runInpaintingManually || isSD
},
})

View File

@ -6,6 +6,7 @@
--page-bg-light: rgb(255, 255, 255, 0.5); --page-bg-light: rgb(255, 255, 255, 0.5);
--page-text-color: #040404; --page-text-color: #040404;
--yellow-accent: #ffcc00; --yellow-accent: #ffcc00;
--yellow-accent-light: #ffcc0055;
--link-color: rgb(0, 0, 0); --link-color: rgb(0, 0, 0);
--border-color: rgb(100, 100, 120); --border-color: rgb(100, 100, 120);
--border-color-light: rgba(100, 100, 120, 0.5); --border-color-light: rgba(100, 100, 120, 0.5);
@ -57,4 +58,6 @@
--box-shadow: inset 0 0.5px rgba(255, 255, 255, 0.1), --box-shadow: inset 0 0.5px rgba(255, 255, 255, 0.1),
inset 0 1px 5px hsl(210 16.7% 97.6%), 0px 0px 0px 0.5px hsl(205 10.7% 78%), inset 0 1px 5px hsl(210 16.7% 97.6%), 0px 0px 0px 0.5px hsl(205 10.7% 78%),
0px 2px 1px -1px hsl(205 10.7% 78%), 0 1px hsl(205 10.7% 78%); 0px 2px 1px -1px hsl(205 10.7% 78%), 0 1px hsl(205 10.7% 78%);
--croper-bg: rgba(0, 0, 0, 0.5);
} }

View File

@ -6,6 +6,7 @@
--page-bg-light: #04040488; --page-bg-light: #04040488;
--page-text-color: #f9f9f9; --page-text-color: #f9f9f9;
--yellow-accent: #ffcc00; --yellow-accent: #ffcc00;
--yellow-accent-light: #ffcc0055;
--link-color: var(--yellow-accent); --link-color: var(--yellow-accent);
--border-color: rgb(100, 100, 120); --border-color: rgb(100, 100, 120);
--border-color-light: rgba(102, 102, 102); --border-color-light: rgba(102, 102, 102);
@ -55,4 +56,6 @@
--box-shadow: inset 0 0.5px rgba(255, 255, 255, 0.1), --box-shadow: inset 0 0.5px rgba(255, 255, 255, 0.1),
inset 0 1px 5px hsl(195 7.1% 11%), 0px 0px 0px 0.5px hsl(207 5.6% 31.6%), inset 0 1px 5px hsl(195 7.1% 11%), 0px 0px 0px 0.5px hsl(207 5.6% 31.6%),
0px 2px 1px -1px hsl(207 5.6% 31.6%), 0 1px hsl(207 5.6% 31.6%); 0px 2px 1px -1px hsl(207 5.6% 31.6%), 0 1px hsl(207 5.6% 31.6%);
--croper-bg: rgba(0, 0, 0, 0.5);
} }

View File

@ -9,9 +9,12 @@
@use '../components/Editor/Editor'; @use '../components/Editor/Editor';
@use '../components/LandingPage/LandingPage'; @use '../components/LandingPage/LandingPage';
@use '../components/Header/Header'; @use '../components/Header/Header';
@use '../components/Header/PromptInput';
@use '../components/Header/ThemeChanger'; @use '../components/Header/ThemeChanger';
@use '../components/Shortcuts/Shortcuts'; @use '../components/Shortcuts/Shortcuts';
@use '../components/Settings/Settings.scss'; @use '../components/Settings/Settings.scss';
@use '../components/SidePanel/SidePanel.scss';
@use '../components/Croper/Croper.scss';
// Shared // Shared
@use '../components/FileSelect/FileSelect'; @use '../components/FileSelect/FileSelect';

View File

@ -1241,6 +1241,26 @@
minimatch "^3.0.4" minimatch "^3.0.4"
strip-json-comments "^3.1.1" strip-json-comments "^3.1.1"
"@floating-ui/core@^0.7.3":
version "0.7.3"
resolved "https://registry.npmmirror.com/@floating-ui/core/-/core-0.7.3.tgz#d274116678ffae87f6b60e90f88cc4083eefab86"
integrity sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==
"@floating-ui/dom@^0.5.3":
version "0.5.4"
resolved "https://registry.npmmirror.com/@floating-ui/dom/-/dom-0.5.4.tgz#4eae73f78bcd4bd553ae2ade30e6f1f9c73fe3f1"
integrity sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==
dependencies:
"@floating-ui/core" "^0.7.3"
"@floating-ui/react-dom@0.7.2":
version "0.7.2"
resolved "https://registry.npmmirror.com/@floating-ui/react-dom/-/react-dom-0.7.2.tgz#0bf4ceccb777a140fc535c87eb5d6241c8e89864"
integrity sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg==
dependencies:
"@floating-ui/dom" "^0.5.3"
use-isomorphic-layout-effect "^1.1.1"
"@gar/promisify@^1.0.1": "@gar/promisify@^1.0.1":
version "1.1.2" version "1.1.2"
resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz" resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz"
@ -1566,6 +1586,13 @@
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/primitive@1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.0.0.tgz#e1d8ef30b10ea10e69c76e896f608d9276352253"
integrity sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-arrow@0.1.4": "@radix-ui/react-arrow@0.1.4":
version "0.1.4" version "0.1.4"
resolved "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-0.1.4.tgz#a871448a418cd3507d83840fdd47558cb961672b" resolved "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-0.1.4.tgz#a871448a418cd3507d83840fdd47558cb961672b"
@ -1574,6 +1601,14 @@
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "0.1.4" "@radix-ui/react-primitive" "0.1.4"
"@radix-ui/react-arrow@1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-1.0.0.tgz#c461f4c2cab3317e3d42a1ae62910a4cbb0192a1"
integrity sha512-1MUuv24HCdepi41+qfv125EwMuxgQ+U+h0A9K3BjCO/J8nVRREKHHpkD9clwfnjEDk9hgGzCnff4aUKCPiRepw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.0"
"@radix-ui/react-collection@0.1.5-rc.18": "@radix-ui/react-collection@0.1.5-rc.18":
version "0.1.5-rc.18" version "0.1.5-rc.18"
resolved "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-0.1.5-rc.18.tgz#4dc03a8f464643748c0dad781b472f149d671d5c" resolved "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-0.1.5-rc.18.tgz#4dc03a8f464643748c0dad781b472f149d671d5c"
@ -1599,6 +1634,13 @@
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs@1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz#37595b1f16ec7f228d698590e78eeed18ff218ae"
integrity sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-context@0.1.1": "@radix-ui/react-context@0.1.1":
version "0.1.1" version "0.1.1"
resolved "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-0.1.1.tgz#06996829ea124d9a1bc1dbe3e51f33588fab0875" resolved "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-0.1.1.tgz#06996829ea124d9a1bc1dbe3e51f33588fab0875"
@ -1613,6 +1655,13 @@
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-context@1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.0.0.tgz#f38e30c5859a9fb5e9aa9a9da452ee3ed9e0aee0"
integrity sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-dialog@0.1.8-rc.25": "@radix-ui/react-dialog@0.1.8-rc.25":
version "0.1.8-rc.25" version "0.1.8-rc.25"
resolved "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-0.1.8-rc.25.tgz#dea6af32268b34070346ed5d6d609ff699a1de43" resolved "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-0.1.8-rc.25.tgz#dea6af32268b34070346ed5d6d609ff699a1de43"
@ -1667,6 +1716,18 @@
"@radix-ui/react-use-callback-ref" "0.1.1-rc.18" "@radix-ui/react-use-callback-ref" "0.1.1-rc.18"
"@radix-ui/react-use-escape-keydown" "0.1.1-rc.18" "@radix-ui/react-use-escape-keydown" "0.1.1-rc.18"
"@radix-ui/react-dismissable-layer@1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.0.tgz#35b7826fa262fd84370faef310e627161dffa76b"
integrity sha512-n7kDRfx+LB1zLueRDvZ1Pd0bxdJWDUZNQ/GWoxDn2prnuJKRdxsjulejX/ePkOsLi2tTm6P24mDqlMSgQpsT6g==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.0"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-primitive" "1.0.0"
"@radix-ui/react-use-callback-ref" "1.0.0"
"@radix-ui/react-use-escape-keydown" "1.0.0"
"@radix-ui/react-focus-guards@0.1.1-rc.18": "@radix-ui/react-focus-guards@0.1.1-rc.18":
version "0.1.1-rc.18" version "0.1.1-rc.18"
resolved "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-0.1.1-rc.18.tgz#f0e2ebd3cbfd363a71682e3234b274ab7d7df4ce" resolved "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-0.1.1-rc.18.tgz#f0e2ebd3cbfd363a71682e3234b274ab7d7df4ce"
@ -1674,6 +1735,13 @@
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-focus-guards@1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz#339c1c69c41628c1a5e655f15f7020bf11aa01fa"
integrity sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-focus-scope@0.1.5-rc.18": "@radix-ui/react-focus-scope@0.1.5-rc.18":
version "0.1.5-rc.18" version "0.1.5-rc.18"
resolved "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-0.1.5-rc.18.tgz#e26a0317130687fd3668af8ec68e19e04dc7668f" resolved "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-0.1.5-rc.18.tgz#e26a0317130687fd3668af8ec68e19e04dc7668f"
@ -1684,6 +1752,16 @@
"@radix-ui/react-primitive" "0.1.5-rc.18" "@radix-ui/react-primitive" "0.1.5-rc.18"
"@radix-ui/react-use-callback-ref" "0.1.1-rc.18" "@radix-ui/react-use-callback-ref" "0.1.1-rc.18"
"@radix-ui/react-focus-scope@1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.0.tgz#95a0c1188276dc8933b1eac5f1cdb6471e01ade5"
integrity sha512-C4SWtsULLGf/2L4oGeIHlvWQx7Rf+7cX/vKOAD2dXW0A1b5QXwi3wWeaEgW+wn+SEVrraMUk05vLU9fZZz5HbQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-primitive" "1.0.0"
"@radix-ui/react-use-callback-ref" "1.0.0"
"@radix-ui/react-id@0.1.5": "@radix-ui/react-id@0.1.5":
version "0.1.5" version "0.1.5"
resolved "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-0.1.5.tgz#010d311bedd5a2884c1e9bb6aaaa4e6cc1d1d3b8" resolved "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-0.1.5.tgz#010d311bedd5a2884c1e9bb6aaaa4e6cc1d1d3b8"
@ -1700,6 +1778,14 @@
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect" "0.1.1-rc.18" "@radix-ui/react-use-layout-effect" "0.1.1-rc.18"
"@radix-ui/react-id@1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.0.0.tgz#8d43224910741870a45a8c9d092f25887bb6d11e"
integrity sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect" "1.0.0"
"@radix-ui/react-label@0.1.5": "@radix-ui/react-label@0.1.5":
version "0.1.5" version "0.1.5"
resolved "https://registry.npmmirror.com/@radix-ui/react-label/-/react-label-0.1.5.tgz#12cd965bfc983e0148121d4c99fb8e27a917c45c" resolved "https://registry.npmmirror.com/@radix-ui/react-label/-/react-label-0.1.5.tgz#12cd965bfc983e0148121d4c99fb8e27a917c45c"
@ -1722,6 +1808,28 @@
"@radix-ui/react-id" "0.1.6-rc.18" "@radix-ui/react-id" "0.1.6-rc.18"
"@radix-ui/react-primitive" "0.1.5-rc.18" "@radix-ui/react-primitive" "0.1.5-rc.18"
"@radix-ui/react-popover@^1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-popover/-/react-popover-1.0.0.tgz#5ee72013089fdf9038417fc1eb98a749c17457fd"
integrity sha512-osxFFO0TiZ9ABpEOitZu0R1Fdd+tSpJgAqLZxRLLdZQ7ya0onSODcITp5hXDVuYQeVXH6pKEBGwXN6ZGjZ0a5g==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.0"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-context" "1.0.0"
"@radix-ui/react-dismissable-layer" "1.0.0"
"@radix-ui/react-focus-guards" "1.0.0"
"@radix-ui/react-focus-scope" "1.0.0"
"@radix-ui/react-id" "1.0.0"
"@radix-ui/react-popper" "1.0.0"
"@radix-ui/react-portal" "1.0.0"
"@radix-ui/react-presence" "1.0.0"
"@radix-ui/react-primitive" "1.0.0"
"@radix-ui/react-slot" "1.0.0"
"@radix-ui/react-use-controllable-state" "1.0.0"
aria-hidden "^1.1.1"
react-remove-scroll "2.5.4"
"@radix-ui/react-popper@0.1.4": "@radix-ui/react-popper@0.1.4":
version "0.1.4" version "0.1.4"
resolved "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-0.1.4.tgz#dfc055dcd7dfae6a2eff7a70d333141d15a5d029" resolved "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-0.1.4.tgz#dfc055dcd7dfae6a2eff7a70d333141d15a5d029"
@ -1737,6 +1845,22 @@
"@radix-ui/react-use-size" "0.1.1" "@radix-ui/react-use-size" "0.1.1"
"@radix-ui/rect" "0.1.1" "@radix-ui/rect" "0.1.1"
"@radix-ui/react-popper@1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.0.0.tgz#fb4f937864bf39c48f27f55beee61fa9f2bef93c"
integrity sha512-k2dDd+1Wl0XWAMs9ZvAxxYsB9sOsEhrFQV4CINd7IUZf0wfdye4OHen9siwxvZImbzhgVeKTJi68OQmPRvVdMg==
dependencies:
"@babel/runtime" "^7.13.10"
"@floating-ui/react-dom" "0.7.2"
"@radix-ui/react-arrow" "1.0.0"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-context" "1.0.0"
"@radix-ui/react-primitive" "1.0.0"
"@radix-ui/react-use-layout-effect" "1.0.0"
"@radix-ui/react-use-rect" "1.0.0"
"@radix-ui/react-use-size" "1.0.0"
"@radix-ui/rect" "1.0.0"
"@radix-ui/react-portal@0.1.4": "@radix-ui/react-portal@0.1.4":
version "0.1.4" version "0.1.4"
resolved "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-0.1.4.tgz#17bdce3d7f1a9a0b35cb5e935ab8bc562441a7d2" resolved "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-0.1.4.tgz#17bdce3d7f1a9a0b35cb5e935ab8bc562441a7d2"
@ -1755,6 +1879,14 @@
"@radix-ui/react-primitive" "0.1.5-rc.18" "@radix-ui/react-primitive" "0.1.5-rc.18"
"@radix-ui/react-use-layout-effect" "0.1.1-rc.18" "@radix-ui/react-use-layout-effect" "0.1.1-rc.18"
"@radix-ui/react-portal@1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.0.0.tgz#7220b66743394fabb50c55cb32381395cc4a276b"
integrity sha512-a8qyFO/Xb99d8wQdu4o7qnigNjTPG123uADNecz0eX4usnQEj7o+cG4ZX4zkqq98NYekT7UoEQIjxBNWIFuqTA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.0"
"@radix-ui/react-presence@0.1.2": "@radix-ui/react-presence@0.1.2":
version "0.1.2" version "0.1.2"
resolved "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-0.1.2.tgz#9f11cce3df73cf65bc348e8b76d891f0d54c1fe3" resolved "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-0.1.2.tgz#9f11cce3df73cf65bc348e8b76d891f0d54c1fe3"
@ -1773,6 +1905,15 @@
"@radix-ui/react-compose-refs" "0.1.1-rc.18" "@radix-ui/react-compose-refs" "0.1.1-rc.18"
"@radix-ui/react-use-layout-effect" "0.1.1-rc.18" "@radix-ui/react-use-layout-effect" "0.1.1-rc.18"
"@radix-ui/react-presence@1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.0.0.tgz#814fe46df11f9a468808a6010e3f3ca7e0b2e84a"
integrity sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-use-layout-effect" "1.0.0"
"@radix-ui/react-primitive@0.1.4": "@radix-ui/react-primitive@0.1.4":
version "0.1.4" version "0.1.4"
resolved "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-0.1.4.tgz#6c233cf08b0cb87fecd107e9efecb3f21861edc1" resolved "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-0.1.4.tgz#6c233cf08b0cb87fecd107e9efecb3f21861edc1"
@ -1789,6 +1930,14 @@
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-slot" "0.1.3-rc.18" "@radix-ui/react-slot" "0.1.3-rc.18"
"@radix-ui/react-primitive@1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz#376cd72b0fcd5e0e04d252ed33eb1b1f025af2b0"
integrity sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-slot" "1.0.0"
"@radix-ui/react-select@0.1.2-rc.27": "@radix-ui/react-select@0.1.2-rc.27":
version "0.1.2-rc.27" version "0.1.2-rc.27"
resolved "https://registry.npmmirror.com/@radix-ui/react-select/-/react-select-0.1.2-rc.27.tgz#91948d482b3db8cf83172838dfae0f4bedec9566" resolved "https://registry.npmmirror.com/@radix-ui/react-select/-/react-select-0.1.2-rc.27.tgz#91948d482b3db8cf83172838dfae0f4bedec9566"
@ -1831,6 +1980,14 @@
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "0.1.1-rc.18" "@radix-ui/react-compose-refs" "0.1.1-rc.18"
"@radix-ui/react-slot@1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.0.0.tgz#7fa805b99891dea1e862d8f8fbe07f4d6d0fd698"
integrity sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-switch@^0.1.5": "@radix-ui/react-switch@^0.1.5":
version "0.1.5" version "0.1.5"
resolved "https://registry.npmmirror.com/@radix-ui/react-switch/-/react-switch-0.1.5.tgz#071ffa19a17a47fdc5c5e6f371bd5901c9fef2f4" resolved "https://registry.npmmirror.com/@radix-ui/react-switch/-/react-switch-0.1.5.tgz#071ffa19a17a47fdc5c5e6f371bd5901c9fef2f4"
@ -1915,6 +2072,13 @@
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-use-callback-ref@1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz#9e7b8b6b4946fe3cbe8f748c82a2cce54e7b6a90"
integrity sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-controllable-state@0.1.0": "@radix-ui/react-use-controllable-state@0.1.0":
version "0.1.0" version "0.1.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-0.1.0.tgz#4fced164acfc69a4e34fb9d193afdab973a55de1" resolved "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-0.1.0.tgz#4fced164acfc69a4e34fb9d193afdab973a55de1"
@ -1931,6 +2095,14 @@
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-use-callback-ref" "0.1.1-rc.18" "@radix-ui/react-use-callback-ref" "0.1.1-rc.18"
"@radix-ui/react-use-controllable-state@1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz#a64deaafbbc52d5d407afaa22d493d687c538b7f"
integrity sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-callback-ref" "1.0.0"
"@radix-ui/react-use-escape-keydown@0.1.0": "@radix-ui/react-use-escape-keydown@0.1.0":
version "0.1.0" version "0.1.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-0.1.0.tgz#dc80cb3753e9d1bd992adbad9a149fb6ea941874" resolved "https://registry.npmmirror.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-0.1.0.tgz#dc80cb3753e9d1bd992adbad9a149fb6ea941874"
@ -1947,6 +2119,14 @@
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-use-callback-ref" "0.1.1-rc.18" "@radix-ui/react-use-callback-ref" "0.1.1-rc.18"
"@radix-ui/react-use-escape-keydown@1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.0.tgz#aef375db4736b9de38a5a679f6f49b45a060e5d1"
integrity sha512-JwfBCUIfhXRxKExgIqGa4CQsiMemo1Xt0W/B4ei3fpzpvPENKpMKQ8mZSB6Acj3ebrAEgi2xiQvcI1PAAodvyg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-callback-ref" "1.0.0"
"@radix-ui/react-use-layout-effect@0.1.0": "@radix-ui/react-use-layout-effect@0.1.0":
version "0.1.0" version "0.1.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-0.1.0.tgz#ebf71bd6d2825de8f1fbb984abf2293823f0f223" resolved "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-0.1.0.tgz#ebf71bd6d2825de8f1fbb984abf2293823f0f223"
@ -1961,6 +2141,13 @@
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect@1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz#2fc19e97223a81de64cd3ba1dc42ceffd82374dc"
integrity sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-previous@0.1.1": "@radix-ui/react-use-previous@0.1.1":
version "0.1.1" version "0.1.1"
resolved "https://registry.npmmirror.com/@radix-ui/react-use-previous/-/react-use-previous-0.1.1.tgz#0226017f72267200f6e832a7103760e96a6db5d0" resolved "https://registry.npmmirror.com/@radix-ui/react-use-previous/-/react-use-previous-0.1.1.tgz#0226017f72267200f6e832a7103760e96a6db5d0"
@ -1983,6 +2170,14 @@
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/rect" "0.1.1" "@radix-ui/rect" "0.1.1"
"@radix-ui/react-use-rect@1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-use-rect/-/react-use-rect-1.0.0.tgz#b040cc88a4906b78696cd3a32b075ed5b1423b3e"
integrity sha512-TB7pID8NRMEHxb/qQJpvSt3hQU4sqNPM1VCTjTRjEOa7cEop/QMuq8S6fb/5Tsz64kqSvB9WnwsDHtjnrM9qew==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/rect" "1.0.0"
"@radix-ui/react-use-size@0.1.1": "@radix-ui/react-use-size@0.1.1":
version "0.1.1" version "0.1.1"
resolved "https://registry.npmmirror.com/@radix-ui/react-use-size/-/react-use-size-0.1.1.tgz#f6b75272a5d41c3089ca78c8a2e48e5f204ef90f" resolved "https://registry.npmmirror.com/@radix-ui/react-use-size/-/react-use-size-0.1.1.tgz#f6b75272a5d41c3089ca78c8a2e48e5f204ef90f"
@ -1990,6 +2185,14 @@
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-use-size@1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/react-use-size/-/react-use-size-1.0.0.tgz#a0b455ac826749419f6354dc733e2ca465054771"
integrity sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect" "1.0.0"
"@radix-ui/react-visually-hidden@0.1.4": "@radix-ui/react-visually-hidden@0.1.4":
version "0.1.4" version "0.1.4"
resolved "https://registry.npmmirror.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-0.1.4.tgz#6c75eae34fb5d084b503506fbfc05587ced05f03" resolved "https://registry.npmmirror.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-0.1.4.tgz#6c75eae34fb5d084b503506fbfc05587ced05f03"
@ -2013,6 +2216,13 @@
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/rect@1.0.0":
version "1.0.0"
resolved "https://registry.npmmirror.com/@radix-ui/rect/-/rect-1.0.0.tgz#0dc8e6a829ea2828d53cbc94b81793ba6383bf3c"
integrity sha512-d0O68AYy/9oeEy1DdC07bz1/ZXX+DqCskRd3i4JzLSTXwefzaepQrKjXC7aNM8lTHjFLDO0pDgaEiQ7jEk+HVg==
dependencies:
"@babel/runtime" "^7.13.10"
"@rollup/plugin-node-resolve@^7.1.1": "@rollup/plugin-node-resolve@^7.1.1":
version "7.1.3" version "7.1.3"
resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz" resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz"
@ -2055,6 +2265,11 @@
dependencies: dependencies:
"@sinonjs/commons" "^1.7.0" "@sinonjs/commons" "^1.7.0"
"@socket.io/component-emitter@~3.1.0":
version "3.1.0"
resolved "https://registry.npmmirror.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
"@surma/rollup-plugin-off-main-thread@^1.1.1": "@surma/rollup-plugin-off-main-thread@^1.1.1":
version "1.4.2" version "1.4.2"
resolved "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-1.4.2.tgz" resolved "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-1.4.2.tgz"
@ -4621,6 +4836,13 @@ debug@^3.1.1, debug@^3.2.6, debug@^3.2.7:
dependencies: dependencies:
ms "^2.1.1" ms "^2.1.1"
debug@~4.3.1, debug@~4.3.2:
version "4.3.4"
resolved "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"
decamelize@^1.2.0: decamelize@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz"
@ -5004,6 +5226,22 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0:
dependencies: dependencies:
once "^1.4.0" once "^1.4.0"
engine.io-client@~6.2.1:
version "6.2.2"
resolved "https://registry.npmmirror.com/engine.io-client/-/engine.io-client-6.2.2.tgz#c6c5243167f5943dcd9c4abee1bfc634aa2cbdd0"
integrity sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.1"
engine.io-parser "~5.0.3"
ws "~8.2.3"
xmlhttprequest-ssl "~2.0.0"
engine.io-parser@~5.0.3:
version "5.0.4"
resolved "https://registry.npmmirror.com/engine.io-parser/-/engine.io-parser-5.0.4.tgz#0b13f704fa9271b3ec4f33112410d8f3f41d0fc0"
integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==
enhanced-resolve@^4.3.0: enhanced-resolve@^4.3.0:
version "4.5.0" version "4.5.0"
resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz" resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz"
@ -6215,6 +6453,11 @@ hosted-git-info@^2.1.4:
resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz" resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz"
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
hotkeys-js@3.9.4:
version "3.9.4"
resolved "https://registry.npmmirror.com/hotkeys-js/-/hotkeys-js-3.9.4.tgz#ce1aa4c3a132b6a63a9dd5644fc92b8a9b9cbfb9"
integrity sha512-2zuLt85Ta+gIyvs4N88pCYskNrxf1TFv3LR9t5mdAZIX8BcgQQ48F2opUptvHa6m8zsy5v/a0i9mWzTrlNWU0Q==
hpack.js@^2.1.6: hpack.js@^2.1.6:
version "2.1.6" version "2.1.6"
resolved "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz" resolved "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz"
@ -7785,16 +8028,11 @@ lodash.uniq@^4.5.0:
resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz" resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
"lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.5, lodash@^4.7.0: "lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.5, lodash@^4.7.0:
version "4.17.21" version "4.17.21"
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
loglevel@^1.6.8: loglevel@^1.6.8:
version "1.7.1" version "1.7.1"
resolved "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz" resolved "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz"
@ -8095,6 +8333,11 @@ mississippi@^3.0.0:
stream-each "^1.1.0" stream-each "^1.1.0"
through2 "^2.0.0" through2 "^2.0.0"
mitt@^3.0.0:
version "3.0.0"
resolved "https://registry.npmmirror.com/mitt/-/mitt-3.0.0.tgz#69ef9bd5c80ff6f57473e8d89326d01c414be0bd"
integrity sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==
mixin-deep@^1.2.0: mixin-deep@^1.2.0:
version "1.3.2" version "1.3.2"
resolved "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz" resolved "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz"
@ -9924,6 +10167,13 @@ react-error-overlay@^6.0.9:
resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz" resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz"
integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew== integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==
react-hotkeys-hook@^3.4.7:
version "3.4.7"
resolved "https://registry.npmmirror.com/react-hotkeys-hook/-/react-hotkeys-hook-3.4.7.tgz#e16a0a85f59feed9f48d12cfaf166d7df4c96b7a"
integrity sha512-+bbPmhPAl6ns9VkXkNNyxlmCAIyDAcWbB76O4I0ntr3uWCRuIQf/aRLartUahe9chVMPj+OEzzfk3CQSjclUEQ==
dependencies:
hotkeys-js "3.9.4"
react-is@^16.8.1: react-is@^16.8.1:
version "16.13.1" version "16.13.1"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
@ -9947,6 +10197,25 @@ react-remove-scroll-bar@^2.3.0:
react-style-singleton "^2.2.0" react-style-singleton "^2.2.0"
tslib "^2.0.0" tslib "^2.0.0"
react-remove-scroll-bar@^2.3.3:
version "2.3.3"
resolved "https://registry.npmmirror.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.3.tgz#e291f71b1bb30f5f67f023765b7435f4b2b2cd94"
integrity sha512-i9GMNWwpz8XpUpQ6QlevUtFjHGqnPG4Hxs+wlIJntu/xcsZVEpJcIV71K3ZkqNy2q3GfgvkD7y6t/Sv8ofYSbw==
dependencies:
react-style-singleton "^2.2.1"
tslib "^2.0.0"
react-remove-scroll@2.5.4:
version "2.5.4"
resolved "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.5.4.tgz#afe6491acabde26f628f844b67647645488d2ea0"
integrity sha512-xGVKJJr0SJGQVirVFAUZ2k1QLyO6m+2fy0l8Qawbp5Jgrv3DeLalrfMNBFSlmz5kriGGzsVBtGVnf4pTKIhhWA==
dependencies:
react-remove-scroll-bar "^2.3.3"
react-style-singleton "^2.2.1"
tslib "^2.1.0"
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
react-remove-scroll@^2.4.0: react-remove-scroll@^2.4.0:
version "2.5.1" version "2.5.1"
resolved "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.5.1.tgz#28c318c2e076040e5d6172bf28aab2916ad89b46" resolved "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.5.1.tgz#28c318c2e076040e5d6172bf28aab2916ad89b46"
@ -10033,6 +10302,15 @@ react-style-singleton@^2.2.0:
invariant "^2.2.4" invariant "^2.2.4"
tslib "^2.0.0" tslib "^2.0.0"
react-style-singleton@^2.2.1:
version "2.2.1"
resolved "https://registry.npmmirror.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"
integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==
dependencies:
get-nonce "^1.0.0"
invariant "^2.2.4"
tslib "^2.0.0"
react-universal-interface@^0.6.2: react-universal-interface@^0.6.2:
version "0.6.2" version "0.6.2"
resolved "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz" resolved "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz"
@ -10845,6 +11123,24 @@ snapdragon@^0.8.1:
source-map-resolve "^0.5.0" source-map-resolve "^0.5.0"
use "^3.1.0" use "^3.1.0"
socket.io-client@^4.5.2:
version "4.5.2"
resolved "https://registry.npmmirror.com/socket.io-client/-/socket.io-client-4.5.2.tgz#9481518c560388c980c88b01e3cf62f367f04c96"
integrity sha512-naqYfFu7CLDiQ1B7AlLhRXKX3gdeaIMfgigwavDzgJoIUYulc1qHH5+2XflTsXTPY7BlPH5rppJyUjhjrKQKLg==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.2"
engine.io-client "~6.2.1"
socket.io-parser "~4.2.0"
socket.io-parser@~4.2.0:
version "4.2.1"
resolved "https://registry.npmmirror.com/socket.io-parser/-/socket.io-parser-4.2.1.tgz#01c96efa11ded938dcb21cbe590c26af5eff65e5"
integrity sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.1"
sockjs-client@^1.5.0: sockjs-client@^1.5.0:
version "1.5.2" version "1.5.2"
resolved "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.5.2.tgz" resolved "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.5.2.tgz"
@ -11855,6 +12151,11 @@ use-callback-ref@^1.3.0:
dependencies: dependencies:
tslib "^2.0.0" tslib "^2.0.0"
use-isomorphic-layout-effect@^1.1.1:
version "1.1.2"
resolved "https://registry.npmmirror.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"
integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==
use-sidecar@^1.1.2: use-sidecar@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.npmmirror.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" resolved "https://registry.npmmirror.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2"
@ -12410,6 +12711,11 @@ ws@^7.4.6:
resolved "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz" resolved "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz"
integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w== integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==
ws@~8.2.3:
version "8.2.3"
resolved "https://registry.npmmirror.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
xml-name-validator@^3.0.0: xml-name-validator@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz" resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz"
@ -12420,6 +12726,11 @@ xmlchars@^2.2.0:
resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz" resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xmlhttprequest-ssl@~2.0.0:
version "2.0.0"
resolved "https://registry.npmmirror.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67"
integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==
xtend@^4.0.0, xtend@~4.0.1: xtend@^4.0.0, xtend@~4.0.1:
version "4.0.2" version "4.0.2"
resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz"

View File

@ -14,17 +14,17 @@ class InpaintModel:
pad_mod = 8 pad_mod = 8
pad_to_square = False pad_to_square = False
def __init__(self, device): def __init__(self, device, **kwargs):
""" """
Args: Args:
device: device:
""" """
self.device = device self.device = device
self.init_model(device) self.init_model(device, **kwargs)
@abc.abstractmethod @abc.abstractmethod
def init_model(self, device): def init_model(self, device, **kwargs):
... ...
@staticmethod @staticmethod
@ -36,15 +36,19 @@ class InpaintModel:
def forward(self, image, mask, config: Config): def forward(self, image, mask, config: Config):
"""Input images and output images have same size """Input images and output images have same size
images: [H, W, C] RGB images: [H, W, C] RGB
masks: [H, W] 255 masks 区域 masks: [H, W, 1] 255 masks 区域
return: BGR IMAGE return: BGR IMAGE
""" """
... ...
def _pad_forward(self, image, mask, config: Config): def _pad_forward(self, image, mask, config: Config):
origin_height, origin_width = image.shape[:2] origin_height, origin_width = image.shape[:2]
pad_image = pad_img_to_modulo(image, mod=self.pad_mod, square=self.pad_to_square, min_size=self.min_size) pad_image = pad_img_to_modulo(
pad_mask = pad_img_to_modulo(mask, mod=self.pad_mod, square=self.pad_to_square, min_size=self.min_size) image, mod=self.pad_mod, square=self.pad_to_square, min_size=self.min_size
)
pad_mask = pad_img_to_modulo(
mask, mod=self.pad_mod, square=self.pad_to_square, min_size=self.min_size
)
logger.info(f"final forward pad size: {pad_image.shape}") logger.info(f"final forward pad size: {pad_image.shape}")
@ -81,18 +85,30 @@ class InpaintModel:
elif config.hd_strategy == HDStrategy.RESIZE: elif config.hd_strategy == HDStrategy.RESIZE:
if max(image.shape) > config.hd_strategy_resize_limit: if max(image.shape) > config.hd_strategy_resize_limit:
origin_size = image.shape[:2] origin_size = image.shape[:2]
downsize_image = resize_max_size(image, size_limit=config.hd_strategy_resize_limit) downsize_image = resize_max_size(
downsize_mask = resize_max_size(mask, size_limit=config.hd_strategy_resize_limit) image, size_limit=config.hd_strategy_resize_limit
)
downsize_mask = resize_max_size(
mask, size_limit=config.hd_strategy_resize_limit
)
logger.info(f"Run resize strategy, origin size: {image.shape} forward size: {downsize_image.shape}") logger.info(
inpaint_result = self._pad_forward(downsize_image, downsize_mask, config) f"Run resize strategy, origin size: {image.shape} forward size: {downsize_image.shape}"
)
inpaint_result = self._pad_forward(
downsize_image, downsize_mask, config
)
# only paste masked area result # only paste masked area result
inpaint_result = cv2.resize(inpaint_result, inpaint_result = cv2.resize(
(origin_size[1], origin_size[0]), inpaint_result,
interpolation=cv2.INTER_CUBIC) (origin_size[1], origin_size[0]),
interpolation=cv2.INTER_CUBIC,
)
original_pixel_indices = mask < 127 original_pixel_indices = mask < 127
inpaint_result[original_pixel_indices] = image[:, :, ::-1][original_pixel_indices] inpaint_result[original_pixel_indices] = image[:, :, ::-1][
original_pixel_indices
]
if inpaint_result is None: if inpaint_result is None:
inpaint_result = self._pad_forward(image, mask, config) inpaint_result = self._pad_forward(image, mask, config)
@ -133,11 +149,11 @@ class InpaintModel:
if _l < 0: if _l < 0:
r += abs(_l) r += abs(_l)
if _r > img_w: if _r > img_w:
l -= (_r - img_w) l -= _r - img_w
if _t < 0: if _t < 0:
b += abs(_t) b += abs(_t)
if _b > img_h: if _b > img_h:
t -= (_b - img_h) t -= _b - img_h
l = max(l, 0) l = max(l, 0)
r = min(r, img_w) r = min(r, img_w)

View File

@ -1135,7 +1135,7 @@ class FcF(InpaintModel):
pad_mod = 512 pad_mod = 512
pad_to_square = True pad_to_square = True
def init_model(self, device): def init_model(self, device, **kwargs):
seed = 0 seed = 0
random.seed(seed) random.seed(seed)
np.random.seed(seed) np.random.seed(seed)

View File

@ -18,16 +18,7 @@ LAMA_MODEL_URL = os.environ.get(
class LaMa(InpaintModel): class LaMa(InpaintModel):
pad_mod = 8 pad_mod = 8
def __init__(self, device): def init_model(self, device, **kwargs):
"""
Args:
device:
"""
super().__init__(device)
self.device = device
def init_model(self, device):
if os.environ.get("LAMA_MODEL"): if os.environ.get("LAMA_MODEL"):
model_path = os.environ.get("LAMA_MODEL") model_path = os.environ.get("LAMA_MODEL")
if not os.path.exists(model_path): if not os.path.exists(model_path):

View File

@ -11,7 +11,12 @@ from lama_cleaner.schema import Config, LDMSampler
torch.manual_seed(42) torch.manual_seed(42)
import torch.nn as nn import torch.nn as nn
from lama_cleaner.helper import download_model, norm_img, get_cache_path_by_url, load_jit_model from lama_cleaner.helper import (
download_model,
norm_img,
get_cache_path_by_url,
load_jit_model,
)
from lama_cleaner.model.utils import ( from lama_cleaner.model.utils import (
make_beta_schedule, make_beta_schedule,
timestep_embedding, timestep_embedding,
@ -92,7 +97,7 @@ class DDPM(nn.Module):
self.linear_start = linear_start self.linear_start = linear_start
self.linear_end = linear_end self.linear_end = linear_end
assert ( assert (
alphas_cumprod.shape[0] == self.num_timesteps alphas_cumprod.shape[0] == self.num_timesteps
), "alphas have to be defined for each timestep" ), "alphas have to be defined for each timestep"
to_torch = lambda x: torch.tensor(x, dtype=torch.float32).to(self.device) to_torch = lambda x: torch.tensor(x, dtype=torch.float32).to(self.device)
@ -118,7 +123,7 @@ class DDPM(nn.Module):
# calculations for posterior q(x_{t-1} | x_t, x_0) # calculations for posterior q(x_{t-1} | x_t, x_0)
posterior_variance = (1 - self.v_posterior) * betas * ( posterior_variance = (1 - self.v_posterior) * betas * (
1.0 - alphas_cumprod_prev 1.0 - alphas_cumprod_prev
) / (1.0 - alphas_cumprod) + self.v_posterior * betas ) / (1.0 - alphas_cumprod) + self.v_posterior * betas
# above: equal to 1. / (1. / (1. - alpha_cumprod_tm1) + alpha_t / beta_t) # above: equal to 1. / (1. / (1. - alpha_cumprod_tm1) + alpha_t / beta_t)
self.register_buffer("posterior_variance", to_torch(posterior_variance)) self.register_buffer("posterior_variance", to_torch(posterior_variance))
@ -139,17 +144,17 @@ class DDPM(nn.Module):
) )
if self.parameterization == "eps": if self.parameterization == "eps":
lvlb_weights = self.betas ** 2 / ( lvlb_weights = self.betas**2 / (
2 2
* self.posterior_variance * self.posterior_variance
* to_torch(alphas) * to_torch(alphas)
* (1 - self.alphas_cumprod) * (1 - self.alphas_cumprod)
) )
elif self.parameterization == "x0": elif self.parameterization == "x0":
lvlb_weights = ( lvlb_weights = (
0.5 0.5
* np.sqrt(torch.Tensor(alphas_cumprod)) * np.sqrt(torch.Tensor(alphas_cumprod))
/ (2.0 * 1 - torch.Tensor(alphas_cumprod)) / (2.0 * 1 - torch.Tensor(alphas_cumprod))
) )
else: else:
raise NotImplementedError("mu not supported") raise NotImplementedError("mu not supported")
@ -222,12 +227,12 @@ class LatentDiffusion(DDPM):
class LDM(InpaintModel): class LDM(InpaintModel):
pad_mod = 32 pad_mod = 32
def __init__(self, device, fp16: bool = True): def __init__(self, device, fp16: bool = True, **kwargs):
self.fp16 = fp16 self.fp16 = fp16
super().__init__(device) super().__init__(device)
self.device = device self.device = device
def init_model(self, device): def init_model(self, device, **kwargs):
self.diffusion_model = load_jit_model(LDM_DIFFUSION_MODEL_URL, device) self.diffusion_model = load_jit_model(LDM_DIFFUSION_MODEL_URL, device)
self.cond_stage_model_decode = load_jit_model(LDM_DECODE_MODEL_URL, device) self.cond_stage_model_decode = load_jit_model(LDM_DECODE_MODEL_URL, device)
self.cond_stage_model_encode = load_jit_model(LDM_ENCODE_MODEL_URL, device) self.cond_stage_model_encode = load_jit_model(LDM_ENCODE_MODEL_URL, device)

View File

@ -1405,7 +1405,7 @@ class MAT(InpaintModel):
pad_mod = 512 pad_mod = 512
pad_to_square = True pad_to_square = True
def init_model(self, device): def init_model(self, device, **kwargs):
seed = 240 # pick up a random number seed = 240 # pick up a random number
random.seed(seed) random.seed(seed)
np.random.seed(seed) np.random.seed(seed)

176
lama_cleaner/model/sd.py Normal file
View File

@ -0,0 +1,176 @@
import random
import PIL.Image
import cv2
import numpy as np
import torch
from diffusers import PNDMScheduler, DDIMScheduler
from loguru import logger
from lama_cleaner.helper import norm_img
from lama_cleaner.model.base import InpaintModel
from lama_cleaner.schema import Config, SDSampler
#
#
# def preprocess_image(image):
# w, h = image.size
# w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32
# image = image.resize((w, h), resample=PIL.Image.LANCZOS)
# image = np.array(image).astype(np.float32) / 255.0
# image = image[None].transpose(0, 3, 1, 2)
# image = torch.from_numpy(image)
# # [-1, 1]
# return 2.0 * image - 1.0
#
#
# def preprocess_mask(mask):
# mask = mask.convert("L")
# w, h = mask.size
# w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32
# mask = mask.resize((w // 8, h // 8), resample=PIL.Image.NEAREST)
# mask = np.array(mask).astype(np.float32) / 255.0
# mask = np.tile(mask, (4, 1, 1))
# mask = mask[None].transpose(0, 1, 2, 3) # what does this step do?
# mask = 1 - mask # repaint white, keep black
# mask = torch.from_numpy(mask)
# return mask
class SD(InpaintModel):
pad_mod = 32
min_size = 512
def init_model(self, device: torch.device, **kwargs):
from .sd_pipeline import StableDiffusionInpaintPipeline
self.model = StableDiffusionInpaintPipeline.from_pretrained(
self.model_id_or_path,
revision="fp16" if torch.cuda.is_available() else "main",
torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
use_auth_token=kwargs["hf_access_token"],
)
# https://huggingface.co/docs/diffusers/v0.3.0/en/api/pipelines/stable_diffusion#diffusers.StableDiffusionInpaintPipeline.enable_attention_slicing
self.model.enable_attention_slicing()
self.model = self.model.to(device)
self.callbacks = kwargs.pop("callbacks", None)
@torch.cuda.amp.autocast()
def forward(self, image, mask, config: Config):
"""Input image and output image have same size
image: [H, W, C] RGB
mask: [H, W, 1] 255 means area to repaint
return: BGR IMAGE
"""
# image = norm_img(image) # [0, 1]
# image = image * 2 - 1 # [0, 1] -> [-1, 1]
# resize to latent feature map size
# h, w = mask.shape[:2]
# mask = cv2.resize(mask, (h // 8, w // 8), interpolation=cv2.INTER_AREA)
# mask = norm_img(mask)
#
# image = torch.from_numpy(image).unsqueeze(0).to(self.device)
# mask = torch.from_numpy(mask).unsqueeze(0).to(self.device)
if config.sd_sampler == SDSampler.ddim:
scheduler = DDIMScheduler(
beta_start=0.00085,
beta_end=0.012,
beta_schedule="scaled_linear",
clip_sample=False,
set_alpha_to_one=False,
)
elif config.sd_sampler == SDSampler.pndm:
PNDM_kwargs = {
"tensor_format": "pt",
"beta_schedule": "scaled_linear",
"beta_start": 0.00085,
"beta_end": 0.012,
"num_train_timesteps": 1000,
"skip_prk_steps": True,
}
scheduler = PNDMScheduler(**PNDM_kwargs)
else:
raise ValueError(config.sd_sampler)
self.model.scheduler = scheduler
seed = config.sd_seed
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
if config.sd_mask_blur != 0:
k = 2 * config.sd_mask_blur + 1
mask = cv2.GaussianBlur(mask, (k, k), 0)[:, :, np.newaxis]
output = self.model(
prompt=config.prompt,
init_image=PIL.Image.fromarray(image),
mask_image=PIL.Image.fromarray(mask[:, :, -1], mode="L"),
strength=config.sd_strength,
num_inference_steps=config.sd_steps,
guidance_scale=config.sd_guidance_scale,
output_type="np.array",
callbacks=self.callbacks,
).images[0]
output = (output * 255).round().astype("uint8")
output = cv2.cvtColor(output, cv2.COLOR_RGB2BGR)
return output
@torch.no_grad()
def __call__(self, image, mask, config: Config):
"""
images: [H, W, C] RGB, not normalized
masks: [H, W]
return: BGR IMAGE
"""
img_h, img_w = image.shape[:2]
# boxes = boxes_from_mask(mask)
if config.use_croper:
logger.info("use croper")
l, t, w, h = (
config.croper_x,
config.croper_y,
config.croper_width,
config.croper_height,
)
r = l + w
b = t + h
l = max(l, 0)
r = min(r, img_w)
t = max(t, 0)
b = min(b, img_h)
crop_img = image[t:b, l:r, :]
crop_mask = mask[t:b, l:r]
crop_image = self._pad_forward(crop_img, crop_mask, config)
inpaint_result = image[:, :, ::-1]
inpaint_result[t:b, l:r, :] = crop_image
else:
inpaint_result = self._pad_forward(image, mask, config)
return inpaint_result
@staticmethod
def is_downloaded() -> bool:
# model will be downloaded when app start, and can't switch in frontend settings
return True
class SD14(SD):
model_id_or_path = "CompVis/stable-diffusion-v1-4"
class SD15(SD):
model_id_or_path = "CompVis/stable-diffusion-v1-5"

View File

@ -0,0 +1,309 @@
import inspect
from typing import List, Optional, Union, Callable
import numpy as np
import torch
import PIL
from diffusers import DiffusionPipeline, AutoencoderKL, UNet2DConditionModel, DDIMScheduler, PNDMScheduler
from diffusers.pipelines.stable_diffusion import StableDiffusionSafetyChecker, StableDiffusionPipelineOutput
from diffusers.utils import logging
from tqdm.auto import tqdm
from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer
logger = logging.get_logger(__name__)
def preprocess_image(image):
w, h = image.size
w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32
image = image.resize((w, h), resample=PIL.Image.LANCZOS)
image = np.array(image).astype(np.float32) / 255.0
image = image[None].transpose(0, 3, 1, 2)
image = torch.from_numpy(image)
return 2.0 * image - 1.0
def preprocess_mask(mask):
mask = mask.convert("L")
w, h = mask.size
w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32
mask = mask.resize((w // 8, h // 8), resample=PIL.Image.NEAREST)
mask = np.array(mask).astype(np.float32) / 255.0
mask = np.tile(mask, (4, 1, 1))
mask = mask[None].transpose(0, 1, 2, 3) # what does this step do?
mask = 1 - mask # repaint white, keep black
mask = torch.from_numpy(mask)
return mask
class StableDiffusionInpaintPipeline(DiffusionPipeline):
r"""
Pipeline for text-guided image inpainting using Stable Diffusion. *This is an experimental feature*.
This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the
library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.)
Args:
vae ([`AutoencoderKL`]):
Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations.
text_encoder ([`CLIPTextModel`]):
Frozen text-encoder. Stable Diffusion uses the text portion of
[CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically
the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant.
tokenizer (`CLIPTokenizer`):
Tokenizer of class
[CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer).
unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents.
scheduler ([`SchedulerMixin`]):
A scheduler to be used in combination with `unet` to denoise the encoded image latens. Can be one of
[`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`].
safety_checker ([`StableDiffusionSafetyChecker`]):
Classification module that estimates whether generated images could be considered offsensive or harmful.
Please, refer to the [model card](https://huggingface.co/CompVis/stable-diffusion-v1-4) for details.
feature_extractor ([`CLIPFeatureExtractor`]):
Model that extracts features from generated images to be used as inputs for the `safety_checker`.
"""
def __init__(
self,
vae: AutoencoderKL,
text_encoder: CLIPTextModel,
tokenizer: CLIPTokenizer,
unet: UNet2DConditionModel,
scheduler: Union[DDIMScheduler, PNDMScheduler],
safety_checker: StableDiffusionSafetyChecker,
feature_extractor: CLIPFeatureExtractor,
):
super().__init__()
scheduler = scheduler.set_format("pt")
logger.info("`StableDiffusionInpaintPipeline` is experimental and will very likely change in the future.")
self.register_modules(
vae=vae,
text_encoder=text_encoder,
tokenizer=tokenizer,
unet=unet,
scheduler=scheduler,
safety_checker=safety_checker,
feature_extractor=feature_extractor,
)
def enable_attention_slicing(self, slice_size: Optional[Union[str, int]] = "auto"):
r"""
Enable sliced attention computation.
When this option is enabled, the attention module will split the input tensor in slices, to compute attention
in several steps. This is useful to save some memory in exchange for a small speed decrease.
Args:
slice_size (`str` or `int`, *optional*, defaults to `"auto"`):
When `"auto"`, halves the input to the attention heads, so attention will be computed in two steps. If
a number is provided, uses as many slices as `attention_head_dim // slice_size`. In this case,
`attention_head_dim` must be a multiple of `slice_size`.
"""
if slice_size == "auto":
# half the attention head size is usually a good trade-off between
# speed and memory
slice_size = self.unet.config.attention_head_dim // 2
self.unet.set_attention_slice(slice_size)
def disable_attention_slicing(self):
r"""
Disable sliced attention computation. If `enable_attention_slicing` was previously invoked, this method will go
back to computing attention in one step.
"""
# set slice_size = `None` to disable `set_attention_slice`
self.enable_attention_slice(None)
@torch.no_grad()
def __call__(
self,
prompt: Union[str, List[str]],
init_image: Union[torch.FloatTensor, PIL.Image.Image],
mask_image: Union[torch.FloatTensor, PIL.Image.Image],
strength: float = 0.8,
num_inference_steps: Optional[int] = 50,
guidance_scale: Optional[float] = 7.5,
eta: Optional[float] = 0.0,
generator: Optional[torch.Generator] = None,
output_type: Optional[str] = "pil",
return_dict: bool = True,
callbacks: List[Callable[[int], None]] = None
):
r"""
Function invoked when calling the pipeline for generation.
Args:
prompt (`str` or `List[str]`):
The prompt or prompts to guide the image generation.
init_image (`torch.FloatTensor` or `PIL.Image.Image`):
`Image`, or tensor representing an image batch, that will be used as the starting point for the
process. This is the image whose masked region will be inpainted.
mask_image (`torch.FloatTensor` or `PIL.Image.Image`):
`Image`, or tensor representing an image batch, to mask `init_image`. White pixels in the mask will be
replaced by noise and therefore repainted, while black pixels will be preserved. The mask image will be
converted to a single channel (luminance) before use.
strength (`float`, *optional*, defaults to 0.8):
Conceptually, indicates how much to inpaint the masked area. Must be between 0 and 1. When `strength`
is 1, the denoising process will be run on the masked area for the full number of iterations specified
in `num_inference_steps`. `init_image` will be used as a reference for the masked area, adding more
noise to that region the larger the `strength`. If `strength` is 0, no inpainting will occur.
num_inference_steps (`int`, *optional*, defaults to 50):
The reference number of denoising steps. More denoising steps usually lead to a higher quality image at
the expense of slower inference. This parameter will be modulated by `strength`, as explained above.
guidance_scale (`float`, *optional*, defaults to 7.5):
Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598).
`guidance_scale` is defined as `w` of equation 2. of [Imagen
Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale >
1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`,
usually at the expense of lower image quality.
eta (`float`, *optional*, defaults to 0.0):
Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to
[`schedulers.DDIMScheduler`], will be ignored for others.
generator (`torch.Generator`, *optional*):
A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation
deterministic.
output_type (`str`, *optional*, defaults to `"pil"`):
The output format of the generate image. Choose between
[PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `nd.array`.
return_dict (`bool`, *optional*, defaults to `True`):
Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a
plain tuple.
Returns:
[`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`:
[`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple.
When returning a tuple, the first element is a list with the generated images, and the second element is a
list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work"
(nsfw) content, according to the `safety_checker`.
"""
if isinstance(prompt, str):
batch_size = 1
elif isinstance(prompt, list):
batch_size = len(prompt)
else:
raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")
if strength < 0 or strength > 1:
raise ValueError(f"The value of strength should in [0.0, 1.0] but is {strength}")
# set timesteps
accepts_offset = "offset" in set(inspect.signature(self.scheduler.set_timesteps).parameters.keys())
extra_set_kwargs = {}
offset = 0
if accepts_offset:
offset = 1
extra_set_kwargs["offset"] = 1
self.scheduler.set_timesteps(num_inference_steps, **extra_set_kwargs)
# preprocess image
init_image = preprocess_image(init_image).to(self.device)
# encode the init image into latents and scale the latents
init_latent_dist = self.vae.encode(init_image.to(self.device)).latent_dist
init_latents = init_latent_dist.sample(generator=generator)
init_latents = 0.18215 * init_latents
# Expand init_latents for batch_size
init_latents = torch.cat([init_latents] * batch_size)
init_latents_orig = init_latents
# preprocess mask
mask = preprocess_mask(mask_image).to(self.device)
mask = torch.cat([mask] * batch_size)
# check sizes
if not mask.shape == init_latents.shape:
raise ValueError("The mask and init_image should be the same size!")
# get the original timestep using init_timestep
init_timestep = int(num_inference_steps * strength) + offset
init_timestep = min(init_timestep, num_inference_steps)
timesteps = self.scheduler.timesteps[-init_timestep]
timesteps = torch.tensor([timesteps] * batch_size, dtype=torch.long, device=self.device)
# add noise to latents using the timesteps
noise = torch.randn(init_latents.shape, generator=generator, device=self.device)
init_latents = self.scheduler.add_noise(init_latents, noise, timesteps)
# get prompt text embeddings
text_input = self.tokenizer(
prompt,
padding="max_length",
max_length=self.tokenizer.model_max_length,
truncation=True,
return_tensors="pt",
)
text_embeddings = self.text_encoder(text_input.input_ids.to(self.device))[0]
# here `guidance_scale` is defined analog to the guidance weight `w` of equation (2)
# of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1`
# corresponds to doing no classifier free guidance.
do_classifier_free_guidance = guidance_scale > 1.0
# get unconditional embeddings for classifier free guidance
if do_classifier_free_guidance:
max_length = text_input.input_ids.shape[-1]
uncond_input = self.tokenizer(
[""] * batch_size, padding="max_length", max_length=max_length, return_tensors="pt"
)
uncond_embeddings = self.text_encoder(uncond_input.input_ids.to(self.device))[0]
# For classifier free guidance, we need to do two forward passes.
# Here we concatenate the unconditional and text embeddings into a single batch
# to avoid doing two forward passes
text_embeddings = torch.cat([uncond_embeddings, text_embeddings])
# prepare extra kwargs for the scheduler step, since not all schedulers have the same signature
# eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers.
# eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502
# and should be between [0, 1]
accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys())
extra_step_kwargs = {}
if accepts_eta:
extra_step_kwargs["eta"] = eta
latents = init_latents
t_start = max(num_inference_steps - init_timestep + offset, 0)
for i, t in tqdm(enumerate(self.scheduler.timesteps[t_start:])):
# expand the latents if we are doing classifier free guidance
latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents
# predict the noise residual
noise_pred = self.unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample
# perform guidance
if do_classifier_free_guidance:
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
# compute the previous noisy sample x_t -> x_t-1
latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample
# masking
init_latents_proper = self.scheduler.add_noise(init_latents_orig, noise, t)
latents = (init_latents_proper * mask) + (latents * (1 - mask))
if callbacks is not None:
for callback in callbacks:
callback(i)
# scale and decode the image latents with vae
latents = 1 / 0.18215 * latents
image = self.vae.decode(latents).sample
image = (image / 2 + 0.5).clamp(0, 1)
image = image.cpu().permute(0, 2, 3, 1).numpy()
# run safety checker
safety_cheker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to(self.device)
image, has_nsfw_concept = self.safety_checker(images=image, clip_input=safety_cheker_input.pixel_values)
if output_type == "pil":
image = self.numpy_to_pil(image)
if not return_dict:
return (image, has_nsfw_concept)
return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept)

View File

@ -206,7 +206,7 @@ class ZITS(InpaintModel):
pad_mod = 32 pad_mod = 32
pad_to_square = True pad_to_square = True
def __init__(self, device): def __init__(self, device, **kwargs):
""" """
Args: Args:
@ -216,7 +216,7 @@ class ZITS(InpaintModel):
self.device = device self.device = device
self.sample_edge_line_iterations = 1 self.sample_edge_line_iterations = 1
def init_model(self, device): def init_model(self, device, **kwargs):
self.wireframe = load_jit_model(ZITS_WIRE_FRAME_MODEL_URL, device) self.wireframe = load_jit_model(ZITS_WIRE_FRAME_MODEL_URL, device)
self.edge_line = load_jit_model(ZITS_EDGE_LINE_MODEL_URL, device) self.edge_line = load_jit_model(ZITS_EDGE_LINE_MODEL_URL, device)
self.structure_upsample = load_jit_model( self.structure_upsample = load_jit_model(

View File

@ -2,27 +2,23 @@ from lama_cleaner.model.fcf import FcF
from lama_cleaner.model.lama import LaMa from lama_cleaner.model.lama import LaMa
from lama_cleaner.model.ldm import LDM from lama_cleaner.model.ldm import LDM
from lama_cleaner.model.mat import MAT from lama_cleaner.model.mat import MAT
from lama_cleaner.model.sd import SD14
from lama_cleaner.model.zits import ZITS from lama_cleaner.model.zits import ZITS
from lama_cleaner.schema import Config from lama_cleaner.schema import Config
models = { models = {"lama": LaMa, "ldm": LDM, "zits": ZITS, "mat": MAT, "fcf": FcF, "sd1.4": SD14}
'lama': LaMa,
'ldm': LDM,
'zits': ZITS,
'mat': MAT,
'fcf': FcF
}
class ModelManager: class ModelManager:
def __init__(self, name: str, device): def __init__(self, name: str, device, **kwargs):
self.name = name self.name = name
self.device = device self.device = device
self.model = self.init_model(name, device) self.kwargs = kwargs
self.model = self.init_model(name, device, **kwargs)
def init_model(self, name: str, device): def init_model(self, name: str, device, **kwargs):
if name in models: if name in models:
model = models[name](device) model = models[name](device, **kwargs)
else: else:
raise NotImplementedError(f"Not supported model: {name}") raise NotImplementedError(f"Not supported model: {name}")
return model return model
@ -40,7 +36,7 @@ class ModelManager:
if new_name == self.name: if new_name == self.name:
return return
try: try:
self.model = self.init_model(new_name, self.device) self.model = self.init_model(new_name, self.device, **self.kwargs)
self.name = new_name self.name = new_name
except NotImplementedError as e: except NotImplementedError as e:
raise e raise e

View File

@ -7,7 +7,16 @@ def parse_args():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", default=8080, type=int) parser.add_argument("--port", default=8080, type=int)
parser.add_argument("--model", default="lama", choices=["lama", "ldm", "zits", "mat", 'fcf']) parser.add_argument(
"--model",
default="lama",
choices=["lama", "ldm", "zits", "mat", "fcf", "sd1.4"],
)
parser.add_argument(
"--hf_access_token",
default="",
help="huggingface access token. Check how to get token from: https://huggingface.co/docs/hub/security-tokens",
)
parser.add_argument("--device", default="cuda", type=str, choices=["cuda", "cpu"]) parser.add_argument("--device", default="cuda", type=str, choices=["cuda", "cpu"])
parser.add_argument("--gui", action="store_true", help="Launch as desktop app") parser.add_argument("--gui", action="store_true", help="Launch as desktop app")
parser.add_argument( parser.add_argument(
@ -29,4 +38,10 @@ def parse_args():
if imghdr.what(args.input) is None: if imghdr.what(args.input) is None:
parser.error(f"invalid --input: {args.input} is not a valid image file") parser.error(f"invalid --input: {args.input} is not a valid image file")
if args.model.startswith("sd"):
if not args.hf_access_token.startswith("hf_"):
parser.error(
f"sd(stable-diffusion) model requires huggingface access token. Check how to get token from: https://huggingface.co/docs/hub/security-tokens"
)
return args return args

View File

@ -4,14 +4,19 @@ from pydantic import BaseModel
class HDStrategy(str, Enum): class HDStrategy(str, Enum):
ORIGINAL = 'Original' ORIGINAL = "Original"
RESIZE = 'Resize' RESIZE = "Resize"
CROP = 'Crop' CROP = "Crop"
class LDMSampler(str, Enum): class LDMSampler(str, Enum):
ddim = 'ddim' ddim = "ddim"
plms = 'plms' plms = "plms"
class SDSampler(str, Enum):
ddim = "ddim"
pndm = "pndm"
class Config(BaseModel): class Config(BaseModel):
@ -22,3 +27,20 @@ class Config(BaseModel):
hd_strategy_crop_margin: int hd_strategy_crop_margin: int
hd_strategy_crop_trigger_size: int hd_strategy_crop_trigger_size: int
hd_strategy_resize_limit: int hd_strategy_resize_limit: int
prompt: str = ""
# 始终是在原图尺度上的值
use_croper: bool = False
croper_x: int = None
croper_y: int = None
croper_height: int = None
croper_width: int = None
# sd
sd_mask_blur: int = 0
sd_strength: float = 0.75
sd_steps: int = 50
sd_guidance_scale: float = 7.5
sd_sampler: str = SDSampler.ddim
# -1 mean random seed
sd_seed: int = 42

View File

@ -4,6 +4,7 @@ import io
import logging import logging
import multiprocessing import multiprocessing
import os import os
import random
import time import time
import imghdr import imghdr
from pathlib import Path from pathlib import Path
@ -25,7 +26,7 @@ try:
except: except:
pass pass
from flask import Flask, request, send_file, cli from flask import Flask, request, send_file, cli, make_response
# Disable ability for Flask to display warning about using a development server in a production environment. # Disable ability for Flask to display warning about using a development server in a production environment.
# https://gist.github.com/jerblack/735b9953ba1ab6234abb43174210d356 # https://gist.github.com/jerblack/735b9953ba1ab6234abb43174210d356
@ -41,7 +42,7 @@ from lama_cleaner.helper import (
NUM_THREADS = str(multiprocessing.cpu_count()) NUM_THREADS = str(multiprocessing.cpu_count())
# fix libomp problem on windows https://github.com/Sanster/lama-cleaner/issues/56 # fix libomp problem on windows https://github.com/Sanster/lama-cleaner/issues/56
os.environ["KMP_DUPLICATE_LIB_OK"]="True" os.environ["KMP_DUPLICATE_LIB_OK"] = "True"
os.environ["OMP_NUM_THREADS"] = NUM_THREADS os.environ["OMP_NUM_THREADS"] = NUM_THREADS
os.environ["OPENBLAS_NUM_THREADS"] = NUM_THREADS os.environ["OPENBLAS_NUM_THREADS"] = NUM_THREADS
@ -64,6 +65,10 @@ logging.getLogger("werkzeug").addFilter(NoFlaskwebgui())
app = Flask(__name__, static_folder=os.path.join(BUILD_DIR, "static")) app = Flask(__name__, static_folder=os.path.join(BUILD_DIR, "static"))
app.config["JSON_AS_ASCII"] = False app.config["JSON_AS_ASCII"] = False
CORS(app, expose_headers=["Content-Disposition"]) CORS(app, expose_headers=["Content-Disposition"])
# MAX_BUFFER_SIZE = 50 * 1000 * 1000 # 50 MB
# async_mode 优先级: eventlet/gevent_uwsgi/gevent/threading
# only threading works on macOS
# socketio = SocketIO(app, max_http_buffer_size=MAX_BUFFER_SIZE, async_mode='threading')
model: ModelManager = None model: ModelManager = None
device = None device = None
@ -77,6 +82,11 @@ def get_image_ext(img_bytes):
return w return w
def diffuser_callback(step: int):
pass
# socketio.emit('diffusion_step', {'diffusion_step': step})
@app.route("/inpaint", methods=["POST"]) @app.route("/inpaint", methods=["POST"])
def process(): def process():
input = request.files input = request.files
@ -102,8 +112,23 @@ def process():
hd_strategy_crop_margin=form["hdStrategyCropMargin"], hd_strategy_crop_margin=form["hdStrategyCropMargin"],
hd_strategy_crop_trigger_size=form["hdStrategyCropTrigerSize"], hd_strategy_crop_trigger_size=form["hdStrategyCropTrigerSize"],
hd_strategy_resize_limit=form["hdStrategyResizeLimit"], hd_strategy_resize_limit=form["hdStrategyResizeLimit"],
prompt=form["prompt"],
use_croper=form["useCroper"],
croper_x=form["croperX"],
croper_y=form["croperY"],
croper_height=form["croperHeight"],
croper_width=form["croperWidth"],
sd_mask_blur=form["sdMaskBlur"],
sd_strength=form["sdStrength"],
sd_steps=form["sdSteps"],
sd_guidance_scale=form["sdGuidanceScale"],
sd_sampler=form["sdSampler"],
sd_seed=form["sdSeed"],
) )
if config.sd_seed == -1:
config.sd_seed = random.randint(1, 9999999)
logger.info(f"Origin image shape: {original_shape}") logger.info(f"Origin image shape: {original_shape}")
image = resize_max_size(image, size_limit=size_limit, interpolation=interpolation) image = resize_max_size(image, size_limit=size_limit, interpolation=interpolation)
logger.info(f"Resized image shape: {image.shape}") logger.info(f"Resized image shape: {image.shape}")
@ -127,10 +152,15 @@ def process():
) )
ext = get_image_ext(origin_image_bytes) ext = get_image_ext(origin_image_bytes)
return send_file(
io.BytesIO(numpy_to_bytes(res_np_img, ext)), response = make_response(
mimetype=f"image/{ext}", send_file(
io.BytesIO(numpy_to_bytes(res_np_img, ext)),
mimetype=f"image/{ext}",
)
) )
response.headers["X-Seed"] = str(config.sd_seed)
return response
@app.route("/model") @app.route("/model")
@ -184,7 +214,12 @@ def main(args):
device = torch.device(args.device) device = torch.device(args.device)
input_image_path = args.input input_image_path = args.input
model = ModelManager(name=args.model, device=device) model = ModelManager(
name=args.model,
device=device,
hf_access_token=args.hf_access_token,
callbacks=[diffuser_callback],
)
if args.gui: if args.gui:
app_width, app_height = args.gui_size app_width, app_height = args.gui_size
@ -195,4 +230,5 @@ def main(args):
) )
ui.run() ui.run()
else: else:
# TODO: socketio
app.run(host=args.host, port=args.port, debug=args.debug) app.run(host=args.host, port=args.port, debug=args.debug)

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -1,3 +1,4 @@
import os
from pathlib import Path from pathlib import Path
import cv2 import cv2
@ -5,16 +6,16 @@ import pytest
import torch import torch
from lama_cleaner.model_manager import ModelManager from lama_cleaner.model_manager import ModelManager
from lama_cleaner.schema import Config, HDStrategy, LDMSampler from lama_cleaner.schema import Config, HDStrategy, LDMSampler, SDSampler
current_dir = Path(__file__).parent.absolute().resolve() current_dir = Path(__file__).parent.absolute().resolve()
device = 'cuda' if torch.cuda.is_available() else 'cpu' device = 'cuda' if torch.cuda.is_available() else 'cpu'
def get_data(fx=1, fy=1.0): def get_data(fx=1, fy=1.0, img_p=current_dir / "image.png", mask_p=current_dir / "mask.png"):
img = cv2.imread(str(current_dir / "image.png")) img = cv2.imread(str(img_p))
img = cv2.cvtColor(img, cv2.COLOR_BGRA2RGB) img = cv2.cvtColor(img, cv2.COLOR_BGRA2RGB)
mask = cv2.imread(str(current_dir / "mask.png"), cv2.IMREAD_GRAYSCALE) mask = cv2.imread(str(mask_p), cv2.IMREAD_GRAYSCALE)
if fx != 1: if fx != 1:
img = cv2.resize(img, None, fx=fx, fy=fy, interpolation=cv2.INTER_AREA) img = cv2.resize(img, None, fx=fx, fy=fy, interpolation=cv2.INTER_AREA)
@ -35,8 +36,8 @@ def get_config(strategy, **kwargs):
return Config(**data) return Config(**data)
def assert_equal(model, config, gt_name, fx=1, fy=1): def assert_equal(model, config, gt_name, fx=1, fy=1, img_p=current_dir / "image.png", mask_p=current_dir / "mask.png"):
img, mask = get_data(fx=fx, fy=fy) img, mask = get_data(fx=fx, fy=fy, img_p=img_p, mask_p=mask_p)
res = model(img, mask, config) res = model(img, mask, config)
cv2.imwrite( cv2.imwrite(
str(current_dir / gt_name), str(current_dir / gt_name),
@ -153,3 +154,36 @@ def test_fcf(strategy):
fx=3.8, fx=3.8,
fy=2 fy=2
) )
@pytest.mark.parametrize("strategy", [HDStrategy.ORIGINAL])
@pytest.mark.parametrize("sampler", [SDSampler.ddim, SDSampler.pndm])
def test_sd(strategy, sampler, capfd):
def callback(step: int):
print(f"sd_step_{step}")
sd_steps = 50
model = ModelManager(name="sd1.4", device=device, hf_access_token=os.environ['HF_ACCESS_TOKEN'],
callbacks=[callback])
cfg = get_config(strategy, prompt='a cat sitting on a bench', sd_steps=sd_steps)
cfg.sd_sampler = sampler
assert_equal(
model,
cfg,
f"sd_{strategy.capitalize()}_{sampler}_result.png",
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask.png",
)
assert_equal(
model,
cfg,
f"sd_{strategy.capitalize()}_{sampler}_blur_mask_result.png",
img_p=current_dir / "overture-creations-5sI6fQgYIuo.png",
mask_p=current_dir / "overture-creations-5sI6fQgYIuo_mask_blur.png",
)
# captured = capfd.readouterr()
# for i in range(sd_steps):
# assert f'sd_step_{i}' in captured.out

View File

@ -10,3 +10,5 @@ pytest
yacs yacs
markupsafe==2.0.1 markupsafe==2.0.1
scikit-image==0.19.3 scikit-image==0.19.3
diffusers==0.3.0
transformers==4.20.0